前言

朋友圈看到有人转发了一篇“CVE-2024-25600:WordPress Bricks Builder RCE”,感觉挺有意思,点进去看了下,可是从头到尾看得我有点迷糊,本着打破砂锅问到底的原则,本文试图以漏洞挖掘者的视角详细分析这个漏洞,试着讲清楚漏洞真正的成因,也在分析的过程中发现一些新的小东西,比如漏洞只影响1.9.1及之上的版本,网上都在说影响版本是<=1.9.6,其实应该是1.9.1 <= affected version <= 1.9.6

这里想说句题外话,如果一篇文章看得你云里雾里,那不排除一种可能,这篇文章质量不高~

0x01 漏洞宏观流程

漏洞最终触发点是eval执行了攻击者传入的恶意代码,导致任意代码执行
image
其中参数$php_query_raw是攻击者可控的,路由也是攻击者可控的,最后在权限校验部分仅使用nonce进行权限校验,而nonce会泄露在前端源码中,至此《危险函数 -> 用户输入 -> 对应路由 -> 权限绕过》全部满足,最终导致了前台RCE(实际的细节有些复杂…)

pyload如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /WordPress-6.4.3/wp-json/bricks/v1/render\_element HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (X11; Linux x86\_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36
Connection: close
Content-Length: 270
Content-Type: application/json
Accept-Encoding: gzip, deflate,

{
"postId": "1",
"nonce": " a980a714d9",
"element": {
"name": "container",
"settings": {
"hasLoop": "",
"query": {"useQueryEditor": "","queryEditor": "system('calc');","objectType": ""}
}
}
}

0x02 漏洞细节流程

01 危险函数

危险函数是eval,平时挖漏洞时,危险函数可以通过Seay跑一遍后发现,从代码注释中可以看到,漏洞代码是自版本1.9.1才有的
image

02 用户输入

从触发点往上回溯,可看到参数$php_query_raw的值来自于bricks_render_dynamic_data( $query_vars[‘queryEditor’], $post_id )的返回值
image

ctrl+鼠标左键,进去看下bricks_render_dynamic_data对$query_vars[‘queryEditor’]和$post_id有没有什么过滤,可以看到具体实现在render_content中
image

继续跟进render_content,代码逻辑是:
如果第一个参数是数组且至少有一个元素,则直接返回第一个元素。
如果第一个参数中键name对应的值不为空,那么将键name对应的值赋值给第一个元素,否则将第一个参数转化为字符串类型后赋值给第一个参数。
如果第一个参数中不包含字符’{‘,则直接返回第一个参数。
如果第一个参数中包含字符串’{echo:’,那么去掉第一个参数中用来转义的反斜线。
如果$post_id的值为空,那么调用get_the_ID()并将返回值赋值给$post_id,否则$post_id的值不变。
$post_id经get_post()处理后赋值给$post。
将’bricks/dynamic_data/render_content’, $content, $post, $context经apply_filters处理后的返回值返回
(详细讲述代码逻辑太费劲了,估计看的人也费劲,后面只讲基本逻辑)
image

现在我们梳理了bricks_render_dynamic_data内部做了什么,具体返回什么值要看传入什么样的参数,回到原来的地方,Database::$page_data[‘preview_or_post_id’]跟进后值是常量0,现在变量只剩$query_vars[‘queryEditor’],向上找$query_vars[‘queryEditor’]发现没有,只找到$query_vars,$query_vars来自方法prepare_query_vars_from_settings的第一个参数$settings,也就是说,调用prepare_query_vars_from_settings时,第一个参数$settings需要满足:$settings->[‘query’]->[‘useQueryEditor’]存在且不为null、$settings->[‘query’]->[‘queryEditor’]不为空,还有一个条件,$object_type需要是[ ‘post’,’term’,’user’ ]中的一个,$object_type来自于self::get_query_object_type(),跟进get_query_object_type,基本逻辑是:根据全局变量$bricks_loop_query的值决定返回’post’还是’’
image

ctrl+鼠标左键,看下哪些函数调用了prepare_query_vars_from_settings,可以看到只有2个,database.php和query.php,database.php看名字就知道是和数据库打交道的,如果漏洞点在这个文件中,很可能还需要一个sql注入漏洞将恶意代码注入到数据库中,所以优先选择query.php进行深入查看
image

跟进query.php,可以看到在下图111行中调用了prepare_query_vars_from_settings,传入的是$this->settings,向上回溯发现$this->settings来自于$element[‘settings’],并且这些代码处于else子句中,也就是说需要让$query_instance的值为false,$query_instance的值来自于self::get_query_by_element_id( $this->element_id ),$this->element_id的值来自于实例化类Query时传进来的参数$element
image

跟进get_query_by_element_id里面看一下,可以看到如果传进来的$element_id为空的话,则返回false,也就符合上面说的进入else子句
image

然后看下哪些地方实例化了类Query,可以看到一共有15处
image

先看第1处,ajax.php,需要满足$loop_element不存在或为false的时候,才会实例化类Query,向上找发现$loop_element的值默认为false,假如中间没改变$loop_element的值,是没法实例化类Query
image

向下会看到new $element_class_name( $element )这样一行代码,关键点就在这个地方,此处才是漏洞的真正成因,想要执行new $element_class_name( $element )需要$element_class_name表示的类存在,跟进Elements可以看到,里面定义了一个静态属性$elements,初始化之后,回将$element_names中的元素注册到$elements中
image

如下是注册到$elements中
image

再看一下payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /WordPress-6.4.3/wp-json/bricks/v1/render\_element HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (X11; Linux x86\_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36
Connection: close
Content-Length: 270
Content-Type: application/json
Accept-Encoding: gzip, deflate,

{
"postId": "1",
"nonce": " a980a714d9",
"element": {
"name": "container",
"settings": {
"hasLoop": "",
"query": {"useQueryEditor": "","queryEditor": "system('calc');","objectType": ""}
}
}
}

此时我们变成了实例化类Bricks\Element_Container,进到Element_Container类中,可以看到它是继承父类Element,也就是说它能调用的方法不光在它中,还可能在父类中,回到ajax.php,我们看下new $element_class_name( $element )之后的代码,调用了2个方法load和init,其中init在父类Element中,并且init中调用了方法render,然后render中实例化了类Query,满足上面我们分析的条件
image

03 对应路由

回到ajax.php,从注释中就能看到,一段是处理AJAX请求,一段是处理REST API请求
image

04 权限绕过

可以看到代码AJAX中有权限检验,代码REST API中看似没有权限检验,但注释中说了,权限检查在API->render_element_permissions_check()中,跟进render_element_permissions_check后发现,内部其实没进行权限检查,只校验了nonce
image
wordpress中明确提到,nonce不应作为权限验证,最终导致权限绕过
image

总结

漏洞出现在后台的编辑器功能处,用于渲染元素并且预览效果,由于弱权限校验导致权限绕过可直接访问REST API端点,最终导致RCE,最后,感谢ID为zero的师傅分享的源码

参考文章

漏洞发现者对漏洞的分析,总体行,细节不行
https://snicco.io/vulnerability-disclosure/bricks/unauthenticated-rce-in-bricks-1-9-6

补天上一篇还不错的文章
https://forum.butian.net/share/2814

CSDN上一篇还不错的文章
https://blog.csdn.net/shelter1234567/article/details/136503993

AJAX和REST API
https://www.runoob.com/php/php-restful.html
https://www.runoob.com/php/php-ajax-php.html

其他质量不高的文章我就不贴了