همونطور که در پست قبلی بهش اشاره کردیم، جنکینز یسری آسیب پذیری رو در محصولاتش اصلاح کرده ، که دو تا از این آسیب پذیری ها در هسته خود برنامه جنکینز بودن و بقیه در پلاگین هایی که برای جنکینز توسعه داده شدن.
در این پست میخواییم :
- یه نسخه ی آسیب پذیر از جنکینز، روی ویندوز 10 نصب می کنیم. (در صورتی که بخوایین POCها رو تست کنید)
- یسری پیش زمینه در خصوص نحوه ی عملکرد جنکینز مرتبط با آسیب پذیری ها رو بررسی می کنیم.
- گزارش محققای SonarSource در خصوص آسیب پذیری CVE-2024-23897 و CVE-2024-23898 رو بررسی می کنیم.
نصب نسخه ی آسیب پذیر جنکینز روی ویندوز 10 :
برای نصب نسخه ی آسیب پذیر، در قدم اول نیاز داریم که فایل نصبش رو دانلود کنیم. این دو تا آسیب پذیری ، نسخه های زیر رو تحت تاثیر قرار میدن :
- Jenkins weekly up to and including 2.441
- Jenkins LTS up to and including 2.426.2
ما از نسخه ی Jenkins LTS 2.426.1 استفاده میکنیم. برای دانلود این نسخه ، یا نسخه های مشابه ، میتونید از این لینک استفاده کنید. البته همین نسخه رو در کانال تلگراممون هم آپلود کردم.
بعد از اینکه فایل نصب رو دانلود کردید، روش 2 بار کلیک کنید، تا فرایند نصب شروع بشه :
در ادامه یه مسیر برای نصب انتخاب می کنیم که من از همون مسیر پیش فرض استفاده کردم:
در ادامه دو تا گزینه ی زیر رو دارید که باید یکیش رو انتخاب کنید :
- Run service as a local or domain user : اگه در یه محیط اکتیودایرکتوری میخوایید نصب کنید، باید این گزینه رو انتخاب کنید و نام کاربری و پسورد اکتیودایرکتوری رو بهش بدید.
- Run service as LocalSystem : اگه در یه محیط معمولی ویندوز 10 میخوایین نصب کنید، میتونید از این گزینه استفاده کنید. این نوع نصب بدلیل اینکه به جنکینز دسترسی کامل به سیستم رو میده، زیاد توصیه نمیشه. من از این گزینه استفاده کردم.
در مرحله ی بعد، باید یه پورت دلخواه رو انتخاب کنید. فقط پورتی که انتخاب میکنید، نباید با سرویس های دیگه تداخل داشته باشه. هر پورتی رو که زدید گزینه ی Test Port رو بزنید تا براتون تست کنه. من از همون پورت پیش فرض استفاده کردم.
جنکینز یه برنامه ای هست که در جاوا توسعه داده شده بنابراین برای اجرا نیاز به JRE یا JDK داریم. در ادامه نصب باید دایرکتوری جاوا رو به جنکینز بدیم. توجه کنید که این نسخه از نسخه های 11 و 17 و 21 جاوا پشتیبانی میکنه. برای بررسی اینکه اصلا جاوا دارید یا نسخه ی جاواتون چیه، میتونید در یه CMD ، دستور java -version
بزنید. من از Java SE Development Kit 17.0.9 استفاده کردم . فایلش در کانال تلگرامی هم گذاشتم که اگه خواستید ، دانلود و استفاده کنید.
بعد از حل جاوا، باید مسیر اونو به جنکینز بدید :
در ادامه نصب ، feature ها رو مشخص میکنید ، که من بصورت پیش فرض ادامه دادم.
در نهایت ، گزینه ی Install رو انتخاب کنید تا فرایند نصب تکمیل بشه.
اگه همه ی مراحل رو درست رفته باشید، نصب با موفقیت به پایان میرسه.
جنکینز بصورت یه سرویس ویندوزی نصب میشه، برای مشاهده اینکه جنکینز در حال اجراست، Task manager رو بیارید بالا و به تب Services برید، اینجا باید در لیست Jenkins رو بصورت Running مشاهده کنید :
بعد از نصب جنکینز، برای استفاده ازش باید اونو آنلاک کنیم. برای اینکار، مرورگرتون رو باز کنید و آدرس لوکال هاست و پورتی که در مراحل قبلی وارد کردید رو وارد کنید . مثلا برای من 127.0.0.1:8080 یا http://localhost:8080 هست. صفحه ی زیر میاد که باید منتظر بشید تا کاراش تموم بشه. اگه احیانا تو این صفحه گیر کردید، Task Manager رو بیارید بالا، برید قسمت Details و پروسس Java.exe رو End کنید. بعدش برید تو تب Services و سرویس جنکینز رو دوباره Run کنید.
بعد از اینکه صفحه بالا رو رد کردید، می رسید به صفحه ی زیر :
تو این صفحه از ما یسری کار میخواد. باید بریم به اون مسیری که به رنگ قرمز مشخص کرده، با Notepad بازش کنیم و محتوای داخل اونو در کادر Administrator Password وارد کنیم.
در ادامه ، میتونیم پلاگین های مورد نیاز رو نصب کنیم. دو تا گزینه داریم، یکی اینکه جنکینز یسری پلاگین پیشنهادی داره که اغلب توسط کاربرا نصب شدن و گزینه بعدی هم نصب پلاگین هایی انتخابی هست. با توجه به اینکه برای اکسپلویت کردن، محیط باید به محیط پیش فرض نزدیک باشه، من از گزینه پیشنهادی استفاده کردم.
بعد از اینکه پلاگین هارو نصب کردید، وارد صفحه ای میشید که باید یه کاربر ادمین رو تعریف کنید .
بعد از اینکه کاربر ادمین رو تعریف کردید، وارد این صفحه میشید که ازتون میخواد آدرس و پورتی که قبلا تعریف کرده بودید رو برای اینکه بتونه در منابع مختلف استفاده کنه، تایید کنید.
در نهایت ، جنکینز برای استفاده آماده هست.
شکل زیر هم محیط داشبورد جنکینز رو مشاهده میکنید :
پیش زمینه :
جنکینز راه های مختلفی برای امتیاز دهی در اختیار ما قرار میده : گزینه ی ناامن anyone can do anything ، legacy Mode و گزینه ی logged-in users can do anything و … .
امتیاز دهی logged-in users can do anything یه گزینه داره که امکان پرمیشن خوندن رو به کاربر ناشناس میده و در نتیجه همه میتونن پرمیشن خوندن داشته باشن. این ویژگی در گزینه ی legacy mode هم فراهمه.
اگه امتیازدهی logged-in users can do anything رو انتخاب و تیک گزینه ی Allow anonymous read access رو هم بزنیم، همچنین بالای این منو، تیک گزینه ی Allow user to sign up رو هم بزنیم، هر کاربری میتونه ثبت نام کنه و در نتیجه دسترسی خوندن داشته باشه. یعنی یه مهاجم میتونه ثبت نام کنه و با توجه به نوع امتیازدهی که پیکربندی کردیم، پرمیشن خوندن داره.
طبق اسناد جنکینز، کاربری که پرمیشن read-only داره، امکان :
- دسترسی به API پایه جنکینز و API ای که براش تعریف شده رو داره.
- دسترسی به لیست اکانت کاربران و شناسه های committer ها رو در پروژه هایی که قابل مشاهده برای کاربر هست رو داره
- مشاهده و لیست همه ی agent های پیکربندی شده در جنکینز و دسترسی به صفحات summary اونارو داره.
از طرف دیگه، ادمین ها میتونن تقریبا هر کاری رو در یه نمونه جنکینز انجام بدن و از نظر مهاجم ، ادمین ها میتونن کد دلخواه رو در سرور جنکینز اجرا کنن.
مورد دیگه که باید قبل از پرداختن به آسیب پذیری ها در خصوصش بدونیم، ویژگی CLI در جنکینز هست. همونطور که قبلا اشاره کرده بودیم، جنکینز یه CLI داخلی داره که امکان کار در محیط های Shell و اسکریپت نویسی رو فراهم میکنه. این ویژگی در مخزن گیتهاب جنکینز در قسمت hudson/cli پیاده سازی شده. برای دسترسی به دستورات و نحوه استفاده از CLI ، اگه جنکینز رو نصب کرده باشید ، میتونید از http://127.0.0.1:8080/cli هم استفاده کنید.
علاوه بر اینکه میشه دستورات رو از طریق SSH یا کلاینت jenkins-cli.jar ( که از web socket استفاده میکنه) اجرا کرد، محققا متوجه شدن که میشه با ارسال دو درخواست POST به http://jenkins/cli?remoting=false ، دسترسی به این CLI رو داشت. اغلب POCهایی که برای آسیب پذیری CVE-2024-23897 نوشته شدن از این روش استفاده کردن.
وقتی Stapler، که کارش مرتبط کردن یه متد به نقطه پایانی هست، متد مربوط به مسیر cli/ رو دریافت میکنه، نقطه پایانی یه استثناء ()PlainCliEndpointResponse ایجاد میکنه که در نهایت به یه تابع generateResponse منتهی میشه :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { try { UUID uuid = UUID.fromString(req.getHeader("Session")); //... if (req.getHeader("Side").equals("download")) { FullDuplexHttpService service = createService(req, uuid); //... try { service.download(req, rsp); } //... } else { FullDuplexHttpService service = services.get(uuid); //... try { service.upload(req, rsp); } //... } } |
این تابع به یه آپلود کننده و دانلود کننده نیاز داره. دانلود کننده، پاسخ دستور رو برمیگردونه و آپلود کننده یه دستور مشخص شده رو از بدنه درخواست ، اجرا میکنه. جنکینز این دانلود کننده و آپلود کننده رو از طریق UUID در هدر جلسه متصل میکنه. POCهایی که برای آسیب پذیری CVE-2024-23897 نوشته شدن از این روش استفاده کردن.
آسیب پذیری CVE-2024-23897 :
این آسیب پذیری همونطور که اشاره کرده بودیم، یه آسیب پذیری بحرانی هست که به یه مهاجم احرازهویت نشده، امکان خوندن چند خط از یه فایل دلخواه رو میده. اگه مهاجم دارای پرمیشن read-only باشه، میتونه کل فایل رو بخونه. مهاجم میتونه از این آسیب پذیری برای افزایش امتیاز و در نهایت اجرای کد استفاده کنه.
اگه جنکینز طوری پیکربندی شده باشه که یکی از شرایط زیر برقرار باشه، کاربری که احرازهویت نشده، حداقل پرمیشن خوندن رو داره و در نتیجه میتونن این آسیب پذیری رو اکسپلویت کنه:
- امتیاز دهی Legacy mode فعال شده باشه.
- ویژگی signup فعال شده باشه.
- در حالت امتیازدهی logged-in users can do anything ، تیک گزینه ی Allow anonymous read access زده شده باشه.
محققا متوجه شدن که جنکینز وقتی میخواد یه دستور CLI رو که دارای یسری آرگومان هست اجرا کنه، برای تجزیه ی آرگومانها از تابع parseArgument در کتابخونه ی args4j استفاده میکنه.
1 2 3 |
if (!(this instanceof HelpCommand || this instanceof WhoAmICommand)) Jenkins.get().checkPermission(Jenkins.READ); p.parseArgument(args.toArray(new String[0])); |
این تابع هم برای اجرا ، تابع expandAtFiles رو فراخوانی میکنه :
1 2 3 4 5 6 7 8 |
public void parseArgument(final String... args) throws CmdLineException { checkNonNull(args, "args"); String expandedArgs[] = args; if (parserProperties.getAtSyntax()) { expandedArgs = expandAtFiles(args); } |
تابع expandAtFiles هم که بصورت زیر تعریف شده :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private String[] expandAtFiles(String args[]) throws CmdLineException { List<String> result = new ArrayList<String>(); for (String arg : args) { if (arg.startsWith("@")) { File file = new File(arg.substring(1)); if (!file.exists()) throw new CmdLineException(this,Messages.NO_SUCH_FILE,file.getPath()); try { result.addAll(readAllLines(file)); } catch (IOException ex) { throw new CmdLineException(this, "Failed to parse "+file,ex); } } else { result.add(arg); } } return result.toArray(new String[result.size()]); } |
این تابع بررسی میکنه که آیا آرگومان ورودی با کاراکتر @ شروع شده یا نه. اگه شروع شده باشه، اونو بعنوان یه مسیر فایل در نظر میگیره و میره محتوای اونو میخونه و جایگزین آرگومان ها میکنه.
با این شرایط اگه مهاجم بتونه آرگومان هارو کنترل کنه، میتونه فایل دلخواه رو از این طریق بخونه.
یکی از روش هایی که مهاجم میتونه از این آسیب پذیری سوء استفاده کنه، اینه که دستوری رو پیدا کنه که آرگومانهای دلخواه رو بگیره و خروجی رو به کاربر نمایش بده. محققا دستور connect-to-node رو پیشنهاد دادن که یسری رشته رو میگیره و سعی میکنه به هرکدومشون وصل بشه. اگه اتصال موفقیت آمیز نباشه، یه خطا با نام اون node برمیگردونه :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class ConnectNodeCommand extends CLICommand { //... @Argument(metaVar = "NAME", usage = "Agent name, or empty string for built-in node; comma-separated list is supported", required = true, multiValued = true) private List<String> nodes; //... @Override protected int run() throws Exception { //... for (String node_s : hs) { try { Computer computer = Computer.resolveForCLI(node_s); computer.cliConnect(force); } catch (Exception e) { //... final String errorMsg = node_s + ": " + e.getMessage(); stderr.println(errorMsg); //... } } //... } } |
دستور connect-to-node برای اجرا ، معمولا نیاز به پرمیشن CONNECT داره که توسط تابع cliConnect بررسی میشه. اما با توجه به اینکه استثناء قبل از بررسی پرمیشن در تابع resolveForCLI رخ میده، بنابراین این دستور فقط نیاز به پرمیشن read-only داره.
فایلهایی که مورد توجه هکرها برای سوء استفاده از این آسیب پذیری میتونه باشه :
- کلید های SSH
- فایل /etc/passwd, /etc/shadow
- اعتبارنامه ها و اطلاعات حساس پروژه ها
- سورس کدها و داده های تولید محصول
- و … .
برای اصلاح این آسیب پذیری، جنکینز اومده ویژگی expandAtFiles رو در نسخه های اصلاح شده، غیر فعال کرده :
1 2 3 4 5 |
+ public static boolean ALLOW_AT_SYNTAX = SystemProperties.getBoolean(CLICommand.class.getName() + ".allowAtSyntax"); //... - return new CmdLineParser(this); + ParserProperties properties = ParserProperties.defaults().withAtSyntax(ALLOW_AT_SYNTAX); + return new CmdLineParser(this, properties); |
اگه جنکینز رو نصب کرده باشید، نوع شبکه ماشین مجازی رو روی Bridge بزارید و بعد در ماشین مجازی که جنکینز رو نصب کردید، یه CMD باز کنید و آدرس IP اونو با دستور ipconfig بدست بیارید. برای من مقدار 192.168.1.3 بود.
اگه بدرستی ماشین مجازی و هاست رو، شبکه کنید ( شاید نیاز باشه که فایروال رو در ماشین مجازی غیرفعال کنید) و بتونید PING بگیرید، با وارد کردن آدرس IP ماشین مجازی ، برای من 192.168.1.3 ، میتونید صفحه ی لاگین جنکینز رو ببینید. در حقیقت فرض میکنیم که ما بعنوان یه مهاجم میخواییم از 192.168.1.3 سوء استفاده کنیم.
در این صفحه لاگین کنید و بعدش برید به مسیر cli ، مثلا برای من به این صورته : http://192.168.1.3:8080/cli
در این صفحه ، فایل jenkins-cli.jar رو دانلود کنید. میتونید از کانال تلگرامیمون هم دانلود کنید.
این فایل همون CLI جنکینز هست که میتونیم در محیط های SHELL و اسکریپت نویسی ازش استفاده کنیم. همونطور که مشاهده میکنید، با java میتونیم اجراش کنیم.
من میخوای محتوای فایل win.ini که در مسیر C:/windows/win.ini هست رو بخونم :
1 |
java -jar jenkins-cli.jar -s http://192.168.1.3:8080/ help @C:/windows/win.ini |
خروجی این دستور :
خروجی بالا ، برای کاربری هست که احرازهویت نشده و جنکینز بصورت پیش فرض نصب شده. اگه جنکینز بصورت امن پیکربندی نشده باشه که در بالا اشاره کردیم، یا مهاجم بتونه احرازهویت کنه، میتونه کل فایل رو بخونه. مثلا در کد زیر من از دستور connect-node بجای help استفاده کردم و همچنین احرازهویت رو هم انجام دادم تا بتونم کل فایل رو بخونم :
در ادامه ، من یه فایل بنام TEST.txt در دسکتاپ ماشین مجازی ایجاد کردم و توش onhexgroup.ir رو ریختم و دوباره دستورات بالا رو زدم :
هونطور که مشاهده میکنید، ما نتونستیم فایل رو از مسیر یه کاربر دیگه بخونیم. چون بصورت پیش فرض یه محدودیتی هست. اما اگه ادمین در پیکربندی جنکینز اشتباه کنه و دسترسی خوندن رو به همه بده و یا مهاجم بتونه احرازهویت کنه، این امر هم دور میخوره :
برای این آسیب پذیری دو تا PoC منتشر شده که میتونید از اینجا و اینجا دانلود و تست کنید.
این دو تا POC هم از روش بالا استفاده میکنن، فقط بجای jenkins-cli.jar از روشی که محققای Sonar کشف کرده بودن، یعنی ، http://jenkins/cli?remoting=false
استفاده کردن.
خروجی POC توسعه داده شده توسط binganao :
آسیب پذیری CVE-2024-23898 :
این آسیب پذیری شدت بالا داشت و از نوع cross-site WebSocket hijacking (CSWSH) بود. مهاجم میتونه با فریب کاربر برای کلیک رو لینک های مخرب، دستورات CLI دلخواه رو اجرا کنه.
اگه قربانی از مرورگرهایی مانند کروم و Edge استفاده کنه که بصورت پیش فرض ویژگی SameSite رو روی lax تنظیم میکنن، تحت تاثیر این آسیب پذیری قرار نمیگیره. اما با توجه به اینکه قربانی ممکنه از مرورگرهای قدیمی استفاده کنه یا از مرورگرهایی مثله فایرفاکس یا Safari استفاده کنه بصورت پیش فرض این گزینه روشون فعال نیست، میتونه تحت تاثیر قرار بگیره و برای همین این آسیب پذیری شدت بالا داره.
همونطور که بالا اشاره شد، یکی از راههایی که میشه دستورات CLI رو اجرا کنیم، استفاده از jenkins-cli.jar هستش که از web socket ها استفاده میکنه.
سیاستهای SOP و CORS در مرورگرها، روی WebSockets بصورت اجباری اعمال نمیشن، دلیلش هم اینه که این محدوددیتها ،روی پاسخ های HTTP اعمال میشه ، در حالیکه WebSockets روی پروتکل های WS(WebSocket) یا WSS(WebSocketSecure) کار میکنن.
با توجه به اینکه در درخواستهای وب سوکت ، Jenkins-crumb یا Origin header بررسی نمیشه ، بنابراین هر وب سایتی میتونه با استفاده از WebSocket ، مشابه آسیب پذیری های CSRF ، دستورات CLI رو با شناسه قربانی اجرا کنه.
برای اصلاح این آسیب پذیری جنکینز اومده یه بررسی برای origin در نقطه پایانی WebSocket اضافه کرده.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public HttpResponse doWs(StaplerRequest req) { if (!WebSockets.isSupported()) { return HttpResponses.notFound(); } + if (ALLOW == null) { + final String actualOrigin = req.getHeader("Origin"); + final String expectedOrigin = StringUtils.removeEnd(StringUtils.removeEnd(+Jenkins.get().getRootUrlFromRequest(), "/"), req.getContextPath()); + + if (actualOrigin == null || !actualOrigin.equals(expectedOrigin)) { + LOGGER.log(Level.FINE, () -> "Rejecting origin: " + actualOrigin + "; expected was from request: " + +expectedOrigin); + return HttpResponses.forbidden(); + } + } else if (!ALLOW) { + return HttpResponses.forbidden(); + } Authentication authentication = Jenkins.getAuthentication2(); |
منابع: