WordPress RCE를 GPT5.6과 25달러로 발견
1 day ago
7
- GPT5.6 Sol Ultra가 최신 안정판 WordPress를 분석해 인증 전 SQL 주입부터 관리자 계정 생성과 원격 코드 실행(RCE)까지 이어지는 공격 체인을 10여 시간 만에 완성함
- 출발점은 WordPress 5.6부터 제공된 Batch API의 배열 인덱스 불일치로, 재귀 Batch 요청을 결합해 GET 제한과 매개변수 검증을 우회함
- 검증되지 않은 author_exclude 문자열로 UNION 주입을 일으킨 뒤, 조작한 게시물을 메모리 캐시에 넣고 oEmbed 캐시·changeset·순환 참조·훅을 연쇄적으로 악용함
- customize_changeset의 user_id: 1로 관리자 권한을 일시 획득하고 parse_request 훅으로 Batch 요청을 재실행해 새 관리자를 만든 뒤, 백도어 플러그인 ZIP을 업로드해 코드 실행에 도달함
- 월 200달러 구독의 주간 사용량 50%를 비례 계산한 비용은 약 25달러였으며, 사람의 역할은 제품과 공격 표면 선정, 프롬프트 조정 같은 상위 수준의 연구 지휘로 이동할 가능성이 커짐
발견 과정과 실험 조건
- OpenAI가 Cycle Double Cover 추측 해결에 사용했다고 공개한 프롬프트를 보안 연구용으로 수정해 GPT5.6 Sol Ultra에 제공함
- 최신 안정판 WordPress를 main/에 복제하고 .git 디렉터리를 삭제했으며, 종속 코드를 조사할 수 있도록 빈 third_party/ 디렉터리를 준비함
- 프롬프트는 최대 4개 에이전트를 병렬로 사용해 최소 6시간 동안 다양한 공격 경로를 유지하도록 지시함
- 입력 파싱, 문자 집합, 파일 업로드, 오류 처리, 내장 경로, 직렬화, 캐시, 경쟁 조건, 암호화, 타입, 대량 할당 등을 탐색함
- 접근법 계열을 등록하고 특정 전략으로 에이전트가 몰리면 덜 탐색된 영역으로 재배치함
- 구체적인 버그는 적대적 에이전트가 재검증하고, 실패한 경로도 새로운 메커니즘이 생기면 다시 열도록 함
- 변경 이력이나 패치 버전과의 차이, 인터넷 단서를 이용하지 않고 소스 코드 자체에서 신규 취약점을 찾도록 제한함
- 비현실적인 설정이나 공격자가 충족할 수 없는 전제 조건을 피하고자 목표를 “MySQL을 쓰는 일반적인 프로덕션 배포에서 인증 전 RCE”로 명시함
- 약 6시간 뒤 모델이 인증 전 SQL 주입을 찾았고, 기본 WordPress 원격 서버에서 관리자 이메일을 수분 안에 추출해 재현함
- RCE로의 승격을 추가 요청하자 약 4시간 뒤, 비밀번호 크래킹이나 오프라인 계산 없이 읽기 전용 SQL 주입에서 관리자 권한으로 올라가는 체인을 완성함
- 총 작업 시간은 10시간 남짓이며, 주간 사용량의 50%를 소모함. 월 200달러 구독료를 비례 계산한 비용은 약 25달러였음
- 공개 전 주말 동안 운영자가 WordPress를 업그레이드할 시간을 제공했으며, 그사이 Calif와 Hacktron이 GitHub의 다른 PoC보다 먼저 전체 체인을 독립적으로 재현함
- 운영 중인 인스턴스는 wp2shell.com에서 취약 여부를 확인할 수 있음
Batch API의 검증 불일치
- WordPress 5.6에서 도입된 Batch API는 하나의 요청으로 여러 가상 API 요청을 처리함. 엔드포인트 자체에는 인증 없이 접근할 수 있지만, 각 하위 요청에는 인증 정보가 전달됨
- 일반 REST 요청은 다음 순서로 처리됨
- has_valid_params()로 필수 값과 유효성을 검사함
- sanitize_params()로 값을 정제함
- 권한 콜백을 실행함
- 엔드포인트 콜백을 실행함
- Batch API는 성능을 위해 검증과 실행을 두 개의 반복문으로 분리함
- 첫 번째 반복문에서 모든 요청을 검증·정제함
- 두 번째 반복문에서 검증 결과를 확인하고 권한 및 엔드포인트 콜백을 실행함
- 구현은 라우트 매칭 결과인 $matches와 검증 결과인 $validation에서 같은 인덱스끼리 대응한다고 가정함
- 잘못된 요청이 is_wp_error($single_request) 분기로 들어가면 $validation에는 항목을 추가하지만, continue 때문에 $matches에는 추가하지 않음
- 이후 모든 $matches 항목이 한 칸씩 밀림
- 한 요청의 매개변수를 다른 요청의 규칙으로 검증한 뒤, 원래 의도하지 않은 엔드포인트 핸들러에서 실행할 수 있음
- 이 인덱스 불일치를 이용하면 매개변수를 정제하지 않는 다른 엔드포인트의 검증 결과를 Batch 지원 엔드포인트에 적용할 수 있음
author__not_in에서 발생하는 SQL 주입
- GET /wp/v2/posts는 특정 작성자를 결과에서 제외하기 위해 내부 쿼리 변수 author__not_in을 사용함
- 값이 배열이면 각 원소에 absint를 적용해 정수로 만들지만, 스칼라 값이면 그대로 implode한 뒤 SQL의 NOT IN 절에 삽입함
- 정상 호출에서는 공개 매개변수 author_exclude가 정수 배열이어야 하므로 문제가 드러나지 않음
- Batch 인덱스 불일치를 이용하면 문자열 author_exclude를 이를 인식하지 않는 DELETE /wp/v2/posts/1 규칙으로 검증한 뒤 GET /wp/v2/posts에 전달할 수 있음
- Batch API가 GET 하위 요청을 허용하지 않는 제약도 재귀 Batch 호출로 우회함
- 바깥쪽 Batch에서 인덱스 불일치를 일으켜 내부 요청의 method 검증을 건너뜀
- 안쪽 Batch에서 다시 인덱스 불일치를 일으켜 author_exclude 검증을 우회함
- 0) OR 1=1 -- 같은 값을 넣으면 모든 게시물 행이 반환돼 주입 여부를 확인할 수 있음
- 이후 UNION 기반 주입으로 wp_posts와 같은 형태의 행을 만들어 임의의 데이터베이스 값을 유출할 수 있음
- 비밀번호, 재설정 토큰, API 키 등은 데이터베이스에서 해시되므로 관리자 비밀번호가 약하지 않다면 데이터 유출만으로 계정 탈취까지 이어지지는 않음
요청 내 게시물 캐시 조작
- WordPress는 한 요청 안에서 반복 참조되는 WP_Post 객체를 메모리 캐시에 저장해 데이터베이스 왕복을 줄임
- UNION 주입으로 가짜 게시물 행을 반환하면 게시물 ID, 타입, 상태, 부모 관계, 본문 등 상당수 필드를 공격자가 정한 상태로 캐시에 넣을 수 있음
- API 응답에서도 게시물 본문을 후처리하므로, 조작한 본문으로 추가 코드 경로를 실행할 수 있음
- 이 캐시는 요청이 끝나면 사라지고 가짜 게시물도 데이터베이스에 존재하지 않아, 캐시 조작만으로는 요청 간 지속성을 확보할 수 없음
oEmbed 캐시를 이용한 데이터베이스 행 생성
- WordPress의 embeds 기능은 게시물 본문의 [embed]...[/embed] 구문으로 지원되는 원격 콘텐츠를 삽입함
- 매번 HTTP 요청을 보내지 않도록 결과를 wp_posts의 oembed_cache 타입 게시물로 데이터베이스에 저장함
- 상대 경로로 로컬 WordPress 게시물을 임베드하면 HTTP 요청을 생략하며, 참조한 게시물 ID가 실제로 존재하는지는 확인하지 않음
- /?p=10 같은 존재하지 않는 로컬 게시물을 임베드해도 해당 데이터용 oembed_cache 행이 만들어질 수 있음
- 생성된 행을 다시 SQL 주입으로 조회하면서 메모리상의 게시물 타입을 post 등으로 조작하면 데이터베이스와 캐시의 표현이 달라짐
- WordPress는 두 표현을 조정하면서 wp_update_post()를 호출하고, 명시적으로 지정한 ID와 post_content 외 필드는 공격자가 만든 메모리 값을 선호함
- 이에 따라 oembed_cache 행을 일반 게시물로 바꿀 수 있지만, 이 호출에서는 post_content가 임베드 결과로 덮이므로 공격자가 본문까지 제어하지는 못함
customize_changeset을 통한 임시 관리자 권한
- 테마 사용자 정의 작업의 초안은 wp_posts에서 customize_changeset 타입의 특수 게시물로 저장됨
- post_content에는 설정 키별 변경값과 타입, 변경을 수행할 사용자 ID가 담긴 JSON이 들어감
- changeset을 적용할 때 WordPress는 각 항목의 user_id를 읽어 wp_set_current_user()로 현재 사용자를 일시적으로 변경함
- 공격자가 user_id: 1인 changeset을 적용하면 익명 요청 중에도 관리자 신원을 잠시 사용할 수 있음
- 앞선 oEmbed 조정 호출은 post_content를 덮어쓰므로, 악성 changeset JSON을 보존할 별도의 wp_update_post() 호출 경로가 필요함
게시물 부모 순환을 이용한 본문 보존
- WordPress 게시물은 하나의 부모를 가질 수 있지만, 자신이나 하위 게시물을 부모로 두는 순환 구조는 허용하지 않음
- wp_insert_post_parent 필터는 부모 계층을 따라가며 순환을 검사하고, 순환을 발견하면 해당 게시물의 post_parent를 0으로 바꾸는 wp_update_post()를 호출함
- 이 두 번째 호출은 ID와 post_parent만 지정하고 post_content는 덮어쓰지 않음
- SQL 주입으로 메모리상의 게시물을 자기 자신을 부모로 갖는 customize_changeset으로 꾸미면, 순환 복구 과정에서 공격자가 지정한 악성 changeset JSON이 데이터베이스에 기록됨
- 과거 날짜이면서 상태가 future인 changeset을 만들면 WordPress가 이를 적용하고, user_id: 1에 따라 관리자 권한으로 지정된 설정 변경을 수행함
- 관리자 권한은 changeset 작업 중에만 유지되며 완료 후에는 다시 게스트 권한으로 돌아감
동적 훅으로 요청 전체 재실행
- WordPress 훅은 동작(action)과 필터(filter)로 나뉘며, 플러그인이 로그인·게시·스크립트 등록 등 생명주기의 여러 지점에 개입하게 함
- 게시물 상태가 바뀌면 WordPress는 "{$new_status}_{$post->post_type}" 형태의 동적 동작을 실행함
- 정상 게시물이라면 publish_post 같은 이름이 됨
- 메모리의 가짜 게시물은 상태와 타입을 임의로 정할 수 있어 밑줄이 하나 이상 들어가는 원하는 동작 이름을 구성할 수 있음
- 훅 인수는 공격자가 정한 게시물 ID와 WP_Post 객체로 제한되므로, 임의 동작을 직접 유용하게 호출하기는 어려움
- 공격 체인은 상태를 parse, 타입을 request로 조작해 parse_request 훅을 호출함
- parse_request는 요청 생명주기 초기에 실행되는 훅이므로, 이를 다시 호출하면 원래 Batch API 요청 전체가 처음부터 재처리됨
- 재처리는 changeset이 설정한 임시 관리자 신원이 유지되는 동안 진행돼, 첫 실행에서 권한 부족으로 실패했던 관리자 전용 요청이 두 번째 실행에서는 성공함
두 요청으로 완성되는 RCE 체인
- 최종 익스플로잇은 두 개의 HTTP 요청을 사용하며, 실제 게시물과 충돌하지 않을 만큼 큰 ID를 가짜 게시물에 할당함
-
첫 번째 요청: 지속 행 준비
- SQL 주입으로 세 개의 로컬 임베드를 가진 가짜 게시물을 반환해 O, C, D에 대응하는 3개 oembed_cache 행을 생성함
- 세 임베드는 같은 게시물 S를 가리키지만 서로 다른 쿼리 문자열을 사용해 별도의 oEmbed 캐시 해시를 만듦
-
두 번째 요청: 여섯 게시물 조립
- 메모리 캐시에 다음 여섯 가짜 게시물을 구성함
- O: 상태/타입이 publish/oembed_cache이고 부모가 C인 오래된 캐시
- C: 상태/타입이 future/customize_changeset이며 자신을 부모로 갖고 악성 changeset JSON을 포함함
- P: 부모가 D인 draft/page
- D: 상태/타입이 parse/request이며 자신을 부모로 가짐
- S: 임베드 데이터를 제공하는 publish/post
- T: 바깥쪽 임베드를 담은 publish/post
- T의 임베드가 O를 조회하고, 오래된 수정 시각 때문에 S의 캐시를 갱신하게 만듦
- O 갱신 중 부모 C의 순환을 감지하면 C의 부모를 0으로 바꾸면서 메모리상의 customize_changeset과 악성 JSON을 데이터베이스에 기록함
- 과거 날짜의 future changeset이 적용되면서 user_id: 1의 관리자 신원으로 P를 게시함
- P 갱신은 부모 D의 순환을 발견해 D를 기록하고, 조작된 상태와 타입으로 parse_request 동작을 호출함
- Batch 요청에는 처음부터 새 관리자 생성 요청이 포함됨
- 첫 번째 처리에서는 게스트 권한이므로 실패함
- parse_request로 재실행될 때는 임시 관리자 권한이 남아 있어 성공함
- 새 관리자 계정으로 로그인한 뒤 백도어 플러그인 ZIP을 업로드하면 최종적으로 원격 코드 실행에 도달함
AI 보안 연구에서 달라지는 역할
- 전체 체인에서 특히 창의적인 단계는 재귀 Batch 호출로 GET 제한을 우회한 점, 캐시와 changeset을 결합해 관리자 권한을 얻은 점, 가짜 게시물로 parse_request를 호출해 요청을 재실행한 점임
- 일반적인 우월성을 단정할 수는 없지만, AI 없이 보안 연구자가 같은 체인을 10시간 안에 발견하고 완성하기는 불가능했을 것이라는 평가임
- 원래 Batch 버그를 미리 제공하더라도 그 시간 안에 RCE까지 구성할 수 있을지는 확신하기 어렵다고 봄
- 서로 떨어진 여러 코드 가젯을 찾아 하나의 체인으로 연결하는 능력에서 GPT5.6 Sol Ultra가 이전 GPT5.5보다 크게 발전했다고 판단함
- 모델이 기술적 익스플로잇 개발을 더 많이 담당할수록 사람은 조사할 제품과 공격 표면, 투입 시간, 연구 방향을 정하고 모델이 빗나갈 때 교정하는 역할에 집중하게 됨
- 이러한 메타 연구 역량은 아직 AI가 잘 처리하지 못하며, 모델의 기술적 능력이 높아질수록 더 중요해질 것으로 전망함
-
Homepage
-
Tech blog
- WordPress RCE를 GPT5.6과 25달러로 발견