[CVE-2022-29078] One-day 취약점 분석
CVE-2022-29078는 Node js의 ejs 3.1.6 버전에서 발생하는 RCE 취약점이다.
1. 환경구성
// index.js
const express = require('express')
const app = express()
const port = 3000
app.set('view engine', 'ejs');
app.get('/page', (req,res) => {
res.render('../ejs/views/page', req.query);
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}!`)
})
<!-- views/page.ejs -->
<html>
<head>
<title>Page</title>
</head>
<body>
<h1>Page</h1>
<p>Welcome to the page!</p>
<%= name %>
</body>
</html>
우선 테스트 환경을 구성하기 위해 간단한 웹서버와 ejs 템플릿을 만들었다.
해당 웹페이지는 get 요청으로 name 파라미터를 받아 출력해 주는 기능을 구현했다.
2. 디버깅을 통한 취약점 분석
2-1. res.render

rendering 과정을 자세하게 살펴보면 res.render 메서드는 response.js에 선언되어 있다.
res.render에 두번째 인자로 들어온 req.query는 opts에 초기화되고 self.locals는 현재 코드에서 없으므로 opts는 req.query값만 가지고 app.render 메서드로 전달된다.
2-2. app.render

app.render메서드는 application.js에 선언되어 있다.
app.render에 두번째 인자로 들어온 req.query는 병합 과정을 거쳐 renderOptions로 들어가게 된다.

그 후, tryRender 메서드로 전달된다.
2-3. tryRender

tryRender메서드는 application.js에 선언되어 있으며 renderOptions을 view.render 메서드로 전달한다.
2-4. View.prototype.render

View.prototype.render메서드는 view.js에 선언되어 있으며 options(renderOptions)를 this.engine(ejs) 메서드로 전달한다.
2-5. renderFile

renderFile 메서드는 ejs.js에 선언되어있다.
여기서 data = args.shift();를 통해 data 변수에 options이 key:vaule로 저장되게 되며 data의 settings[’view option’]은 viewOpts변수에 저장된다.
여기서 viewOpts가 존재한다면 shallowCopy메서드로 넘겨진다.
2-6. shallowCopy

이때 전달된 opts에 viewOpts값을 복사하게 되면서 취약점의 원인이 된다.
2-7. templet rendering

ejs의 템플릿 렌더링 로직에서 js 코드를 prepended 변수에 초기화하는 부분이 있는데 만약에 opts.outputFunctionName이 존재한다면 prepended 변수 값에 opts.outputFunctionName값을 추가한다.
만약 이때 opts.outputFunctionName에 공격자의 js 코드가 들어가게 된다면 해당 코드가 실행되며 취약점이 발생하게 된다.
3. POC
그렇다면 취약점은 어떻게 발생시킬 수 있을까?
2-6에서 설명한 것처럼 opts와 viewOpts는 복사의 과정을 거친다. 여기서 viewOpts는 settings[’view option’]의 값이고 opts에 settings[’view option’]값이 들어가는 것이다.
만약에 settings[’view option’]값에 outputFunctionName 키를 가진 데이터가 있다면 opts.outputFunctionName의 값으로 되어 js 코드로 들어갈 수 있다.
이를 통해 settings[’view option’][’outputFunctionName’] = ‘실행할 js코드’ 형태로 req.query에 전송한다면 아래와 같은 로직으로 실행이 가능하다.
prepended += ' var ' + '실행 할 js 코드' + ' = __append;' + '\n';
RCE payload
?name=123&settings[view options][outputFunctionName]=fake1;process.mainModule.require('child_process').execSync('id > result.txt');fake2
결과 사진
