跟踪一下log4j2的安全漏洞
- 总结
- bug出现的根源
- 解决方案
- 官方网站对LookUp介绍
- 代码跟踪
- pom依赖
- 日志格式
- Main方法
- rmi本地服务
- bug的源码定位
总结
开源项目不易,功能少了推广不动(很有意思的时,这次的安全问题大多数人都不知道日志框架有这功能…),功能多了容易出bug;充分考虑扩展性的同时容易误触碰到一些严重安全问题 难呀!!!
bug出现的根源
slf4j2的表达式解析支持从不同上下文中获取数据,包括但不限与: 日志上下文, 环境变量 ,系统环境变量 , JNDI等; JNDI为访问的资源提供了统一的调用接口, 其中包括了远程资源调用,debug中发现四种协议: ldap,rmi,dns,iiop;
本次出现的根源就是: 调用远程资源后序列化时可以注入到服务器中任意代码; 最为严重的是,攻击者可以通过当前服务器作为跳板,完全进入被攻击者的内网中,我的世界就成了"我"的世界了
解决方案
- 没有引入log4j-core或者jdk 大于 jdk8 8u191(网上说的,没深究)的没有安全问题
- 升级log4j 的sdk版本;升级到 2.15.0
- 控制服务器出口白名单,如果服务器只是内部在调用不访问外部那么严重程度就大大降低(一般生产环境网关才对外暴露)
- log4j2.formatMsgNoLookups=true;源码类: org.apache.logging.log4j.core.util.Constants 注释如下: LOG4J2-2109 if true, MessagePatternConverter will always operate as though %m{nolookups} is configured.
官方网站对LookUp功能介绍
LookUps
代码跟踪
pom依赖
代码语言:javascript复制<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
日志格式
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
Main方法
代码语言:javascript复制public class Main {
static Logger logger = LogManager.getLogger();
public static void main(String[] args) {
ThreadContext.put("msg", "hello world!");
lookUpPrint("contextMapLookUp : ---> {}", "${ctx:msg}");
lookUpPrint("dateLookUp : ---> {}", "${date:MM-dd-yyyy}");
//@see ProcessEnvironment
lookUpPrint("environmentLookUp : ---> {}", "${env:SystemDrive}");
lookUpPrint("eventLookUp : ---> {} - {}", "${event:ThreadName}", "${event:ThreadId}");
lookUpPrint("javaLookUp : ---> {} ", "${java:locale}");
lookUpPrint("jndi-rmi-LookUp : ---> {} ","${jndi:rmi://localhost:1099/bug}");
//用来debug DNSClient
// lookUpPrint("jndi-dns-LookUp : ---> {} ","${jndi:dns://www.baidu.com.}");
lookUpPrint("JvmArgs-lookUp : ---> {} ","${jvmrunargs:hostName}");
lookUpPrint("log4jLookUp : ---> {} ","${log4j:configLocation}");
MainMapLookup.setMainArguments("abc", "123");
lookUpPrint("mainLookUp : ---> {} ","${main:abc}");
lookUpPrint("sysLookUp : ---> {} ", "${sys:java.vm.version}");
}
private static void lookUpPrint(String format, String... message) {
logger.info(format, message);
}
}
rmi本地服务
代码语言:javascript复制public class RMIServer {
static class Bug{
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
ReferenceWrapper wrapper = new ReferenceWrapper(new Reference(Bug.class.getName()));
registry.bind("bug", wrapper);
}
}
bug的源码定位
由于log4j2的调用链路比较复杂,逐级debug还是很麻烦的; 可以直接随机搜索一个LookUp的类,快速定位到具体的类中然后debug就可以看到方法调用栈;
直接贴出关于JNDILookUp的部分源码
代码语言:javascript复制@Plugin(name = "jndi", category = StrLookup.CATEGORY)
public class JndiLookup extends AbstractLookup {
@Override
public String lookup(final LogEvent event, final String key) {
if (key == null) {
return null;
}
final String jndiName = convertJndiName(key);
try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {
return Objects.toString(jndiManager.lookup(jndiName), null);
} catch (final NamingException e) {
LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, e);
return null;
}
}
}
发现源码很容易看懂; Plugin的name代表是注入的方式; demo中对于不知道的方式就debug进来然后看下数据的来源就可以知道可以注入什么属性了; dndiManager.lookup(jndiName)里面有很多操作空间; 截一个图可以看下:
我们可以看到jndi的上下文有很多中; 虽然有几个是tomcat或者spring的实现类,但是rt包下依旧有不少; 我们要挑选出支持远程调用的,那么就找到了GenericURLContext这个上下文; 我们可以看到这个类的实现类如下:
至此已经知道了可能出现bug的代码入口; 继续跟踪bug出现的具体点, GenericURLContext:
代码语言:javascript复制 public Object lookup(String var1) throws NamingException {
ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);
Context var3 = (Context)var2.getResolvedObj();
Object var4;
try {
var4 = var3.lookup(var2.getRemainingName());
} finally {
var3.close();
}
return var4;
}
同时给出一段debug的图
通过上下文进行lookup;代码如下:
代码语言:javascript复制 public Object lookup(Name var1) throws NamingException {
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}
return this.decodeObject(var2, var1.getPrefix(1));
}
}
在decodeObject中通过远程代理调用远端服务,会有反序列化的过程,这里直接贴出MarshallInputStream中的代码片段
代码语言:javascript复制 protected Class<?> resolveClass(ObjectStreamClass var1) throws IOException, ClassNotFoundException {
Object var2 = this.readLocation();
String var3 = var1.getName();
ClassLoader var4 = this.skipDefaultResolveClass ? null : latestUserDefinedLoader();
String var5 = null;
if (!this.useCodebaseOnly && var2 instanceof String) {
var5 = (String)var2;
}
try {
return RMIClassLoader.loadClass(var5, var3, var4);
} catch (AccessControlException var9) {
return this.checkSunClass(var3, var9);
} catch (ClassNotFoundException var10) {
try {
if (Character.isLowerCase(var3.charAt(0)) && var3.indexOf(46) == -1) {
return super.resolveClass(var1);
}
} catch (ClassNotFoundException var8) {
}
throw var10;
}
}
到了这里,bug的场景已经还原了; RMIClassLoader.loadClass(var5,var3,var4) 会加载远程类, 这里就是攻击者正式开始攻击的地方了;
比如写一个static静态代码块; 那么静态代码块是跟着类走的; 类在加载的时候就会执行静态代码块,如果静态代码块里的代码是攻击代码,那么我的世界正式成为"我"的世界了…