问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2024-25600 WordPress Bricks Builder远程代码执行漏洞分析
漏洞分析
前言 朋友圈看到有人转发了一篇“CVE-2024-25600:WordPress Bricks Builder RCE”,感觉挺有意思,点进去看了下,可是从头到尾看得我有点迷糊,本着打破砂锅问到底的原则,本文试图以漏洞挖掘者...
前言 == 朋友圈看到有人转发了一篇“CVE-2024-25600:WordPress Bricks Builder RCE”,感觉挺有意思,点进去看了下,可是从头到尾看得我有点迷糊,本着打破砂锅问到底的原则,本文试图以漏洞挖掘者的视角详细分析这个漏洞,试着讲清楚漏洞真正的成因,也在分析的过程中发现一些新的小东西,比如漏洞只影响1.9.1及之上的版本,网上都在说影响版本是<=1.9.6,其实应该是1.9.1 <= affected version <= 1.9.6 这里想说句题外话,如果一篇文章看得你云里雾里,那不排除一种可能,这篇文章质量不高~ 0x01 漏洞宏观流程 =========== 漏洞最终触发点是eval执行了攻击者传入的恶意代码,导致任意代码执行  其中参数$php\_query\_raw是攻击者可控的,路由也是攻击者可控的,最后在权限校验部分仅使用nonce进行权限校验,而nonce会泄露在前端源码中,至此,危险函数 -> 用户输入 -> 对应路由 -> 权限绕过,全部满足,最终导致了前台RCE(实际的细节有些复杂...) pyload如下 ```php 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才有的  02 用户输入 ------- 从触发点往上回溯,可看到参数$php\_query\_raw的值来自于bricks\_render\_dynamic\_data( $query\_vars\['queryEditor'\], $post\_id )的返回值  ctrl+鼠标左键,进去看下bricks\_render\_dynamic\_data对$query\_vars\['queryEditor'\]和$post\_id有没有什么过滤,可以看到具体实现在render\_content中  继续跟进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处理后的返回值返回 (详细讲述代码逻辑太费劲了,估计看的人也费劲,后面只讲基本逻辑)  现在我们梳理了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'还是''  ctrl+鼠标左键,看下哪些函数调用了prepare\_query\_vars\_from\_settings,可以看到只有2个,database.php和query.php,database.php看名字就知道是和数据库打交道的,如果漏洞点在这个文件中,很可能还需要一个sql注入漏洞将恶意代码注入到数据库中,所以优先选择query.php进行深入查看  跟进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  跟进get\_query\_by\_element\_id里面看一下,可以看到如果传进来的$element\_id为空的话,则返回false,也就符合上面说的进入else子句  然后看下哪些地方实例化了类Query,可以看到一共有15处  先看第1处,ajax.php,需要满足$loop\_element不存在或为false的时候,才会实例化类Query,向上找发现$loop\_element的值默认为false,假如中间没改变$loop\_element的值,是没法实例化类Query  向下会看到new $element\_class\_name( $element )这样一行代码,关键点就在这个地方,此处才是漏洞的真正成因,想要执行new $element\_class\_name( $element )需要$element\_class\_name表示的类存在,跟进Elements可以看到,里面定义了一个静态属性$elements,初始化之后,回将$element\_names中的元素注册到$elements中  如下是注册到$elements中  再看一下payload ```php 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,满足上面我们分析的条件  03 对应路由 ------- 回到ajax.php,从注释中就能看到,一段是处理AJAX请求,一段是处理REST API请求  04 权限绕过 ------- 可以看到代码AJAX中有权限检验,代码REST API中看似没有权限检验,但注释中说了,权限检查在API->render\_element\_permissions\_check()中,跟进render\_element\_permissions\_check后发现,内部其实没进行权限检查,只校验了nonce  wordpress中明确提到,nonce不应作为权限验证,最终导致权限绕过  05 攻击流程梳理 --------- 利用前端泄露的nonce值可以访问一个REST API接口 -> 从REST API接口中的方法render\_element到最终触发代码执行的方法eval中间,以一个很巧妙的方式触发了漏洞,关键代码如下 ```php new $element_class_name( $element ) ``` 就是说,实例化时,类的名字和类的参数都是变量,攻击者通过构造符合条件的类名字和类参数,最终直达方法eval 总结 == 漏洞出现在后台的编辑器功能处,用于渲染元素并且预览效果,由于弱权限校验导致权限绕过可直接访问REST API端点,最终导致RCE,最后,感谢ID为zero的师傅分享的源码
发表于 2025-03-26 09:35:24
阅读 ( 3026 )
分类:
漏洞分析
1 推荐
收藏
1 条评论
Pseudoknot
6天前
还有可能就是我太菜,看不懂
请先
登录
后评论
请先
登录
后评论
ybdt
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!