
تحليل مفصل لـ CVE-2024-27198: تجاوز المصادقة في JetBrains TeamCity يؤدي إلى تنفيذ التعليمات البرمجية عن بُعد، مع شرح على مستوى الكود وعرض الاستغلال.
TeamCity هو خادم تكامل مستمر ونشر يوفر اختبارات وحدة مستمرة جاهزة للاستخدام، وتحليل جودة الكود، وتقارير مبكرة عن مشاكل البناء
تظهر الثغرة في مكتبة تسمح للمهاجمين بالوصول إلى نقاط نهاية غير مصادق عليها بشكل تعسفي.
الكود القابل للاختراق موجود في مكتبة web-openapi.jar في directory to the lib
الصنف 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 هو عرض الصفحة المطلوبة في واجهة المستخدم. هنا تبدأ الثغرة. لاحظ أن 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 بقيمة True إذا كان كائن modelAndView يحتوي على اسم وكان path في الرابط لا ينتهي بـ .jsp. على سبيل المثال، الرابط الذي يحقق هذه الشروط هو http://localhost:8111/random_string والرابط غير الصالح سيكون http://localhost:8111/admin/admin.html أو http://localhost:8111/admin.jsp. كما ناقشنا أعلاه، يجب ألا نصل إلى أي شيء يسبب إعادة توجيه، وعادةً ما تكون هذه صفحات تتطلب تفويضًا (Authorization). بعد ذلك، سيخصص البرنامج متغيرًا باسم jspFromRequest والذي سيستدعي الطريقة getJspFromRequest(). دعنا ننتقل الآن إلى تلك الطريقة، وسأشرح الكود المتبقي بعد ذلك. توجد عبارة if ستتحقق مما إذا كان isControllerRequestWithViewName صحيحًا، وكانت نتيجة استدعاء الطريقة getJspFromRequest() غير فارغة، ويجب ألا يساوي اسم كائن modelAndView الصفحة التي نريد طلبها عبر
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);
}
سيكون لدينا صورة شاملة لكيفية شكل الرابط:
.jspjsp قيمة path. على سبيل المثال، /random?jsp=/random سيكون غير صالحjsp بـ .jspتوفر TeamCity واجهة برمجة تطبيقات REST لدمج التطبيقات الخارجية وإنشاء تفاعلات برمجية مع خادم TeamCity. كما تسمح بالوصول إلى الموارد عبر مسارات URL. يمكنك البدء في العمل مع REST API بفتح الرابط
http://<TeamCity Server host>:<port>/app/rest/serverفي متصفحك: تعطيك هذه الصفحة عدة مؤشرات لاستكشاف الواجهة.
تقدم TeamCity واجهة REST API تسمح لنا بالوصول إلى الموارد الحساسة إذا تمكنا من تجاوز المصادقة. في هذه الحالة، على سبيل المثال، سنحاول الوصول إلى /app/rest/server.

كالمعتاد، سيعيد الخادم توجيهنا إلى /login.html عندما نطلب /app/rest/server. دعنا ننشئ رابطًا مثاليًا لتجاوز هذا القيد. أولًا، يمكن أن يكون مسارنا أي شيء طالما أنه يُرجع رمز الحالة 404 أو حتى 200 مثل login.html. بعد ذلك، سنستخدم jsp لطلب /app/rest/server، لكن يجب أن ينتهي بـ .jsp. في هذه الحالة، هناك حتى حيلتان يمكننا استخدامهما لتجاوز هذا الفحص. يمكننا استخدام الفاصلة المنقوطة ; كمحدد للمعاملات: /app/rest/server;.jsp، وهذه المرة سيتم التعامل مع .jsp كمعامل ثانٍ. أما التجاوز الثاني فهو استخدام جزء URI #: /app/rest/server%23.jsp. ما يأتي خلف جزء URI لن يُستخدم للتوجيه (Routing)، بل يُستخدم فقط للتنقل في الصفحة. لاحظ أن الحرف يجب أن يكون مرمّزًا لرابط (URL-encoded)، وإلا سيتجاهله المتصفح أولًا.

بهذه التقنية، يمكننا حتى إنشاء مستخدم جديد بصلاحيات مدير (Admin Privilege). ذكرت وثائق TeamCity أنه يمكننا إنشاء مستخدم جديد عبر نقطة النهاية /app/rest/users.

دعنا نذهب إلى لوحة الإدارة ونتحقق مما إذا كان هناك أي مستخدم جديد تم إنشاؤه.

كما هو متوقع، تم إنشاء مستخدم جديد بصلاحيات مدير (Admin Privilege). بصلاحيات المدير، يمكننا التحكم الكامل في الخادم. ومع ذلك، يمكننا الذهاب أبعد من ذلك من خلال الحصول على تنفيذ التعليمات البرمجية عن بُعد. هذه الثغرة (CVE) تؤثر على جميع الإصدارات قبل 2023.11.4، لكن هذا الـ RCE ممكن فقط للإصدارات السابقة لـ 2023.11. هناك نقطة نهاية غير موثقة /app/rest/debug/processes تسمح للمستخدم بصلاحيات المدير بتنفيذ أوامر تعسفية. سنرسل طلب POST مع معاملَي طلب في رابط الطلب. لنظام ويندوز سيكون ?exePath=cmd.exe¶ms=/c%20[our command here] ومع لينكس سيكون ?exePath=/bin/sh¶ms=-c%20[our command here]. أنا أشغّل TeamCity على ويندوز، لذا سيكون المسار الكامل هو /app/rest/debug/processes?exePath=cmd.exe¶ms=/c%20whoami

أعاد الخادم خطأ 403 يفيد بأن الطلب يفتقر إلى csrf token. توفر وثائق TeamCity أيضًا نقطة النهاية لاسترجاع الرمز، وهي /authenticationTest.html?csrf.
بعد استرجاع الرمز، سنضيفه إلى ترويسة الطلب X-TC-CSRF-Token أو معامل HTTP tc-csrf-token، وهنا اخترت X-TC-CSRF-Token.

بعد توفير csrf token، نجحنا في تنفيذ الأوامر عن بُعد، مما يمنحنا تحكمًا كاملاً في الخادم.
في هذه الثغرة (CVE)، لم يكن الخلل الأمني في التحقق من المدخلات، بل في منطق الكود. هذا التطبيق معقد حقًا لدرجة أن المطورين سيرتكبون الأخطاء بطريقة ما. تعلمنا أيضًا تقنية جديدة لتجاوز المصادقة. آمل أن تتعلم شيئًا مفيدًا من هذا التحليل. اختراق سعيد!!!