通过 SSTI 在 Spring Boot 错误页面上使用 Akamai WAF 绕过进行 RCE
前言
这篇文章主要讲述了作者与Dark9T (@UsmanMansha)在 Bugcrowd 上合作的成功经历。他们成功绕过了 Akamai WAF,并在运行 Spring Boot 应用程序上使用 Spring 表达式语言注入实现了远程代码执行(P1)。这是他们在该程序上找到的第二个通过 SSTI 实现 RCE,第一个 RCE 在程序实施了 WAF 后,他们在应用程序的另一个部分成功绕过了它。文章详细介绍了他们是如何做到的。
在作者的帮助下,他们发现了一个关于 Spring Boot 错误页面问题的漏洞。该漏洞的基本原理是,Spring Boot 的易受攻击版本会使用 SpEL(Spring 表达式语言)表达式将抛出的异常的错误消息渲染到页面本身中。受影响的 Spring Boot 框架版本将允许对这个表达式进行递归评估,因此包含有效 SpEL 表达式的错误消息(例如 $(7*7))将在渲染错误页面时进行评估。
在成功利用 SpEL 注入后,作者详细介绍了他们采取的步骤,包括尝试显而易见的方法、确定如何获取任意类的引用以及构建攻击载荷。最终,他们成功地构建了一个绕过 WAF 并执行远程代码执行的有效负载。
文章以作者对于 WAF 绕过过程中的深入思考和技术挑战的总结作为结束。
概括
这篇文章讲述了我与Dark9T ( @UsmanMansha ) 在 Bugcrowd 上托管的私人程序上进行的一次成功合作。我们最终能够绕过 Akamai WAF 并在运行 Spring Boot 的应用程序上使用 Spring 表达式语言注入实现远程代码执行 (P1)。这是我们在该程序上发现的第二个通过 SSTI 的 RCE,在第一个之后,该程序实现了一个 WAF,我们能够在应用程序的不同部分绕过它。请继续阅读,了解我们是如何做到的!
图 1:已解决的错误的屏幕截图
介绍
乌斯曼在 Slack 服务器上联系了我,我们都是该服务器的成员。他们发现了潜在的 SSTI,但由于 Akamai WAF 而无法利用它:
图2:Slack消息截图
快速浏览后,这似乎是Github 上描述的著名 Spring Boot 错误页面问题的一个案例- 请注意,据我所知,从未为此发布过 CVE。此漏洞已以各种形式被报道,例如0xdeadpoool在其博客上对此进行了报道。
该漏洞的基本原理是,存在漏洞的 Spring Boot 版本会Exception使用 SpEL(Spring 表达式语言)表达式将抛出的错误消息渲染到页面本身。Spring Boot 框架的易受攻击版本将允许对该表达式进行递归计算,因此$(7*7)在呈现错误页面时将计算包含有效 SpEL 表达式(例如 )的错误消息。
在这种情况下,我们可以看到q易受攻击的 URL 的参数支持类型注入${x*y},并在错误文本中返回数学结果:
图 3:初始 SpEL 测试的屏幕截图
通过 SpEL 进行 RCE 的步骤
如果您之前没有使用过此类易受攻击的应用程序,我强烈建议您使用https://github.com/jzheaux/spel-injection等应用程序进行一些练习,您可以在其中尝试 SpEL 的构建方式以及在 Spring 应用程序中处理(并可能受到保护)。虽然此应用程序不直接处理此特定漏洞,但 SpEL 在 Spring 生态系统中使用得如此频繁,因此值得进行一些练习并熟悉代码。
本博客不会向您介绍 Spring 表达式语言,因为该主题非常复杂,本质上它是一种允许基于上下文导航 Spring 对象的语言,类似于其他服务器端模板语言。它在各种 Spring 框架组件中的许多地方使用,并且可用对象和数据的确切范围在很大程度上取决于它的使用位置。通常,您可以执行 Java 方法、构造对象等 - 不如 FreeMarker 或 Velocity 强大,但风险状况相似。您可以在 Spring 参考文档中阅读有关 SpEL 及其语法的信息。
一般来说,SpEL 的目标是最终调用方法java.lang.Runtime.exec
或java.lang.ProcessBuilder.start
允许执行攻击者选择的操作系统命令,使用如下所示的表达式:
${T(java.lang.Runtime).getRuntime().exec("<my command here>")}
如果您想要命令的输出,表达式会变得有点复杂,但让我们从这里开始。
快速说明 - 花费的时间/精力
了解我的人都知道,我主要是一名手动测试人员,依靠我丰富的开发/架构经验而不是蛮力来发现棘手的错误。尽管阅读博客文章可能会让人觉得错误很明显或者特定路径很明显,但为了提供一些统计数据,从 Usman 的最初 Slack 消息到完整的 RCE 需要我:
大约 500 次手工尝试绕过 WAF
从最初的尝试到第一次成功的 RCE(执行命令)大约需要 14 个小时——
uname -a
请注意,我休息了一会儿吃饭、散步、思考解决方案等——这并不是连续 14 个小时!
我之所以将这些内容包括在内,是因为博客文章通常会使这种错误“看起来”比实际情况容易得多,从而引导读者走上冒名顶替综合症等的黑暗道路,只是强化了这一点,即使您知道自己在做什么。正在做的事情,有时候 bug 真的很棘手!不要放弃!😄
第 1 步 - 尝试显而易见的方法
首先,我们必须确定如何访问该类java.lang.Runtime,以便我们可以获得它的实例,并在其上调用该exec方法。我们尝试了最明显的${T(java.lang.Runtime)}- 这是通过名称引用 Java 类的 SpEL 简写,当然它被 Akamai WAF 阻止了:
图 4:第一次尝试
由于 Akamai WAF 妨碍了我,我怀疑这行不通,但是当尝试解决 WAF 时,从您知道有效的小事情构建到更大、更复杂的有效负载非常重要。当试图避免 WAF 规则时,RCE、SQLi、XSS 或任何复杂的有效负载都是如此,通常 WAF 被编码为识别明显的有效负载,但(正如我们将看到的)无法找出复杂的有效负载。
第 2 步 - 弄清楚如何获取任意类
通常,基于 Java 的代码注入漏洞的下一阶段是弄清楚如何获取对任意 的引用Class,从中我们可以使用直接方法调用或基于反射的调用来获取我们想要的方法。
最简单的方法是执行如下操作(在本例中有效):
${2.class}
回复:
class java.lang.Integer
这是一个好兆头,我们知道我们可以访问该java.lang.Integer
Class
对象(如果您需要复习一下[https://stackoverflow.com/questions/1215881/the-difference- Between-classes-objects-and-instances](这个SO答案是一个好的开始)),从这里我们应该能够获得forName实例化任意类的方法。我们来试试吧!
${2.class.forName("java.lang.String")}
回复:
<H1>Access Denied</H1>
You don't have permission to access ...
正如预期的那样,使用带有字符串的方法的明显有效负载forName不起作用,并且很容易被 Akamai WAF 检测到。在下一轮探索中,我能够确定对单引号和双引号应用了某种转换,导致使用这些字符中的任何一个的表达式格式错误。因此,即使我们可以到达该Class.forName方法,我们也无法采取类似的直接路线${2.class.forName("java.lang.Runtime")...},而是需要找到其他方法来构造要实例化的类的名称。
第 3 步 - 弄清楚如何获取任意字符串
我知道,出于多种原因,需要能够构建任意字符串才能实现完整的 RCE:
要实例化或引用的类的名称
方法名称(很可能.exec()也被 WAF 阻止)
要执行的命令
请记住,我不能使用任一类型的引号字符,因此在这种情况下不可能进行简单的字符串连接。我需要找到一种方法将整数值(ASCII 或十六进制)转换为字符,然后连接字符以形成String.
我已经多次遇到过这种情况,无论是单独还是在协作中,我总是参考Java API 文档,其中有很多关于可用方法和类的有用信息,尽管我熟记许多核心 Java 类,我经常会发现一些隐藏的宝石可以完全满足我的需要!
Java 标准库中的一些明显选择:
java.lang.String构造函数,采用字节数组(受到mykong和Bealdung的启发)
java.lang.Character.toString方法,在Javadoc中描述
经过一番实验,我确定基本上不可能调用任何构造函数,因为在 SpEL 中调用构造函数的两种方法,无论是new、T()、还是通过反射,newInstance也都被 WAF 阻止了。
所以看起来这个java.lang.Character.toString方法是可行的,只有一个问题......
第 4 步 - 弄清楚如何获取对java.lang.Character类的引用
由于java.lang.Character.toString是类上的静态方法java.lang.Character,因此我只需要对此类型的对象的引用即可访问该方法。因为 SpEL 是动态的,所以我不认为它像在静态 Java 代码中那样支持转换,例如(char)99
- 并且不幸的java.lang.Class是被 WAF 阻止,所以我无法使用该java.lang.Class.cast方法。
所以我最终得到了以下链:
弄清楚如何获取对象的String引用
调用java.lang.String.charAt该对象上的方法(返回 a java.lang.Character)
在此角色上调用静态方法 - 因为它是静态方法,所以它的值是toString什么并不重要Character
因此,我终于有了构建任意所需的小工具String:
${(2.toString()+2).charAt(0).class.toString(99)}
回复
c
请注意,这99是字符 的 ASCII 值c。成功!
由于+允许该字符通过 WAF,并且在这种情况下,我现在能够使用这种单个字符串联的方法构建字符串。
第 5 步 - 构建攻击负载
所以,现在我们有了我们需要的一个成分 - 构建任意的String- 我们还需要一个成分,这是一种调用该java.lang.Runtime.exec方法的方式。我最终使用了一种与此处描述的技术类似的技术,基本上如下:
使用反射来访问该Class.forName方法
java.lang.Runtime使用要传递的值构建一个字符串forName
使用反射来访问java.lang.Runtime.getRuntime方法(需要获取类的实例来调用方法)
使用值构建一个字符串exec和/或使用反射来查找类exec的方法java.lang.Runtime
使用 RCE 负载值构建一个字符串以传递给该exec方法
在这个开发阶段,我花了很多时间迭代各种反射调用的输出。这一点尤其重要,因为不同的 JVM 将返回不同的值,特别是当您使用java.lang.Class.getMethods反射技术时。
不要盲目调用反射方法!有些危险的方法java.lang.Runtime
会shutdown
立即终止 JVM!
第 6 步 -(浪费时间)尝试解决GET长度限制
此时我意识到,对于某些有效负载,如果我构建一个长 RCE 命令(例如 annslookup或类似命令),我最终会得到一个非常长的有效负载。我的单个字符的有效负载c是 45 个字节长 ( ${(2.toString()+2).charAt(0).class.toString(99)}
)!
由于GET某些浏览器和/或服务器强制要求请求最大长度约为 2kb,这意味着我可以构建的最长请求String长度可能只有大约 45 个字符——这是一个大问题!
此时,我最终陷入了一些困境,以找出如何更有效地String从字节列表创建 a 。我尝试了很多东西,几乎有一个使用 SpEL 简洁的集合投影功能来工作,但不幸的是,我被这个目标运行的版本中的 Spring bug 阻止了。最终我找不到任何更有效的方法来String逐个角色地构建角色。
从这个意义上说,我最终很幸运,服务器接受了GET超过 2kb 的请求(最终有效负载略低于 3kb),并且通常在大多数服务器上,在 4kb 之前是安全的。
第 7 步 - 构造最终有效负载
在第 5 步之后,我基本上已经拥有了构建最终有效负载所需的所有部分,这本质上是以下有效负载的翻译:
org.apache.commons.io.IOUtils.toString(java.lang.Runtime.getRuntime().exec("uname -a").getInputStream())
图 5:最终有效负载
我不会以文本形式提供有效负载,因为我不希望有人盲目地复制粘贴到它可能无法工作的上下文中,但希望这篇文章为您提供了构建自己的有效负载以绕过的方法a WAF 和服务器端限制。
最后的想法
我发现 WAF 绕过了 RCE 和 SQL 注入等关键漏洞,这是一些最有趣的漏洞。当然,回报是好的,但这类错误确实需要深入了解特定错误为何起作用以及它执行的上下文。
在这种情况下,需要深入了解 Java 和 SpEL 功能来构建既可以绕过 Akamai WAF 又可以在其执行上下文中工作的有效负载。
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。