学习学习学习

文件写入

/app/system/include/class/web.class.php中的__destruct()存在任意文件写入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
		global $_M;
		//读取缓冲区数据
		$output = str_replace(array('<!--<!---->','<!---->','<!--fck-->','<!--fck','fck-->','',"\r",substr($admin_url,0,-1)),'',ob_get_contents());
		ob_end_clean();//清空缓冲区

		...

		if($_M['form']['html_filename'] && $_M['form']['metinfonow'] == $_M['config']['met_member_force']){
			$filename = urldecode($_M['form']['html_filename']);
			if(stristr(PHP_OS,"WIN")) {
				$filename = @iconv("utf-8", "GBK", $filename);
			}
			if(stristr($filename, '.php')){
				jsoncallback(array('suc'=>0));
			}
			if(file_put_contents(PATH_WEB.$filename, $output)){
				jsoncallback(array('suc'=>1));
			}else{
				jsoncallback(array('suc'=>0));
			}
		}else{
			echo $output;//输出内容
		}

从缓冲区中获取内容,然后从请求的html_filename获取文件名,验证metinfonow与数据库中的met_member_force是否一致,若不一致则直接输出缓冲区中的内容,一致则将缓冲区中内容写入html_filename提供的文件名中。可以看到,这里没有任何限制文件内容和文件后缀的代码。

如何调用

经过不懈的寻找,我们发现在个人中心的头像上传处调用了web app/system/include/module/uploadify.class.php

1
2
3
4
5
6
7
class uploadify extends web {
	public $upfile;
	function __construct(){
		parent::__construct();
		global $_M;
		$this->upfile = new upfile();
	}

可以看到uploadify这个类继承了web类,虽然这里限制了文件上传的后缀,但是其报错信息是在缓冲区中,我们正好可以利用这个报错,来控制将来要写入文件的内容。 这样就将phpinfo()写入到缓冲区了。 接下来就是如何获取数据库中的met_member_force

sql注入

值得一提的是,这个cms将单引号过滤了,并且几乎所有的sql语句的参数都是由单引号包裹。 app/system/include/function/common.func.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
/**
 * 对字符串进行SQL注入过滤
 * @param  string/array	$string	处理的字符串或数组
 * @return array                返回处理好的字符串或数组
 */
function sqlinsert($string){
	if(is_array($string)){
		foreach($string as $key => $val) {
			$string[$key] = sqlinsert($val);
		}
	}else{
		$string_old = $string;
		$string = str_ireplace("\\","/",$string);
		$string = str_ireplace("\"","/",$string);
		$string = str_ireplace("'","/",$string);
		$string = str_ireplace("*","/",$string);
		$string = str_ireplace("%5C","/",$string);
		$string = str_ireplace("%22","/",$string);
		$string = str_ireplace("%27","/",$string);
		$string = str_ireplace("%2A","/",$string);
		$string = str_ireplace("~","/",$string);
		$string = str_ireplace("select", "\sel\ect", $string);
		$string = str_ireplace("insert", "\ins\ert", $string);
		$string = str_ireplace("update", "\up\date", $string);
		$string = str_ireplace("delete", "\de\lete", $string);
		$string = str_ireplace("union", "\un\ion", $string);
		$string = str_ireplace("into", "\in\to", $string);
		$string = str_ireplace("load_file", "\load\_\file", $string);
		$string = str_ireplace("outfile", "\out\file", $string);
		$string = str_ireplace("sleep", "\sle\ep", $string);
		$string = strip_tags($string);

		if($string_old!=$string){
			$string='';
		}
		$string = trim($string);
	}
	return $string;
}

依旧是经过我们不懈的寻找,在app/system/message/web/message.class.php中的checktime()中,发现了一处没有被单引号包裹并且参数可控的sql语句 $met_fd_time=DB::get_one("select * from {$_M[table][config]} where lang ='{$_M[form][lang]}' and name= 'met_fd_time' and columnid = {$_M[form][id]}"); 虽然只能盲注,但是又总是比没有好。 于是经过我们不懈的努力,waf绕过了 message/?action=add&id=44 or name=0x6d65745f6d656d6265725f666f726365 and substr(value,1,1)=0x66&lang=cn

POC

这样一个完整的调用链就完成了,首先通过sql注入获得met_member_force,再利用文件后缀不合法的报错将php代码写入缓冲区,在web类销毁的时候成功将代码写入我们提供的文件名中 密码保护2333

最后

为了方便验证,所以关闭了前台验证码。如果有验证码,可以考虑机器学习或者接入打码平台