Struts2漏洞复现(一)——开发基础及执行流程

目录

0 背景

        对学习的知识做复习总结,也是到处找文章学的,编写内容可能是大段复制过来,尽可能表明出处,若有遗漏请担待,侵权即删。

        本次的环境是struts2-core-2.0.6,主要是想了解struts2的基本框架执行流程和struts2审计的基础知识。(一开始写了一顿2.5.13的,后来发现漏洞版本比较低,重写一版吧)

参考资料

Struts2框架(一) | 云开

1 struts2基本框架及入门

1.1 struts2概述

        struts2相当于SSM架构中的springmvc,主要负责web层,作为控制器来建立模型与视图之间的数据交互。其核心内容包括:interceptor、action和valuestack。

1.2 servlet和struts2

        servlet的一次请求流程如下:

        Web容器(中间件,或许都已Tomcat为例)负责Socket连接、封装request和response,以及管理Servlet。其通过注解或web.xml文件配置过滤器Filter和Servlet处理类,将路径和具体的Filter和Servlet处理类关联起来。此外,像jsp文件的动态加载,解析成.java文件都是由Tomcat等中间件完成的,所以它们有自己的一套类加载机制。web.xml是Web应用相关的配置文件,主要用来配置过滤器、监听器、Servlert的路径匹配等信息,Tomcat通过这个文件初始化配置。

        struts2的一次请求流程如下:

        相比于Servlet,Struts2架构增加了一个统一调度中心——FilterDispatcher(核心过滤器,不同版本可能不同),并通过一系列的默认Struts2拦截器对请求进行处理,最后找到路径对应的Action来处理请求,也可以简单地把Action看作一个Servlet。

        struts.xml是Struts2架构的配置文件,用来配置拦截器、Action路径匹配等。

1.3 struts2案例——登录

        要实现一个基本的struts2案例,主要完成三步工作:web.xml配置核心过滤器信息、struts.xml配置Action和过滤器信息、创建相应Action类、jsp页面。

(0)项目创建

        Java:1.8;IDEA2022;Tomcat9。PS:Java版本过高需要通过Jakarta创建项目,我这里的是通过Java Enterprise创建的;两者无太大差别,但包名有所不同。Tomcat版本不易过高,我一开始用tomcat10没办法部署,改了9可以。

        配置Tomcat并运行:

(1)struts2依赖和web.xml配置

        struts2核心过滤器StrutsPrepareAndExecuteFilter本质仍然是一个Filter,需要在web.xml中配置,以便Web容器找到。

        <filter></filter>:配置过滤器的名字和Filter处理类在哪;<filter-mapping>:配置过滤器过滤哪些路径(作用域);两者通过<filter-name>关联起来,共同作用使一个Filter生效。例如在上述例子中,定义了一个名为struts2的过滤器实例,其具体的过滤器处理类全限定名为org.apache.struts2.dispatcher.filter.FilterDispatcher(不同版本可能不一样),其匹配的路径为/*(即全部路径)。所以上述的功能就是所有请求都经过StrutsPrepareAndExecuteFilter过滤器。

(2)struts.xml配置

        struts.xml的struts2框架的核心配置文件,其定义具体路径-处理类、以及返回页面的关系,以及配置一些struts2的全局变量。下图表示,当请求/login的时候,会调用com.example.ss001.action.LoginActionlogin方法来处理。如果login方法返回success或error,则返回/success.jsp或login.jsp页面。

(3)相关jsp页面和action处理类

        login.jsp

        LoginAction

        success.jsp

        index.jsp

        运行结果:

1.4 Action详解

        struts2框架中的Action主要是用来完成业务逻辑的操作,相当于Servlet和SpirngMVC中的Controller等。

1.4.1 创建Action类

        创建Action类的常见方式:普通类创建,类实现Action接口,类继承ActionSupport类。

(1)普通类创建:在上述的登录案例就是这种方式,所有内容自己定义和实现。

(2)实现Action接口

package com.opensymphony.xwork2;

public interface Action {
    String SUCCESS = "success";
    String NONE = "none";
    String ERROR = "error";
    String INPUT = "input";
    String LOGIN = "login";

    String execute() throws Exception;
}

        Action接口定义了集中返回常量类型和一个execute抽象方法。

(3)继承ActionSupport类        

        这是最常用的方法,ActionSupport类实现了Action接口,也实现了一些列的方法,可以方便使用。

1.4.2 访问Action

        创建Action其实怎样都不所谓,重点在于业务的实现。要实现Action的访问需要在struts.xml中做好配置。一般来说,有两种访问方式:

1)通过<action>标签来配置响应的Action处理类,method方法指定调用方法。如果没有method则默认访问execute方法。一般来说是单对单的模式,一个<action><method>标签匹配一个类方法

2)通过通配符*简化配置。

        例如一个BookAction类:

public class BookAction {

    public void add() {
        System.out.println("BookAction add()");
    }

    public void del() {
        System.out.println("BookAction del()");
    }

    public void update() {
        System.out.println("BookAction update()");
    }

    public void find() {
        System.out.println("BookAction find()");
    }
}

        通过方式1)的struts.xml如下:

<action name="book_add" class="party.laucloud.web.action.BookAction"    method="add"></action>
<action name="book_del" class="party.laucloud.web.action.BookAction"    method="del"></action>
<action name="book_update" class="party.laucloud.web.action.BookAction" method="update"></action>
<action name="book_find" class="party.laucloud.web.action.BookAction"    method="find"></action>

           通过方式2)的struts.xml如下:  {1}和*配套使用,{1}代表第一个通配符的内容

<action name="book_*" class="party.laucloud.web.action.BookAction"        method="{1}"></action>

1.5 数据封装

        数据封装解决的问题是,Action如何获取前端传过来的数据。一般包括:属性驱动和模型驱动两种方式。实际上,具体封装处理是在interceptor里面完成,详细流程可看第三部分。

1.5.1 属性驱动

(1)直接型

        直接在Action类中定义相应的属性变量,并提供get/set方法(和.net很像)。

        login1.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录页面</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/login" method="post">
        username:<input type="text" name="username" /><br>
        password:<input type="password" name="password" /><br>
        <input type="submit" name="login" />
    </form>
</body>
</html>

        LoginAction.java:

public class LoginAction {

    private String username;

    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String login() {
        System.out.println("LoginAction login()");
        System.out.println(username + "   " + password);
        if ("tom".equals(username) && "123".equals(password)) {
            return "success";
        } else {
            return "failer";
        }
    }
}

(2)间接型

        先通过定义一个实体类,然后通过该该实体类封装属性。实体类一般单独作为一个类,放在pojo文件夹下,这里为了方便显示,就在放在Action里了。既要改变jsp又要改变Action

        login2.jsp:这里的name使用了user.的形式。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录页面</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/login2" method="post">
        username:<input type="text" name="user.username" /><br>
        password:<input type="password" name="user.password" /><br>
        <input type="submit" name="login" />
    </form>
</body>
</html>

        LoginAction2

public class LoginAction2 {

    private User user;

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public String login() {
        System.out.println(user.getUsername() + "   " + user.getPassword());
        if ("tom".equals(user.getUsername()) && "123".equals(user.getPassword())) {
            return "success";
        } else {
            return "failer";
        }
    }
}

public class User {

    private String username;

    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

1.5.2 模型驱动

        模型驱动的重点在于改变Action的编写。如实现ModelDriven接口、实例化javaBean、重写getModel返回javaBean。

        LoginAction3

public class LoginAction3 implements ModelDriven<User> {

    private User user = new User();

    @Override
    public User getModel() {
        return user;
    }

    public String login() {
        System.out.println(user.getUsername() + "   " + user.getPassword());
        if ("tom".equals(user.getUsername()) && "123".equals(user.getPassword())) {
            return "success";
        } else {
            return "failer";
        }
    }
}

2 struts2执行流程

2.1 struts2初始化

        当Tomcat启动时,会从web.xml中将struts2核心过滤器加入FilterChain中,并逐个调用Filter的init()方法。struts2的工作起点是其核心过滤器FilterDispatcher,其init()方法如下。

        init()主要做了一些变量的初始化,以供doFilter()方法处理请求的时候使用。Dispatcher对象是用来封装配置信息,以便后续doFilter的时候进行处理。

2.1.1 this.filterConfig = filterConfig;

        filterConfig是Filter的配置类,可以用来获取web.xml中Filter的配置信息。这个其实是Tomcat的内容,等到Tomcat源码分析的时候再讲吧。

2.1.2 this.dispatcher = this.createDispatcher(filterConfig);

        调用this.createDisPatcher(filrerConfig)创建Dispatcher实例。这里的params是一个Map,封装了web.xml的信息,以键值对的形式存储起来。ServletContext是一个servlet上下文,也包含了web.xml的信息。这两个参数在第dispatcher.init的时候用到,主要用来配置日志和文件管理信息。

2.1.3 this.dispatcher.init();

        这里主要用来解析各种配置文件,初始化相应的对象,并交给IOC容器管理。这些初始化变量都会存在configurationManager中。其实这些上下文或者配置啥的本质都是Map,然后把这些Map封装起来,并提供相应方式方便访问读取。

        引用一张图(忘了出处了。。)

        其中struts-default.xml声明了一大堆的interceptor,用来做请求处理。

2.1.4 this.pathPrefixes = this.parse(packages);

        只是将相应的静态资源路径保存到pathPrefixes这个List<String>中。

2.2 请求访问执行流程

        这里发起请求是利用1.3的登录案例作为调试演示。由于当服务器收到请求时,会调用过滤器的doFilter()方法,struts2核心过滤器的doFilter()如下。154-156行只是获取上下文和作用域对象。

2.2.1 UtilTimerStack.push(timerKey);

        这里主要做一些时间和线程的处理(防止冲突),第一次访问的时候,会直接return出来。

2.2.2 request = this.prepareDispatcherAndWrapRequest(request, response);

        对reques进行包装。

2.2.3 mapping = actionMapper.getMapping(request, this.dispatcher.getConfigurationManager());

        configurationManager在初始化的时候存放了各种配置信息。跟进getMapping方法.

        getUri():获取请求路径,在这里是/LoginAction.action

        dropExtension(uri):这里就是判断是否支持后缀请求,并将后缀去掉,也就是现在url变成了/LoginAction。

        this.parseNameAndNamespace(uri, mapping, configManager);根据uri解析命名namespace和name。

        this.handleSpecialParameters(request, mapping);获取并处理请求参数。

       if (this.allowDynamicMethodCalls) : 由于此处获取的mapping不为空,会走else的逻辑,allowDynamicMethodCalls在2.0.6中是true,这个参数的作用是允许通过action!method的方式访问具体的方法,例如在这个例子中就是请求路径是/LoginAction!login,就可以利用LoginAction中的login方法来处理该请求。

 2.2.4 this.dispatcher.serviceAction(request, response, servletContext, mapping);

        由于mapping不为空,这里会直接进入190行的代码,也是doFilter最后的代码。前面的重点是找到action和处理action。继续跟进serviceAction()函数。具体如下:

 2.2.4.1 Map<String, Object> extraContext = this.createContextMap(request, response, mapping, context);和ValueStack stack = (ValueStack)request.getAttribute("struts.valueStack");

        将各种作用域和请求参数做封装整合,本质是一个Map。

        ValueStack stack = (ValueStack)request.getAttribute("struts.valueStack");用于创建一个值栈ValueStack,ValueStack是一个数据中心,request中没有ValueStack所以这里会返回null。

2.2.4.2 ActionProxy proxy = ((ActionProxyFactory)config.getContainer().getInstance(ActionProxyFactory.class)).createActionProxy(namespace, name, extraContext, true, false);

        这里创建一个代理类,用于后面代理invoke递归执行各种拦截器、处理action、处理返回结果等。并且会在这里创建一个OgnlValueStack,用来解析ognl表达式。

        重点是在prepare()方法里面。config = configuration.getRuntimeConfiguration().getActionConfig(namespace, actionName);主要是去创建响应请求的actionConfig信息:处理类的名字、方法、参数等等。

        然后就是创建代理执行类invocation = new DefaultActionInvocation(objectFactory, unknownHandler, this, extraContext); 也会init()创建OgnlValueStack中,存放于invocation的stack中。并且ActionContext和ValueStack是你中有我我中有你。

        resolveMethod();:着就是获取请求对应action的处理方法,如果请求是 ! 这种形式的话,actionProxy的method在后面的流程中会被赋值并再次执行 resolveMethod()方法;如果不是的话就从actionConfig中获取响应的方法,actionConfig对应struts.xml,如果在struts.xml中没有指定的方法,就会默认执行action的execute方法。

2.2.4.3 proxy.execute();

        这里会开始调用代理类执行invoke方式,开始递归调用interceptor和action。

    public String invoke() throws Exception {
    	String profileKey = "invoke: ";
    	try {
    		UtilTimerStack.push(profileKey);
    		
    		if (executed) {
    			throw new IllegalStateException("Action has already executed");
    		}

    		if (interceptors.hasNext()) {

                //1. 获取interceptor并递归调用
    			final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();
    			UtilTimerStack.profile("interceptor: "+interceptor.getName(), 
    					new UtilTimerStack.ProfilingBlock<String>() {
							public String doProfiling() throws Exception {
				    			resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
				    			return null;
							}
    			});
    		} else {
                // 2. 调用完interceptor后就调用action
    			resultCode = invokeActionOnly();
    		}

    		// this is needed because the result will be executed, then control will return to the Interceptor, which will
    		// return above and flow through again
    		if (!executed) {
    			if (preResultListeners != null) {
    				for (Iterator iterator = preResultListeners.iterator();
    					iterator.hasNext();) {
    					PreResultListener listener = (PreResultListener) iterator.next();
    					
    					String _profileKey="preResultListener: ";
    					try {
    						UtilTimerStack.push(_profileKey);
    						listener.beforeResult(this, resultCode);
    					}
    					finally {
    						UtilTimerStack.pop(_profileKey);
    					}
    				}
    			}

    			// now execute the result, if we're supposed to
    			if (proxy.getExecuteResult()) {
                    // 3. 执行结果返回
    				executeResult();
    			}

    			executed = true;
    		}

    		return resultCode;
    	}
    	finally {
    		UtilTimerStack.pop(profileKey);
    	}
    }

        执行拦截器:resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);会遍历interceptors执行全部的拦截器,以ParameterFilterInterceptor为例,发现执行完自身过滤器逻辑后,会递归调用invocation.invoke(),直至遍历执行所有拦截器

        执行action:resultCode = invokeActionOnly(); 反射调用执行Loginaction的login方法。

        处理Result:executeResult();

2.2.5 总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值