چگونه با شکستن PHP، توانستیم به سایت بزرگ سال نفوذ و 20000 دلار کسب کنیم؟

چگونه با شکستن PHP، توانستیم به سایت بزرگ سال نفوذ و 20000 دلار کسب کنیم؟

همه چیز با بررسی سایت بزرگ سال، سپس شکستن PHP و در آخر با دور زدن هر دو شروع شد…

خلاصه:

  • ما موفق به پیدا کردن آسیب پذیری «اجرای کد از راه دور (RCE)» بر روی com شده و توانستیم 20000 دلار باگ بانتی در هکروان به دست آوریم.
  • ما دو آسیب پذیری use-after-free (نوعی آسیب پذیری که به استفادۀ نادرست از حافظۀ پویا در حین اجرای برنامه مربوط می شود – نوعی آسیب پذیری که در اثر اشتباه دولوپر نرم افزار ایجاد شده و ممکن است به نتایج فاجعه باری نظیر RCE یا افشای داده های حساس منجر گردد) در الگوریتم جمع آوری زبالۀ PHP پیدا کردیم.
  • این آسیب پذیری ها بر روی تابع unserialize موجود در PHP قابل اکسپلویت بودند.
  • علاوه بر این ما موفق به کسب 2000 دلار از کمیتۀ باگ بانتی اینترنت نیز شدیم (جایزۀ هکروان)

کردیت ها:

این پروژه توسط Dario Weißer (@haxonaut)، cutz و Ruslan Habalov (@evonide) محقق شد. از cutz بابت مشارکت در تهیۀ این مقاله بسیار ممنونم.

برنامۀ باگ بانتی سایت بزرگ سال و پاداش های نسبتاً زیاد آن بر روی پلتفرم هکروان، توجه ما را به خود جلب کرد. به همین دلیل بود که چشم انداز یک مهاجم حرفه ای را به خود گرفتیم که قصد نفوذ به سیستم با عمیق ترین حالت ممکن را داشت. مهم ترین هدف ما، پیدا کردن آسیب پذیری اجرای کد از راه دور (RCE) بر روی سیستم مذکور بود. از این رو، هر کاری از دستمان بر می آمد انجام دادیم و به آنچه که سایت بزرگ سال بر پایۀ آن ساخته شده بود، یعنی PHP، حمله ور شدیم.

کشف آسیب پذیری:

پس از بررسی پلتفرم، خیلی سریع متوجه استفاده از تابع unserialize بر روی این وب سایت شدیم. چندین مسیر (یعنی هر جایی که شما در آن امکان آپلود تصاویر هات و… داشتید) درگیر این مسئله بودند، برای مثال:

  • http://www.target.com/album_upload/create
  • http://www.target.com/uploading/photo

در تمامی این موارد، پارامتری تحت عنوان cookie از دیتای POST، unserialize شده و در ادامه توسط هدرهای Set-Cookie بازتاب پیدا می کرد. نمونه درخواست:

شکستن PHP

به منظور تایید هر چه بیشتر این مسئله می توان آرایه ای حاوی یک آبجکت ایجاد نمود:

قالب پاسخ:

در وهلۀ اول شاید این پاسخ یک افشای اطلاعات ساده به نظر برسد، اما در حالت کلی، گفته شده که استفاده از ورودی کاربر روی تابع unserialize ایدۀ بدی است:

تکنیک های اکسپلویت استاندارد در اصطلاح نیازمند برنامه نویسی دارایی گرا (POP) بوده که به معنی سو استفاده از کلاس های از پیش موجود از طریق «توابع جادویی» هستند که به طور ویژه تعریف شده اند؛ تا به این طریق بتوان مسیرهای کد ناخواسته و مخرب را راه اندازی نمود. متاسفانه، برای ما بسیار دشوار بود که دربارۀ فریم ورک ها و آبجکت های عمومی PHP مورد استفاده در سایت بزرگ سال ،اطلاعات جمع آوری نماییم. چندین کلاس از فریم ورک های رایج مورد استفاده به صورت آزمایشی تست شد، بدون این که با موفقیتی همراه باشد.

توضیح آسیب پذیری:

تابع unserialize اصلی خود به تنهایی پیچیده است چرا که شامل بیش از 1200 خط کد به زبان PHP 5.6 می باشد. علاوه بر این، بسیاری از کلاس های درونی PHP، تابع unserilize مخصوص به خود را دارند. با توجه به پشتیبانی زبان برنامه نویسی PHP از ساختارهایی نظیر آبجکت ها، آرایه ها، اعداد صحیح (integers)، رشته ها (strings) یا حتی ارجاع ها، تعجبی ندارد که نسبت به آسیب پذیری های خرابی حافظه مستعد باشد. متاسفانه، هیچ آسیب پذیری شناخته شده ای از این نوع برای نسخه های جدیدتر PHP نظیر PHP 5.6 یا PHP 7 وجود نداشت، به خصوص این که تابع unserialize از گذشته توجه های بسیاری را به خود جلب کرده بود (برای مثال phpcodz). از این رو، بررسی این تابع را می توان با فشردن لیمویی مقایسه کرد که از قبل به طور کامل فشرده شده است. بالاخره آسیب پذیری های بالقوۀ این تابع، پس از جلب این همه توجه و رفع مشکلات متعدد امنیتی آن، می بایست به طور کامل برطرف شده و امن می بود، مگر نه؟

فاز کردن و ریش ریش کردن تابع unserialize:

برای پاسخ دادن به سوال بخش قبل، Dario فازری را اجرا کرد که به طور ویژه برای فاز کردن رشته های سریال شده از طریق تابع unserialize ایجاد شده بود. اجرای این فازر با PHP 7 خیلی سریع به رفتاری غیرمنتظره منجر شد. هر چند که این رفتار هنگام تست سرور سایت بزرگ سال مجدداً مشاهده نشد؛ از این رو ما نتیجه گرفتیم که این سرور از نسخۀ PHP 5 استفاده می کند.

با این حال، اجرای فازر بر روی نسخۀ جدیدتر PHP 5، بدون هیچ موفقیتی بیش از یک ترابایت لاگ تولید کرد. در نهایت، پس از صرف تلاش های فراوان بر روی عملیات فازینگ، آن رفتار غیرمنتظره دوباره تکرار شد. حال چندین سوال می بایست جواب داده میشد: آیا این مورد یک مسئلۀ امنیتی محسوب می شود؟ اگر اینطور است، فقط می توانیم به صورت محلی آن را اکسپلویت کنیم یا این کار از راه دور هم ممکن است؟ در ادامه، این فازر با تولید داده های غیر قابل پرینت با سایزی بیش از 200 کیلوبایت، وضعیت را پیچیده تر نیز کرد.

تحلیل رفتار غیر منتظره:

زمان زیادی برای تحلیل مسائل لازم بود. سرانجام، ما توانستیم یک گزارش عملکرد (PoC) دقیق از یک آسیب پذیری خرابی حافظۀ در حال اجرا – که به اصلاح از آن تحت عنوان آسیب پذیری use-after-free نیز یاد می شود – بیابیم. پس از بررسی های بیشتر متوجه شدیم که علت اصلی می تواند ناشی از الگوریتم جمع آوری زبالۀ PHP باشد، یعنی کامپوننتی از PHP که هیچ گونه ارتباطی با تابع unserialize ندارد. با این حال، بلافاصه پس از این که تابع unserialize کار خود را تمام می کرد، تعامل بین این دو کامپوننت اتفاق می افتاد. در نتیجه، مورد خیلی مناسبی برای اکسپلویت از راه دور به نظر نمی رسید. پس از تحلیل های بیشتر، تلاش های فراوان و دستیابی به درک عمیق تری از دلایل ریشه ای مشکل مربوطه، موفق به یافتن یک آسیب پذیری use-after-free شدیم که ما را نسبت به اکسپلویت از راه دور امیدوار نمود.

لینک های آسیب پذیری:

پیچیدگی بالای آسیب پذیری های PHP یافته شده و روند کشف آن ها، ما را ملزم به نوشتن مقالات مجزا نمود. برای کسب اطلاعات بیشتر در مورد جزئیات، به رایت آپ Dario در خصوص فاز کردن تابع unserialize مراجعه نمایید.

علاوه بر این، ما مقاله ای دربارۀ نحوۀ شکستن الگوریتم جمع آوری زبالۀ PHP و تابع Unserialize تهیه کرده ایم.

بهره برداری:

حتی اکسپلویت همین آسیب پذیری use-after-freeی امیدوارکننده نیز به طرز چشمگیری دشوار بود. به ویژه این که شامل چندین مرحله اکسپلویت میشد.

با توجه به این که هدف اصلی ما اجرای کد دلخواه بود، نیاز داشتیم که به نحوی اشاره گر دستورالعمل CPU که از آن تحت عنوان RIP روی x86_64 یاد می شود را دستکاری نماییم. این مسئله معمولاً موانع زیر را سر راه دارد:

  1. stack و heap (که شامل هر نوع ورودی احتمالی کاربر نیز هستند) همانند هر بخش قابل نوشتن دیگری، غیر قابل اجرا هستند.
  2. حتی اگر قادر به کنترل کردن اشاره گر دستورالعمل باشید، می بایست بدانید که قصد اجرای چه چیزی را دارید، برای مثال شما می بایست آدرس معتبری از بخش قابل اجرای حافظه داشته باشید. به این منظور، روش رایج این است که تابع system را که قادر به اجرای دستور shell است، فراخوانی نمایید. در زمینۀ PHP، معمولاً تنها کافی است تا zend_eval_string را اجرا نمایید که اغلب نیز اجرا می شود؛ برای مثال هنگامی که دستور “eval(‘echo 1337;’);” را در اسکریپت PHP خود می نویسید، این امکان برایتان فراهم می شود که بدون نیاز به انتقال به سایر کتابخانه های درگیر، کد PHP دلخواه خود را اجرا نمایید.

برای غلبه بر مشکل اول می توان از برنامه نویسی بازگشت گرا (ROP) استفاده کرد، که در آن می توانید از قطعات حافظۀ قابل اجرا و از پیش موجود خود باینری و کتابخانه های آن استفاده نمایید. با این حال، برای حل مشکل دوم می بایست آدرس صحیح zend_eval_string را پیدا کنید. اغلب اوقات هنگامی که یک برنامۀ به صورت پویا لینک شده اجرا می گردد، بارگزاری کنندۀ (loader) موجود، روند مربوطه را به 0x400000 که آدرس بارگزاری استاندارد بر روی x86_64 محسوب می شود، نگاشت می کند. در صورتی که قبلاً به نحوی فایل اجرایی صحیح PHP را به دست آورده اید (برای مثال از طریق پیدا کردن بستۀ دقیق که توسط تارگت ارسال شده است)، می توانید به طور محلی تنها به دنبال آفست برای تابعی که مدنظرتان است، بگردید. ما متوجه شدیم که سایت بزرگ سال از یک نسخۀ کامپایل شدۀ سفارشی از php5-cgi استفاده می کند، بنابراین همین مسئله باعث شد که تعیین نسخۀ دقیق PHP و به دست آوردن هر گونه اطلاعات در رابطه با طرح حافظۀ مربوط به کل فرایند PHP برایمان بسیار دشوار باشد.

 

افشای باینری PHP و اشاره گرهای موردنیاز

اکسپلویت آسیب پذیری های use-after-fee در PHP معمولاً از قوانین مشابهی پیروی می کند. به محض این که قادر به پر کردن حافظۀ آزاد شده ای شوید که در ادامه قرار است دوباره به عنوان یک متغیر PHP داخلی مورد استفاده قرار گیرد (به اصطلاح zvals)، می توانید بردارهایی تولید کنید که امکان خواندن از حافظۀ دلخواه و اجرای کد را برایتان فراهم می نمایند.

 

آماده سازی افشای حافظه

همانطور که قبلاً نیز اشاره شد، ما نیاز داشتیم تا اطلاعات بیشتری دربارۀ PHP باینری سایت بزرگ سال به دست آوریم. از این رو، اولین گام برای ما این بود که از آسیب پذیری use-after-free به منظور تزریق یک zval که نشان دهندۀ یک رشتۀ PHP است، سو استفاده کنیم. تعریف ساختار zval در PHP 5.6 به شکل زیر است:

که در آن، فیلد zvalue_value به عنوان یک union تعریف شده و در نتیجه امکان type juggling (تغییر نوع) را به راحتی میسر نموده است.

یک متغیر PHP از نوع رشته، یک zval نوع 6 محسوب می شود. در نتیجه، با union به عنوان ساختاری برخورد می کند که شامل یک اشاره گر char و یک فیلد طول (length) است. در نتیجه، ساختن یک رشتۀ zval با نقطۀ شروع و طول دلخواه، نشت اطلاعات قدرتمندی را ایجاد می کند که این نشت اطلاعات زمانی که تابع setcookie() موجود در سایت بزرگ سال، مقدار zval تزریقی را در هدر پاسخ منعکس می کند، اتفاق خواهد افتاد.

یافتن پایه و اساس تصویری PHP

معمولاً روند کار را می توان از افشای باینری شروع کرد که همانطور که قبلاً نیز گفته شد، از آدرس 0x400000 آغاز می گردد. متاسفانه، سرور سایت بزرگ سال از مکانیزم های حفاظتی نظیر PIE و SLR استفاده می کرد که پایه تصویری فرآیند و کتابخانه های اشتراکی آن را تصادفی می کرد. این امر به حالت پیشفرض تبدیل شده، چرا که بیشتر توزیع ها، بسته هایی را ارسال می کنند که کد مستقل از موقعیت را فعال می کند.

چالش بعدی این بود: پیدا کردن آدرس صحیح بارگذاری باینری

اولین بخش دشوار مرحلۀ مذکور این بود که به نحوی یک آدرس معتبر به عنوان نقطۀ شروع افشا به دست آوریم. در این قسمت، دانستن جزئیاتی دربارۀ نحوۀ مدیریت حافظۀ داخلی PHP می تواند بسیار مفید باشد. به محض این که یک zval آزاد می شود، PHP هشت بایت اول آن را با آدرس قطعۀ آزاد شدۀ قبلی بازنویسی می کند. از این رو، به عنوان یک ترفند برای به دست آوردن اولین آدرس معتبر، می توان یک zval عددی ایجاد نمود، سپس این zval عددی را آزاد کرده و سرانجام، از یک اشاره گر آویزان به این zval برای دستیابی به آدرس فعلی آن استفاده نمود.

از آنجایی که php-cgi چندین ورکر اجرا می کند که به سادگی از یک فرآیند اصلی نشات میگیرند (فورک می شوند)، مادامی که شما دیتایی با اندازۀ مشابه ارسال کنید، طرح حافظه معمولاً هیچ گاه بین درخواست های مختلف تغییر نمی یابد. همچنین به همین خاطر است که می توانیم درخواست پشت درخواست ارسال کنیم و از طریق تنظیم نقطه شروع رشتۀ zval جعلی به آدرس های مختلف، هر بار بخش متفاوتی از حافظه را افشا نماییم. با این حال، به دست آوردن آدرس heap یک قطعۀ آزاد شده، به خودی خود برای یافتن سرنخ از محل قابل اجرا کافی نیست. علت این مسئله نیز کمبود هر گونه اطلاعات مفید در اطراف آن قطعه است.

برای به دست آوردن آدرس های جالب، تکنیک نسبتاً پیچیده ای وجود دارد که نیازمند چندین مرتبه آزادسازی و تخصیص ساختارهای PHP در روند unserialization است (ROP in PHP applications، اسلاید 67). با توجه به ذات آسیب پذیری و به منظور کاهش هر چه بیشتر پیچیدگی، ما از ترفند خود استفاده کرده ایم.

با استفاده از یک رشتۀ سریالی شده نظیر “i:0;a:0:{}i:0;a:0:{}[…]i:0;a:0:{}” به عنوان بخشی از پیلود unserilize شدۀ کلی، می توانیم تابع unserilize را وادار کنیم تا آرایه های خالی متعدد ایجاد کرده و پس از خاتمۀ کار، آن ها را آزادسازی نماید. هنگام مقداردهی اولیۀ آرایه، PHP در ادامه کار برای zval و hashtable آن حافظه اختصاص می دهد. یک ورودی hashtable پیشفرض برای آرایه های خالی، سمبل uninitialized_bucket است. روی هم رفته، ما توانستیم به قطعه ای از حافظه دست یابیم که شبیه حالت زیر بود:

شکستن PHP

آدرس 0xeae040 در آدرس سمبل uninitialized_bucket متعلق به PHP، به طور مستقیم به بخش  BSS از PHP اشاره می کند. می توانید ببینید که این امر چندین بار در همسایگی آخرین چانک و تکۀ آزاد شده رخ می دهد. همانطور که قبلاً نیز گفته شد، بیشتر آرایه های خالی آزاد شدند. بنابراین، از طریق سواستفاده از شرایطی که در آن برخی از ورودی های hashtable به صورت تغییرنیافته در heap باقی مانده اند، ما قادر به افشای این سمبل ویژه بودیم.

سرانجام، ما قادر به اعمال یک اسکن رو به عقب page-wise شدیم که از آدرس سمبل uninitialized_bucket آغاز شده تا هدر ELF را پیدا کند:

شکستن PHP

افشای بخش های جالب باینری PHP

در این نقطه، شرایط ما همه چیز را پیچیده تر نیز کرد، چرا که قادر به افشای تنها یک کیلوبایت داده به ازای هر درخواست بودیم (این امر به علت محدودیت های اعمال شده برای سایز هدر توسط وب سرور سایت بزرگ سال بود). یک باینری PHP می تواند تا حدود 30 مگابایت حجم داشته باشد. با در نظر گرفتن یک درخواست به ازای هر یک ثانیه، تکمیل شدن این افشا نزدیک به 8 ساعت و 20 دقیقه طول می کشید. از آنجایی که می ترسیدیم روند اکسپلویت ما هر لحظه به هر دلیلی قطع شود، لازم بود تا حد امکان سریع و مخفیانه عمل نماییم. به همین دلیل بود که لازم شد مقداری اکتشافی و هیوریستیک اجرا کنیم تا از قبل، بخش هایی که جالب به نظر می رسیدند را حدس بزنیم. با این اوصاف، می توانستیم هر ساختاری را که در رشتۀ ELF و جدول سمبل به آن ارجاع داده شده بود را ریزالو کنیم. تکنیک های دیگری نظیر ret2dlresolve وجود دارند که امکان حذف کل فرایند افشا را فراهم می سازند، اما امکان اعمال آن ها در اینجا میسر نبود، چرا که این امر به ایجاد ساختارهای دادۀ بیشتر و کسب اطلاع از محل های متفاوت حافظه نیاز دارد.

برای به دست آوردن آدرس zend_eval_string، در ابتدا می بایست هدرهای برنامۀ ELF را که در آفست 32 قرار دارند بیابید، سپس تا زمانی که یک ورودی هدر برنامه نوع دو (PY_DYNAMIC) بیابید، به سمت جلو اسکن کنید تا بخش پویای ELF را به دست آورید. این بخش در نهایت حاوی رفرنسی به رشته و جدول سمبل (نوع 5 و 6) است که می توانید با استفاده از فیلدهای اندازه به طور کامل از آن روبرداری کرده و هر تابعی را که آدرس مجازی آن مدنظرتان است، بگیرید. به عنوان روش جایگزین، همچنین می توانید با استفاده از hashtable (DT_HASH) توابع را سریع تر پیدا کنید، هر چند که در این سناریو، این امر اهمیت چندانی ندارد چرا که به هر حال شما می توانید خیلی سریع بین جداول به صورت محلی پیمایش کنید. علاوه بر zend_eval_string، ما به سمبل های بیشتر و مکان متغیرهای POST خود علاقه داشتیم (چرا که بعدها قرار بود از آن ها به عنوان یک پشتۀ ROP استفاده شود).

 

افشای آدرس دیتای POST ما

برای پیدا کردن آدرس دیتای POST ارائه شده، تنها کافی است اشاره گرهای بیشتری را با خواندن موارد زیر افشا کنید:

پیمایش این زنجیره و رشته، پیچیده به نظر می رسد، اما تنها کافی است تعدادی اشاره گر با آفست صحیح را از ارجاع خارج نمایید و به این ترتیب به سرعت قادر خواهید بود جریان stdin:// را که به دیتای POST داخل heap اشاره دارد، پیدا کنید.

 

آماده سازی پیلود ROP

بخش دوم از این پروسه، در حقیقت با کنترل کردن فرآیند PHP و فراهم کردن امکان اجرای کد سر و کار دارد. برای به وقوع پیوستن این امر، لازم است در ابتدا در مورد اینکه چگونه می توان اشاره گر دستورالعمل را تغییر داد، صحبت کنیم.

 

در اختیار گرفتن اشاره گر دستورالعمل

ما پیلودمان را طوری تنظیم کردیم که حاوی یک آبجکت جعلی (به جای رشتۀ zval قبلی) همراه با اشاره گری به جدول به طور ویژه ایجاد شدۀ zend_object_handlers باشد. این جدول در ذات خود، آرایه ای از اشاره گرهای تابع است که تعریف ساختار آن را می توان در بخش زیر پیدا نمود:

هنگام ساختن چنین جدول zend_object_handlers جعلی، می توانیم به سادگی add_ref را به هر شکلی که بخواهیم، تنظیم کنیم. تابع پشت این اشاره گر معمولاً افزایش شمارندۀ مرجع این آبجکت را کنترل می کند. به محض این که آبجکت ساخته شدۀ جعلی ما به عنوان یک پارامتر به setcookie فرستاده شود، اتفاقات زیر رخ می دهد:

در اینجا، با توجه به “s|sl[…]”، می توان فهمید که setcookie در انتظار یک رشته به عنوان اولین و دومین پارامتر خود است (| ابتدای پارامترهای اختیاری را نشان می دهد). از این رو، سعی خواهد کرد تا آبجکتی را که به عنوان دومین پارامتر به آن ارسال کرده ایم، به شکل یک رشته درآورد (به اصطلاح cast کند). در نهایت، در آن زمان است که _zval_copy_ctor اجرا خواهد شد.

به طور ویژه، این امر موجب ایجاد یک فراخوانی به تابع add_ref با آدرس آبجکت ما به عنوان یک پارامتر خواهد شد (برای توضیحات بیشتر به کتاب PHP Internals – Copying zvals مراجعه نمایید). اسمبلی مربوطه چنین حالتی خواهد داشت:

در اینجا RDI اولین آرگیومنت تابع _zval_copy_ctor_func  بوده و البته آدرس آبجکت جعلی zval (همان zvalue در سورس کد فوق) ما نیز محسوب می گردد. همانطور که پیش از این، در تعریف _zvalue_value typedef نیز مشاهده کردیم، یک آبجکت حاوی عنصری تحت عنوان obj است که از نوع zend_object_value بوده و به شکل زیر تعریف می گردد:

از این رو 0x8(%rdi) به دومین ورودی در _zend_object_value اشاره خواهد کرد که به آدرس اولین ورودی zend_object_handlers ما مربوط می شود. همانطور که قبلاً نیز اشاره شد، این ورودی همان تابع add_ref سفارشی ما بوده و همچنین این توضیح را می دهد که چرا ما بر روی RAX کنترل داریم.

برای دور زدن مشکل حافظۀ غیر قابل اجرا که قبلاً در موردش صحبت کردیم، لازم بود اطلاعات بیشتری به دست آوریم. به خصوص این که ما نیاز داشتیم تا گجت های به درد به خور را جمع آوری کرده و امکان چرخش پشته (stack) را برای زنجیرۀ ROP خود آماده کنیم، چرا که هنوز کنترل کافی بر روی پشته وجود نداشت.

افشای گجت های ROP

حال ما می توانیم به ترتیب اشاره گر add_ref یا RAX را تنظیم کنیم تا به این طریق اشاره گر دستورالعمل را در اختیار بگیریم. اگر چه این کار، یک نقطۀ شروع به شما می دهد، اما اجرای تمامی گجت های ROP ارائه شدۀ شما را تضمین نمی کند، چرا که پس از بازگشت از اولین گجت، CPU آدرس دستورالعمل بعدی را از پشتۀ (stack) فعلی برمی دارد. ما هیچ کنترلی بر روی این پشته نداریم، در نتیجه می بایست پشتۀ مذکور را داخل زنجیرۀ ROP خود می چرخاندیم. به همین خاطر بود که گام بعدی ما، کپی کردن RAX داخل RSP و ادامه دادن کار از آنجا بود. با استفاده از یک نسخۀ کامپایل شدۀ محلی از PHP، ما به دنبال انتخاب هایی مناسب برای گجت های چرخش پشته گشتیم و متوجه شدیم php_stream_bucket_split حاوی تکه کد زیر است:

از این کد برای تغییر RSP استفاده شد تا به دیتای POST ارائه شده توسط زنجیرۀ ROP اشاره کند و به طور موثری تمامی فراخوانی های گجت مربوطه را به حالت زنجیره وار درآورد.

طبق قرارداد فراخوانی x86_64، دو پارامتر اول یک تابع، RDI و RSI هستند، در نتیجه می بایست یک گجت pop %rdi  و pop %rsi نیز پیدا می کردیم. این گجت ها بسیار رایج هستند و در نتیجه خیلی راحت نیز پیدا می شوند. هر چند که ما هنوز ایده ای در خصوص این که آیا این گجت ها واقعاً روی نسخۀ PHP مورداستفاده در سایت بزرگ سال وجود دارند یا خیر، نداشتیم. از این رو، می بایست به صورت دستی حضور آن ها را تایید می کردیم.

 

 تایید حضور گجت های ROP موردنیاز

بردار افشای اطلاعات (infoleak) این امکان را به ما داد تا به سرعت از حالت غیر اسمبلی شدۀ php_stream_bucket_split روبرداری کرده و بررسی کنیم که آیا گجت چرخش پشته (stack) بر روی نسخۀ از راه دور موجود است یا خیر. خوشبختانه، فقط به اصلاحاتی جزئی درآفست های گجت ها نیاز بود. در نهایت نیز به منظور بررسی درستی تمامی آدرس ها بررسی هایی انجام دادیم:

 

ایجاد پشتۀ ROP

پیلود ROP نهایی که به طور موثری zend_eval_string(code); exit(0); را اجرا نمود، مشابۀ تکه کد زیر بود:

از آنجایی که چرخش پشته حاوی pop %r13 و pop %r14 بود، پدینگ 0xdeadbeef داخل زنجیرۀ باقیمانده امری ضروری برای ادامه دادن به روند تنظیم RDI بود. RDI به عنوان اولین پارامتر ورودی zend_eval_string برای ارجاع دادن به کدی که می بایست اجرا شود، لازم است. این کد درست بعد از زنجیرۀ ROP قرار دارد. همچنین لازم بود که دقیقاً مقدار یکسانی از داده بین هر درخواست ارسال شود تا تمامی آفست های محاسبه شده صحیح باقی بمانند. این امر از طریق تنظیم پدینگ های مختلف در هر جایی که لازم بود، انجام شد.

گام بعدی این بود که در نهایت با برگشت به مفسر PHP، اجرای کد را آغاز کنیم. در حقیقت تکنیک های دیگر نظیر return2libc نیز نسبتاً کاربردی هستند، اما منجر به ایجاد مشکلات دیگری می شوند که با استفاده از PHP راحت تر می توان با آن ها سر و کله زد.

بازگشت به PHP

توانایی اجرای کد PHP دلخواه گام مهمی است، اما توانایی مشاهدۀ خروجی آن نیز به همان میزان مهم است؛ مگر این که شخصی بخواهد تنها با کانال های فرعی آن برای دریافت پاسخ کار کند. بنابراین، بخش دشوار باقیمانده این بود که به نحوی خروجی را روی وب سایت سایت بزرگ سال نمایش دهیم.

خاتمۀ تمیز PHP

معمولاً php-cgi محتوای تولید شده را به وب سرور پس می فرستد تا بر روی وب سایت نمایش داده شود، اما جریان کنترلی را به نحوی خراب می کند که موجب خاتمۀ غیرطبیعی PHP شده و نتیجه هیچ گاه به سرور PHP نخواهد رسید. برای حل این مشکل، ما به سادگی به PHP گفتیم که از پاسخ های بافر نشدۀ مستقیمی استفاده کند که معمولاً برای HTTP streaming مورد استفاده قرار می گیرند:

این کار سرانجام برای ما این امکان را فراهم کرد تا بدون نگرانی دربارۀ روتین های cleanupای که هنگام ارسال داده از فرآیند CGI به وب سرور رخ می دهد، بتوانیم به طور مستقیم تک تک خروجی های تولید شده توسط پیلود PHP را دریافت کنیم. این مسئله همچنین با به حداقل رساندن تعداد خطاها و خرابی های احتمالی، موجب افزایش مخفی بودن عملیات در حال انجام شد.

به طور خلاصه، پیلود ما حاوی یک آبجکت جعلی بود که داخل خود یک اشاره گر تابع add_ref نیز داشت و به اولین گجت ROP ما اشاره می کرد. دیاگرام زیر، مفهوم توضیح داده را به تصویر می کشد:

نسخۀ نهایی آبجکت zval ساخته شده

پیلود ما همراه با پشتۀ ROPمان که بر روی دیتای POST ارائه شده بود، کارهای زیر را انجام می داد:

  1. آبجکت جعلی ما را ایجاد کرد که بعدها به عنوان یک پارامتر به setcookie ارسال شد.
  2. باعث فراخوانی تابع add_ref شد، یعنی به ما اجازه داد کنترل شمارندۀ برنامه را به دست آوریم.
  3. زنجیرۀ ROPمان در ادامه تمامی رجیسترها/پارامترهای گفته شده را آماده نمود.
  4. در مرحله بعدی، توانستیم با فراخوانی zend_eval_string، کد PHP دلخواهمان را اجرا نماییم.
  5. سرانجام، ضمن دریافت خروجی از بدنۀ پاسخ، یک خاتمۀ تمیز برای فرآیند مذکور رقم زدیم.

هنگام اجرای کد فوق، به دیدی عالی از فایل ‘/etc/passwd’ متعلق به سایت بزرگ سال دست یافتیم. با توجه به ذات حمله‏مان، قادر بودیم دستورات دیگری نیز اجرا کنیم یا با خروج از PHP، syscall های دلخواه اجرا نماییم. هر چند که در این نقطه، استفاده از PHP صرف برایمان راحت تر بود. سرانجام، ما جزئیات بیشتری دربارۀ سیستم زیربنایی سایت بزرگ سال به دست آوردیم، به سرعت یک گزارش آماده کرده و آن را به برنامۀ سایت بزرگ سال بر روی پلتفرم هکروان ارسال نمودیم.

جدول زمانی:

در زیر، جدول زمانی فرآیند افشا را مشاهده می نمایید:

  • 2016-05-30 – به سایت بزرگ سال نفوذ کرده و مشکل را بر روی بستر هکروان به آن ها گزارش دادیم. چند ساعت بعد، سایت بزرگ سال با حذف فراخوانی ها به تابع unserialize، به سرعت مشکل را برطرف نمود.
  • 2016-06-14 – جایزه ای به مبلغ 20000 دلار دریافت کردیم.
  • 2016-06-16 – مشکلات پیدا شده را به php.net گزارش کردیم.
  • 2016-06-21 – هر دو آسیب پذیری در ریپازیتوری امنیتی PHP رفع شدند.
  • 2016-06-27 – پاداش IBB هکروان را به مبلغ 2000 دلار دریافت کردیم (1000 دلار به ازای هر آسیب پذیری).
  • 2016-07-22 – سایت بزرگ سال مشکل گزارش شده را در هکروان ریزالو کرد.

نتیجه گیری

ما موفق به اجرای کد از راه دور شد و قادر به انجام کارهای زیر بودیم:

  • از کل پایگاه دادۀ سایت بزرگ سال از جمله تمامی اطلاعات حساس کاربران روبرداری کنیم.
  • رفتار کاربر را بروی پلتفرم ردیابی و مشاهده نماییم.
  • سورس کد کامل موجود بر روی تمامی سایت های میزبانی شده بر روی سرور را افشا کنیم.
  • تا عمق زیادی وارد شبکه و ریشۀ سیستم شویم.

البته هیچ یک از موارد ذکر شده را انجام نداده و بسیار مراقب بودیم تا به اسکوپ و محدودیت های برنامۀ باگ بانتی احترام بگذاریم. علاوه بر این، ما توانستیم دو آسیب پذیری روز صفر (zero day) در الگوریتم جمع آوری زبالۀ PHP پیدا کنیم. این آسیب پذیری ها اگر چه در حوزۀ متفاوتی از PHP بودند، اما می توانستند از راه دور و با قابلیت اطمینان بالا در زمینۀ unserialize نیز اکسپلویت شوند.

استفاده از ورودی کاربر بر روی تابع unserilize به عنوان ایدۀ بسیار بدی شناخته می شود. به خصوص این که حدود 10 سال از آشکار شدن اولین ضعف آن می گذرد. متاسفانه، حتی امروزه نیز بسیار از دولوپرها این باور را دارند که unserialize تنها در نسخه های قدیمی PHP یا هنگام ترکیب با کلاس های نا امن، خطرناک محسوب می شود. ما عمیقا امیدواریم از طریق این مقاله این باور نادرست را از بین برده باشیم. لطفا در نهایت یک میخ به تابوت unserilize بزنید تا مانترای زیر منسوخ گردد:

شما هرگز نباید ورودی کاربر را روی unserilize استفاده کنید. تصور این که استفاده از نسخۀ به روز PHP  در چنین سناریوهایی برای محافظت از userialize کافی است، ایدۀ بسیار بدی محسوب می شود. یا از این کار اجتناب کنید یا از متد های سریال سازی با پیچیدگی کمتر نظیر JSON استفاده نمایید.

تا به امروز جدیدترین نسخه های PHP دربردارندۀ اصلاحات لازم هستند. از این رو، شما می بایست نسخه های PHP 5 و PHP 7 را بر این اساس به روز نمایید.

تشکر فراوان از تیم سایت بزرگ سال به خاطر:

  • پاسخ های بسیار مودبانه و مناسب.
  • توجه واقعی به مسئلۀ امنیت (و این که همچون بسیاری از شرکت های دیگر به این مسئله تظاهر نمی کنند)
  • بخشندگی زیادی که در خصوص بانتی 20000 دلاری انجام دادند.

علاوه بر این، تشکر فراوان از دولوپرهای PHP برای رفع سریع مشکل و همچنین کمیتۀ باگ بانتی اینترنتی بابت پاداش 2000 دلاری.

در نهایت ما می خواهیم در خصوص لزوم وجود چنین برنامه هایی صحبت کنیم. همانطور که مشاهده می کنید، پیشنهاد پاداش های بالای باگ بانتی می تواند انگیزۀ محققان امنیتی را در خصوص پیدا کردن آسیب پذیری در نرم افزار زیربنایی تا حد زیادی افزایش دهد. این مسئله می تواند بر روی سایت ها و سرویس های دیگر نیز اثر مثبتی داشته باشد.

لطفا بررسی دو رایت آپ دیگر ما در خصوص آسیب پذیری های PHP و نحوۀ کشف آن ها را نیز فراموش نکنید.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *