日志的作用
日志记录主要用来监视代码中变量的变化情况,周期性的记录到文件*其他应用进行统计分析工作;跟踪代码运行时轨迹,作为日后审计的依据;担当集成开发环境中的调试器的作用,向文件或控制台打印代码的调试信息。因此,对于程序员来说,日志记录非常重要。
log4j2
Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。此次漏洞的出现,正是由用于 Log4j 2 提供的 lookup 功能造成的,该功能允许开发者通过一些协议去读取相应环境中的配置。
但在实现的过程中,并未对输入进行严格的判断,只要外部用户输入的数据会被日志记录,即可造成远程代码执行,从而造成漏洞的发生。简单来说,就是在打印日志时,如果发现日志内容中包含关键词 ${,那么这个里面包含的内容会当做变量来进行替换,导致攻击者可以任意执行命令。
比如:
logger.info("client ip: {}", clientIp)
影响版本 :
2.0 ≤ Apache Log4j <= 2.14.1
影响判断方式:
用户只需排查Java应用是否引入 log4j-api , log4j-core 两个jar。若存在应用使用,极大可能会受到影响。
影响规模
这一次漏洞的影响面之所以如此之大,主要还是log4j2的使用面实在是太广了。
- 一方面现在Java技术栈在Web、后端开发、大数据等领域应用非常广泛,国内除了阿里巴巴、京东、美团等一大片以Java为主要技术栈的公司外,还有多如牛毛的中小企业选择Java。
- 另一方面,还有好多像kafka、elasticsearch、flink这样的大量中间件都是用Java语言开发的。
在上面这些开发过程中,大量使用了log4j2作为日志输出。只要一个不留神,输出的日志有外部输入混进来,那直接就是远程代码执行rce
漏洞根源
lookup,顾名思义就是查找、搜索的意思,那在log4j2中,就是允许在输出日志的时候,通过某种方式去查找要输出的内容。lookup相当于是一个接口。
- JNDI即
Java Naming and Directory Interface
(JAVA命名和目录接口),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。
简单粗暴理解:有一个类似于字典的数据源,你可以通过JNDI接口,传一个name进去,就能获取到对象了。
- LDAP即
Lightweight Directory Access Protocol
(轻量级目录访问协议),目录是一个为查询、浏览和搜索而优化的专业分布式数据库,它呈树状结构组织数据,就好象Linux/Unix系统中的文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好像它的名字一样。
看不懂?看不懂就对了!
这个东西用在统一身份认证领域比较多,但今天也不是这篇文章的重点。你只需要简单粗暴理解:有一个类似于字典的数据源,你可以通过LDAP协议,传一个name进去,就能获取到数据。
- JNDI还支持一个叫命名引用(Naming References)的方式,可以通过远程下载一个class文件,然后下载后加载起来构建对象。
旧版
新版
修复后的log4j2在JNDI lookup中增加了很多的限制:
- 1.默认不再支持二次跳转(也就是命名引用)的方式获取对象
- 2.只有在
log4j2.allowedLdapClasses
列表中指定的class才能获取- 3.只有远程地址是本地地址或者在
log4j2.allowedLdapHosts
列表中指定的地址才能获取
log4j2漏洞的解决
验证代码准备:
在项目登录接口中增加类似如下代码:
@PostMapping("login")
public R<?> login(@RequestBody LoginBody form) {
//form.getUsername()="${java:version}"
logger.info("登录:{}",form.getUsername());
// 用户登录
LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
// 获取登录token
return R.ok(tokenService.createToken(userInfo));
}
或
public class App {
private static Logger logger = LogManager.getLogger(App.class);
public static void main(String[] args) {
logger.info("user {}", "${java:version}");
System.out.println("Hello World!");
}
}
存在漏洞时输出结果:org.example.App - user Java version 1.8.0_181
无漏洞时的输出结果:org.example.App - user ${java:version}
1. 紧急缓解措施
(1)修改 jvm 参数 -Dlog4j2.formatMsgNoLookups=true
(2)修改配置 log4j2.formatMsgNoLookups=True
在应用程序入口logger初始化之前,写入代码
System.setProperty("log4j2.formatMsgNoLookups", "true");
输出结果:org.example.App - user ${java:version}
(3)将系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS
设置为 true
2. 下载最新的log4J
如2.15.0版本,直接将本地仓库中2.0至2.14.1版本里的jar,用2.15.0更名后替换
-
第一步:在Project视图中展开External Libraries找到Log4J相关jar依赖,并进行如下操作:对应jar上右键->copy->Absolute Path 复制到文件管理器中打开
-
第二步:将2.15.0中对应jar复制一份更名成2.14.1中jar的名称并覆盖2.14.1中的jar
输出结果:org.example.App - user ${java:version}
结论:有效
3、从项目里强制修改直接依赖或间接依赖的Log4J版本
以Idea Maven项目spring-boot-starter-logging依赖包为例:
- 第一步:打开pom.xml
ctrl+鼠标左键 单击artifactId内容spring-boot-starter-logging进入如下界面:
原始配置:
- 第二步:将2.*版本更改成2.15.0
强制修改后:
第三步:Reimport
输出结果:org.example.App - user ${java:version}
结论:有效
对于正式环境有以下解决方案:
方案一:根据1中的方式一在bat中加入如下设置:
set _JAVA_OPTIONS=-Dlog4j2.formatMsgNoLookups=true
方案二:对于jar包与主程序分离的项目,可以直接将正式环境2.15.0的jar更名后直接覆盖原来的版本。
方案三:对于jar包与主程序统一打包的项目,在开发环境做对应更改后重新打包部署。