CVE-2023-41892分析
CraftCMS rce分析
影响版本:4.0.0-RC1 <= Craft CMS <= 4.4.14
开源地址:https://github.com/craftcms/cms/releases/
这里直接用ACTF2023的环境起:CraftCMS
搭建好环境:
先对src/vendor/craftcms/cms/src/controllers/ConditionsController.php进行分析:
问题出在了beforeAction()方法中,config参数和name参数是传入的请求体参数,可控。config传入json会被解码,然后返回数组赋值给$baseConfig,然后$baseConfig数组中的name键值赋值给$config。这里有几个方法,挨个分析下:
remove(),返回并释放一个键值
1 |
|
$conditionsService
为函数Craft::$app->getConditions();根据定义返回一个Conditions的对象,跟进查看getConditions()
方法的定义
$conditionsService->createCondition($config);
跟进:
class是传入参数$config的class键值。50行判断是不是实现了ConditionInterface接口的对象(phpstorm按ctrl+alt+b查看实现类),不是则抛出异常。最终会return一个ConditionInterface对象
接下来分析configure()
方法:
倒是很简单,将传入的第二个参数($baseConfig,也就是从请求体获取的config参数),列为键值对$name => $value,然后将传入的对象中name属性的值进行修改。这里不妨想一想,如果对对象的某个未定义或者私有或受保护的值进行修改,会发生什么事呢?
那么如果对象中有__set()方法,就会自动调用其
全局搜索下有__set()方法的类(phpstorm按ctrl+shift+f组合键可以全局搜索,按ctrl+f12可以查看当前类的实现方法以及父类中的实现方法):
注意到configure()传入的是继承ConditionInterface接口的对象,查看哪些类实现了这个接口(phpstorm按ctrl+alt+b):
挑一个BaseCondition跟踪:
按Ctrl+f12查找到有继承Compoent的__set()方法,跟过去看看
改变的未定义属性名和值作为参数传入__set()中,会先进行一个字符串拼接判断当前类中方法是否存在,否则会进入之后的判断。name以”on”开头时没有什么特殊的操作,on()方法内并没有什么值得关注的地方。这里把目光锁定在第二个elseif判断上,以”as “(注意as后面有个空格!)开头的话,判断$value是否为Behavior的对象,不是的话会调用createObject()方法并以$value为参数。
现在跟进到createObject(),分析
当传入参数中的有键为”class”或”__class”,则最后会执行static::$container->get($class, $params, $type);
将__class键值作为第一个参数传入,第三个参数$type也就是一整个as后面的一整个json(除了__class键值对被销毁)
static::$container->get($class, $params, $type);
调用了build()方法,class将class作为第一个参数传入,而上文提到的$type作为形参$config的值传入。
跟进到build()方法里,
1 |
|
开局一个list(),将$this->getDependencies($class)
的执行结果返回给$reflection变量和$dependencies变量;
那就去看一下$this->getDependencies($class)
是个什么方法,返回的东西是什么:
之前光知道php本身自带一种类似反射的动态机制,但是下面也是第一次了解到php确实有一整套类似于java的完整的反射机制:
返回的是这两个变量的数组,那就返回去找他们第一次出现的位置:
一开始给$dependencies赋值为空,一目了然,但是$reflection这是个什么?ReflectionClass?反射?有点熟悉,有点意思。
去查阅官方文档,”报告了一个类的有关信息”。从字面上不难理解就是返回一个类的信息,比如一个类中有什么成员属性,有什么方法(自定义方法,构造器,析构器等等)。
然后就是通过调用$reflection对象中的getConstructor()方法来获取构造器
之后对代码的分析大部分都在下图:(如果你想在官方文档查阅某个原生方法的具体实现,可以先用phpstorm按ctrl+b查看该原生方法在哪个类中定义,再去官方文档中定位该类,接着定位类下方法,如果没找到也有可能是其父类的方法)
此处给$dependencies添加键值对了,键名为构造器参数名的键值为一个对象,该对象只有两个属性,className(即对应参数的类型(自定义类)),以及是否可选(isOptional,这里自己稍微跟进一下isNulledParam()方法就好了不做赘述)。
稍微总结一下$this->getDependencies($class)
这一部分的分析:传入一个类,然后返回这个类构造器的参数列表($dependencies),以及这个类的反射类对象($reflections)
现在回到build()
方法
先获取反射后的参数列表(为下面构造类实例传入参数做准备)
$addDependencies
是个数组,从$config
中获取”__construct()”键值对后的值,这里要注意$addDependencies
是个数组。调用了$this->validateDependencies($addDependencies);
,这是什么?跟进简单看一下
其实就是个判断”__construct()”数组的键名是否同为字符串或者同为数字,如果不是就抛出异常,比如说我传入参数后$addDependencies
为[{“hello”:1},{“world”:1}]就合法,[{“hello”:1},{1:1}]就非法,这个不算什么大问题(这里是随便举的例子,但是要根据实际类填写构造函数的参数名以及值才行,具体为什么接着往下看)
这里对参数名是否为整数进行判断,然后都会调用一个mergeDependencies()
方法,这是什么?
实现代码很简单,联系到上面所分析的$depandencies是什么,是一个构造器的参数列表,作用就一目了然了,就是给参数列表中的参数赋值,返回一个带值的$dependencies参数列
重点在最后这几行:通过newInstanceArgs()
方法返回一个对象,并传入$dependencies参数,很明显了吧,经过上面的分析,此时$dependencies已经是一个带值的参数列了,至此我们已经可以完整实现远程创建一个实例了,423-425甚至可以通过控制传入参数来修改类属性的值
到现在,下面先好好梳理一下:
1 |
|
很好!下面就是寻找可利用类的过程了。
怎么找?因为我们并不能控制类对象任意方法的调用,但是我们可以创建实例,也就是说构造器和析构器我们是可以触发的。这里有两种方向:
1.寻找构造函数__construct()中有可利用的危险函数,参数由构造器传入
2.构造函数没法利用,但是析构函数存在可利用危险函数,参数可控,那就通过build()方法中的这一部分来控制参数
全局搜索危险方法:
根据前人的指引,存在的可利用危险方法是call_user_func(),全局搜索(ctrl+shift+f)
在FnStream的析构函数中,这。。。拜托怎么可以这么完美。。。直接就调用了?参数直接可控了?
很好,但是也不可以高兴的太早了。。。这个回调函数没有参数。。也就意味着我们只能执行一点无参函数。。在脑袋可触及的范围内,我能想到的可利用也就只有一个phpinfo了。。这好歹算rce了。读个phpinfo()也许有点敏感数据吧(大概。。比如环境变量?啊吧啊吧)
FnStream的namespace
先瞅一眼构造函数:
好好好,_fn_和键名拼接,键值赋值给拼接后的属性,那,瞅一眼上面在作祟的call_user_func(),岂不是直接传close=>phpinfo就好啦?
狠狠构造poc:
1 |
|
另一种思路,不管构造器了,直接走build()方法中给成员赋值的路线
1 |
|
预备备!!打!。。
。。。
。。。。?好像并不符合预期。。
偷偷瞄一眼大家普遍用的poc,欸,action=conditions/render是什么?
这里暂时没弄明白。。后来看别的师傅的复现得知是在craft\web\Application的一个点,思路是在windows本地起环境然后打断点调试,把payload直接打然后进行调试,那么第一个正向发现这个点的师傅是怎么做到的呢。。。我们不得而知
具体的操作我就不去看了(晕),大概的原理如下:
总之先加上去一起传再说:
1 |
|
好好好,好歹有点动静了。
后来发现不能用BaseCondition?换成别的ConditionInterface的实现类就没问题?哪里出问题了?其他的Condition不也是继承了BaseCondition嘛?
1 |
|
到这里利用FnStream还暂时没办法造成危害严重的rce。这里继续从构造函数还有析构函数搜索危险可利用函数。
通过搜索,发现yii\rbac\PhpManager
的loadFromFile()
方法中有一处明显的文件包含
,通过查找该方法的用途,找到当前类下的load()
方法调用了loadFromFile()
,包含的文件名是当前类的itemFile,assignmentFile,ruleFile变量。似乎如果只要能控制这些变量的值就能做到文件包含了。再向上看看谁调用了load()
方法。
该类的init()
方法中调用了load()
方法。
原本想跟踪parent::init()到父类的init()方法。结果直接来到了yii\base\BaseObject
,啊?直接看到了构造函数处,传入config数组,Yii::configure($this,$config)
,这是什么?configure()是什么方法?前文提到该方法用于修改该类对象中名字为键名的元素的值,赋值为键值。构造函数中甚至”贴心”地调用了init()
好好好,这下利用链不是完全构造完成了吗?因为PhpManager中没有显式定义构造函数,因此新建实例时会调用其父类的构造函数,也就是BaseObject类中的。
利用链:
1 |
|
payload:(itemFile换成随便那三个被包含的变量都行)
1 |
|
或:
1 |
|
CVE-2023-41892分析