
CVE-2025-22963에 대한 개념 증명 익스플로잇, Teedy v1.11의 CSRF 취약점으로 사용자 정보 변경 엔드포인트를 통해 계정 탈취가 가능합니다.
Teedy v1.11 이하 버전의 사이트 간 요청 위조(CSRF) 취약점으로 인해 원격 공격자가 현재 인증된 사용자의 사용자 정보(사용자 이름, 비밀번호 등)를 변경하도록 강제할 수 있습니다.
사용자 정보 변경 엔드포인트는 /api/user/:username입니다.
이 엔드포인트는 인증을 위해 쿠키의 auth_token만 확인합니다.
auth_token 값은 다른 출처 웹사이트에서 사용될 수 있습니다.
POST /api/user/admin HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded;charset=utf-8
Content-Length: 173
Origin: http://localhost:8080
Connection: keep-alive
Referer: http://localhost:8080/
Cookie: auth_token=890b02eb-3e4b-4134-a523-56093a25952b
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
username=admin&groups=administrators&email=admin%40example.com&totp_enabled=false&storage_quota=0&storage_current=0&disabled=false&password=superSecure&passwordconfirm=superSecure
따라서 최종 사용자로 하여금 /api/user/:username에 요청을 보내는 웹사이트를 방문하도록 하면 최종 사용자의 정보가 원격 공격자의 정보로 변경됩니다.
사용자 정보에는 비밀번호가 포함됩니다.
이는 계정 탈취로 이어질 수 있습니다.
이 공격은 원격 공격자가 최종 사용자의 사용자 이름을 알고 있을 때 가능합니다.
이 공격은 관리자 및 게스트 사용자 모두에게 악용 가능합니다.
이러한 상황에서 공격자는 피해자 사용자의 비밀번호를 공격자의 비밀번호로 설정하고 공격자의 비밀번호로 피해자 사용자에 로그인하여 피해자 사용자를 탈취할 수 있습니다.
이 취약점을 수정하려면 /api/user/:username에 요청할 때 CSRF 토큰을 첨부하십시오.
POST /api/user/admin HTTP/1.1
...
username=admin&groups=administrators&email=admin%40example.com&totp_enabled=false&storage_quota=0&storage_current=0&disabled=false&password=superSecure&passwordconfirm=superSecure&csrf_token=<randomly generated token>
이는 공격자가 CSRF 토큰 값을 알 수 없기 때문에 CSRF 공격을 방어합니다.
CSRF 토큰을 안전하게 사용하려면 다음 단계를 따르십시오.
먼저 docs/docs-web/src/main/java/com/sismics/docs/rest/util 디렉터리에 다음 파일을 생성합니다.
package com.sismics.docs.rest.util;
import java.util.Map;
import java.util.HashMap;
import java.security.SecureRandom;
import com.sismics.docs.rest.util.RandomTokenGenerator;
/**
* CSRF Token Class that manages users' CSRF Token
*/
public class CSRFToken {
private static CSRFToken instance = new CSRFToken();
private Map<String, String> tokenMap = new HashMap<String, String>();
private CSRFToken() {}
public static CSRFToken getInstance() {
return instance;
}
public void setToken(String userName, String token) {
tokenMap.put(userName, token);
}
public String getToken(String userName) {
if (tokenMap.containsKey(userName)) {
return tokenMap.get(userName);
}
// avoid using predictable string because bruteforcing this value could lead to bypass CSRF check
return RandomTokenGenerator.generate();
}
public boolean validate(String userName, String token) {
return this.getToken(userName).equals(token);
}
}
RandomTokenGenerator.java
package com.sismics.docs.rest.util;
import java.security.SecureRandom;
import java.lang.StringBuilder;
import java.lang.String;
/**
* A token generator for CSRF Token
*/
public class RandomTokenGenerator {
public static String generate() {
byte[] bytes = new byte[16];
SecureRandom rand = new SecureRandom();
rand.nextBytes(bytes);
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
그런 다음 다음 파일들을 다음과 같이 변경합니다.

sismics - Teedy(https://github.com/sismics/docs)
portswigger - 사이트 간 요청 위조(CSRF)(https://portswigger.net/web-security/csrf)
Mozilla - 접근 제어 시나리오 예제(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#examples_of_access_control_scenarios)
취약점 보고 방법을 가르쳐 주신 Ayato님께 감사드립니다.