青岑web入门学习wp
靶场介绍:
最近我等于刷到一个新靶场挺好玩的
新搭建的,对新手很友好,这里推荐给大家
还可以加入群聊和师傅们一起交流,进步
快哉,快哉
本篇博客的知识点来源ai or 大佬的博客(我会放链接的)
ai成分高,望大家原谅
1、basic:

总结:先看源码和抓包,再找注入点和逻辑问题,最后构造 payload 拿 flag。多做题、多总结,就能形成自己的做题节奏。
直接f12得到flag:

flag{56abffc9-f44f-4c90-a8a4-9fc66954ebfb}
2、BASIC_1
我们查看f12发现被封了


一样查看源码得到flag:
flag{b997595d-f02c-4f3b-857b-c22433293d3e}
3、basic_2
抓取提交的包

发现无论提交什么内容is_admin一直为0
修改is_admin

得到flag
4、basic_3
查看源码

放到控制台运行,得到密码

得到flag

5、basic_4

继续看源码

var _0 = [81, 67, 67, 84, 70, 95, 86, 73, 80, 95, 50, 48, 50, 54];
就是邀请码
我们需要把_0转换成字符串得到
QCCTF_VIP_2026
得到flag

basic_5

直接看源码逻辑发现
积分(currentScore)的计算完全是在前端(浏览器里)进行的。服务器并不知道你到底答了几道题,它只看你最后点击“领取奖励”时发送过去的 score 是多少。
虽然提交时调用了 encryptData 对 { score: currentScore } 进行了加密(本质上是 Base64 编码 + URL 编码),并且服务器返回的数据也用同样的方式加密了,但加密和解密函数都在前端代码里,并且是全局可见的。
// 直接调用页面原本的加密函数,伪造 1000 积分发送给后端 fetch('/claim', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data: encryptData({ score: 1000 }) }), }) .then(res => res.json()) .then(res => { if (res.data) { // 调用页面原本的解密函数查看结果 const result = decryptData(res.data); console.log("🎉 成功拿到 Flag: ", result); alert("Flag是: \n" + result.flag); } else { console.log("❌ 请求失败或未返回数据: ", res); } }) .catch(err => console.error("发生错误: ", err));得到flag
或者使用自动做题脚本
// 设置一个定时器,每隔 50 毫秒执行一次答题循环 let autoAnswerInterval = setInterval(function() { // 1. 获取当前分数 let currentScore = parseInt(document.getElementById('score-num').innerText); // 2. 判断是否达到 1000 分 if (currentScore >= 1000) { clearInterval(autoAnswerInterval); // 停止定时器 console.log("🎉 分数已达到 1000!正在自动点击领取奖励..."); document.getElementById('claim-btn').click(); // 触发领取按钮 return; } // 3. 读取屏幕上的题目 (例如:"13 - 11 = ?") let questionText = document.getElementById('question-text').innerText; // 4. 提取算式并计算 (去掉末尾的 " = ?") let mathExpression = questionText.replace(' = ?', ''); let answer = eval(mathExpression); // 5. 将计算出的答案填入输入框 document.getElementById('answer-input').value = answer; // 6. 点击提交按钮 document.getElementById('submit-answer-btn').click(); // 控制台打印进度(可选,如果嫌吵可以删掉这一行) console.log(`当前进度: ${currentScore}/1000 | 刚刚解答: ${questionText.replace('?', answer)}`); }, 50); // 50毫秒答一题,大约50秒就能答完1000题。如果觉得太快或卡顿,可以把 50 改成 100 或 200。import requests import json import base64 # 替换成你当前的实际题目地址和端口 TARGET_URL = "http://docker.qingcen.net:38964/claim" def get_flag(): # 1. 构造我们需要伪造的满分数据 payload = {"score": 1000} # 2. 将字典转换为紧凑的 JSON 字符串 (类似前端的 JSON.stringify) json_str = json.dumps(payload, separators=(',', ':')) # 3. 模拟前端的加密逻辑:btoa(unescape(encodeURIComponent(jsonString))) # 在 Python 中,直接将字符串转为 utf-8 字节流,然后进行 base64 编码即可完美等效! encrypted_bytes = base64.b64encode(json_str.encode('utf-8')) encrypted_data = encrypted_bytes.decode('utf-8') print(f"[*] 正在发送伪造的 1000 分请求...\n[*] 加密 payload: {encrypted_data}") # 4. 发送 POST 请求给后端 headers = {'Content-Type': 'application/json'} data = {"data": encrypted_data} try: response = requests.post(TARGET_URL, headers=headers, json=data) res_json = response.json() # 5. 模拟前端的解密逻辑并提取 Flag if "data" in res_json: # 对应前端的 decryptData(encodedString) dec_bytes = base64.b64decode(res_json["data"]) dec_str = dec_bytes.decode('utf-8') result = json.loads(dec_str) if "flag" in result: print("\n🎉 成功拿到 Flag!") print("-" * 30) print(result["flag"]) print("-" * 30) else: print(f"[-] 未找到 Flag,服务器返回: {result}") else: print(f"[-] 请求失败或服务器返回异常: {res_json}") except Exception as e: print(f"[-] 发生错误: {e}") if __name__ == "__main__": get_flag()EZ_PHP

要求是a能通过if的判断,而且要等于0
b要求不是数字而且需要大于2026
/?a=abc&b=3333as得到flag
ezphp_1

思路很清晰,我们 需要get方法来传入qc,把我传入的 JSON 字符串解析成 PHP 数组
在数组中寻找 "QCCTF" 这个值,并返回它的键名 (索引),然后索引需要是1
JSON 字符串本质上就是一个符合特定语法规则的普通字符串。它的核心规则非常简单,主要由以下几种结构组成:
1. 基本语法规则
- 数据在名称/值对中:也就是键值对(Key-Value pairs)。
- 数据由逗号
,分隔。 - 大括号
{}保存对象(Object)。 - 中括号
[]保存数组(Array)。 - 双引号
"":在 JSON 中,所有的键名(Key)和字符串类型的值(Value)都必须使用双引号,绝对不能使用单引号。
2. JSON 支持的数据类型(值)
JSON 的值(Value)只能是以下几种数据类型:
- 字符串 (String):必须用双引号括起来,如
"hello"。 - 数字 (Number):整数或浮点数,如
123或3.14。 - 对象 (Object):用
{}括起来的键值对集合。 - 数组 (Array):用
[]括起来的有序值集合。 - 布尔值 (Boolean):
true或false(不要加引号)。 - 空值 (Null):
null(不要加引号)。
ezphp_2

在 PHP 中,任何非空字符串跟布尔值 true 进行弱比较时,结果都是相等的(即 "QCyyds" == true 会判定为真)。但是,它们在强类型比较时是绝对不相等的(字符串不等于布尔值)
<?php show_source(__FILE__); // 在页面上显示这行代码本身 include("flag.php"); // 引入包含真实 flag 的文件,此时 $flag 变量已被赋值 // 【关卡 1】检查参数是否存在 if (!isset($_GET['qc']) || $_GET['qc'] === '') exit("no"); // 解释:通过 GET 请求获取名为 'qc' 的参数。如果没传这个参数,或者传了空值,就退出并输出 "no"。 // 【数据处理】解析 JSON 并强制转为数组 $qc = (array)json_decode($_GET['qc'], true); // 解释:把你传进来的 'qc' 参数当做 JSON 字符串来解析,转成 PHP 的数组赋值给 $qc。 // 【关卡 2】寻找 "QCCTF" if (array_search("QCCTF", $qc) === false) die("no..."); // 解释:在 $qc 这个数组的第一层里找,必须包含 "QCCTF" 这个字符串。找不到就死掉并输出 "no..."。 // 【关卡 3】寻找 "QCyyds"(关键漏洞点) if (array_search("QCyyds", $qc["n"]) === false) die("no..."); // 解释:在 $qc 数组里面,必须有一个键名为 "n" 的子数组。在这个子数组里,必须能找到 "QCyyds"。找不到就输出 "no..."。 // 【关卡 4】遍历检查(死胡同?) foreach ($qc["n"] as $val) { if ($val === "QCyyds") die("no......"); } // 解释:把 $qc["n"] 里的元素挨个拿出来检查,如果遇到哪个元素**完全等于** "QCyyds",就死掉并输出 "no......"。 echo $flag; // 如果以上所有关卡都通过,输出 Flag!在 PHP 的 json_decode 函数中,第二个参数 true 是一个非常关键的设置。
它的作用是:强制将解析后的 JSON 数据转换成 PHP 的“关联数组(Associative Array)”,而不是“PHP 对象(Object)”。
1. 如果加上 true(题目中的情况)
PHP
$result = json_decode('{"name": "QCCTF", "score": 100}', true); 这时候,PHP 会把它变成一个数组。 你要获取里面的值,必须使用数组的语法(中括号):
- 获取名字:
$result['name'] - 获取分数:
$result['score']
2. 如果不加 true(默认情况)
PHP
$result = json_decode('{"name": "QCCTF", "score": 100}'); 这时候,PHP 会把它变成一个 stdClass 对象。 你要获取里面的值,必须使用对象的语法(箭头):
- 获取名字:
$result->name - 获取分数:
$result->score
http://docker.qingcen.net:38987/?qc={%22A%22:%22QCCTF%22,%22n%22:[true]}得到flag
ezmd5_1:

http://docker.qingcen.net:39005/?a=QNKCDZO&b=s214587387a就是两个传入的数不能相同,但是md5值需要相同
ezmd5_2

/?a[]=2323&b[]=43要求就是a和b的值不相等
但是MD5的值必须相等
数组绕过的特点:
报错并返回 NULL:因为 md5() 无法处理数组,它会抛出一个 Warning(警告),正如截图下方显示的:Warning: md5() expects parameter 1 to be string, array given...。最关键的是,报错后它会返回 NULL。
ezmd5_3

像上一题一样用数组,不多解释了
注意这个就行了
强类型比较绝对不会自动转换数据类型(不会把它当成科学计数法)。它要求等号两边的值不仅类型要一样,内容必须一个字符不差地完全一样。
md5('QNKCDZO')="0e830400451993494058024219903391"md5('240610708')="0e462097431906509019562988736854"
因此我们用数组进行绕过
ezmd5_4

从倒数第 6 个字符开始,截取 6 个字符是d54e23
这种需要找特定 MD5 尾部(或头部)的题型,在 CTF 中非常常见,通常被称为工作量证明(PoW, Proof of Work)。
因为 MD5 算法是不可逆的,我们只能用“暴力穷举”的方式,不断生成新的字符串去试,直到碰巧撞上最后 6 位是 d54e23 的那个。
import hashlib # 题目要求的尾部特征 target_suffix = 'd54e23' print(f"[*] 开始爆破,寻找 MD5 最后 6 位为 {target_suffix} 的字符串...") # 使用数字作为基础进行遍历,速度最快 i = 0 while True: # 将数字转换成字符串 test_str = str(i) # 计算 MD5 (注意要把字符串 encode 成 bytes 才能算) md5_hash = hashlib.md5(test_str.encode('utf-8')).hexdigest() # 截取最后 6 位进行比对 if md5_hash[-6:] == target_suffix: print(f"\n[+] 爆破成功!") print(f"[-] 满足条件的字符串 ($qc): {test_str}") print(f"[-] 它的完整 MD5 值为: {md5_hash}") break i += 1ezmd5

因为是等于的弱判断,我们只要找一个md5的值为0的科学计数法的就行
http://docker.qingcen.net:39013/?QC=26120得到flag
ezmd5_5:md5? sha1!!!

SHA-1 (Secure Hash Algorithm 1) 是一种跟 MD5 非常类似的密码散列函数(哈希算法)。
简单来说,它的作用和 MD5 一样:你给它一段字符串,它经过复杂的数学运算后,吐出一串固定长度的乱码(SHA-1 输出的是 40 位的十六进制字符,而 MD5 是 32 位)。它同样具有不可逆的特性。
所以这道题我们可以使用数组,也可以使用魔术字符串
这里我直接提供两个经典的 SHA-1 魔术哈希字符串:
- 字符串1:
aaroZmOk(它的 SHA-1 是0e66507019969427134894567494305185566735) - 字符串2:
aaK1STfY(它的 SHA-1 是0e76658526655756207688271159624026011393)
ezmd5_6

因为数组直接报null所以我们还可以使用数组直接得到flag
/?a[]=12&b[]=2ezsql_3
ezcmd

代码用了 escapeshellcmd() 来过滤你传入的 POST 参数。这个函数会转义 &, ;, | 等符号,所以你不能用它们来执行多条命令(比如 ls;cat flag)。
我们先查看根目录

发现有flag
于是查看flag,得到flag

ezcmd_1

我们首先分析php代码:其意思是帮助我们ping一个地址5次(我们传入的cmd值)
但是我们可以注意到,并没有任何绕过
这里没有任何过滤函数(没有 escapeshellcmd,也没有其他替换逻辑)。它就是简单粗暴地把你输入的 $cmd 拼在 ping -c 5 后面,然后交给系统执行!
怎么绕过?
在 Linux 系统中,有一条神级的规则:我们可以用特殊符号把多条命令串在一行里执行。 最常用的符号就是分号; (还有&& 或 ||)。
于是我们先看一下根目录的文件有哪些

可以看到flag,之后cat得到flag
ezcmd_2

system($cmd." >/dev/null 2>&1");不难发现重点就在这里
后面的 >/dev/null 2>&1 是 Linux 里的重定向符。它的意思是:把这条命令产生的所有正常输出(1)和报错信息(2),统统扔进 /dev/null 这个系统的“无底洞”垃圾桶里。
即 无回显 (Blind)
只要明白这个代码的作用这个题目也就简单了

我们只需要加个分号,使得传入的cmd后面的代码无效,也就解决了这个问题
;是命令分隔符。#是单行注释的符号。

这样思路和前面就一样了,找到flag的位置,然后cat直接读取flag
ezcmd_3

我们的重点代码不难看出就是
if (strpos($cmd, ' ') !== false) { die('no space allowed'); }意思就是我们不能使用空格
使用重定向符 <(最简单,专门用来读文件)
在 Linux 中,你可以用 < 把文件内容输入给命令,这就巧妙地避开了空格。
- 正常写法:
cat /flag - 无空格写法:
cat</flag
使用系统内置变量 $IFS(万能空格替代品)
$IFS(Internal Field Separator)是 Linux 系统的一个内置环境变量,它的默认值正好就是空格、制表符和换行符。只要在命令里写上它,系统执行时就会自动把它变成空格。
- 正常写法:
ls /或cat /flag - 无空格写法:
ls$IFS/或cat${IFS}/flag(加{}是为了防止变量名和后面的字母粘连起冲突)。

- 只要是带参数的命令(比如
ls /): 只能用$IFS替代空格,例如ls$IFS/。 - 如果是读取文件内容的命令(比如
cat /flag): 既可以用$IFS(cat$IFS/flag),也可以用<输入重定向(cat</flag)。 <的限制:<只能用来读取“文件”(File),不能用来读取“目录”(Directory)。/是个目录,所以 Shell 连第一步打开它都做不到,直接报错了。
但是在cat的时候你会发现什么反应都没有
别怀疑自己,你的 Payload cmd=cat$IFS/flag; 和 cmd=cat</flag; 都是完全正确的!
下面是错误分析,可能是我插件出问题了,但是也是知识点就放在这了
可以直接到后面看flag
出题人把 cat 命令给删了(或禁用了)
当你执行 cat /flag 时,因为 cat 不存在,Linux 系统其实是报错了的(类似 sh: cat: command not found)。
但是! Linux 的输出分为“标准输出 (stdout)”和“标准错误 (stderr)”。
系统的报错信息默认走的是“标准错误”通道
而 PHP 的 system() 函数只负责把“标准输出”的内容打印到网页上。
结果就是:报错信息被系统截留了,网页上什么也抓取不到,看起来就像是没有任何反应。
more / less:本来是用来分页看长文件的,但小文件它也会直接打印出来。
cmd=more$IFS/flag;或cmd=more</flag;
tail:默认读取文件的最后 10 行。
cmd=tail$IFS/flag;或cmd=tail</flag;
head:默认读取文件的前 10 行。
cmd=head$IFS/flag;或cmd=head</flag;
nl:读取文件,并顺便给每行加上行号。
cmd=nl$IFS/flag;或cmd=nl</flag;
tac:cat 的反向命令,把文件从最后一行倒着打印出来。(最常用、成功率最高的替代品!)
cmd=tac$IFS/flag;或cmd=tac</flag;

ezcmd_4

我的评价是一点都不可爱,很丑,很讨厌,吓我一跳
网页名字算是个提示,robots


成功到达
- 使用
escapeshellcmd()对输入进行转义。escapeshellcmd()会在绝大多数 shell 特殊字符(如$,\,',",;,|等)前面加上反斜杠\强行转义 - 使用
preg_match()对转义后的字符串进行黑名单校验(忽略大小写/i)。 - 如果没有匹配到黑名单内容,则执行
system()。
既然我们无法读取带有 flag 关键字的文件,那我们就直接打印系统当前的所有环境变量。
env被过滤了。printenv包含了env,也被过滤了。- 但是,
export和set这两个可以打印环境变量的命令,

好吧,环境变量里面没有flag,那说明flag还是藏在flag那个目录下

- 过正则: 字符串是
eval a=fl;b=ag;dd if=/$a$b,里面没有出现flag,也没有触发任何被 ban 的读文件命令。 - 过转义: 经过
escapeshellcmd()处理后,它变成了eval a=fl\;b=ag\;dd if=/\$a\$b。 - 二次解析: bash 把这串字符丢给
eval执行。eval会去掉多余的转义符重新解析,最终实际执行的命令变回了完美的a=fl; b=ag; dd if=/$a$b。 - 拿 Flag:
dd命令执行,将/flag的内容原封不动地输出在网页上。

eval:Bash 的内置命令。它的作用是接收后面的参数,把它们组合成一个字符串,然后再把这个字符串当做真正的 Shell 命令执行一次。a=fl;b=ag;:变量赋值。我们把敏感词flag拆成了两半,分别赋值给变量a和b。这样就完美绕过了正则中对flag关键字的拦截。dd if=/$a$b:读取文件。dd是 Linux 下的一个强大的数据复制和转换工具,这里利用if=(input file)参数来指定输入文件。/$a$b拼接后其实就是/flag。
ezcmd_5

?何意味?
字母都不让我用了
可以使用 $'\NNN' 的格式来表示一个字符,其中 NNN 是该字符的八进制 ASCII 码。 比如:c 的八进制是 143,a 是 141,t 是 164。 所以 $'\143\141\164' 在 Shell 眼里,就等于 cat。

cmd=$'\143\141\164' $'\057\146\154\141\147\056\164\170\164'ezcmd_6

我们首先打印出当前目录下文件

cat得到flag

ezcmd_7

GET方法,qc传入的值不能是flag
error_reporting(0);关闭 PHP 错误报告。这在 CTF 中很常见,目的是不让页面报出详细的错误信息,增加盲猜的难度。if(isset($_GET['qc'])){检查 URL 中是否通过 GET 方法传入了名为qc的参数。比如你的 URL 长这样:http://.../?qc=xxx。$qc = $_GET['qc'];将传入的参数值赋给变量$qc。if(!preg_match("/flag/i", $qc)){这是本题的核心考点(黑名单过滤)。preg_match是 PHP 的正则匹配函数。/flag/i 匹配字符串 "flag"。- 后面的
i表示大小写不敏感。也就是它会拦截flag,FLAG,FlAg,fLaG等所有大小写组合。 - 前面的
!代表非。所以这句代码的意思是:如果传入的$qc中不包含 "flag" 这个词(忽略大小写),才进入下一步。
eval($qc);漏洞触发点。 如果绕过了上面的过滤,这里就会把你传入的$qc字符串当作 PHP 代码直接执行
只要能看懂就很容易了,我们直接用*绕过,得到flag

ezcmd_8
这个就是把system给禁止了

既然不能用 system(),PHP 还有很多其他的系统命令执行函数,比如 passthru(), exec(), shell_exec() 或者是反引号 `。


得到flag
ezcmd_9

又过滤了空格,我直接复制ai了
招式一:使用 PHP 原生函数(最优雅、最推荐)
不要忘了,你输入的内容是交给 eval() 当作 PHP 代码 来执行的。谁说一定要调用 Linux 系统命令去读文件呢?PHP 本身就自带了读取文件的函数,而调用这些函数是不需要空格的!
- Payload 1:
?qc=show_source('flag'); - Payload 2:
?qc=readfile('flag'); - 原理: 这两个都是 PHP 内置的读取文件并输出的函数。整个字符串里既没有
system,也没有空格,完美绕过正则!
招式二:继续使用“参数偷渡”大法(最万能)
我们上一题用过的方法在这里依然管用!因为正则过滤只针对 $_GET['qc'],不检查其他参数。我们只要保证 qc 里面没有空格和 system 即可。
- Payload:
?qc=eval($_GET['x']);&x=passthru('cat flag'); - 原理:
- 你传给
qc的是eval($_GET['x']);。仔细看,这串代码里没有任何空格,也没有 system!顺利过关。 - 接下来
eval去执行时,会抓取参数x的值。 - 参数
x里的passthru('cat flag');包含空格,但它并没有经过正则的检查,所以被成功执行。(注:这里用passthru或exec替代了被禁用的system)。
- 你传给
招式三:利用 Linux 系统的空格替代符(进阶姿势)
如果你非要执着于执行系统命令,在 Linux 中其实有很多字符可以用来代替空格。最常见的是利用 ${IFS} (内部字段分隔符) 或者 < (输入重定向)。
- Payload 1 (使用 ${IFS}):
?qc=passthru('cat${IFS}flag'); - Payload 2 (使用重定向 <):
?qc=passthru('cat<flag'); - 原理: 在 Linux 解释命令时,
${IFS}会被当作空格处理,<会把flag文件的内容重定向给cat命令。这样我们在 PHP 层面就没有输入真正的空格字符,从而绕过了正则。

我尝试了前两个都没用,三才有用(这里才说是以为我不想一个人尝试浪费时间哈哈哈哈哈哈哈)

得到flag
ezcmd_10
这次题目把分号给过滤掉了。

但是我们可以用?>对吧,毕竟它在php的最后

这里是flag.txt,所以我们直接用readfile就可以得出了,system也可以


ezcmd_11


鬼知道flag.php在哪,我们既然知道是flag.php文件,直接highlight

ezcmd_12函数套娃法

既然特殊符号都不能用了,那我们就使用函数套娃法
我们需要完成以下逻辑链条:
- 获取当前目录路径: 使用
getcwd()函数,它会返回当前工作目录(比如/var/www/html)。 - 读取目录里的文件列表: 使用
scandir()函数。scandir(getcwd())会返回一个数组,里面包含了当前目录下的所有文件:['.', '..', 'flag.php', 'index.php']。 - 提取出
flag.php: 数组有了,怎么把flag.php单独取出来喂给读取函数呢?- 因为严格的 PHP 语法限制,很多提取数组元素的函数(比如
next()、end())需要传入变量引用,直接嵌套会报错。 - 终极黑科技: 我们先用
array_flip()把数组的键和值反转,让文件名变成键名;然后再用array_rand()随机抽取一个键名!
- 因为严格的 PHP 语法限制,很多提取数组元素的函数(比如
- 高亮显示文件: 最后套上我们熟悉的
show_source()。
在 PHP 的官方设定里,show_source() 和 highlight_file() 是完全等价的别名关系。它们的作用一模一样:都是把文件内容读取出来,并加上 HTML 语法高亮输出到网页上。

ezcmd_13

这是一道非常经典的 preg_replace 函数 /e 修饰符代码执行漏洞。
在 PHP(5.5.0 以下版本)中,preg_replace 的 /e 修饰符有一个极其危险的特性:它会把替换后的字符串作为 PHP 代码来执行。
我们先来详细分析一下这个php代码:
- 三元运算符 (
? :):前两行是在通过 GET 方式(也就是 URL 地址栏)获取你传过去的两个参数re和str。isset()是检查参数存不存在。翻译成大白话就是:“如果你在网址里传了re,我就把它存进变量$re里;如果你没传,我就给它塞一个空字符串''。”对于$str同理。 - 安全检查(if 语句):接着判断,如果
$re或者$str任何一个是空的,就执行highlight_file(__FILE__)把当前页面的源代码打印出来给你看,然后exit退出程序。 - 结论:这就是为什么你一打开网页,什么都不输入的时候,页面上会显示这些代码的原因。这也是在告诉你:必须要同时提供
re和str两个参数才能继续往下走。
/e 修饰符就藏在 preg_replace 的第一个参数里! 我们把第一个参数拼接一下看得更清楚:'/(' . $re . ')/ei'
- 在正则表达式中,最外层的两个斜杠
/和/是定界符,用来把正则规则包裹起来。 - 斜杠中间的
(' . $re . ')是匹配规则,它会把你传入的$re参数拼接到这里。 - 定界符后面的字母就是修饰符:这里的
ei就是修饰符。i代表忽略大小写,而e就是传说中万恶之源的 eval(执行)修饰符!
preg_replace(规则, 替换成什么, 目标字符串) 这个函数原本的作用是:在“目标字符串”中寻找符合“规则”的部分,并把它“替换”掉。
结合这段代码,它的逻辑是: 在字符串 $str 中,寻找符合正则 $re 的内容,找到之后,把它放到第二个参数 'strtolower("\\1")' 里面替换掉(\\1 代表正则匹配到的第一组内容),最后 echo 打印出来
存在 /e 修饰符。当 PHP 发现有 /e 时,它不会把第二个参数当成普通的文本替换,而是把它当成 PHP 代码去执行!
Payload 是怎么生效的? 假设我们按照之前的思路传入:?re=.*&str={${system('ls')}}
- 代码把参数代入,第一个参数变成了
/(.*)/ei。.* 是通配符,意思是匹配一切内容。 - 第二个参数是
'strtolower("\\1")'。 - 第三个参数是
{${system('ls')}}。 - 匹配开始:正则
.*成功匹配到了整个字符串{${system('ls')}},所以\\1的值变成了{${system('ls')}}。 - 准备替换:第二个参数变成了
strtolower("{${system('ls')}}")。 - 致命一击:因为有
/e修饰符,PHP 会把这句strtolower(...)当作代码执行。而在 PHP 的双引号字符串中,如果遇到${}或者{${}}这种复杂变量解析语法,PHP 引擎会先执行大括号里面的函数,获取它的返回值! - 于是,
system('ls')就这样被无情地执行了。
- PHP 扫描到了
{${ ... }}结构。 - PHP 的规则是:遇到这种结构,必须先执行大括号里面最深处的代码,用它的结果来当作变量名!
在 PHP 中,strtolower 的全称可以理解为 "string to lower"(字符串转小写)

当 preg_replace 使用 /e 修饰符时,PHP 为了防止某些注入问题,会在将匹配到的结果(即你的 \1)替换到代码字符串之前,自动对结果执行一次 addslashes() 函数。 addslashes() 会在单引号 (')、双引号 (")、反斜杠 (\) 和 NULL 字符前加上反斜杠进行转义。


1. 以前为什么要加分号?(因为你在写“完整的句子”)
在正常的 PHP 代码或者 eval() 函数中,你输入的是一段完整的执行逻辑。 比如:system('ls'); 这就好比人类语言里的一个完整句子,分号 ; 就是句号。PHP 引擎看到分号,才知道:“哦,这行命令结束了,我去执行它。” 如果你写 eval("system('ls')")(不带分号),PHP 就会报错,因为它觉得你的话还没说完。
2. 为什么这里不能加分号?(因为你在写“填空题”)
回到我们现在的 Payload:strtolower("{${system($_GET[cmd])}}")。
注意,system(...) 此时并不是作为一行独立的代码在运行,它是被包裹在双引号字符串的 {${ }} 结构里面的。
PHP 对双引号里的 {${ }} 结构的解析规则是:
- “请在括号里给我一个表达式(可以得出结果的短语),我会把它的结果当成变量名。”
在这个特定的结构里,PHP 期待的是一个“词”或“短语”(也就是函数调用的返回值),而不是一个“完整的句子”。
system($_GET[cmd])是一个表达式,它会去执行系统命令,并返回最后一行结果。这完全符合 PHP 的期待。- 如果你加上分号写成
{${system($_GET[cmd]);}},在 PHP 的字符串解析器看来,就相当于在填空题的格子里硬塞进了一个句号,这破坏了字符串变量解析的语法规则,PHP 解析器会直接不认识,抛出语法错误(Syntax Error)。
ezinfoleak

看到信息,base64解码

暴力穿越到根目录去找

得到flag
ezinfoleak_1

通过下面的显示可以看出,它吞掉了我们的../

于是我们写成....//,它吞掉我们中间的../我们前面和后面又拼成了../
ezinfoleak_2

ezinfoleak_4

dirsearch工具扫描一下

发现是.git 源码泄露漏洞

使用githack
GitHub - lijiejie/GitHack: A `.git` folder disclosure exploit · GitHub

得到flag
ezinfoleak_5
也是看烟花的页面
那估计思路还是差不多,先dirsearch扫描一下,感觉像.git泄露


然后发送个post请求就ok了(注意url后面把文件加上)

ezinfoleak_6

换成了管理系统的界面了,扫一下目录吧

发现是.svn 源码泄露
/.svn/entries(200 OK): 这是老版本 SVN 的特征文件,证明存在泄露。/.svn/wc.db(200 OK, 120KB): 这是真正的“宝藏”。在较新版本的 SVN(1.7 及以上)中,wc.db是一个 SQLite 数据库文件。它不仅记录了整个网站的目录结构、文件列表,其关联的目录中还直接包含了所有源代码的原始副本 (pristine copies)。
如何使用 dvcs-ripper
dvcs-ripper 文件夹里有几个不同的脚本,对应不同的版本控制系统:
rip-git.pl:用于提取.git目录(最常用)rip-svn.pl:用于提取.svn目录rip-hg.pl:用于提取.hg(Mercurial) 目录rip-bzr.pl:用于提取.bzr(Bazaar) 目录

我们还原出了flag.php

ezinfoleak_7

Mercurial (hg) 和我们常听说的 Git 一样,都是一种版本控制系统,程序员用它来管理源代码的版本、提交记录和分支。
- Git 生成的隐藏目录叫
.git/ - Mercurial 生成的隐藏目录叫
.hg/
注意最后要加上 /.hg/

得到flag
ezinfoleak_8
查看源码

什么是 Vim 缓存文件泄露?
在 Linux 系统里,程序员经常使用 vim 这个命令行文本编辑器来写代码。 当 vim 打开一个文件(比如 flag.txt)时,为了防止电脑突然断电导致没保存,它会在同级目录下自动生成一个隐藏的临时备份文件(也叫交换文件 Swap file)。
如果程序员是非正常退出 vim(比如直接关掉终端窗口,或者系统崩溃),这个隐藏的备份文件就会被遗留在服务器上。
对于文件 flag.txt,它的 Vim 缓存文件通常会被命名为: 👉 .flag.txt.swp

直接出flag
ezinfoleak_9
没思路,dirsearch跑一下看看吧

.DS_Store是什么: 这是苹果 Mac 电脑系统特有的隐藏文件,用来记录文件夹的自定义属性。- 它为什么在这里: 说明写这个网站的程序员用的是 Mac 电脑,并且在把代码上传到服务器时,不小心把这个 Mac 自己生成的隐藏文件也传上去了

wget http://docker.qingcen.net:35334/.DS_Store下载这个文件,然后用strings来读取
ezfu
上传文件,我们先上传一个get方法的一句话木马

之后通过get传参,打印环境(因为我看过根目录下没有线索了)

得到flag
ezfu_1
这个题目只能上传图片什么的,我们把我们的php文件改成jpg,然后抓包拦截
把文件后缀名改回php就行了,就能上传成功了
flag还是在环境中

ezfu_2

我们再上传刚刚的图片的时候,发现它这次检查内容了

我们在木马前加个gif的开头看看能不能骗过它

失败了

我们把后缀也改成gif发现可以上传成功,于是抓包

还是失败了
GIF 头伪造和欺骗 HTTP 数据包检查都失败了
我们尝试一下木马图

也失败了
那我们试试绕过吧

ezfl

在源码中得到线索

flag is in /flag.txt

得到flag
ezfl_1



?file=php://filter/read=convert.base64-encode/resource=flag.php?file=- 这是目标网页(比如你题目里的应用)接收输入的一个参数。后台代码可能使用了类似
include($_GET['file']);的危险函数,并且没有对输入做严格过滤。
- 这是目标网页(比如你题目里的应用)接收输入的一个参数。后台代码可能使用了类似
php://- 这是 PHP 提供的一个内置伪协议(Wrapper),用于访问各个输入/输出(I/O)流。
filter/- 这是
php://协议下的一个特定功能:数据流过滤器。它的作用是允许你在读取或写入数据流时,对数据进行处理(过滤或转换)。
- 这是
read=- 指定接下来的过滤器将应用在读取操作上(即从目标文件中读取数据时)。
convert.base64-encode- 这是最核心的过滤器指令。它告诉 PHP 引擎:“请把你读取到的目标文件内容,先转换(编码)成 Base64 格式的字符串。”
/resource=- 语法上的分隔符,用来指示后面跟着的参数是要读取和过滤的目标对象路径。
flag.php- 这就是你想读取的目标文件。

ezfl_2

?file=php://filter/convert.iconv.utf-8.utf-16/resource=flag.php