• [WooYun-2016-226888] SpringBoot框架SpEL漏洞复现与原理详细分析


    0x01 前言:

    这是2016年爆出的一个洞,在 CVE 上没有找到对应编号,经过多方资料查阅最终确认是唐朝实验室在乌云上提交的该通用漏洞,缺陷编号为WooYun-2016-226888。漏洞的产生主要来自程序员没有做异常处理 (开发要养成好习惯✊),使用了 springboot 默认的报错页面 (Whitelabel Error Page),默认报错页面会把判断参数是否是 SpEL 表达式,如果是就进行解析,进而导致一些危险操作的发生。由于继 2016年后依旧频频出现相关框架出现 SpEL 表达式注入漏洞,具有学习价值。通过这次复现与代码审计分析,彻底弄明白 SpEL 表达式注入漏洞的原理。

    在 github issue 又看到早在 2015年就有人提出过这个漏洞,哎呀不管啦,就当乌云的编号算最初发现的,23333在这里插入图片描述


    0x02 版本范围:

    Spring Boot <= 1.3.0


    0x03 漏洞复现:

    1、前期准备:(本地复现):

    • java 1.8
    • maven

    2、创建 springboot 项目,引入 springboot 1 依赖, 并编写一个 Controller方法:

    • pom.xml 引入如下依赖:
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
                <version>1.3.0.RELEASEversion>
            dependency>
        dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 编写一个 Controller方法:

    在这里插入图片描述

    2、启动springboot服务, 使用恶意 SpEL 表达式参数访问 getInfo 接口:

    弹出计算器 SPEL 表达式:${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}

    在这里插入图片描述

    漏洞复现完毕,计算器成功弹出。可能会有小伙伴疑惑为什么不直接 exec(“clac”), 而是转来转去?后面慢慢揭晓~~


    0x04 原理分析:

    1、前置知识:

    • SpEL是什么?

    SpEL是Spring提供的一种的表达式语言,支持在运行时查询和操作对象。SpEL提供了对应的解析器 SpelExpressionParser 用于解析SpEL表达式。

    • 造成漏洞的表达式类型:

    Types 类: 可以使用T运算符来指定java.lang.Class的实例,静态方法也通过此运算符调用。除了java.lang包下的类,其余的类均需使用全限定类名。

    • 使用案例:(Types 类)
    ExpressionParser parser = new SpelExpressionParser();
    Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
    Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
    
    • 1
    • 2
    • 3
    • springmvc 工作流程图:

    在这里插入图片描述


    2、代码审计分析:

    • 1、先在会出现报错的地方打个断点:

    在这里插入图片描述

    • 2、直接跳到 parseInt() 执行的 invoke(), 抛出异常:java.lang.reflect.InvocationTargetException

    在这里插入图片描述

    • 3、异常一直向外抛,到了前置控制器 DispatcherServlet 的 doDispatch 方法中, 然后执行 processDispatchResult() 方法 (处理handler执行结果,不管结果是ModelAndView还是异常,最终都被处理为ModelAndView)。

    在这里插入图片描述

    • 4、进入 processDispatchResult() 调用 processHandlerException() 判断是否有异常,若无异常则走正常流程,若有异常则需要进行处理 mv = processHandlerException(request, response, handler, exception)。 再接着就是遍历spring已经注册的异常处理解析器直到有处理器返回mav。然而并没有找到, 抛出异常至 DispatcherServlet 的doDispatch 方法中。

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    • 5、doDispatch 方法中执行完后,开始调用 publishRequestHandledEvent() 发布请求处理完后的事件。

    在这里插入图片描述

    • 6、经过一段又臭又长的跟踪,最后来到了 StandardHostValve类 180行的 throwable(), 查找应用程序级错误页面,执行又跳到了 DispatcherServlet 的 doDispatch() 方法,看下参数,发现request对象里面的参数发生了变化,变成请求 127.0.0.1 下的 /error 页面,并携带了我们访问/getInfo 接口的参数。

    在这里插入图片描述
    在这里插入图片描述

    • 6、刚出 DispatcherServlet 的 doDispatch() 现在又进去了,这次是访问springboot默认提供的报错error页面,进入核心方法 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()) 获取 ModelAndView

    在这里插入图片描述

    • 7、继续跟进来到了 RequestMappingHandlerAdapter 的 invokeHandlerMethod 设置参数解析器和返回值处理器。里面调用了 invocableMethod.invokeAndHandle() 对请求参数进行处理,并将返回值封装为一个ModelAndView对象, 最后通过getModelAndView()对封装的ModelAndView进行处理,主要判断当前请求是否进行重定向, 如果进行了重定向还会判断是否需要将flashAttributes封装到新的请求中。

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    • 8、现在执行完 ha.handle(), 拿到了 ModelAndView, 开始调用 processDispatchResult() 处理 mv.handler执行的结果, 就是对 ModeAndView 进行视图渲染。render() 方法主要有以下两个功能: 1、根据view名称封装view视图对象;2、渲染数据。

    在这里插入图片描述
    在这里插入图片描述

    • 9、执行 resolveViewName() 返回了熟悉的html页面。但是数据还没有渲染进去。这下就需要 view.render() 出场了。

    在这里插入图片描述

    • 10、步入 view.render(), 看看里面做了什么操作。这里的逻辑也比较简单,继续跟进replacePlaceholders方法,方法名称翻译过来就是替换占位符,也就是替换上面的 html template 模板里的 ${参数}。

    在这里插入图片描述

    • 11、继续跟进replacePlaceholders方法,可见又调用了paseStringValue方法,继续跟进paseStringValue方法,就会看到重点逻辑了。

    在这里插入图片描述
    在这里插入图片描述

    • 12、上面的逻辑就是在 html template 模板里找到 ${参数} 的位置,并通过 placeholderResolver.resolvePlaceholder(placeholder) 获取真实的值进行填充替换,循环往复直到全部替换完成。我们的恶意代码 (${T(Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}) 是替换 ${message} ,所以我们直接跳到对 ${message} 的替换处理。

    在这里插入图片描述

    • 13、看到下面还有一个 HtmlUtils.htmlEscape() 方法,做开发的应该不陌生,这是对特殊字符进行编码转义的,防止报错页面 (Whitelabel Error Page)出现 XSS 漏洞。进去看看过滤了什么特殊字符。

    在这里插入图片描述
    在这里插入图片描述

    转义了特殊字符:<, >, ", &, ’

    • 14、如果我们的恶意代码是 ${T(Runtime).getRuntime().exec('calc')}, 就会被转成${T(Runtime).getRuntime().exec('clac')}, 被转义后就会出现问题。不妨我们把 value 变量值覆盖成 ${T(Runtime).getRuntime().exec('calc')}, 我们继续往下看。

    在这里插入图片描述

    • 15、转义以后返回 proval = For input string: "${T(Runtime).getRuntime().exec('calc')}", 有没发现,字符串里面还有 ${} 表达式。

    在这里插入图片描述

    • 16、可以看到如果 proval 不为空,再次进行 ${} 模板匹配。最终匹配到了 T(Runtime).getRuntime().exec('calc'),这不就是一个 Type 类型的Spel表达式吗?

    在这里插入图片描述

    • 但是执行 Spel 解析的时候直接报错了,怎么 Spel 解析器不认转义后的单引号?翻一翻Spring 官方文档,最终找到了答案。

    在这里插入图片描述
    在这里插入图片描述

    • 这下我们知道了转义以后的编码中存在 &, 属于非法字符,导致无法正常解析弹出计算器。因为不能出现单双引号,所以借助一些String类的特性,可以传入byte数组。比如:
    ${T(Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
    ${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}
    
    • 1
    • 2

    在这里插入图片描述

    到这里不仅搞清楚 低版本springboot 没做统一异常处理前提下抛出异常时,可能会被SpEL注入攻击的原理,也找到了payload被过滤的具体方式。


    3、总结:

    通过本次原理分析,更深入的理解了 springmvc 工作流程。其实经过以上分析,不难发现,可以执行代码和对类进行操作是SpeL表达式模块所提供的正常功能。Spel表达式注入漏洞有一定的特殊性,正常在编写项目的过程中很难会使用到该功能,但他存在于大的框架项目中,因此一旦被挖掘出来其危害是很大的,除了spel还有很多类似的框架级表达式注入漏洞,在接下来的一段时间内会继续跟踪学习。

  • 相关阅读:
    天锐绿盾 | 如何防止开发部门源代码泄露、外泄?
    HTML5学习总结
    打造高逼格、可视化的监控系统平台
    开发调试工具:USB转IIC/I2C/SPI/UART适配器模块可编程开发板
    2022腾讯全球数字生态大会【存储专场】它来了|预约有礼
    [Java] 多线程
    70. 爬楼梯
    iOS App开发成本高背后的解释
    The Sandbox Alpha 第三季游戏体验推荐|《爱是永恒》
    广度搜索解决迷宫问题
  • 原文地址:https://blog.csdn.net/haduwi/article/details/126326511