본문 바로가기

프로그래밍/Spring

Log4j CVE-2021-44228 취약점을 프로젝트에서 직접 점검하고 보완하는 방법

2021년 12월 13일 한 주를 시작하는 월요일에 가뜩이나 급작스런 상황이 벌어졌다. 그것은 Apache 재단에서 배포하는 log4j2(log4j 2.X 버전)에 제로데이 취약점이 발견되었기 때문이다. 제로데이 취약점이란 라이브러리를 개발한 개발자보다 해당 라이브러리를 사용하는 사용자가 먼저 발견한 취약점을 얘기한다. 라이브러리를 개발한 개발자가 발견한 취약점이면 대응방안을 발표하고 이를 수정 보완한 업데이트 버전이 올라오기 때문에 큰 문제가 발생하지는 않겠지만 개발자가 아닌 사용자가 먼저 취약점을 발견했을 경우 개발자 입장에서는 취약점의 존재 자체를 모르기 때문에 취약점을 발견한 사용자가 이를 악용할 소지가 있기 때문이다. 다행히도 이번 취약점은 log4j 를 개발한 Apache 재단 및 이를 사용하는 메이저급 단체(예를 들면 Spring 을 개발한 Pivotal 이나 대한민국의 전자정부 프레임워크 커뮤니티..)에서 빠르게 이 취약점을 대응하는 방법을 소개해주었다.

 

취약점을 대응하는 방법은 여러가지가 있는데 이 글에서 소개하는 것은 log4j2.formatMsgNoLookups 프로퍼티 항목을 true 로 설정하는 대응 방법에 대한 내용이다. 이 글에서는 자신의 프로젝트에 이를 적용하지 않았을때 어떤 결과를 가져오는지 확인하는 방법과 이를 적용했을때 어떤 결과를 가져오는지 확인하는 방법을 알려줌으로써 개발자 스스로 직접 손쉽게 테스트 할 수 있는 방법을 소개하고자 한다. 테스트에 사용된 환경은 얼마전에 소개된 전자정부 프레임워크 4.0 베타 버전에서 eGovFrame Boot Web Project에서 생성된 샘플 코드를 적용해서 만들었다. 개발툴은 자기가 사용하는 툴이면 어느것이든 상관없다. 그리고 전자정부 프레임워크가 아니더라도 Spring 에서 같은 방법으로 적용할 수 있으며 Spring Boot 에서는 이것과는 방법이 다르지만 이것보다 더 쉽게 적용할 수 있다(관련 내용은 여기를 참조한다). 이 글에서 사용된 log4j 버전은 2.13.3 버전을 사용했으며 jdk 는  jdk 11 을 사용했다(log4j 2.13.3 버전은 jdk 8 부터 지원하기 때문에 jdk 8 사용자도 처리 가능하다)

 

아래의 그림은 샘플 프로젝트의 구조를 보여주고 있다.

샘플 프로젝트 구조

샘플 프로젝트로 만든 egovbootweb 프로젝트 구조를 보면 우리가 아는 Spring Framework 프로젝트 구조와 다를 것이 없다. 여기서 빨간색 박스 처리된 src/main/resources 디렉토리에 있는 log4j.system.properties 파일을 눈여겨보자. 이 파일은 대부분 존재하지 않을 것이다. 그러면 src/main/resources 디렉토리에 이 파일을 만들면 된다. 그림의 오른쪽을 보면 해당 파일을 열어놓았는데 log4j2.formatMsgNoLookups=true 설정을 해놨지만 현재는 주석처리를 해놓았다. 즉 log4j 의 시스템 프로퍼티를 설정하는데 사용되는 log4j.system.properties 파일을 src/main/resources 디렉토리에 만들어는 놓았으나 log4j2.formatMsgNoLookups=true 설정이 적용은 안되어 있는 상태인것이다. 이 상태를 이해하고 아래의 테스트용 소스코드를 살펴보자

테스트용 샘플 코드

위의 그림은 프로젝트에서 제공되는 샘플 코드의 일부이다. 자신의 프로젝트에서 사용하는 @Controller 어노테이션이 적용된 클래스의 어떤 URL과 매핑되는 메소드를 사용해도 상관은 없다. 다만 지금부터 설명한 내용은 반드시 따라줘야 한다. 빨간색 박스로 표시된 1,2,3 번에 대해 설명하도록 하겠다

 

① : org.apache.log4j.Logger 클래스와 org.apache.logging.log4j.util.PropertiesUtil 클래스를 import 해준다

② : 해당 클래스에서 사용할 logger 를 설정해준다. getLogger 메소드에 들어가는 파라미터로는 해당 Conntroler 클래스를 넣어주면 된다. 여기서는 1번에서 import 한 Apache log4j 에서 제공되는 logger를 사용했지만 자신이 사용하는 프로젝트에서는 Lombok의 @Slfj4 어노테이션을 사용하거나 Slf4j 에서 제공하는 logger를 사용할 수도 있다. 사실 이 경우에 대한 테스트는 해보지를 않았는데 만약 Lombok 이나 Slf4j 에서 제공하는 logger를 사용하게 될 경우 기존의 logger를 그대로 이용하면서 테스트해보고 문제가 발생하면 Apache Logger 로 바꿔서 테스트해본다

③ : 테스트 대상이 되는 메소드의 파라미터에 HttpServletRequest request를 추가한뒤 3번의 코드를 입력한다. 현재 3번의 코드는 1번에서 import 한 Logger 클래스를 사용하고 있지만 2번 설명했을때도 언급했다시피 Lombok 이나 Slf4j 에서 제공하는 logger를 사용할 경우 그거에 맞춰서 log 의 info 메소드를 사용하면 된다.

 

이제 테스트 코드 작성은 다 완료되었다. 그 다음으로 필요한게 Rest API 를 테스트 할 수 있는 프로그램이나 git을 설치하면 같이 설치되는 git bash가 필요하다. Rest API 를 테스트 할 수 있는 프로그램은 자기가 가지고 있는게 있으면 그걸 써도 되지만 만약 가지고 있는게 없으면 크롬 브라우저의 확장 프로그램으로 Advanced Rest Client 라는 프로그램이 있다. 이것을 설치하도록 한다. 이 프로그램에 대한 사용방법은 이 글에서는 별도로 언급하지는 않겠다. 구글에서 검색하면 나오기 때문에 사용방법을 모르겠다 싶으면 검색하면 찾을수 있다. 아래의 그림은 Advanced Rest Client(이제부터 ARC 라 하겠다) 프로그램에서 테스트 하는 그림이다.

Advanced Rest Client 에서의 테스트

위에서 보여줬던 테스트 코드를 보면 이 테스트 코드는 Get 방식을 사용하고 있고 관련 url 은 /egovSampleList.do 를 사용하고 있다. 그래서 ARC에서 method는 GET, Host는 http://localhost:8080 을 사용했고 Path 로는 /egovSampleList.do 를 사용했다. 눈여겨 봐야 할 항목은 빨간색 박스 표시를 해놓은 Header 값이다. Header 이름을 X-Api-Version 으로 했는데 위에서 올려놓은 샘플 코드를 보면 request.getHeader("X-Api-Version") 코드를 사용해서 X-Api-Version 헤더값을 읽어오는 부분이 있다. 그래서 Header 이름을 X-Api-Version 으로 했다. 이 Header 에 대한 값으로 ${jndi:ldap://127.0.0.1/a} 를 사용했는데 바로 이러한 값의 형태가 취약점을 이용하게 되는 코드가 되는것이다. ARC 에서 위의 그림과 같이 설정한뒤 개발툴에서 해당 프로젝트를 개발툴과 연동되어 있는 WAS 에서 실행시킨뒤 (흔히들 이클립스에 톰캣 등록한뒤 프로젝트를 톰캣에 등록해서 사용할텐데 그걸 실행시키라는 의미) ARC 에서 Send 버튼을 클릭하게 되면 개발툴에 떠 있는 프로젝트가 ARC 에서 보낸 요청을 받아서 처리하게 된다. 만약 ARC 가 아니라 git bash 를 이용할 경우 curl을 아래의 그림과 같이 사용하면 된다(꼭 git bash를 사용할 필요는 없다. curl 만 이용할 수 있기만 하면 된다)

git bash 에서 curl 을 사용해서 테스트 하는 방법

아래의 그림은 ARC 나 curl을 통해 이 요청을 처리했을때 개발툴에서 출력하는 로그를 보여주고 있다.

취약점 공격이 발생한 로그

관련 로그만 보여줄려고 그림에서 보여주는 내용의 앞단 부분을 지우고 캡쳐했다. 로그 내용을 보면 알겠지만 log4j2.formatMsgNoLookups 항목이 존재하지 않다는 것을 확인시켜주고 있으며 Error 가 발생한 것을 볼 수 있다. 이 에러는 위에서 X-Api-Version 헤더에 설정한 값인 ${jndi:ldap://127.0.0.1/a} 를 이용해서 실제 ldap 통신을 시도했지만 실패했음을 나타내고 있다. 즉 서버의 Resource 자원을 접근할려는 시도로 보면 된다. 이것이 바로 취약점을 이용한 공격이 되는 것이다. 만약 ldap으로 접근할 수 있는 자원의 이름을 안다면 해당 자원을 탈취하는 것도 가능해지는 것이다.

 

이렇게 취약점을 확인할 수 있는 방법을 알았으니 이제 취약점을 패치하는 방법도 알아보자. 아래 그림과 같이 위의 설명에서 주석처리 했던 log4j.system.properties 파일의 log4j2.formatMsgNoLookups 항목에서 주석처리를 제거해보자.

log4j.system.properties 파일에서 주석처리가 제거된 log4j2.formatMsgNoLookups 항목

이렇게 한뒤 ARC 나 curl 을 이용해서 위의 방법과 동일하게 테스트를 진행해보면 아래와 같은 로그를 볼 수 있다.

정상적인 로그 출력

로그 내용을 보면 log4j2.formatMsgNoLookups 항목이 존재하고 있으며 항목의 설정값이 true로 되어 있는 것을 확인할 수 있다. 그리고 ldap 통신을 시도하는 동작을 하지 않으면서 로그 출력을 마무리 짓고 있다.

 

이렇게 자신이 만든 프로젝트에서 직접 확인할 수 있는 방법을 소개했다. java -D 옵션을 이용해서 log4j2.formatMsgNoLookups 항목을 설정하는 것은 WAS를 이용하고 있는 경우에는 WAS의 java 실행 옵션을 건드려야 하는 부분이기 때문에 그것을 알지 못하면 설정 하기 어렵고, OS 환경변수나 계정의 환경변수로 등록하고자 할 경우 리눅스의 환경변수 설정에 대해 알지 못하면 설정 하기 어렵다. 그래서 자신의 프로젝트에서 직접 취약점을 확인하고 이것이 올바르게 패치되었는지 검증하는 방법을 이 글을 통해 설명했다. 이 글을 통해 도움이 되기를 바란다.

 

만약 수정을 했는데도 위의 그림과 같은 동작을 하지 않으면 Maven 이나 Gradle 의 clean 명령을 이용해서 프로젝트를 클린한뒤 다시 재빌드 한뒤 다시 WAS 를 실행해보도록 한다. 이 과정에서 개발툴과 연동되어 있는 WAS 가 실행중인지 체크하고 실행중이면 정지시킨뒤에 Maven 이나 Gradle 의 clean 명령을 이용해서 프로젝트를 클린한뒤 다시 재빌드를 한 후 다시 WAS 를 실행해보기를 바란다.

 

긴급추가사항

 

위의 방법으로 처리를 할 경우 JNDI Lookup 기능을 활용한 자원탈취 기능은 막기는 하겠지만 JNDI Lookup 기능 자체는 막은 것이 아니기 때문에 공격자가 잘못된 자원 주소 패턴으로 JNDI Lookup 기능을 반복적으로 호출해서 존재하지도 않는 자원을 찾게 하는 DDOS 공격이 발생할 수 있다고 알려졌다.

 

관련 내용은 https://github.com/advisories/GHSA-7rjr-3q55-vv33 를 보면 나와있다.

 

2021년 12월 15일 현재 log4j의 최신버전인 2.15의 경우 이 JNDI Lookup 기능 활성화에 대한 기본 옵션을 disable 로 설정했을 뿐이지 JNDI Lookup 기능 자체를 아예 없앤것은 아니기 때문에 언제든 이 옵션을 enable 로 수정할 수 있고 이렇게 enable 해놓고 위의 관련 설정을 하게 되면 이번에 발생한 취약점은 해결이 되더라도 위에서 예시를 들었던 잘못된 자원 주소를 인위적으로 계속 반복적으로 호출하게 되면 잘못된 자원 주소로 자원을 찾을려고 반복적으로 시도하기 때문에 이런 형태의 DDOS 공격이 발생한다는 얘기이다.

 

그래서 이 문제를 해결할려면 사용하는 JDK 가 7일 경우엔 log4j 버전을 2.12.2 버전(현재 출시되어 있는 상태는 아니지만 곧 출시한다고 함) 으로 업데이트 하거나 JDK 가 8일 경우엔 log4j 버전을 2.16 버전으로 업데이트 한다. 관련 내용은 여기를 참조하도록 한다.

 

만약 이 두 방법을 모두 사용할 수 없는 상황일 경우엔 jar 파일들이 있는 디렉토리에서 리눅스 기준으로는 zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class 명령을 실행하거나 윈도우 기준으로는 반디집 같은 압축 프로그램을 사용할 경우 log4j-core-버전.jar 파일을 더블클릭해서 안에 있는 클래스들이 보이게끔 해놓고 org/apache/logging/log4j/core/lookup/JndiLookup.class 를 찾아 마우스 우클릭 하면 삭제메뉴가 나오게 되는데 이를 클릭해서 JndiLookup 클래스를 삭제해버리면 된다(당연 해당 패키지가 존재하지 않거나 패키지 안에 JndiLookup 클래스가 없으면 안해도 된다)