WordPress核心框架严重漏洞wp2shell:从路由错位到SQL注入再到RCE"

admin 2026-10-20 03:18:13 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

WordPress 核心框架严重漏洞 wp2shell :从路由错位到 SQL 注入再到 RCE

原创

KCyber
KCyber

自在安全

2026年7月19日 22:18
北京

在小说阅读器读本章

去阅读

最近 WordPress披露的wp2shell漏洞应该是近几年来WordPress核心框架 (非插件)中出现的最严重的漏洞了。通过组合CVE-2026-63030和CVE-2026-63137两个漏洞,可实现SQL注入。但是默认条件下该SQL注入只是读权限无法直接getshell,所以后续实现RCE 的过程也很精彩。

REST Batch 索引错位

REST server 构造函数中注册的内置终结点 /batch/v1 :

'/batch/v1' => array(
    'callback' => array( $this, 'serve_batch_request_v1' ),
    'methods'  => 'POST',
    ...
)

serve_batch_request_v1 会遍历传入的 requests :

foreach ( $batch_request['requests'] as $args ) {
    $parsed_url = wp_parse_url( $args['path'] );

    if ( false === $parsed_url ) {
        $requests[] = newWP_Error(
            'parse_path_failed',
            __( 'Could not parse the path.' ),
            array( 'status' => 400 )
        );

        continue;
    }

    $single_request = newWP_REST_Request(
        $args['method'] ?? 'POST',
        $parsed_url['path']
    );

    ...
    $requests[] = $single_request;
}

因此,只要构造一个无法被 wp_parse_url 正常解析的 path ,就可以让 $requests 中产生一个错误占位:

$requests[0] = WP_Error('parse_path_failed');

关键漏洞点在检验循环中,遇到 WP_Error 时,只写了 $validation,却没有同步写 $matches :

foreach ( $requests as $single_request ) {
    if ( is_wp_error( $single_request ) ) {
        $has_error    = true;
        $validation[] = $single_request;
        continue;
    }

    $match     = $this->match_request_to_handler( $single_request );
    $matches[] = $match;
    ...
}

比如外层 batch 如下:

[
  {\"method\":\"POST\",\"path\":\"http://:\"},
  {\"method\":\"POST\",\"path\":\"/wp/v2/posts\",\"body\":{\"requests\":[...]}},
  {\"method\":\"POST\",\"path\":\"/batch/v1\"}
]

解析的结果如下:

$requests = [
    0 => WP_Error('parse_path_failed'),
    1 => WP_REST_Request('POST', '/wp/v2/posts', body=inner_batch),
    2 => WP_REST_Request('POST', '/batch/v1'),
];

匹配的结果却变成:

$matches = [
    0 => match('/wp/v2/posts'),
    1 => match('/batch/v1'),
];

即 $requests 比 $matches 对了一个错误请求的元素。往后走执行阶段继续按照 $requests 的所有拉提取元素:

foreach ( $requests as $i => $single_request ) {
    if ( is_wp_error( $single_request ) ) {
        $result      = $this->error_to_response( $single_request );
        $responses[] = $this->envelope_response( $result, false )->get_data();
        continue;
    }

    ...
    $match = $matches[ $i ];
    ...
    list( $route, $handler ) = $match;
    ...
    $result = $this->respond_to_request(
        $single_request,
        $route,
        $handler,
        $error
    );
}

当外层执行到 $i=1 时,取值如下:

$requests[1] = /wp/v2/posts 请求对象
$matches[1]  = /batch/v1 的 handle

所以这个 POST 请求对象会被交给 serve_batch_request_v1 处理,只要 body 中放了 requests 属性,就会被当成嵌套的 batch 继续处理。这是第一层混淆的错误。

内层 batch 可以继续复用同一个错位问题:

[
  {\"method\":\"POST\",\"path\":\"http://:\"},
  {\"method\":\"GET\",\"path\":\"/wp/v2/categories?author_exclude=<SQL>\"},
  {\"method\":\"GET\",\"path\":\"/wp/v2/posts\"}
]

内层解析:

$requests = [
    0 => WP_Error('parse_path_failed'),
    1 => WP_REST_Request('GET', '/wp/v2/categories?author_exclude=<SQL>'),
    2 => WP_REST_Request('GET', '/wp/v2/posts'),
];

内层匹配如下:

$matches = [
    0 => match('/wp/v2/categories'),
    1 => match('/wp/v2/posts'),
];

$i=1 时,取值如下:

$requests[1] = categories 请求对象
$matches[1]  = posts collection handler

比如,类似如下的请求对象将会交给 WP_REST_Posts_Controller::get_items 处理:

/wp/v2/categories?author_exclude=payload

SQL注入

在 WP_REST_Posts_Controller::get_items 中存在参数到 WP_Query 的映射:

$parameter_mappings = array(
    'author'         => 'author__in',
    'author_exclude' => 'author__not_in',
    'exclude'        => 'post__not_in',
    'include'        => 'post__in',
    ...
);

foreach ( $parameter_mappings as $api_param => $wp_param ) {
    if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
        $args[ $wp_param ] = $request[ $api_param ];
    }
}

posts collection schema 中确实注册 author_exclude:

$query_params['author_exclude'] = array(
    'description' => __( 'Ensure result set excludes posts assigned to specific authors.' ),
    'type'        => 'array',
    'items'       => array(
        'type' => 'integer',
    ),
    'default'     => array(),
);

注意这里的 $registered 来自 posts controller 自身,而 $request 却是错位后的 categories 请求对象。只要 categories 请求的查询参数中带有 author_exclude ,posts handler 就会把它映射为:

$args['author__not_in'] = $request['author_exclude'];

而 WP_Query::get_posts 对 author__not_in 的处理如下:

if ( ! empty( $query_vars['author__not_in'] ) ) {
    if ( is_array( $query_vars['author__not_in'] ) ) {
        $query_vars['author__not_in'] = array_unique(
            array_map( 'absint', $query_vars['author__not_in'] )
        );
        sort( $query_vars['author__not_in'] );
    }
    $author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
    $where         .= \" AND {$wpdb->posts}.post_author NOT IN ($author__not_in) \";

这里只有 is_array 分支内采用执行 absint ,如果输入的是字符串类型就会跳过清洗,最终原始的字符串会直接拼接进入 SQL 查询,这就是最终的 SQL 注入点。

目前的 SQL注入只有读权限,可以通过时间延迟注入进行利用,也可以利用union联合查询来控制WP_Query返回的数据集,但是没有写权限。原作者构造的利用链的精彩之处在于:没有强制将只读SQL注入来变成数据库写权限,也不是读取管理员hash来强制爆破,而是尝试利用WP自身的对象缓存、文章类型和权限切换流程,把伪造的查询结果变成应用层状态执行。这里利用到了Customizer changeset以及依赖父子关系触发等等。有了新建的管理员账户后,通过上传恶意插件就可以getshell 了。

修复方式

在 7.0.2 的修复版本中在两个地方做了修复。其中,在 WP_Error 分支补了 $matches:

 foreach ( $requests as $single_request ) {
     if ( is_wp_error( $single_request ) ) {
         $has_error    = true;
         $matches[]    = $single_request; // fix
         $validation[] = $single_request;
         continue;
     }

另外将 author__not_in 改成统一走 wp_parse_id_list:

 if ( ! empty( $query_vars['author__not_in'] ) ) {
    $author__not_in_id_list = wp_parse_id_list( $query_vars['author__not_in'] );
    if ( count( $author__not_in_id_list ) > 0 ) {
        sort( $author__not_in_id_list );
        $where .= sprintf(
            \" AND {$wpdb->posts}.post_author NOT IN (%s) \",
            implode( ',', $author__not_in_id_list )
        );
        $query_vars['author__not_in'] = $author__not_in_id_list;
    }
 }

wp_parse_id_list 内部会调用 wp_parse_list,再对每个元素执行 absint。因此字符串型 SQL payload 不会再以 SQL 片段的形式进入 NOT IN。

由于传播、利用此文档提供的信息而造成任何直接或间接的后果及损害,均由使用本人负责,公众号及文章作者不为此承担任何责任。


WordPress核心框架严重漏洞wp2shell:从路由错位到SQL注入再到RCE" 网络安全文章

WordPress核心框架严重漏洞wp2shell:从路由错位到SQL注入再到RCE"

WordPress核心框架wp2shell漏洞通过组合CVE-2026-63030和CVE-2026-63137实现SQL注入,利用RESTBatch索引错位将categories请求错误路由到postshandler,导致author_exclude参数未清洗直接拼接SQL。虽仅读权限,但通过Customizerchangeset和权限切换可新建管理员账户并上传恶意插件实现RCE。修复版本7.0.2补全了matches数组并统一使用wp_parse_id_list清洗输入。建议立即升级。
看见世界" 网络安全文章

看见世界"

作者回顾2025年初半年未写作的时光,反思人生追求与幸福并非来自比较,而是源于真实经历。通过阅读李飞飞的《我看见的世界》思考视觉与认知,结合旅行和日常生活的普通瞬间,感悟生活痕迹由书籍、旅行、一顿饭等点滴积累而成。
评论:0   参与:  0