[MoeCTF 2024]Web 官方wp(Sxrhhh)¶
这里是 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
,可以看到 "响应标头" 一项中有 Flag
和 Nextpage
两个头。记录并进入下一关。
此题知识点为 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
,然后点击即可。
以上为例,我们将按钮 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
,也就是说不要求大小写。所以我们直接这么传:
flag7¶
最后,题目提示 eval($_POST['what']);
,这是典型的 php 一句话木马。我们使用蚁剑连接就可以,但是我们也可以直接获取 flag:
最后,我们也就获得了所有的 flag, 拼接在一起,即:
最后通过 base64 解码即可。如果不知道的话,根目录下还有提示:
这是一个很明显的 base64 编码,之后遇到应该可以认出来。
垫刀之路01: MoeCTF?启动!¶
这题开始是一共 7 题的垫刀之路系列,考点单一,重点是让大家学到东西。
第一题题目提示是远程命令执行,我们在上面运行的所有东西都是直接直接作为 linux 命令执行后返回结果。所以我们可以直接使用 cat /flag
命令,得到提示,flag 在环境变量里。我们使用以下命令都可以:
垫刀之路02: 普通的文件上传¶
这题是文件上传,暂时没有过滤,所以我们构建一个 php 文件,比如 evil.php
,内容为:
然后根据题目提示,访问 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注入。
具体过程不展示,总之就是单引号的万能密码:
把上面的填在密码那里就行。
垫刀之路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
模块并执行系统命令。
这里有几个注意点,如果使用 os.system()
函数,可能会没有回显。这应该是控制台的特性,我们换个方案就好了。
第二点就是如果你访问的是 /flag
,就会提醒 “远在天边,近在眼前”, 意思就是,你不要跑到根目录找 flag 了,就在工作目录下面。用意和之前一样,既然已经无成本找文件运行命令了,那还是希望你多找找,多认识几个可能藏 flag 的地方。
静态网页¶
这题原型是我自己的博客,把所有链接都删了所以只有一个首页。题目提示这是一个静态网页,一般不会有什么可攻打的地方,所以思考相关考点,也就是查看源代码之类的。所以我们查看源代码,在底部发现一行中文注释:好想知道她是怎么换衣服的啊啊啊
,在 ctf 题目中莫名出现中文注释比较突兀,我觉得这很明显是个提示,应该往右下角 live2d 小人换衣服这个细节去考虑。
另外还有一点,就是如果你发现右下角小人可能有点突兀,如果你闲着没事干,和小人玩起来了,可能会发现他会说:你再点我我也不会告诉你我的衣服是向后端请求的!
,这句话也有点突兀,没注意可能就略过去了。但是如果注意到了,细看就会发现端倪:为什么静态网页会有后端。
两个点,都像你提示换衣服会向后端请求。我们打开 devtools,查看 "网络" 选项卡,点击小人的换衣服,我们可以看见多出来几个请求。我们点击 get
这个请求,双击查看内容,是一个 json 文件。我们在底部可以看见所谓的 flag
。内容为:Please turn to final1l1l_challenge.php
所以我们访问 /final1l1l_challenge.php
即可。同样也是 php 代码审计,要求传入一个 a
和 b
参数,要求 a 和 b 都不能是纯数字,并且 a 要求 a == 0
。这个涉及到 php 的弱类型比较。在php8以下,开头为0的或者开头不为数字的字符串可以和 0 弱比较相等。所以我们传入 a=0a
即可绕过前半部分。
后半部分要求是 md5($a) == $b[$a]
,有的师傅可能认为这是 md5 绕过,并且说绕不过去。其实只要静下来定睛一看就能看出来,这个表达式其实并没有对 a
参数做出任何限制,全是对 b
的限制。所以细看就能知道,我们要求的是 b[a]
就是 a 的 md5 值。至于如何赋值,请看下文:
可以看到,想要传数组,直接把 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
就是 %
本身。
文章热度:0次阅读