
S2B AI Assistant – ChatBot、ChatGPT、OpenAI、コンテンツ&画像ジェネレーター <= 1.7.7 - 認証済み(編集者以上)任意ファイルアップロード
WordPress の S2B AI Assistant プラグイン(バージョン 2.47 以前)には、任意のファイルアップロードの脆弱性が存在します。これにより、編集者以上の権限を持つ認証済み 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() メソッドに存在します。このメソッドは、s2b_store_chatbot_upload アクションを持つ /wp-admin/admin-post.php エンドポイントによって呼び出されます。アップロード機能は、PHP ファイルを含む危険なファイルタイプを明示的に許可するカスタム拡張子ホワイトリストを使用しています。これにより、編集者以上の権限を持つ認証済み 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;
}