My Sticky Bar Plugin <= 2.8.6
The My Sticky Bar plugin for WordPress is vulnerable to SQL injection via the
stickymenu_contact_lead_formAJAX 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 withesc_sql()andsanitize_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 | add_action('wp_ajax_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 | if( isset($params) && !empty($params) ){ |
这段代码使用 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 | foreach( $postArr as $key => $val ){ |
$key 来自于 $postArr,**$postArr** 来自于 $POST 所以虽然说对值进行了校验,但却没有校验字段名(数组的 key)。
构造 exp
1 | POST /wp-admin/admin-ajax.php?action=stickymenu_contact_lead_form |
0x02 调试
按道理讲,debug_field 就直接被放进 insert 语句了,但是还得慢慢调试,因为我的代码放在 docker 里面,所以还得靠 gpt 帮我写一下 xdeug 配置的文件
1 | { |
断点直接打到 insert,然后 在 VSCode 的 WATCH 里加这几个表达式:
1 | $wpdb->last_query → 最后执行的SQL语句 |
这两个是 WordPress $wpdb 对象里非常常用的调试属性,开发和代码审计都会用到。
启动调试
1 | Unknown column 'debug_field' in 'INSERT INTO' |
可以看到debug_field确实被写进去了,只是不存在这个字段所以报错了,这里打一个时间盲注入
1 | widget_id=0&contact_name=alice&message_date`)values('1',(select(sleep(5))))# |