
======================
TeamCity는 지속적 통합 및 배포 서버로, 즉시 사용 가능한 지속적 단위 테스트, 코드 품질 분석, 빌드 문제 조기 보고 기능을 제공합니다.
이 취약점은 공격자가 인증되지 않은 임의의 엔드포인트에 접근할 수 있게 하는 라이브러리에서 발생합니다.
취약한 코드는 lib 디렉토리의 web-openapi.jar 라이브러리에 있습니다.
jetbrains.buildServer.controllers.BaseController 클래스는 요청과 응답을 처리하는 역할을 담당하지만, 부적절하게 구현되어 있습니다. 코드가 어떻게 생겼는지 살펴보겠습니다:
public abstract class BaseController extends AbstractController {
//////
public final ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
ModelAndView modelAndView = this.doHandle(request, response);
if (modelAndView != null) {
if (modelAndView.getView() instanceof RedirectView) {
modelAndView.getModel().clear();
} else {
this.updateViewIfRequestHasJspParameter(request, modelAndView);
}
}
ModelAndView 메서드의 주요 목적은 요청된 페이지를 UI에 렌더링하는 것입니다. 여기서 버그가 시작됩니다. 요청이 리다이렉트되지 않는 경우 updateViewIfRequestHasJspParameter가 호출된다는 점에 주목하세요. 취약점의 근본 원인을 찾으려면 더 깊이 조사해야 합니다.
private void updateViewIfRequestHasJspParameter(@NotNull HttpServletRequest request, @NotNull ModelAndView modelAndView) {
boolean isControllerRequestWithViewName = modelAndView.getViewName() != null && !request.getServletPath().endsWith(".jsp");
String jspFromRequest = this.getJspFromRequest(request);
if (isControllerRequestWithViewName && StringUtil.isNotEmpty(jspFromRequest) && !modelAndView.getViewName().equals(jspFromRequest)) {
modelAndView.setViewName(jspFromRequest);
}
}
이 메서드는 특정 조건을 충족할 때 ModelAndView 객체의 뷰 이름을 업데이트하는 데 사용됩니다. isControllerRequestWithViewName은 modelAndView 객체에 이름이 있고 URL의 path가 .jsp로 끝나지 않을 때 True가 됩니다. 예를 들어, 해당 조건을 충족하는 URL은 http://localhost:8111/random_string이며, 유효하지 않은 URL은 http://localhost:8111/admin/admin.html 또는 http://localhost:8111/admin.jsp입니다. 위에서 논의한 대로, 리다이렉션을 유발하는 페이지(일반적으로 인증이 필요한 페이지)에 접근해서는 안 됩니다. 그런 다음 프로그램은 getJspFromRequest() 메서드를 호출하는 jspFromRequest라는 변수를 할당합니다. 이제 해당 메서드로 넘어가겠습니다. 나머지 코드는 이후에 설명하겠습니다. if 문은 isControllerRequestWithViewName 이 true인지, 메서드 호출 결과가 비어 있지 않은지, 그리고 객체의 이름이 우리가 요청하려는 페이지와 같지 않은지 확인합니다.
protected String getJspFromRequest(@NotNull HttpServletRequest request) { String jspFromRequest = request.getParameter("jsp"); return jspFromRequest == null || jspFromRequest.endsWith(".jsp") && !jspFromRequest.contains("admin/") ? jspFromRequest : null; }
이 함수는 먼저 jsp라는 요청 매개변수의 값을 가져옵니다. 이 검사는 jsp가 .jsp로 끝나야 하고 /admin을 포함하지 않아야 함을 보장합니다. 위의 if 문과 결합하면:
if (isControllerRequestWithViewName && StringUtil.isNotEmpty(jspFromRequest) && !modelAndView.getViewName().equals(jspFromRequest)) {
modelAndView.setViewName(jspFromRequest);
}
URL이 어떻게 생겼는지 전체적인 그림을 파악할 수 있습니다:
.jsp를 포함하지 않아야 합니다.jsp 요청 매개변수는 path와 같지 않아야 합니다. 예를 들어, /random?jsp=/random은 유효하지 않습니다.jsp가 .jsp로 끝나야 한다는 것입니다.TeamCity는 외부 애플리케이션을 통합하고 TeamCity 서버와 스크립트 상호 작용을 생성하기 위한 REST API를 제공합니다. URL 경로를 통해 리소스에 접근할 수 있습니다. 브라우저에서
http://<TeamCity Server host>:<port>/app/rest/serverURL을 열어 REST API 작업을 시작할 수 있습니다. 이 페이지는 API를 탐색하기 위한 몇 가지 포인터를 제공합니다.
TeamCity는 인증을 우회할 수 있다면 민감한 리소스에 접근할 수 있게 해주는 REST API를 제공합니다. 예를 들어, 이 경우 /app/rest/server에 접근을 시도해 보겠습니다.

예상대로, /app/rest/server에 요청하면 서버는 /login.html로 리다이렉트합니다. 이 제한을 우회할 완벽한 URL을 구성해 봅시다. 먼저 경로는 404 상태 코드 또는 200을 반환하는 한 무엇이든 될 수 있습니다(예: login.html). 다음으로 jsp를 사용하여 /app/rest/server에 요청하되, .jsp로 끝나야 합니다. 이 상황에서 이 검사를 우회하는 데 사용할 수 있는 두 가지 트릭이 있습니다. 세미콜론 ;을 매개변수 구분자로 사용할 수 있습니다: /app/rest/server;.jsp, 이 경우 .jsp는 두 번째 매개변수로 처리됩니다. 두 번째 우회 방법은 URI 프래그먼트 #을 사용하는 것입니다: /app/rest/server%23.jsp. URI 프래그먼트 뒤의 내용은 라우팅에 사용되지 않으며, 페이지 내 탐색에만 사용됩니다. 해당 문자는 URL 인코딩이 필요합니다. 그렇지 않으면 브라우저가 먼저 이를 무시합니다.

이 기법을 사용하면 관리자 권한을 가진 새 사용자를 만들 수도 있습니다. TeamCity 문서에 따르면 /app/rest/users 엔드포인트를 통해 새 사용자를 만들 수 있습니다.

관리자 패널로 이동하여 새 사용자가 생성되었는지 확인해 봅시다.

예상대로 관리자 권한을 가진 새 사용자가 생성되었습니다. 관리자 권한으로 서버를 완전히 제어할 수 있습니다. 하지만 원격 코드 실행을 획득하면 더 나아갈 수 있습니다. 이 CVE는 2023.11.4 이전의 모든 버전에 영향을 미치지만, 이 RCE는 2023.11 이전 버전에서만 가능합니다. 관리자 권한이 있는 사용자가 임의의 명령을 실행할 수 있게 해주는 문서화되지 않은 엔드포인트 /app/rest/debug/processes가 있습니다. 요청 URL에 2개의 요청 매개변수를 포함한 POST 요청을 보냅니다. Windows의 경우 ?exePath=cmd.exe¶ms=/c%20[our command here]이고, Linux의 경우 ?exePath=/bin/sh¶ms=-c%20[our command here]입니다. 저는 Windows에서 TeamCity를 실행 중이므로 전체 경로는 /app/rest/debug/processes?exePath=cmd.exe¶ms=/c%20whoami가 됩니다.

서버는 요청에 csrf token이 없다는 403 오류를 반환합니다. TeamCity 문서는 토큰을 검색하기 위한 엔드포인트인 /authenticationTest.html?csrf도 제공합니다.
토큰을 검색한 후 요청 헤더 X-TC-CSRF-Token 또는 tc-csrf-token HTTP 매개변수에 추가합니다. 여기서는 X-TC-CSRF-Token을 선택했습니다.

csrf token을 제공한 후 원격으로 명령 실행에 성공하여 서버에 대한 완전한 제어 권한을 얻습니다.
이 CVE에서 보안 결함은 입력 검증이 아니라 코드의 로직에 있었습니다. 이 애플리케이션은 매우 복잡해서 개발자들이 어떻게든 실수를 하게 됩니다. 또한 인증을 우회하는 새로운 기법을 배웠습니다. 이 분석을 통해 유용한 것을 배우셨기를 바랍니다. 즐거운 해킹 되세요!!!
getJspFromRequest()modelAndView