
S2B AI Assistant – 챗봇, ChatGPT, OpenAI, 콘텐츠 및 이미지 생성기 <= 1.7.7 - Authenticated (Editor+) Arbitrary File Upload
WordPress S2B AI Assistant 플러그인 (버전 2.47 이하)에는 임의 파일 업로드 취약점이 존재합니다. 이 취약점으로 인해 Editor 이상의 역할을 가진 인증된 WordPress 사용자가 악성 PHP 파일을 서버에 업로드하여 원격 코드 실행으로 이어질 수 있습니다.
원격 공격자가 shell.php를 업로드하고 원격 코드를 실행하는 방법을 보여주는 POC CVE-2025-12973.py가 제공됩니다.
python3 ./CVE-2025-12973.py http://techcorp.cc editor $PASSWORD
[+] Target: http://techcorp.cc
[+] Username: editor
[+] Nonce obtained: a15be47119
[+] File uploaded successfully!
[+] Shell URL: http://techcorp.cc/wp-content/uploads/2025/11/shell.php
[+] Command output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
이 취약점은 Utils.php 파일의 storeFile() 메서드에 존재하며, 이 메서드는 /wp-admin/admin-post.php 엔드포인트에서 s2b_store_chatbot_upload 액션으로 호출됩니다. 업로드 기능은 사용자 정의 파일 확장자 허용 목록을 사용하는데, 여기에 PHP 파일을 포함한 위험한 파일 유형이 명시적으로 허용됩니다. 이로 인해 Editor 이상의 역할을 가진 인증된 WordPress 사용자가 서버에서 실행될 수 있는 PHP 파일을 포함한 임의의 파일을 업로드할 수 있습니다.
소스: $_FILES['s2baia_chatbot_config_database']의 사용자 입력 (300행)
싱크: $wp_filesystem->put_contents($outfile, $file_content, FS_CHMOD_FILE) (344행)
취약점은 다음과 같은 이유로 발생합니다:
AdminChatBotController.php에 등록됩니다.processAssistantUpload() 메서드가 업로드 요청을 처리합니다 (1103행).$_FILES['s2baia_chatbot_config_database']의 사용자 제어 파일 데이터가 적절한 검증 없이 직접 처리됩니다.checkAllowedFilesearchExtensions() (320행)를 사용한 사용자 정의 확장자 허용 목록 검사만 적용되며, 이 검사는 .php 확장자를 명시적으로 허용합니다 (366행).wp-content/uploads/YYYY/MM/에 저장됩니다.파일: lib/helpers/Utils.php
행: 289-348
public static function storeFile($targetDir) {
global $wp_filesystem;
// Initialize WP_Filesystem
if (!function_exists('request_filesystem_credentials')) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
if (!WP_Filesystem()) {
return '';
}
if (!isset($_FILES) || !is_array($_FILES) || !isset($_FILES['s2baia_chatbot_config_database'])) { // SOURCE: User input ([line 300](https://plugins.trac.wordpress.org/browser/s2b-ai-assistant/trunk/lib/helpers/Utils.php#L300))
return '';
}
if (!isset($_FILES['s2baia_chatbot_config_database']['error']) || !isset($_FILES['s2baia_chatbot_config_database']['name']) || !isset($_FILES['s2baia_chatbot_config_database']['size']) || !isset($_FILES['s2baia_chatbot_config_database']['tmp_name'])) {
return '';
}
$chunk = isset($_REQUEST["chunk"]) ? (int) $_REQUEST["chunk"] : 0;
$name = sanitize_file_name($_FILES['s2baia_chatbot_config_database']['name']);
if (strlen($name) == 0) {
return '';
}
$finfo = pathinfo($name);
if (is_array($finfo)) {
$fname = sanitize_file_name($finfo['filename']);
$fext = $finfo['extension'];
if (!self::checkAllowedFilesearchExtensions($fext)) { // Only custom whitelist check ([line 320](https://plugins.trac.wordpress.org/browser/s2b-ai-assistant/trunk/lib/helpers/Utils.php#L320))
return '';
}
if ($wp_filesystem->exists($targetDir . DIRECTORY_SEPARATOR . $name)) {
$timest = time();
$name = $fname . '_' . $timest . '_' . random_int(1000, 9999) . '.' . $fext;
}
}
$tmp_name = sanitize_text_field($_FILES['s2baia_chatbot_config_database']['tmp_name']);
$outfile = $targetDir . DIRECTORY_SEPARATOR . $name;
// Open the output file and write contents using WP_Filesystem
if ($chunk === 0) {
$wp_filesystem->put_contents($outfile, '', FS_CHMOD_FILE);
}
// Read the temporary file and append its contents to the output file
$file_content = $wp_filesystem->get_contents($tmp_name);
if ($file_content === false) {
return '';
}
// Append content to the file
if (!$wp_filesystem->put_contents($outfile, $file_content, FS_CHMOD_FILE)) { // SINK: Direct file write ([line 344](https://plugins.trac.wordpress.org/browser/s2b-ai-assistant/trunk/lib/helpers/Utils.php#L344))
return '';
}
// Delete the temporary file using WordPress method
// ... rest of function
}
파일: lib/helpers/Utils.php
행: 354-383
public static function checkAllowedFilesearchExtensions($ext) {
switch ($ext) {
case 'c':
case 'cs':
case 'cpp':
case 'doc':
case 'docx':
case 'html':
case 'java':
case 'json':
case 'md':
case 'pdf':
case 'php': // Explicitly allows PHP files ([line 366](https://plugins.trac.wordpress.org/browser/s2b-ai-assistant/trunk/lib/helpers/Utils.php#L366))
case 'pptx':
case 'py':
case 'rb':
case 'tex':
case 'txt':
case 'css':
case 'js':
case 'sh':
case 'ts':
return true;
default:
return false;
}
return false;
}