Struts2漏洞系列之S2-001利用表单错误进行远程代码执行

Posted 安恒信息安全研究院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Struts2漏洞系列之S2-001利用表单错误进行远程代码执行相关的知识,希望对你有一定的参考价值。

Smi1e@Pentes7eam

漏洞信息: https://cwiki.apache.org/confluence/display/WW/S2-001

当我们提交表单并验证失败时,由于Strust2默认会原样返回用户输入的值而且不会跳转到新的页面,因此当返回用户输入的值并进行标签解析时,如果开启了 altSyntax ,会调用 translateVariables 方法对标签中表单名进行 OGNL 表达式递归解析返回 ValueStack 值栈中同名属性的值。因此我们可以构造特定的表单值让其进行 OGNL 表达式解析从而达到任意代码执行。

漏洞复现

altSyntax 功能允许将 OGNL 表达式插入到文本字符串中并以递归方式处理。

使用struts2的 s 标签提交表单,如果验证失败则会在服务端进行一次 OGNL 表达式验证。

影响范围

Struts 2.0.0 - Struts 2.0.8 , WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5

漏洞分析

首先Struts2的运行流程是

  1. HTTP请求经过一系列的过滤器,最后到达FilterDispatcher过滤器。

  2. FilterDispatcher将请求转发给ActionMapper,判断该请求是否需要处理。

  3. 如果该请求需要处理,FilterDispatcher会创建一个ActionProxy来进行后续的处理。

  4. ActionProxy拿着HTTP请求,询问struts.xml该调用哪一个Action进行处理。

  5. 当知道目标Action之后,实例化一个ActionInvocation来进行调用。

  6. 然后运行在Action之前的拦截器。

  7. 运行Action,生成一个Result

  8. Result根据页面模板和标签库,生成要响应的内容。

  9. 根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。

在漏洞关键位置 xwork-2.0-beta-1.jar!/com/opensymphony/xwork2/util/TextParseUtil.class中的translateVariables 方法下断点。

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

前面都是一些应用初始化、调度、Struts2 拦截器执行、Action执行等操作,直接跳过。

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

因为 Action 的返回值为error,所以doForward会跳转到 index.jsp,后面再进行一系列过滤器调用、JspServlet 调用解析jsp等操作。

我们的 index.jsp 文件

 
   
   
 
  1. <%@ page language="java" contentType="text/html; charset=UTF-8"

  2. pageEncoding="UTF-8"%>

  3. <%@ taglib prefix="s" uri="/struts-tags" %>

  4. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

  5. <html>

  6. <head>

  7. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

  8. <title>S2-001</title>

  9. </head>

  10. <body>

  11. <h2>S2-001 Demo</h2>

  12. <p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>

  13. <s:form action="login">

  14. <s:textfield name="username" label="username" />

  15. <s:textfield name="password" label="password" />

  16. <s:submit></s:submit>

  17. </s:form>

  18. </body>

  19. </html>

直接跟到 org.apache.struts2.views.jsp.ComponentTagSupport 的 doStartTag 方法,这里会对jsp标签进行解析,从 form 标签开始,我们步入到解析 textfield 标签时。

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

执行完 doStartTag 后会再次回到 index.jsp,此时遇到了相应的闭合标签 />,会跳转到 doEndTag():

 
   
   
 
  1. public int doEndTag() throws JspException {

  2. this.component.end(this.pageContext.getOut(), this.getBody());

  3. this.component = null;

  4. return 6;

  5. }

跟进 component.end(),到达org.apache.struts2.components.UIBean#end

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

继续跟入this.evaluateParams();,由于开启了 altSyntaxexpr = %{password}

altSyntax 功能是 Struts2 框架用于处理标签内容的一种新语法(不同于普通的 HTML ),该功能主要作用在于支持对标签中的 OGNL 表达式进行解析并执行。altSyntax 功能在处理标签时,对 OGNL 表达式的解析能力实际上是依赖于开源组件 XWork

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

继续跟入this.findValue(expr, valueClazz);

 
   
   
 
  1. protected Object findValue(String expr, Class toType) {

  2. if (this.altSyntax() && toType == String.class) {

  3. return TextParseUtil.translateVariables('%', expr, this.stack);

  4. } else {

  5. if (this.altSyntax() && expr.startsWith("%{") && expr.endsWith("}")) {

  6. expr = expr.substring(2, expr.length() - 1);

  7. }


  8. return this.getStack().findValue(expr, toType);

  9. }

  10. }

由于开启了 altSyntax ,toType 为 class.java.lang.string,跟入com.opensymphony.xwork2.util.TextParseUtil中的translateVariables('%', expr, this.stack);

 
   
   
 
  1. public static String translateVariables(char open, String expression, ValueStack stack) {

  2. return translateVariables(open, expression, stack, String.class, (TextParseUtil.ParsedValueEvaluator)null).toString();

  3. }

最终来到translateVariables()方法

 
   
   
 
  1. public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {

  2. Object result = expression;


  3. while(true) {

  4. int start = expression.indexOf(open + "{");

  5. int length = expression.length();

  6. int x = start + 2;

  7. int count = 1;


  8. while(start != -1 && x < length && count != 0) {

  9. char c = expression.charAt(x++);

  10. if (c == '{') {

  11. ++count;

  12. } else if (c == '}') {

  13. --count;

  14. }

  15. }


  16. int end = x - 1;

  17. if (start == -1 || end == -1 || count != 0) {

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

  19. }


  20. String var = expression.substring(start + 2, end);

  21. Object o = stack.findValue(var, asType);

  22. if (evaluator != null) {

  23. o = evaluator.evaluate(o);

  24. }


  25. String left = expression.substring(0, start);

  26. String right = expression.substring(end + 1);

  27. if (o != null) {

  28. if (TextUtils.stringSet(left)) {

  29. result = left + o;

  30. } else {

  31. result = o;

  32. }


  33. if (TextUtils.stringSet(right)) {

  34. result = result + right;

  35. }


  36. expression = left + o + right;

  37. } else {

  38. result = left + right;

  39. expression = left + right;

  40. }

  41. }

  42. }

这里的 expression 为 %{password} ,先经过while循环确定出表达式的start和end,然后取出来password赋值给var变量

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

接着调用stack.findValue(var, asType);,这里的stack为OgnlValueStack,它是ValueStack的实现类。

ValueStack 是 Struts2 的一个接口,字面意义为值栈,类似于一个数据中转站,Struts2 的数据都保存在 ValueStack 中。客户端发起一个请求 struts2 会创建一个 Action 实例同时创建一个 OgnlValueStack 值栈实例,OgnlValueStack 贯穿整个 Action 的生命周期。Struts2 中使用 OGNL 将请求 Action 的参数封装为对象存储到值栈中,并通过 OGNL 表达式读取值栈中的对象属性值。

ValueStack 中有两个主要区域

  • CompoundRoot 区域:是一个ArrayList,存储了 Action 实例,它作为 OgnlContext 的 Root 对象。获取root数据不需要加 #

  • context 区域:即 OgnlContext 上下文,是一个Map,放置web开发常用的对象数据的引用。request、session、parameters、application等。获取 context 数据需要加 #

操作值栈,通常指的是操作 ValueStack 中的 root 区域。

OgnlValueStack 的 findValue 方法可以在 CompoundRoot 中从栈顶向栈底找查找对象的属性值。

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

跟进 OgnlUtil.getValue(expr, this.context, this.root, asType);

 
   
   
 
  1. public static Object getValue(String name, Map context, Object root, Class resultType) throws OgnlException {

  2. return Ognl.getValue(compile(name), context, root, resultType);

  3. }

Ognl.getValue(compile(name), context, root, resultType); 会根据 root 和 context 对象对表达式 compile(name) 进行解析。

这里因为 name 为 password ,会从 root 栈中 LoginAction 对象中 获取到我们提交的参数 %{1+1} 并返回。

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

返回值 o 进行一系列处理后,最后又赋值给 expression ,接着会再次进入 translateVariables 进行解析,然后进入下一次 while 循环

Struts2漏洞系列之【S2-001】利用表单错误进行远程代码执行

这次是 1+1 进入到 stack.findValue,从而在 Ognl.getValue(compile(name), context, root, resultType); 中进行表达式解析也就是执行我们的payload最终返回 2

漏洞修复

 
   
   
 
  1. public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) {

  2. // deal with the "pure" expressions first!

  3. //expression = expression.trim();

  4. Object result = expression;

  5. int loopCount = 1;

  6. int pos = 0;

  7. while (true) {


  8. int start = expression.indexOf(open + "{", pos);

  9. if (start == -1) {

  10. pos = 0;

  11. loopCount++;

  12. start = expression.indexOf(open + "{");

  13. }

  14. if (loopCount > maxLoopCount) {

  15. // translateVariables prevent infinite loop / expression recursive evaluation

  16. break;

  17. }

  18. int length = expression.length();

  19. int x = start + 2;

  20. int end;

  21. char c;

  22. int count = 1;

  23. while (start != -1 && x < length && count != 0) {

  24. c = expression.charAt(x++);

  25. if (c == '{') {

  26. count++;

  27. } else if (c == '}') {

  28. count--;

  29. }

  30. }

  31. end = x - 1;

  32. ......

  33. }

  34. }

漏洞的原因是当我们提交表单并验证失败时,由于Strust2默认会原样返回用户输入的值而且不会跳转到新的页面,因此当返回用户输入的值并进行标签解析时,如果开启了 altSyntax ,会调用 translateVariables 方法对标签中表单名进行 OGNL 表达式递归解析返回 ValueStack 值栈中同名属性的值。

例如我们提交的表单名为 password 值为%{1+1},由于开启了altSyntax 会使用 %{}包裹,并调用 translateVariables 对 %{password} 进行 OGNL 表达式解析,此时值栈中栈顶为 LoginAction ,他的 password 属性值为我们提交的 %{1+1} ,在执行 ParametersInterceptor 拦截器时就已经 set 进去了,解析出来以后发现有%{}标签包裹,因此继续下一个while循环进行解析,直到返回没有 %{} 标签包裹的值 2 才跳出while循环并返回。

可以看到修复代码中加入了一个 maxLoopCount 参数和 if 判断条件来限制递归解析次数。

 
   
   
 
  1. if (loopCount > maxLoopCount) {

  2. // translateVariables prevent infinite loop / expression recursive evaluation

  3. break;

  4. }

表单验证错误并不是该漏洞的产生的原因

Struts2-命令-代码执行漏洞分析系列 S2-001

当我们把 Action的 password 属性硬编码为 %{1+1} 时,依然会执行payload,因此表单验证错误并不是该漏洞产生的原因,不过表单验证错误是这个漏洞出现的场景之一。

在struts2框架中,配置了 Validation,倘若验证出错往往默认会原样返回用户输入的值而且不会跳转到新的页面,而在最后解析页面时解析了用户输入的值,从而执行payload。在实际场景中,比如登陆等处,往往会配置了Validation,比如限制用户名长度,数字的范围等等,从而成为了该漏洞的高发区。


关于我们

人才招聘

一、高级攻防研究员


工作地点:

1.杭州/重庆/上海/北京;


岗位职责:
1.前沿攻防技术研究;
2.负责完成定向渗透测试任务;
3.负责红队工具的研发。

任职要求:
1.三年以上相关工作经验,若满足以下所有条件,则可忽略此要求;
2.熟练掌握Cobalt Strike、Empire、Metasploit等后渗透工具的使用;
3.熟练掌握工作组/域环境下的各种渗透思路、手段;
4.具有大型、复杂网络环境的渗透测试经验;
5.具有独立的漏洞挖掘、研究能力;
6.熟练至少一门开发语言,不局限于C/C++、Java、php、Python等;
7.良好的沟通能力和团队协作能力。

加分项:
1.红队工具开发经验;
2.有良好的技术笔记习惯。


联系人:姜女士
邮箱:double.jiang@dbappsecurity.com.cn
手机;15167179002,微信同号

以上是关于Struts2漏洞系列之S2-001利用表单错误进行远程代码执行的主要内容,如果未能解决你的问题,请参考以下文章

Struts2著名RCE漏洞引发的十年之思

Struts2 漏洞集合

Struts2变种来袭

开篇Struts2历史漏洞系列分析文章

Struts2那些事

Struts S2-052漏洞利用之Meterpreter