第一届好靶场MISC-SQL流量分析
SQL(Structured Query Language,结构化查询语言)是用于访问和管理关系型数据库(如 MySQL、PostgreSQL、SQL Server、Oracle 等)的标准语言。
学习 SQL,最核心的是掌握它的四大分类,其中最常用的是 DQL(数据查询) 和 DML(数据操作)。
下面为你系统梳理最常见、最核心的 SQL 语法:
1. SQL 基础规则
- 不区分大小写:
SELECT和select效果一样,但为了可读性,通常关键字大写,表名和列名小写。 - 分号结尾:每条完整的 SQL 语句通常以分号
;结束。 - 单引号:文本字符串和日期类型的值需要用单引号括起来(例如
'张三'),数字不需要。
2. DQL:数据查询语言(最常用的部分)
这是 SQL 中使用频率最高的语法,用于从数据库中提取数据。
基础查询:
SELECT column1, column2 FROM table_name;
(如果想查询所有列,可以使用 SELECT \* FROM table_name;)
条件过滤 (WHERE):
SELECT name, age FROM users WHERE age > 18 AND status = 'active';
排序 (ORDER BY):
SELECT name, score FROM students ORDER BY score DESC; -- DESC 为降序,ASC 为升序(默认)
限制返回数量 (LIMIT):
SELECT * FROM orders ORDER BY create_time DESC LIMIT 10; -- 只返回最新的 10 条记录
分组与聚合 (GROUP BY & 聚合函数):
常用的聚合函数包括 COUNT() 计数、SUM() 求和、AVG() 平均值、MAX()/MIN() 最大最小值。
SELECT department, COUNT(*) as employee_count FROM employees GROUP BY department;
3. DML:数据操作语言(增、删、改)
用于向表中添加、更新或删除数据。
插入数据 (INSERT INTO):
INSERT INTO users (username, email, age)
VALUES ('张三', 'zhangsan@example.com', 25);
更新数据 (UPDATE):
⚠️ 危险警告:更新数据时一定要加
WHERE条件!如果不加,会把整张表的所有数据都改掉。
UPDATE users SET age = 26, status = 'inactive' WHERE username = '张三';
删除数据 (DELETE):
⚠️ 危险警告:同理,删除数据务必带上
WHERE条件。
DELETE FROM users WHERE id = 1001;
4. 多表连接 (JOIN)
在实际业务中,数据通常分散在不同的表里,需要用到连接查询。最常用的是内连接(INNER JOIN)和左连接(LEFT JOIN)。
SQL
SELECT users.name, orders.order_no, orders.amount FROM users JOIN orders ON users.id = orders.user_id; -- 通过 user_id 将 users 表和 orders 表关联起来
5. DDL:数据定义语言(表结构操作)
用于创建、修改或删除数据库和表本身。
创建表 (CREATE TABLE):
CREATE TABLE products (
id INT PRIMARY KEY, -- 主键,唯一标识
name VARCHAR(100) NOT NULL, -- 字符串类型,不能为空
price DECIMAL(10, 2), -- 小数类型,保留两位
created_at DATE -- 日期类型
);
删除表 (DROP TABLE):
DROP TABLE products; -- 直接把整张表和里面的数据一起抹除
常见的SQL注入
① 引号的用法
category = '计算机' ← 字符串用单引号包起来
攻击者利用这一点:如果网站直接把用户输入拼进 SQL:
-- 正常:
SELECT * FROM books WHERE category = '计算机'
-- 攻击者输入: 计算机' OR 1=1-- -- 变成了:
SELECT * FROM books WHERE category = '计算机' OR 1=1--'
注意看: ' 把前半的引号闭合了,后面的 OR 1=1 就成了可执行的 SQL 代码,-- 把剩下的引号变注释。
② 注释符(-- 和 #)
-- SQL标准注释,后面所有内容忽略(注意后面要跟空格!) \# MySQL专属注释 /* */ 多行注释
你在流量包里看到 -- xxxx(随机字符)就是注释,攻击者用它来"砍掉"原 SQL 语句后半截。
③ UNION / UNION ALL(合并查询)
SELECT 1, 2, 3 UNION ALL SELECT 'a', 'b', 'c'
规则: 上下两个 SELECT 的列数必须相同。攻击者用 UNION 往结果里夹带私货。
④ CHR() / ASCII 编码
CHR(113) || CHR(106) || CHR(118) -- 返回 'qjv'
CHR(数字) 把数字转成对应字符:
- CHR(65) = 'A'
- CHR(97) = 'a'
- CHR(39) = ''`(单引号)
|| 是字符串拼接。攻击者为什么自找麻烦?——因为有些字符在 URL 里不能直接传,用数字就稳了。
⑤ information_schema(偷表结构)
-- 查所有数据库名
SELECT schema_name FROM information_schema.schemata
-- 查某个库的所有表名
SELECT table_name FROM information_schema.tables WHERE table_schema='数据库名'
-- 查某个表的所有列名
SELECT column_name FROM information_schema.columns WHERE table_name='表名'
这是sqlmap最核心的操作——先枚举你有什么表,再枚举表有什么列,最后把数据掏空。
| 手法 | 特征 | 流量包里的例子 |
|---|---|---|
| 闭合测试 | ' " ) ')) | test' → 看页面报不报错 |
| 布尔盲注 | AND 1=1 vs AND 1=2 | category=test' AND 4820=5440 AND 'DgnK'='DgnK |
| 时间盲注 | SLEEP(5) PG_SLEEP(5) | category=test';SELECT PG_SLEEP(5)-- |
| UNION注入 | UNION ALL SELECT NULL,... | category=-1319' UNION ALL SELECT CHR(113)... |
| 报错注入 | CASE WHEN ... THEN ... ELSE ... END | category=(CASE WHEN (8011=9069) THEN 8011 ELSE 8011(SELECT 8011...) END) |
| 堆叠查询 | ; SELECT ... | 分号隔开多条SQL语句 |
四、怎么区分不同的数据库?
不同数据库语法略有不同,攻击者往往先发几个探针来判断:
| 数据库 | 特有函数/语法 | 流量包里出现 |
|---|---|---|
| MySQL | SLEEP(5)、information_schema、#注释 | ✅ 大量使用 |
| PostgreSQL | PG_SLEEP(5)、CHR() | |
| SQL Server | xp_cmdshell、WAITFOR DELAY '0:0:5' | ✅ xp_cmdshell 也出现了 |
| SQLite | sqlite_master | 没出现 |
Q1:提交攻击者的IP
192.168.9.25
可以发现全部流量都来自192.168.9.25这个地址,点击就送了

Q2:攻击者使用了什工具进行攻击?请提交工具名称和版本
sqlmap/1.0#stable
可以发现的是流量都是http协议,筛选HTTP协议明文信息
在No.62中我们发现出现了sqlmap

Q3:攻击者针对哪个参数进行了注入?
category
我们先研究一下sqlmap注入了什么参数信息吧::pic

%打头的都是url协议,我们需要进行转化
9937%20AND%201%3D1%20UNION%20ALL%20SELECT%201%2CNULL%2C%27%3Cscript%3Ealert%28%22XSS%22%29%3C%2Fscript%3E%27%2Ctable_name%20FROM%20information_schema.tables%20WHERE%202%3E1--%2F%2A%2A%2F%3B%20EXEC%20xp_cmdshell%28%27cat%20..%2F..%2F..%2Fetc%2Fpasswd%27%29%23
转义之后
9937 AND 1=1 UNION ALL SELECT 1,NULL,'<script>alert("XSS")</script>',table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC xp_cmdshell('cat ../../../../etc/passwd')#
这就是很典型的sql语法了,出现了关键词AND SELECT这些语句查询
| 片段 | 解释 |
|---|---|
9937 AND 1=1 | 凑语法用的,让前面的条件成立 |
UNION ALL SELECT | 关键操作——UNION 可以把查询结果合并,攻击者想通过它从数据库里偷数据 |
1, NULL, '<script>alert("XSS")</script>', table_name | 选了4列数据,其中第3列塞了段 XSS 代码,第4列直接从数据库里取表名 |
FROM information_schema.tables | MySQL 里存储所有表名的地方——攻击者在列举你有哪些表 |
WHERE 2>1 | 总是真,相当于 "给我全部" |
--/**/; EXEC xp_cmdshell(...) | 前半 --/**/ 是注释掉后面,但这里还塞了 xp_cmdshell——这是 SQL Server 执行系统命令的功能,想读服务器上的密码文件 |
仔细观察可以发现http请求和参数
category参数出现了多次
http.request.uri contains "category="

Q4.该注入使用了什么数据库函数?
PG_SLEEP
HTTP 基础
GET /search?q=&category=test HTTP/1.1
- GET = 请求方式(去拿数据)
- /search = 页面路径
- ? 后面的 = 查询参数,格式:参数名=参数值,多个用 & 隔开
- 参数会传给后端代码使用
HTTP 请求报文的结构非常严格,无论是用浏览器正常访问,还是用 Burp Suite 等工具抓包改包,面对的都是这四个核心部分:请求行、请求头、空行、请求体。
1. 请求行 (Request Line)
请求报文的第一行,由三个要素组成,中间用空格隔开:
- 方法 (Method): 比如
GET、POST、OPTIONS等。它决定了对资源的操作意图。 - URL / URI: 请求的目标路径(这里是
/login.php)。 - 协议版本: 声明通信使用的协议,通常是
HTTP/1.1或HTTP/1.0。
2. 请求头 (Request Headers)
紧跟在请求行之后,由多行 Key: Value 格式的字段组成,是客户端向服务器传递附加信息的载体。在 CTF 的 Web 安全方向中,这里往往是漏洞利用的“重灾区”:
- Host: 指定目标服务器的域名和端口(HTTP/1.1 强制要求包含)。
- User-Agent: 声明客户端类型、操作系统及版本(有时会成为 SQL 注入的测试点)。
- Cookie: 携带会话凭证,用于维持状态。
- X-Forwarded-For (XFF): 虽非标准字段,但常用于识别通过 HTTP 代理连接的客户端原始 IP(经常被用来伪造 IP 绕过限制)。
- Content-Type / Content-Length: 告诉服务器请求体的数据格式(如 JSON、表单)和具体字节长度。
3. 空行 (Blank Line)
在协议解析中,这是极其关键的一环。 在最后一个请求头之后,必须有一个没有任何内容的空行。在底层内存和网络字节流中,它的十六进制表示是 0x0D 0x0A,即回车换行符(CRLF,\r\n)。
Web 服务器(如 Nginx、Apache)的解析器就是靠读取到这个连续的 \r\n\r\n 来判断“请求头结束了,接下来的数据属于请求体”。如果通过代理服务器转发时,攻击者巧妙地操控了数据包中的换行符,就可能引发 HTTP 请求走私 (HTTP Request Smuggling) 或 CRLF 注入攻击。
4.请求体 (Request Body)
实际传输的数据载荷。
- 如果是
GET请求,通常没有请求体(参数直接拼接在请求行的 URL 后面,例如?id=1)。 - 如果是
POST或PUT请求,这里就会存放真实的表单数据、JSON 字符串或上传的二进制文件。请求体的内容必须与请求头中的Content-Type和Content-Length严格匹配,否则服务器可能会拒绝解析或导致进程阻塞。
底层视角: 在 TCP/IP 协议栈中,这一整块有着特定格式的文本字符串,最终都会作为应用层的载荷(Payload),被打包塞进 TCP 的数据段中,等待 TCP 三次握手完成后,推送到服务器的指定端口。
URL 编码
URL 里有些特殊字符不能直接出现,需要编码:
| 字符 | 编码 | 原因 |
|---|---|---|
| ' | %27 | 会和 URL 边界混淆 |
| 空格 | %20 | URL 不允许有空格 |
| = | %3D | 参数值里的等号 |
| -- | %2D%2D | SQL 注释符号 |
Web 后端如何工作
用户输入 → HTTP请求 → 服务器代码 → SQL查询 → 数据库 → 返回结果 → 显示页面
典型代码(Python 伪代码):
用户通过URL传进来的 category 值
category = request.GET['category'] # ← 从HTTP参数取值
直接拼进SQL语句(有漏洞!)
sql = "SELECT * FROM books WHERE type = '" + category + "'"
发送给数据库执行
result = database.query(sql)
关键: 如果 category = 计算机' OR 1=1--,拼出来的 SQL 就变成了:
SELECT * FROM books WHERE type = '计算机' OR 1=1--' ' 闭合了前面的引号,OR 1=1 成了有效代码,-- 把后面注释掉。这就是SQL注入的本质。
SQL 注入核心概念
1.引号闭合
WHERE type = '用户输入'
用户输入 ' OR 1=1-- →
WHERE type = '' OR 1=1--'
↑闭合↑ ↑新代码↑↑注释↑
2. 注释符
| 字符 | 编码 | 原因 |
|---|---|---|
| ' | %27 | 会和 URL 边界混淆 |
| 空格 | %20 | URL 不允许有空格 |
| = | %3D | 参数值里的等号 |
| -- | %2D%2D | SQL 注释符号 |
上面这些都是科普,我们找常见用到的数据库函数就行
攻击者如果用了某个数据库函数,那函数名一定会出现在 HTTP 请求的 URL 里。你直接在 Wireshark 里搜函数名,命中的就是攻击者用过的。
| 你要搜的 | 说明 |
|---|---|
| SLEEP | MySQL 时间盲注 |
| PG_SLEEP | PostgreSQL 时间盲注 |
| CHR | 数字转字符 |
| CASE | 条件判断 |
| UNION | 合并查询 |
| SUBSTRING | 截取字符串 |
| LENGTH | 取长度 |
| DATABASE | 当前数据库名 |
| VERSION | 数据库版本 |
| CONCAT | 拼接字符串 |
http.request.uri contains "PG_SLEEP"
PG_SLEEP 次数7606
CHR 次数7401
CASE次数 7340
UNION次数170
SUBSTRING次数7260
LENGTH次数0
VERSION次数9
CONCART次数2
| 函数 | 次数 | 说明 | 揭示的信息 |
|---|---|---|---|
| CHR | 7401 | 数字→字符 | 数据提取靠 CHR() 拼字符串 |
| SUBSTRING | 7260 | 截取字符串 | 逐字符地提取数据 |
| PG_SLEEP | 7606 | PostgreSQL 延迟函数 | 用的是时间盲注 |
| CASE | 7340 | 条件判断 | 也用布尔盲注 |
| UNION | 170 | 合并查询 | UNION注入只试了一小部分 |
| VERSION | 8 | 数据库版本 | 只查了几次版本(指纹识别) |
| CONCAT | 2 | 字符串拼接 | 只出现2次(MySQL风格,几乎没用) |
| LENGTH | 0 | 取字符串长度 | 没用 |
PG_SLEEP(7306) + CASE(7340) + SUBSTRING(7260) + CHR(7401) 都是 7600 级别,而 UNION 只有 170。
这说明什么?——攻击者绝大部分时间在一比特一比特地猜数据(盲注),而不是一次性把数据查出来(UNION注入只用了170次,试了试没成主力)。
盲注的典型流程:
- 用 SUBSTRING(字段, 位置, 1) 取第1个字符
- 用 CASE WHEN (取到的字符=某个值) THEN PG_SLEEP(5) ELSE 0 END
- 页面延迟5秒 → 猜对了;秒回 → 猜错了
- 换一个字符继续...把整段数据一位一位猜出来
结论2:目标数据库 = PostgreSQL
PG_SLEEP 是 PostgreSQL 专属函数(MySQL 的叫 SLEEP),而且用了 7300+ 次,是绝对主力。
反观 MySQL 风格的 CONCAT 只有 2 次——攻击者早期试探过 MySQL,发现不对就放弃了。
证据链: PG_SLEEP 7306次 ← PostgreSQL 专属,铁证 CHR + || 拼接 ← PostgreSQL/Oracle 风格 CONCAT 2次 ← MySQL 风格,几乎没被用
▎ PG_SLEEP()、CHR()、SUBSTRING()、CASE WHEN,指向 PostgreSQL 数据库的时间/布尔盲注。
Q5.攻击者枚举出数据库中共有几张表?
攻击者每提取一张表名,就会发一组"猜字符"的请求。你顺着请求里 OFFSET 的编号,就能数出有几张表。
5
http.request.uri contains "pg_tables"
http.request.uri:这是 Wireshark 定义的协议字段。它特指客户端发起的 HTTP 请求(Request)中的请求行里的 URI 部分。比如对于请求 GET /index.php?id=1 HTTP/1.1,这里的 URI 就是 /index.php?id=1。
contains:这是 Wireshark 的过滤操作符,表示“包含”。它不需要完全匹配整个字段,只要字段中出现了后面的字符串,就会把该数据包筛选出来。
"pg_tables":这是你要查找的特定目标字符串。
为什么是uri?uri又是什么?
所有的 URL 都是 URI,但并非所有的 URI 都是 URL。
核心区别:标识 (Identifier) vs 定位 (Locator)
- URI (统一资源标识符):它的唯一目的是“证明这个资源是谁”。它不在乎你用什么方式去获取这个资源,也不关心它的物理位置,只要能唯一区分它就行。
- 现实比喻:你的身份证号(能证明你是谁,但光看号码不知道你现在身处何地)。
- URL (统一资源定位符):它不仅能证明资源是谁,还必须“提供具体的访问路径和协议”,明确告诉你用什么方法去哪里获取它。
- 现实比喻:你的详细收货地址加上物流方式(比如:顺丰快递送到北京市海淀区某某路1号)。
| 概念 | 在 HTTP 请求中的体现 | 核心特征 |
|---|---|---|
| URI | 请求行中的 /article.php?id=1 | 仅标识了服务器内部的资源路径。它没有提供协议类型(如 HTTP)和目标主机名。 |
| URL | 完整的 [http://192.168.1.100/article.php?id=1](http://192.168.1.100/article.php?id=1) | 包含了访问协议 (http://)、主机位置 (192.168.1.100) 和具体路径。这是一个完整的网络坐标。 |
对No.4663进行 info的url解析
SQL翻译是统计多少个不同的命名空间,还记得我们这个数据库是哪一个咩?PG_SLEEP是PostgreSQL的特有函数
tablename 就是 PostgreSQL 的 pg_tables 视图里,专门存放"表的名字"那一列
扩展一下别的数据库视图
| 数据库 | "名单"叫什么 | "表名字"列叫什么 |
|---|---|---|
| PostgreSQL | pg_tables | tablename |
| MySQL | information_schema.tables | TABLE_NAME |
| SQL Server | sys.tables / INFORMATION_SCHEMA.TABLES | name |
所以我们找找语句包含tablename吧
http.request.uri constains "tablename"
对No.7071的url进行解析
还得在了解一个语法就是SQL的跳转语法
OFFSET N -- 跳过前 N 行
OFFSET 只是"跳过",它本身不取任何数据。 单看 OFFSET,它只是把游标往后挪。
LIMIT 是 SQL 中用来限制查询结果返回行数的语法。
SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename OFFSET 0 LIMIT 1 -- 跳过0行,然后取1行 → 第1行 OFFSET 1 LIMIT 1 -- 跳过1行,然后取1行 → 第2行 OFFSET 2 LIMIT 1 -- 跳过2行,然后取1行 → 第3行
http.request.uri contains "tablename" and http.request.uri contains "OFFSET%200"
%20在url是空格意思,%20 0是选出第一张表
可以发现存在%200-%204说明是5张表
对No.7195进行解析(需要右键选择复制再选择摘要为文本)

GET /search?q=&category=test' ← 闭合引号
AND 9996=(CASE WHEN ( ← 布尔包装
ASCII(SUBSTRING((
SELECT tablename ← 提取表名
FROM pg_tables
WHERE schemaname='public'
ORDER BY tablename
OFFSET 0 LIMIT 1 ← ① 第1张表
)::text FROM 1 FOR 1)) ← ② 第1个字符
> 341578) ← ③ 阈值(探路值)
THEN (SELECT 9996 FROM PG_SLEEP(1)) ← ④ 真→睡1秒
ELSE 9996 END)
AND 'obFL'='obFL ← 收尾凑语法
| 位置 | 内容 | 大白话含义 |
|---|---|---|
| ① | OFFSET 0 | 锁定表:跳过前面 0 张表,精准提取名单上的第 1 张表的名字。 |
| ② | FROM 1 FOR 1 | 锁定字符:这是 SUBSTRING(字符串 FROM 起始位置 FOR 长度) 的标准语法。意思是从第 1 个位置开始,只截取 1 个字符。 |
| ③ | > 341578 | 猜数字游戏:设定的对比阈值(探路值)。在这个判断中,如果前一个字符转换成的值大于 341578,条件就成立。 |
| ④ | PG_SLEEP(1) | 信号回传:如果前面的条件成立,就让 PostgreSQL 数据库“睡” 1 秒钟,从而让网页加载变慢,以此作为“猜对了”的接头暗号。 |
观察可以发现7670之后请求间隔了1s,说明7670的语句判断正确触发了PG_SLEEP(1)
看看No.7670发生了什么语句吧

第一个字符的ascii大于>64成立,二分查找通过
No.7685的ASCII > 96通过
No.7716 ASCII>104不成立
区间收缩96,104
No.7748 ASCII>98不成立
收缩区间96,98
No.7764 ASCII > 97成立
收缩区间97,98
No.7780 ASCII != 98 成立
说明这个值就是98 (b)
ASCII表格查询链接:
以此类推找到!=判断就行
No.7795 ASCII > 96
No.7827 ASCII > 104
No.7843 ASCII > 108
No.7859 ASCII > 110
No.7875 ASCII > 111 不成立
范围收缩110,111
No.7891 ASCII != 110 不成立
第二个ASCII值是111 (0)
No.7905 ASCII > 96成立
第三个字符是ASCII = 111 (0)
...以此类推(其实发现sleep之后发现ASCII突然变小了就回到上一条去看看是不是!=)
第四个字符是ASCII = 107 (k)
第五个字符是ASCII = 115 (s)
第六个字符是ASCII = 0 (空格)
结合就是books
以此类推
这五张表达的名字就是 Books,borrow_records,notices,secret_flag,users
Q6:攻击者从哪张表格获取了flag?提交表名
secert_flag
从名字我们可以推断出是secret_flag了,不过找找证据看看
http.request.uri contains "secret_flag"
筛选出来的内容对No.16426分析

数一下 secret_flag 表有几行,看看结果数字的第一个字符是不是比'3'大。是的话睡1秒告诉我,不是就秒回。
No.16473告诉我们这个行只有1行
然后看到这个数据表是有434匹配的,所以我们要先知道这个表有哪些列
对当前的wireshark筛选选择文件-导出分组解析结果为csv,用excel打开分析好一些。
大概在这里你就可以发现筛选的是列信息了(当然你可以也可以使用代码转换),然后就可以发现是筛选出三个列信息是flag_value、id和created_at三个信息

Q7.提交 flag
flag{8673db96fa2dceeb}
继续接着上面的数据
看看flag_value值有什么吧
http.request.uri contains "secret_flag" && http.request.uri contains "flag_value"
手法就和前面的一样了,这里也不赘述了

Q8.攻击者获取了管理员账户信息,管理员密码是什么?
123456
同样的方法去找user表
http.request.uri contains "users"
然后还原的数据是这个(提取数据给ai找的,懒得一个个匹配数据了)
| 用户 | username | password (MD5) | phone | |
|---|---|---|---|---|
| 1 | admin | e10adc3949ba59abbe56e057f20f883e | admin@tushuguan.cn | 13800000001 |
| 2 | zhangwei | c33367701511b4f6020ec61ded352059 | zhangwei@tushuguan.cn | 13800000002 |
| 3 | liming | 96e79218965eb72c92a549dd5a330112 | liming@corp.cn | 15900001001 |
| 4 | wangfang | 25d55ad283aa400af464c76d713c07ad | wangfang@corp.cn | 15900001002 |
| 5 | chenxi | 5f4dcc3b5aa765d61d8327deb882cf99 | chenxi@corp.cn | 15900001003 |
然后把拿到的hash值进行解析


评论区
评论加载中...