WooCommerce 2.0 - 2.4.1存在CVE-2024-9756漏洞
WordPress Order Attachments for WooCommerce 是WordPress 中的WooCommerce插件,该插件旨在帮助用户将文件附加到其订单中。
一、基本情况
该插件允许管理员和客户在订单创建或编辑时上传文件,例如合同、发票、保修单等,并将这些文件存储在订单中,方便后续查看和管理。
栋科技漏洞库关注到WooCommerce 2.0 - 2.4.1版本缺少对经身份验证(订阅者+)限制任意文件上传授权,漏洞追踪为CVE-2024-9756。
二、漏洞分析
CVE-2024-9756是WooCommerce插件 2.0 - 2.4.1版本由于缺少对经过身份验证的(订阅者+)限制,而导致的任意文件上传的授权漏洞。
该漏洞源于wcoa_add_attachment AJAX的动作缺少能力检查,导致具有订阅者级别及以上访问权限认证攻击者可以上传有限类型的文件。
该漏洞可导致WordPress的WooCommerce插件的Order Attachments易受未经授权的有限的任意文件上传的攻击,漏洞的CVSS评分7.2。
漏洞具体存在于Attachment.php和Ajax.php两个文件中,具体源码如下:
order-attachments-for-woocommerce/tags/2.4.0/src/WCOA/Attachments/Attachment.php
<?php
namespace DirectSoftware\WCOA\Utils;
use DirectSoftware\WCOA\Attachments\Attachment;
use DirectSoftware\WCOA\Common\Notification;
use JsonException;
/**
* @author d.gubala
*/
class Ajax
{
private static ?Ajax $instance = null;
public static function getInstance(): Ajax
{
if (self::$instance === null)
{
self::$instance = new Ajax();
}
return self::$instance;
}
private function __construct()
{
add_action( 'wp_ajax_wcoa_add_attachment', [$this, 'add_attachment']);
add_action( 'wp_ajax_wcoa_send_email_to_customer', [$this, 'send_email_to_customer']);
}
public static function add_attachment(): void
{
header('Content-Type: application/json; charset=utf-8');
$response = [
'status' => 'error',
'code' => -1,
'message' => __('Error saving media file.')
];
if (isset($_FILES['attachment']['name']))
{
$file = $_FILES['attachment'];
$order_id = $_POST['order_id'];
$attachment = new Attachment($file, $order_id);
$result = $attachment->save();
if ($result !== false)
{
$response = [
'status' => 'success',
'code' => 0,
'message' => sprintf(__('%s media file attached.'), 1),
'data' => $result
];
}
} else {
$response['message'] = __('No file was uploaded.');
}
try
{
print json_encode($response, JSON_THROW_ON_ERROR);
}
catch (JsonException $e)
{
print sprintf("{\"status\": \"success\", \"code\": 0, \"message\": \"%s\"}", $e->getMessage());
}
wp_die();
}
public static function send_email_to_customer(): void
{
header('Content-Type: application/json; charset=utf-8');
$response = [
'status' => 'error',
'code' => -1
];
$attachment_id = -1;
if (isset($_POST['order_id']))
{
$attachment_id = 0;
if (isset($_POST['attachment_id']))
{
$res = $_POST['attachment_id'];
if (is_numeric($res))
{
$attachment_id = (int)$res;
}
}
$notification = new Notification($_POST['order_id'], $attachment_id);
$notification->send_email();
$response = [
'status' => 'success',
'code' => 0
];
}
Logger::getInstance()->info(sprintf("Notification sent for attachment ID %s related to order ID %s.", $attachment_id, $_POST['order_id']), $response);
try
{
print json_encode($response, JSON_THROW_ON_ERROR);
}
catch (JsonException)
{
print "{\"status\": \"success\", \"code\": 0}";
}
wp_die();
}
}
order-attachments-for-woocommerce/tags/2.4.0/src/WCOA/Attachments/Attachment.php
<?php
namespace DirectSoftware\WCOA\Attachments;
use Automattic\WooCommerce\Utilities\OrderUtil;
use DirectSoftware\WCOA\Common\Notification;
use DirectSoftware\WCOA\Common\Options;
use DirectSoftware\WCOA\Kernel;
use DirectSoftware\WCOA\Utils\Logger;
/**
* @author d.gubala
*/
class Attachment
{
private array $file;
private int $order_id;
private Kernel $kernel;
private Logger $logger;
public const META_KEY = "_wcoa_attachment_id";
public function __construct(array $file, int $order_id)
{
$this->file = $file;
$this->order_id = $order_id;
$this->kernel = Kernel::getInstance();
$this->logger = Logger::getInstance();
}
public function save(): bool|array
{
$this->logger->info('File save initiated.');
$attachment = $this->upload();
if ($attachment !== false)
{
$notification = new Notification($this->order_id, $attachment['id']);
$notification->create_note();
if (Options::get('email_enabled') === true)
{
$notification->send_email();
}
$this->logger->info('File save completed.');
return $attachment;
}
$this->logger->error('File save failed.');
return false;
}
private function upload(): bool|array
{
require_once(ABSPATH . 'wp-admin/includes/image.php');
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/media.php');
// check if the file exists
if (!isset($this->file['name']))
{
$this->logger->warning('No file to upload.', $this->file);
return false;
}
$this->file['name'] = $this->get_attachment_prefix() . $this->file['name'];
// get the file type from the file name
$file_type = wp_check_filetype(basename($this->file['name']));
// create file in the upload folder
$attachment = wp_upload_bits($this->file['name'], null, file_get_contents($this->file['tmp_name']));
$filename = $attachment['file'];
$title = preg_replace('/\.[^.]+$/', '', basename($filename));
$metadata = [
'post_mime_type' => $file_type['type'],
'post_title' => $title,
'post_content' => '',
'post_status' => 'inherit',
'guid' => $attachment['url']
];
// insert attachment to database and return attachment id
$attachment_id = wp_insert_attachment($metadata, $attachment['url'] );
$inserted_metadata = wp_generate_attachment_metadata($attachment_id, $filename);
wp_update_attachment_metadata($attachment_id, $inserted_metadata);
if ( $this->kernel->hpos_is_enabled() && OrderUtil::get_order_type($this->order_id) === 'shop_order')
{
$order = wc_get_order($this->order_id);
if ($order)
{
$order->add_meta_data(self::META_KEY, $attachment_id);
$order->save();
}
}
else
{
add_post_meta($this->order_id, self::META_KEY, $attachment_id);
}
$result = [
'id' => $attachment_id,
'title' => $title,
'url' => $attachment['url']
];
$this->logger->info('File upload result:', $result);
return $result;
}
/**
* @since 2.2.2
* @param int $order_id
* @return array
*/
public static function get_all_by_order(int $order_id): array
{
global $wpdb;
if (Kernel::getInstance()->hpos_is_enabled())
{
$command = sprintf("SELECT attachment.post_title, attachment.guid
FROM %swc_orders_meta AS attachment_meta
JOIN %s AS attachment ON attachment_meta.meta_value = attachment.ID
WHERE attachment_meta.meta_key = '_wcoa_attachment_id'
AND attachment.post_type = 'attachment'
AND attachment_meta.order_id = %d;",
$wpdb->prefix, $wpdb->posts, $order_id);
}
else
{
$command = sprintf("SELECT attachment.post_title, attachment.guid
FROM %s AS attachment_meta
JOIN %s AS attachment ON attachment_meta.meta_value = attachment.ID
WHERE attachment_meta.meta_key = '_wcoa_attachment_id'
AND attachment.post_type = 'attachment'
AND attachment_meta.post_id = %d;",
$wpdb->postmeta, $wpdb->posts, $order_id);
}
$result = $wpdb->get_results($command, ARRAY_A);
return empty($result) ? [] : $result;
}
/**
* @since 2.2.2
* @param int $user_id
* @return array
*/
public static function get_all_by_user(int $user_id): array
{
global $wpdb;
if (Kernel::getInstance()->hpos_is_enabled())
{
$command = sprintf("SELECT
attachment.ID AS attachment_id,
attachment.post_date,
attachment.post_title,
attachment_meta.order_id AS order_id,
attachment.guid,
user_order.customer_id
FROM %swc_orders_meta AS attachment_meta
JOIN %s AS attachment ON attachment_meta.meta_value = attachment.ID
JOIN %swc_orders AS user_order ON attachment_meta.order_id = user_order.id
WHERE attachment_meta.meta_key = '_wcoa_attachment_id'
AND attachment.post_type = 'attachment'
AND user_order.customer_id = %d;",
$wpdb->prefix, $wpdb->posts, $wpdb->prefix, $user_id);
}
else
{
$command = sprintf("SELECT
attachment.ID AS attachment_id,
attachment.post_date,
attachment.post_title,
attachment_meta.post_id AS order_id,
attachment.guid,
user_order.post_author
FROM %s AS attachment_meta
JOIN %s AS attachment ON attachment_meta.meta_value = attachment.ID
JOIN %s AS user_order ON attachment_meta.post_id = user_order.ID
WHERE attachment_meta.meta_key = '_wcoa_attachment_id'
AND attachment.post_type = 'attachment'
AND user_order.post_author = %d;",
$wpdb->postmeta, $wpdb->posts, $wpdb->posts, $user_id);
}
$result = $wpdb->get_results($command, ARRAY_A);
return empty($result) ? [] : $result;
}
/**
* @since 2.2.2
* @return array
*/
public static function get_all(): array
{
global $wpdb;
if (Kernel::getInstance()->hpos_is_enabled())
{
$command = sprintf("SELECT
attachment.ID AS attachment_id,
attachment.post_author,
attachment.post_date,
attachment.post_title,
attachment_meta.order_id AS order_id,
attachment.guid FROM %swc_orders_meta AS attachment_meta
JOIN %s AS attachment ON attachment_meta.meta_value = attachment.ID
WHERE attachment_meta.meta_key = '_wcoa_attachment_id'
AND attachment.post_type = 'attachment';",
$wpdb->prefix, $wpdb->posts);
}
else
{
$command = sprintf("SELECT
attachment.ID AS attachment_id,
attachment.post_author,
attachment.post_date,
attachment.post_title,
attachment_meta.post_id AS order_id,
attachment.guid FROM %s AS attachment_meta
JOIN %s AS attachment ON attachment_meta.meta_value = attachment.ID
WHERE attachment_meta.meta_key = '_wcoa_attachment_id'
AND attachment.post_type = 'attachment';",
$wpdb->postmeta, $wpdb->posts);
}
$result = $wpdb->get_results($command, ARRAY_A);
return empty($result) ? [] : $result;
}
public static function get_list(int $order_id = 0, int $user_id = 0): array
{
if ($user_id !== 0)
{
return self::prepare_customer_list($user_id);
}
return self::prepare_list($order_id);
}
private static function prepare_list(int $order_id = 0): array
{
global $wpdb;
$sql_check = "SELECT COUNT(*) FROM $wpdb->postmeta AS order_meta
JOIN $wpdb->postmeta AS attachment_meta ON order_meta.meta_value = attachment_meta.post_id
JOIN $wpdb->posts AS attachments ON order_meta.meta_value = attachments.ID
WHERE order_meta.meta_key = '_wcoa_attachment_id' and attachment_meta.meta_key = '_wp_attached_file'";
if ($order_id !== 0)
{
$sql_check .= " AND order_meta.post_id = $order_id;";
}
$counter = $wpdb->get_var($sql_check);
if ($counter <= 0)
{
return [];
}
$sql = "SELECT order_meta.post_id AS order_id, order_meta.meta_value AS post_id, attachments.post_author,
attachments.post_date, attachments.post_title, attachments.guid FROM $wpdb->postmeta AS order_meta
JOIN $wpdb->postmeta AS attachment_meta ON order_meta.meta_value = attachment_meta.post_id
JOIN $wpdb->posts AS attachments ON order_meta.meta_value = attachments.ID
WHERE order_meta.meta_key = '_wcoa_attachment_id' AND attachment_meta.meta_key = '_wp_attached_file'";
if ($order_id !== 0)
{
$sql .= " AND order_meta.post_id = $order_id";
}
return $wpdb->get_results($sql, ARRAY_A);
}
public static function prepare_customer_list(int $user_id): array
{
global $wpdb;
$sql_check = sprintf("SELECT COUNT(*) FROM %s AS attachment
JOIN %s AS attachment_meta ON (attachment_meta.meta_value = attachment.ID)
JOIN %s AS orders ON attachment_meta.post_id = orders.post_id
WHERE attachment.post_type = 'attachment' AND attachment_meta.meta_key = '_wcoa_attachment_id'
AND orders.meta_key = '_customer_user' AND attachment.post_mime_type != '' AND orders.meta_value = %d;",
$wpdb->posts, $wpdb->postmeta, $wpdb->postmeta, $user_id);
$counter = $wpdb->get_var($sql_check);
if (is_numeric($counter) && (int)$counter <= 0)
{
return [];
}
$sql = sprintf("SELECT orders.post_id AS 'order_id', attachment.post_date, attachment.guid
FROM %s AS attachment
JOIN %s AS attachment_meta ON (attachment_meta.meta_value = attachment.ID)
JOIN %s AS orders ON attachment_meta.post_id = orders.post_id
WHERE attachment.post_type = 'attachment' AND attachment_meta.meta_key = '_wcoa_attachment_id'
AND orders.meta_key = '_customer_user' AND attachment.post_mime_type != '' AND orders.meta_value = %d;",
$wpdb->posts, $wpdb->postmeta, $wpdb->postmeta, $user_id);
return $wpdb->get_results($sql, ARRAY_A);
}
public static function get_url(int $order_id, string $order_key, int $attachment_id = 0): bool|string
{
global $wpdb;
$counter = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->postmeta WHERE post_id = $order_id AND meta_value = '$order_key';");
if ($counter < 1)
{
return false;
}
if ($attachment_id === 0)
{
$result = $wpdb->get_var("SELECT MIN(meta_value) AS qty FROM $wpdb->postmeta where post_id = $order_id and meta_key = '_wcoa_attachment_id'");
if (!is_numeric($result) )
{
return false;
}
$attachment_id = (int)$result;
}
$url = get_the_guid($attachment_id);
if ($url === '' || str_contains($url, '?p=1'))
{
return false;
}
return $url;
}
public static function get_public_url(int $order_id, int $attachment_id): string
{
global $wpdb;
$key = $wpdb->get_var("SELECT meta_value FROM $wpdb->postmeta WHERE post_id = $order_id AND meta_key = '_order_key'");
if (empty($key))
{
return false;
}
$path = "?wcoa_attachment_for_order=$order_id&key=$key&id=$attachment_id";
return get_home_url(null, $path);
}
/**
* @since 2.2.0
* @return string
*/
private function get_attachment_prefix(): string
{
$default = '';
if (get_option('wcoa_general'))
{
$prefix = get_option( 'wcoa_general')['attachment_prefix'] ?? '';
if (str_contains($prefix, "{order_number}"))
{
return str_replace("{order_number}", $this->order_id, $prefix);
}
return empty($prefix) ? $default : trim($prefix);
}
return $default;
}
}
三、影响范围
WordPress WooCommerce Affected Version = 2.0 - 2.4.1
四、修复建议
WordPress WooCommerce Patched Version >= 2.5.0
五、参考链接
https://www.wordfence.com/threat-intel/vulnerabilities/id/0dfc8957-78b8-4c55-ba95-52d95b086341
https://plugins.trac.wordpress.org/powser/order-attachments-for-woocommerce/tags/2.4.0/src/WCOA/Utils/Ajax.php
https://plugins.trac.wordpress.org/powser/order-attachments-for-woocommerce/tags/2.4.0/src/WCOA/Attachments/Attachment.php