java的防护机制其实有很多种,最开始是对于java.rmi.server.useCodebaseOnly配置,从而限制rmi load class;但是jndi利用点可以多种,可以用reference去绑定第三方的factoryclass;从而导致采用urlclassloader去进行远程加载,这种加载机制和rmi load class还不一样,所以java.rmi.server.useCodebaseOnly这个配置无法限制jndi的注入;但是可以限制rmi的反序列化攻击;具体的可以看下面这个文章,我感觉写的挺好的,也挺详细;
https://xz.aliyun.com/t/6660
之后java在JDK 6u141, JDK 7u131, JDK 8u121 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false导致第二条攻击道路也被禁止;相关的jndi注入原理可以看我上篇文章;本文简单分享两个利用点,其实利用点很多种,本文先简要分析一下;
上篇文章记述过相关的jndi注入的原理,也提到jdk8在112之后开启了相关的防护机制,默认不允许从远程加载class;如果加载则会报错;具体看下图:
可以看到已经发生了jndi注入,相关的恶意注入已经发生,但是最后的结果是本地没有去远程恶意的class-server上进行加载,而且本地还报错出trustURLCodebase,提示需要设置为true;显然已经是做了相关的防护;除此之外还有jep290的相关防护
JEP290是Java底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事
那么针对于这种防护机制一些之前提到的rmi反序列化攻击某些gadget也会受到影响;rmi的反序列化从某种程度上来算也是jndi注入的一种,那么这种jep290的防护方式也有效的杜绝了一些攻击;
根据之前说的一些原理,可以看到jndi直接攻击rmi反序列化已经很难利用,除非找到新的gadget,而且相关的要求也是得register接受object类型的参数,主要是因为server端会对传入的类型进行相应的判断,从而调用不同的解析方法进行反序列化;看下图就可明白:
不过这种方法在本地攻击远程的时候可以采用debugger的方法进行绕过;但是常规情况可以看到在UnicastRef下unmarshalValue函数会对相关的序列化流进行处理,会对相关的规定类型进行判断;从而调用相关函数,比如一个函数参数指定为int;那么解析的时候传入对象的序列化流,最后会调用readInt进行反序列化读取处理;可以看到对String也有相关的处理,当规定的type不是上面的一些指定type比如object的时候,就会调用readObject进行反序列化,可进行相关的rmi反序列化攻击;
那么有很多师傅估计也在想,ldap其实可以返回一个reference对象;通过一些属性的赋值,可以导致最后的处理流程和rmi的差不多,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广;不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false;
1:Reference角度来看,我们可以不用远程的Factory;采用本地的factory进行相关的处理;
2:ldap新的角度来看,可以直接赋值JavaSerializeData属性从而直接反序列化数据从而攻击本地的gadget;
我本地采用了一个高版本的jdk;jdk8u282;来进行相关的测试;主要得思路就是第一种;在本地找相关的Factory进行利用;因为采用的是本地的class做攻击,所以本地的factory需要继承javax.naming.spi.ObjectFactory接口,至于原因,在上篇文章有提到过,远程不用继承接口是因为在进行实例化之前就可以进行恶意代码执行,所以无所谓,但是本地的class;我们无法控制static中的代码,所以在实例化之前是无法命令执行的,而且实例化触发构造函数构造函数中的逻辑也无法控制(但是也有可能利用),上篇文章也说过实例化完之后会调用getObjectInstance函数,所以我们这里把主要的逻辑放在getObjectInstance函数中;所以思路就是找一个本地的Class,继承javax.naming.spi.ObjectFactory接口,并且其中有getObjectInstance函数;其实有一个符合的class;就是tomcat中广泛被用到的BeanFactory;org.apache.naming.factory.BeanFactory;
先来看一下如果还是请求远程的rmiserver情况下,依然可以拿到完整的wrapper,对其进行解析的时候就会发现相关的Factoryclass获取的地方出现了问题:
所以更好的验证了,在高版本下攻击者无法向远程的恶意factoryclass进行请求拿到class去恶意执行;
我为了测试方便直接在本地开了一个恶意的server;然后jndi请求本地;
这里有个细节的点,因为在BeanFactory的源码中最开始有限制;可以简单看一下下图:
要求最开始处理的obj要继承ResourceRef;所以这也是为什么在绑定对象的时候需要new一个ResourceRef的原因;ResourceRef也是继承于Reference的;调试跟进下逻辑;
可以看到已经成功请求到了相关的返回值;
然后对相应返回的wrapper进行decodeObject的处理:
相关的处理就和低版本jndi注入的后续处理差不多一样了;简单看一下相关的逻辑:
可以看到后续对factoryClass的相关处理逻辑如下:
因为设置的是本地的BeanFactory;所以这里自然不会触发相关的限制;可以成功对其进行实例化操作,这也就是之前为什么说在本地找的FactoryClass一定要继承相关的ObjectFactory接口,原因就在此,如果没有继承,这里实例化的时候会触发类型转换的错误,jdk低版本下我们不需要转的原因是将恶意的代码放在了static代码块中,后续会进行class初始化操作导致直接执行,在实例化前已经出发恶意的命令,所以实例化就无关紧要;
实例化结束之后就会去处理相关的class;当然自然是利用factory去进行相关的处理;如下图;这个流程就发生在ObjectFactory中;
这里跟着代码走一下:
发现会拿到forceString属性的相关值,这里攻击者是可以控制相应server的;给相关的属性进行赋值;这里结合之前在本地启的恶意registry服务就可明白;跟着走下流程看看后续会如何操作;
可以看到后续的处理是先对forceString的value进行按照,
进行分割,分割完了之后会检索等号的位置,然后按照等号再进行相关的前后截取;也就是相当于再次按照等号进行分割,分隔成为propName和param;然后可以看到利用反射去调用了ELProcessor下的相关方法,方法的参数类型在前面已经定义过,在上图可清楚的看到是String;
跟进去细致的看一下,发现果然是eval;所以相关的逻辑就已经很明显;
最后利用反射调用相关class下的eval方法;相关参数可以看到就是最开始被分隔的param的value;在此环境中也就是s1mple的value;所以最后逻辑就很明显了;直接反射调用方法进行相关的执行,反射调用到此:
执行EL表达式;看一下表达式就可明白是调用了Script引擎;ScriptEngineManager获得脚本引擎管理,然后getEngineByName拿到相关的引擎,相当于是最后在js引擎里利用eval执行了相关的命令从而触发rce;后续就不跟了;最后效果:
所以总结一下要找到相关的可利用的classname需要以下几个要求:
JDK或者常用库的类
有public修饰的无参构造方法
public修饰的只有一个String.class类型参数的方法,且该方法可以造成漏洞
public无参构造方法的原因是因为getConstructor函数,根据代码写的规范只能拿到无参的public的构造器;否则就会报错导致流程异常;至于第三点也应该已经很清楚了,利用了反射,但是没有进行setAccessible;所以也不难理解;
至于上述说的第二种ldap的方法,也可以直接如下检测一下,本地导入Commons-Collections依赖,然后利用ysoserial生成恶意的序列化对象,然后直接传回来;
这里有一个细节,javaClassName不能为空,因为本地会先进行判断,如果为空就不会继续相关的流程;相关流程在上篇文章也提过,相关代码如下:
1 | if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { |
最后本地ldap一下导致rce;
不过这种方法相对比较拘束,前提是本地得有相关的依赖,且存在gadget;才可进行如此的攻击;因为此过程不涉及去远程加载class的情况,所以也可以进行相关的绕过;
那么除了以上的两种利用点之外还有一些其他的利用点;当然这些点都是对classname的选择上的一些点,并不是Factoryclass的点,因为BeanFactory下可以实现调用任意classname下的任意方法了;所以我们只需要寻找合适的classname即可;
javax.management.loading.MLet这个类;选择这个class主要是因为其满足于上述的几点要求,另外其继承于java.net.URLClassLoader;这个是一个很有意思的点;
这个方法我们是可以直接通过反射进行相关的调用的;看一下这个函数的逻辑,new一个URL;然后contains一下如果没有新的url加载就addurl进去;又因为是继承URLClassLoader这个class肯定继承ClassLoader这个class;去到ClassLoader这个类里看一下;
可以看到可以loadClass;那么这里的思路就已经出来了,我们可以给forceString属性多赋值;然后再put的时候从前往后进行put;最后调用反射进行相关方法的调用,参数自然就是param的value;所以首先来说我们这里是可以进行一个本地class的加载;如果本地存在相应的class,那么自然会被加载,加载完成之后就会继续下一轮反射,继续后面的方法,否则将会触发报错;从而影响相关流程;
利用这个点我们就可以来通过loadClass的方法来判断本地是否存在一些特殊的class;为接下来的攻击进行探测;举个例子;如果想利用Commons-Collections,那么拿里面的一个恶意class来进行探测看看;跟踪个流程看看;
可以看到我的相关代码做了下检测ChainedTransformer这个class;看一下远程的请求效果;
可以看到存在ChainedTransformer这个class;所以过了相关的加载,然后走向了后面的流程,addUrl之后进行URL上的class加载,有一次外带请求;类似一种盲注的感觉,从而达到判断是否存在相关class的方法,然后通过后续的一些rce进行攻击,比如ldap的JavaSerializerData,如果探测到有ELProcesser就可直接进行rce等等的方法;
具体的可以跟一下流程就知道:跳过前面的加载流程直接进入到处理的点;
可以看到是利用循环将按照逗号分隔之后的三个propName进行了put;
可以看到已经形成了相应的映射关系;然后在后续就会进行反射invoke执行相关函数,并且参数也可控,之前已经分析过;这里不过多赘述;
可以看到相关的调用是按照最开始put的顺序进行调用;所以这里任何一个环节出了问题,程序都将会进入catch;从而影响整体程序的流程;这里来调试一下,当正确加载之后的逻辑;
当正确加载之后可以看到会进入第二个put的流程,为了鲜明,我最开始的相关值是按照a b c的顺序进行传入,可以看到先进行a然后再进行b;诸如此类;这个方法显然是添加url;
这个classlaode的url是全新的;所以contain自然为false;就会将新的url进行add,从而可以从url上远程加载class;第三次就会进行远程class的加载;
所以这套流程主要是判断一些本地的攻击class是否存在,当然看到这肯定有很多朋友想远程加载恶意的class进行rce;但是我们看一下相关的load;
可以看到resolve默认为false;所以就算造成了恶意加载也不会进行初始化,相关远程恶意class的静态代码也不会执行;仅仅是一个加载到jvm的过程;所以这种方法只是用来探测本地的gadget的链;当然也不排除实际开发中有工作人员重写相关的loadClass造成远程加载恶意class的利用rce点;
一些xxe利用点,比如snakeYaml,和一些XStream等一些的操作;因为都是直接使用类中的接口直接进行处理,而且处理类型也是String的,所以结合BeanFactory也可直接利用;
1 | public static void main(String[] args){ |
至于XStream也是类似;就不放了;
其实除了上述的一些利用方法;还有其他的一些很别致的利用方法,不使用BeanFactory;通过搜索所有实现javax.naming.spi.ObjectFactory
接口的类,然后挨个查看代码,其中发现了一个Tomcat的工厂类org.apache.catalina.users.MemoryUserDatabaseFactory
可以简单看一下相关的处理逻辑;也是在getObjectInstance方法,这也是漏洞利用的方法;可以看到也是进行了Reference的继承情况判断;这里对与ClassName有相关的要求;后续的处理主要逻辑,先实例化MemoryUserDatabase对象,然后从get到pathname属性的值;调用setter方法对MemoryUserDatabase对象中的属性进行设置value;后续也会从Reference中get到readonly和watchSource这些属性然后分别setter;然后调用MemoryUserDatabase下的open方法;追溯过去看一下相关的逻辑;
相关逻辑也不是很难懂,因为之前调用过setter方法设置pathname;可以看到这里调用getPathname方法拿到相关的value;然后用pathname转换成URI;然后在try代码里调用openConnection方法进行请求;然后getInputStream方法拿到相关的请求结果,然后对结果进行处理,可以看到后须实例化了Digester实例;然后后续调用setFeature方法;这个方法很熟悉了,一般是相关XML解析器中提供的安全接口进行相关的设置;防止加载外部实体之类的一些操作,所以看到这个点就知道是加载xml的实例了;后续给tomcat-user/group;role这些属性赋值;然后调用parse进一步解析xml;
所以总而言之这个点是对xml文件进行解析的操作;所以这个点就会存在xxe的漏洞;
1 | public static void main(String[] args){ |
可以看到最后是进入到了parse函数进行相关的解析;就是常规的xml解析了;自然会存在xxe漏洞;
success;
不过在实现的过程中,有一点需要注意,因为需要用到一个不是很常用的jar。在默认情况下,这个jar是不会被添加到环境中的,所以还需要我们实际去手动添加这个jar;在tomcat目录下的bin目录下;tomcat-juli.jar这个jar包,因为MemoryUserDatabase这个class在实例化的时候需要执行
1 | private static final Log log = LogFactory.getLog(MemoryUserDatabase.class); |
这个代码;所以需要相关的jar包中的class实现;
这个xxe的触发方法算是一种比较别致的新式方法,因为调用的目标实现基类不再是BeanFactory;而是MemoryUserDatabaseFactory;
这个rce点也是在上述那个地方,之前说在解析xml之前会进行三行特殊的代码;放下来看一下;
1 | digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true); |
我上述说的是给三个属性值进行赋值,其实更准确的来说,这三行代码是给相关的解析属性设置相关的factory实例去进行解析;这里需要对digester的解析流程有个大概的理解,digester在解析xml的过程中,碰到一些标签之后会为其实例化相关的实例;这是三行代码的作用就是为其生成的相关的实例设置factory;那么这里就来细致分析一下内部的流程;看到在处理的时候实例化了MemoryGroupCreationFactory对象,跟进去看看逻辑;
这里看一下role的处理逻辑,因为在分析的时候,看到这个class中的默认处理xml是tomcat里的user配置文件,先来看一下原本规范的xml的规范;
那么实际攻击过程之中,就可以伪造相关格式的xml,让底层代码对其进行解析;实际看一下相关的解析逻辑:
可以看到在解析到相关的标签之后,就会去调用相关的Factory下的createObject函数;相关的Factory在之前有相关的设置;就是之前的三行代码:
1 | digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true); |
追进去看一下内部的解析规则;可以看到通过getValue方法拿到xml中的值;然后调用database属性对应对象的createRole方法。跟进去看一下;
看到是将相关的解析的value经过put方法放入了roles;roles本质上来说是一个hashmap;然后return返回hashmap;解析的流程大概到这了就差不多了;
要明白上述的这些流程都发生在MemoryUserDatabase的open函数里;那么在open函数处理完之后就会进行一些if判断从而进入MemoryUserDatabase的save函数;这也是我们需要去利用的方法;在之前也通过一些属性赋值使其满足;进入到save中有一段关键的代码;可以看下图;
不卖关子了,这个点是一个文件写入的点;可以看到最开始xml中的相关数据被解析,然后会写入到新的xml文件中,这个xml文件的路径我们可控;看一下最后写入的结果:
发现成功的写入;这个路径我解释一下,因为我为了复现漏洞,我这只是引入了tomcat的lib;并没有配置tomcat;所以相关的环境变量不存在,所以我自己set了一个;
1 | System.setProperty("catalina.base","/tmp"); |
那么上述流程是如何到达写文件的地方的,我这边简要分析记录一下:
可以看到在open函数触发完之后会有一个if判断,判断之后会进入save函数;追过去看一下是否可控;因为调用的是database;也就是MemoryUserDatabase实例下的getReadonly函数;
1 | public boolean getReadonly() { |
可以看到是根据readonly这个属性来判断的;回溯一下Factory;
可以看到readonly我们实际可控,因为是从Reference中get到的;所以我们可以通过外部set可控;所以就最终可控让流程进入save方法的处理逻辑中;
其实如果去调试跟进的话,会发现还有个另外的问题需要解决;
可以很显然的看到,在主要的save函数中存在一个isWritable函数;这个函数主要是判断是否可写;因为save函数中的实现逻辑是一个写入文件的逻辑;
1 | public boolean isWritable() { |
可以看到会将目录是否存在纳入判断条件;如果目录不存在那么就会返回false;所以要使用这个攻击方法;还需要实现目录的创建;这里就会有一定的局限性;不过好在h2数据库依赖中,有一个类中的createDirectory方法可以利用;
调用的方法,还需要结合tomcat中的BeanFactory这个Factory来进行触发;相关的代码如下所示;
1 | public static void main(String[] args) throws Exception { |
可以通过这种方法直接用jndi注入进行创建目录;创建目录的问题已经解决;在实际攻击中我们需要请求外部的xml文件从而进行相关属性的注入,然后通过save函数进行写入到相关的目录之下;也就是针对于我vps攻击而言;
就需要创建 http: 120.53.29.60这两个目录;然后再使用
1 | public static void main(String[] args) throws Exception { |
这段去攻击目标使其出发save函数;在save中解析的时候,"http://127.0.0.1:9900/exp.xml"
会被解析成为"http:/127.0.0.1:9900/"
这样的目录结构,然后会将相关的解析结果写入到这个目录下的xml文件中,文件名就是exp.xml;
OK~;解决了目录的问题,那么现在就是考虑漏洞的利用了;这里因为可以直接写文件,而且文件的内容我们可控,那么现在的思路就是可以考虑两个点:
一:写入配置文件为Tomcat新增用户;
二:直接控制文件名写入jsp;从而访问触发jsp木马;
针对于第一种方法,tomcat中的用户管理是放置在tomcat-user.xml文件里,可以看一下默认的文件;
可以看到原本的xml文件具有相关的规范;可以用这种默认的规范直接来进行相关的注入;password设置为自定义的password从而覆盖掉原本的tomcat-user.xml文件;从而导致添加用户;不过这种攻击方式在实际过程中影响比较的大,修改配置文件,往往会影响到实际的运作,所以不是很推荐;但是也是可行的;
因为vps到期了还没续,所以我这里简单用本地模拟一下远程的攻击:
远程文件放在/tmp下的exp.xml中;具体的内容如上图;然后经过攻击;之前用常规的BeanFactory类攻击去创建了相关的目录;需要有H2的相关依赖;所以这个点就可直接去利用了;
可以看到已经成功写入;可以结合目录越迁进行覆盖tomcat的配置文件,至于要利用BeanFactroy进行文件目录创建则是因为在linux或者unix下,如果没有已经存在的目录,使用目录越迁符是没有用处的,会报错;所以要先创建相关的目录,才可以结合目录越迁符进行使用;
针对于第二种方法:
写入webshell;因为写入文件,所以这些利用方式就显得比较的常规;通过上述的一些表述可以看到,相关的文件名可以看到是在最后的pathname的文件名所决定的,这里不要有相关的误解,并不是只是xml后缀的文件才可被parse;如果这里后缀是jsp的话只要文件内容是xml的文件格式,那么发起相关请求之后也是可以成功的被解析;所以写入jsp文件,那么设置的文件名就需要为jsp后缀,内容设置成如下:
1 | <?xml version="1.0" encoding="UTF-8"?> |
在解析之后就会向相关的可控目录下写入jsp文件,如果将其写入到相关的jsp目录,那么就可直接访问进行攻击;这里测试和之前的一样,在/tmp目录下的exp.jsp为服务端的模拟,经过测试写入相关的文件,攻击的时候需要远程也启用一个tomcat-server;然后在相关的目录下放jsp,然后远程访问我们的tomcat-server;然后解析写入固定的目录;这里假设tomcat的默认jsp目录在/tmp/test下,远程攻击方也需要在远程的tmp的test目录下放置exp.jsp;
1 | public static void main(String[] args) throws Exception { |
因为这处的目录越迁对于tomcat来说可有可无,实际上还是访问的远程的webapps/ROOT/s1mple.jsp这个文件;在本地之前已经使用BeanFactory进行jndi注入创建了目录,所以最后也会通过目录越迁向本地的/webapps/ROOT/s1mple.jsp中写入相关的文件;我本地测试为了方便就直接测试是否可以成功写入即可了,并不采用目录越迁,具体看下图:
可以看到成功写入,我这里没有使用目录越迁是因为我没有导入tomcat,只是导入了lib;但是由于tomcat目录的限制问题,实际攻击的时候是不会出现我上图的攻击偏移情况的,还是因为我本地失去了tomcat的原本访问目录限制的原因;可以看我上图,相关jsp文件里采用xml格式写入恶意的代码,因为一些尖括号的问题采取了html实体编码,最后可以成功写入右图;可以直接进行攻击;