This is beta version of Ravro English website. Some dynamic texts are in Persian. If you need support, contact with support@ravro.ir.
تصاحب حساب کاربری از زیرِ بغلِ مار

تصاحب حساب کاربری از زیرِ بغلِ مار

40

انتخاب این عنوان بی‌دلیل نیست. در بسیاری از پروژه‌های تست نفوذی که انجام داده‌ام، زمانی که یک مشکل کوچک گزارش می‌شود، گاهی این دیدگاه وجود دارد که کارشناس امنیت بیش از حد روی جزئیات کم‌اهمیت تمرکز کرده یا به‌اصطلاح «دنبال زیرِ بغلِ مار می‌گردد».

اما تجربه نشان داده است که همین اشتباه‌های کوچک و کم‌اهمیت، وقتی کنار هم قرار می‌گیرند، زنجیره‌وار به یکدیگر گره می‌خورند و می‌توانند نقطه شروع یک حمله جدی باشند. هدف از انتخاب این عنوان نیز نشان‌دادن همین موضوع است: اینکه یک مورد ظاهراً ساده چگونه می‌تواند در نهایت هزینه‌ای بسیار بزرگ برای یک سازمان به بار بیاورد.

در این نوشتار، یکی از جذاب‌ترین آسیب‌پذیری‌هایی را که تاکنون با آن مواجه شده‌ام مرور می‌کنم؛ زنجیره‌ای که در نهایت به تصاحب کامل حساب کاربری روی یک صرافی ارز دیجیتال منجر شد و در جریان یکی از رویدادهای راورو کشف شد.

نقطه شروع این زنجیره، یک آسیب‌پذیری پیچیده نبود؛ تنها نام یک پوشه بود.

در ادامه، مسیر کشف این آسیب‌پذیری را همان‌طور که اتفاق افتاد روایت می‌کنم؛ از اولین سرنخ و سناریویی که شکست خورد تا رسیدن به زنجیره نهایی Account Takeover.

درباره نام‌ها: به دلیل عدم دریافت مجوز انتشار نام واقعی مجموعه، نام صرافی، ارائه‌دهنده سرویس ابری، Bucketها و مسیرهای قابل‌شناسایی در نسخه عمومی این مقاله تغییر داده شده‌اند. در نمونه‌ها از دامنه فرضی test-exchange.ir، سرویس ابری s3.cloud-test.ir و مسیر فرضی /asset-bridge/ استفاده شده است.

همه‌چیز از آدرس یک تصویر شروع شد

هنگام بررسی فایل‌های استاتیک سایت، آدرس یکی از تصاویر توجهم را جلب کرد. ساختار آدرس تقریباً به شکل زیر بود:

1https://test-exchange.ir/asset-bridge/test-exchange-public/banners/banner.jpg
2

در این آدرس، بخش زیر برایم جالب بود:

1test-exchange-public
2

این نام به الگوهایی شباهت داشت که معمولاً برای نام‌گذاری فضای ذخیره‌سازی عمومی یا Bucketهای Object Storage استفاده می‌شوند.

البته صرف وجود چنین نامی در URL چیزی را اثبات نمی‌کرد. test-exchange-public می‌توانست فقط نام یک پوشه، یک Route در برنامه یا بخشی از ساختار عادی فایل‌های سایت باشد.

در این مرحله فقط یک فرضیه شکل گرفت:

شاید فایل‌های این مسیر از یک سرویس Object Storage دریافت می‌شوند.

بررسی هدرهای پاسخ

اولین قدم، بررسی Response Headers تصویر بود.

در برخی موارد، هدرهای پاسخ می‌توانند اطلاعاتی درباره CDN، سرویس ذخیره‌سازی یا فناوری مورد استفاده افشا کنند. برخی سرویس‌های سازگار با S3 نیز هدرهای مخصوص خود را در پاسخ قرار می‌دهند.

اما در این مورد هدر مفیدی وجود نداشت.

هیچ نشانه‌ای پیدا نکردم که ثابت کند فایل از Object Storage دریافت می‌شود. بنابراین test-exchange-public همچنان فقط یک نام مشکوک در URL بود.

درخواست یک فایل نامعتبر

برای به‌دست‌آوردن اطلاعات بیشتر، نام فایل را به مقداری تغییر دادم که مطمئن بودم وجود ندارد:

1https://test-exchange.ir/asset-bridge/test-exchange-public/banners/testss.jpg
2

اگر پشت این مسیر یک سرویس سازگار با S3 قرار داشت، ممکن بود خطایی مانند موارد زیر برگردد:

1NoSuchKey
2AccessDenied
3NoSuchBucket
4

یا یک پاسخ XML دریافت شود که اطلاعاتی درباره Bucket یا Object درخواست‌شده در اختیارم قرار دهد.

اما به‌جای دریافت یک خطای مشخص، فقط صفحه‌ای خالی نمایش داده شد.

این رفتار نیز چیزی را ثابت نمی‌کرد. ممکن بود پاسخ اصلی سرویس در مسیر کنترل شده باشد. بنابراین هنوز نمی‌توانستم مشخص کنم فایل‌ها از چه زیرساختی دریافت می‌شوند.

بررسی زیردامنه‌های مجموعه

در مرحله بعد، زیردامنه‌های مجموعه را بررسی کردم. هدفم این بود که ببینم آیا خود مجموعه یک سرویس ابری یا Object Storage اختصاصی روی یکی از زیردامنه‌هایش دارد یا خیر.

نام‌هایی شبیه موارد زیر می‌توانستند به چنین زیرساختی مربوط باشند:

1storage.example.com
2s3.example.com
3objects.example.com
4assets.example.com
5media.example.com
6cdn.example.com
7

همچنین رکوردهای DNS و CNAME می‌توانستند نشان دهند که یکی از زیردامنه‌ها به یک زیرساخت ذخیره‌سازی متصل شده است.

زیردامنه‌های شناسایی‌شده را بررسی کردم، اما به نتیجه‌ای نرسیدم. هیچ Endpoint یا زیردامنه مشخصی پیدا نشد که نشان دهد مجموعه سرویس Object Storage را روی دامنه‌های خودش ارائه می‌دهد.

پس احتمال دیگری را در نظر گرفتم:

شاید مجموعه از سرویس Object Storage یکی از ارائه‌دهندگان ابری داخلی استفاده می‌کند.

پیداکردن Bucket روی یک سرویس ابری داخلی

چند ارائه‌دهنده ابری داخلی مطرح را که سرویس Object Storage سازگار با S3 ارائه می‌کردند بررسی کردم.

روی یکی از این سرویس‌ها، Bucketی دقیقاً با همان نام پیدا شد:

1GET https://s3.cloud-test.ir/test-exchange-public
2

پاسخ دریافتی مشابه نمونه زیر بود:

1<?xml version="1.0" encoding="UTF-8"?>
2<Error>
3  <Code>AccessDenied</Code>
4  <Message/>
5  <BucketName>test-exchange-public</BucketName>
6  <RequestId>tx0000...-default</RequestId>
7  <HostId>...-default-default</HostId>
8</Error>
9

پاسخ AccessDenied نشان می‌داد Bucketی با نام test-exchange-public روی این سرویس وجود دارد، اما دسترسی ناشناس به عملیات درخواست‌شده مجاز نیست.

اگر چنین Bucketی وجود نداشت، انتظار می‌رفت خطایی مانند NoSuchBucket برگردد.

بااین‌حال، وجود یک Bucket هم‌نام روی یک سرویس ابری هنوز ثابت نمی‌کرد که فایل‌های صرافی واقعاً از همین Bucket دریافت می‌شوند. این مورد می‌توانست فقط یک تشابه اسمی باشد.

شکست اولین سناریو

اولین سناریویی که در ذهن داشتم با پاسخ AccessDenied شکست خورد.

با توجه به نام test-exchange-public و نحوه استفاده آن در مسیر تصویر، حدس می‌زدم Bucket به‌صورت عمومی پیکربندی شده باشد.

در یک حالت، ممکن بود دسترسی مشاهده فهرست فایل‌ها فعال باشد و بتوانم تمام Objectهای موجود در Bucket را مشاهده کنم. این دسترسی می‌توانست فایل‌های فراموش‌شده، Backupها، مسیرهای داخلی یا سایر اطلاعات قابل‌توجه را آشکار کند.

اگر خوش‌شانس‌تر بودم و دسترسی نوشتن نیز به‌اشتباه برای کاربران ناشناس فعال شده بود، می‌توانستم بررسی کنم که آیا امکان جایگزین‌کردن یکی از تصاویر مورد استفاده در سایت وجود دارد یا خیر.

برای مثال، اگر تصویر اسلایدر صفحه اصلی مستقیماً از همان Bucket دریافت می‌شد و امکان بازنویسی آن وجود داشت، می‌توانستم با تغییر تصویر، صفحه اصلی سایت را Deface کنم.

اما پاسخ AccessDenied نشان داد امکان مشاهده فهرست Objectهای Bucket وجود ندارد. آزمایش دسترسی نوشتن روی همان Bucket نیز موفق نبود.

در نتیجه، اولین سناریوی من برای مشاهده کامل فایل‌های Bucket یا تغییر مستقیم تصویر صفحه اصلی در همان مرحله شکست خورد.

پیش از ادامه مسیر کشف، لازم است کمی درباره ساختاری که در حال بررسی آن بودم توضیح بدهم.

Object Storage چیست؟

Object Storage یک مدل ذخیره‌سازی داده است که در آن هر فایل به‌صورت یک Object نگهداری می‌شود. هر Object معمولاً از سه بخش تشکیل شده است:

1• محتوای اصلی فایل؛
2• Metadata؛
3• یک شناسه یکتا به نام Object Key.
4

Objectها داخل فضاهایی به نام Bucket قرار می‌گیرند. برای مثال:

1Bucket: test-exchange-public
2Object Key: banners/banner.jpg
3

در Object Storage ساختار پوشه‌ها معمولاً واقعی نیست. برای مثال، مقدار زیر در اصل یک Object Key کامل است:

1banners/banner.jpg
2

علامت / بیشتر برای ایجاد ساختاری شبیه پوشه و خوانایی بهتر استفاده می‌شود.

دسترسی به Objectها از طریق HTTP و API انجام می‌شود. یک ساختار متداول می‌تواند به شکل زیر باشد:

1https://<endpoint>/<bucket-name>/<object-key>
2

یکی از شناخته‌شده‌ترین سرویس‌های Object Storage، Amazon S3 است. بسیاری از سرویس‌های ابری دیگر، از جمله برخی ارائه‌دهندگان داخلی، API سازگار با S3 ارائه می‌کنند.

تمرکز این مقاله نیز روی همین سرویس‌های S3-Compatible است.

معرفی ابزارها و دستورات

برای بررسی این نوع آسیب‌پذیری‌ها و کار با سرویس‌های Object Storage سازگار با S3، از ابزارهای خط فرمان استفاده می‌شود. در این مقاله ما با ابزار رسمی Amazon، یعنی AWS CLI، پیش می‌رویم. این ابزار به‌صورت استاندارد برای کار با Amazon S3 ساخته شده است، اما با کمک گزینهٔ endpoint-url می‌توان از آن روی سرویس‌های داخلیِ سازگار با S3 نیز استفاده کرد.

مهم‌ترین ویژگی AWS CLI برای کار ما این است که امکان ارسال درخواست بدون اعتبارنامه و به‌صورت ناشناس را فراهم می‌کند؛ همین موضوع آن را برای آزمودن پیکربندی‌های نادرستِ دسترسی روی Bucketها بسیار مناسب می‌سازد.

دستورها و گزینه‌هایی که در ادامهٔ مقاله بیشتر با آن‌ها سروکار داریم، به‌ترتیب زیر هستند:

1aws s3 ls
2aws s3 cp
3aws s3 rm
4--no-sign-request
5--endpoint-url
6--recursive
7

aws s3 ls : فهرست‌کردن Objectهای موجود در یک Bucket. اگر دسترسی ListBucket به‌اشتباه برای کاربران ناشناس فعال شده باشد، با این دستور می‌توان نام تمام فایل‌های داخل Bucket را مشاهده کرد.

aws s3 cp : کپی، آپلود یا دانلود فایل. برای دریافت یک Object (معادل عملیات GetObject) یا قراردادن یک فایل جدید داخل Bucket (معادل عملیات PutObject) از همین دستور استفاده می‌شود.

aws s3 rm : حذف یک Object از داخل Bucket (معادل عملیات DeleteObject). در صورتی که دسترسی نوشتن برای کاربران ناشناس باز مانده باشد، این دستور هم قابل سوءاستفاده است.

no-sign-request : درخواست را بدون هیچ اعتبارنامه‌ای و به‌صورت کاملاً ناشناس ارسال می‌کند؛ دقیقاً همان حالتی که برای آزمودن دسترسی عمومیِ یک Bucket به آن نیاز داریم.

endpoint-url : نشانی سرویس مقصد را مشخص می‌کند. چون سرویس موردنظر متعلق به Amazon نیست و یک سرویس داخلیِ سازگار با S3 است، باید Endpoint آن را به‌صورت دستی معرفی کنیم.

recursive : باعث می‌شود عملیات به‌صورت بازگشتی روی همهٔ مسیرها و زیرشاخه‌های یک Bucket اجرا شود.

ابزار دیگری هم به نام MinIO Client، یا همان mc، وجود دارد که کلاینتی سبک برای کار با سرویس‌های سازگار با S3 است و دستورهایی کموبیش مشابه دارد. با این حال، در این مقاله تمرکز اصلی ما بر ابزار Amazon، یعنی AWS CLI، است و مسیر کار را با همین ابزار پیش می‌بریم.

دسترسی ناشناس در S3

دسترسی به Bucketها و Objectها از طریق Policyها، ACLها و سایر تنظیمات امنیتی کنترل می‌شود. عملیات مهمی که هنگام بررسی یک Bucket موردتوجه قرار می‌گیرند شامل موارد زیر هستند:

1ListBucket
2GetObject
3PutObject
4DeleteObject
5

دریافت یک Object

اگر دسترسی GetObject عمومی باشد، هر شخصی که مسیر کامل یک Object را بداند می‌تواند آن را بدون اعتبارنامه دریافت کند:

1https://s3.cloud-test.ir/example-bucket/images/logo.png
2

اما عمومی‌بودن GetObject به این معنا نیست که فهرست کامل فایل‌ها نیز قابل‌مشاهده است.

ممکن است دریافت یک فایل مشخص مجاز باشد، اما کاربر ناشناس نتواند نام سایر Objectهای موجود در Bucket را مشاهده کند.

مشاهده فهرست کامل Bucket

اگر دسترسی ListBucket برای کاربران ناشناس فعال باشد، می‌توان بدون Access Key و Secret Key فهرست Objectهای Bucket را مشاهده کرد.

یک درخواست مستقیم ممکن است پاسخ XML مشابه زیر برگرداند:

1GET https://s3.cloud-test.ir/example-bucket/?list-type=2
2
1<ListBucketResult>
2  <Contents>
3    <Key>images/logo.png</Key>
4  </Contents>
5  <Contents>
6    <Key>backup/database.sql</Key>
7  </Contents>
8  <Contents>
9    <Key>documents/users.csv</Key>
10  </Contents>
11</ListBucketResult>
12

با AWS CLI نیز می‌توان دسترسی ناشناس را آزمایش کرد:

1aws s3 ls s3://example-bucket/ \
2  --no-sign-request \
3  --endpoint-url https://s3.cloud-test.ir
4

برای مشاهده بازگشتی تمام فایل‌ها:

1aws s3 ls s3://example-bucket/ \
2  --recursive \
3  --no-sign-request \
4  --endpoint-url https://s3.cloud-test.ir
5

اگر PutObject به‌اشتباه برای کاربران ناشناس مجاز شده باشد، می‌توان بدون اعتبارنامه فایل جدیدی داخل Bucket قرار داد:

1aws s3 cp test.txt s3://example-bucket/test.txt \
2  --no-sign-request \
3  --endpoint-url https://s3.cloud-test.ir
4

بسته به Policy اعمال‌شده، ممکن است عملیات دیگری مانند بازنویسی یا حذف Objectها نیز در دسترس باشند.

در سناریوی اولیه من، پاسخ AccessDenied نشان می‌داد حداقل امکان Listکردن Bucket اصلی برای کاربر ناشناس وجود ندارد. بنابراین باید مسیر دیگری را برای ادامه بررسی پیدا می‌کردم.

اثبات ارتباط Bucket با صرافی

صرف پیداکردن یک Bucket هم‌نام روی سرویس ابری کافی نبود.

برای اثبات ارتباط، یکی از فایل‌هایی را که مسیر دقیق آن روی صرافی می‌دانستم انتخاب کردم.

مسیر فایل روی دامنه صرافی:

1https://test-exchange.ir/asset-bridge/test-exchange-public/banners/banner.jpg
2

سپس همان Object Key را روی Endpoint سرویس Object Storage قرار دادم:

1https://s3.cloud-test.ir/test-exchange-public/banners/banner.jpg
2

فایل از هر دو آدرس قابل‌دریافت بود.

برای اطمینان، فقط ظاهر تصویر را مقایسه نکردم. محتوای فایل و مشخصات پاسخ نیز با یکدیگر تطبیق داده شدند.

در این مرحله مشخص شد فایلی که از مسیر صرافی نمایش داده می‌شود همان Object موجود در Bucket سرویس ابری است.

ساختار مسیر تقریباً به شکل زیر عمل می‌کرد:

1/asset-bridge/<bucket-name>/<object-key>
2

یعنی مسیری روی دامنه اصلی صرافی، فایل متناظر را از Object Storage دریافت و به کاربر تحویل می‌داد.

ممکن بود این رفتار از طریق Reverse Proxy، بازنویسی مسیر یا سازوکاری مشابه پیاده‌سازی شده باشد. جزئیات دقیق پیاده‌سازی در اختیار من نبود؛ اما رفتار قابل‌مشاهده مشخص بود.

آیا مسیر فقط به Bucket صرافی محدود بود؟

پس از اثبات ارتباط، سؤال بعدی برایم این بود:

آیا مسیر /asset-bridge/ فقط برای Bucket متعلق به صرافی کار می‌کند یا می‌توان نام Bucket دیگری را نیز در URL قرار داد؟

ساختار آدرس نشان می‌داد نام Bucket بخشی از مسیر است:

1/asset-bridge/<bucket-name>/<object-key>
2

برای آزمایش این فرضیه، ابتدا باید یک Object عمومی از Bucket دیگری روی همان سرویس پیدا می‌کردم.

پیداکردن Objectهای ایندکس‌شده با Google Dork

برای پیداکردن فایل‌های عمومی روی Endpoint سرویس ابری، از یک Google Dork ساده استفاده کردم:

1site:s3.cloud-test.ir
2

نتایج جست‌وجو تعدادی Object ایندکس‌شده متعلق به Bucketهای مختلف روی همان سرویس را نمایش می‌داد.

یکی از فایل‌های عمومی موجود در نتایج را انتخاب کردم. فرض کنیم آدرس آن به شکل زیر بود:

1https://s3.cloud-test.ir/another-public-bucket/images/sample.jpg
2

از روی این آدرس، نام Bucket و Object Key مشخص بود:

1Bucket: another-public-bucket
2Object Key: images/sample.jpg
3

سپس همان مقادیر را در مسیر صرافی قرار دادم:

1https://test-exchange.ir/asset-bridge/another-public-bucket/images/sample.jpg
2

فایل با موفقیت از دامنه صرافی نمایش داده شد.

این نتیجه نشان داد مسیر موردنظر فقط به Bucket متعلق به صرافی محدود نیست و می‌تواند Objectهای Bucketهای دیگر روی همان سرویس را نیز ارائه کند؛ به شرط آنکه Object موردنظر از سمت Object Storage قابل‌خواندن باشد.

رفتار کلی مسیر به شکل زیر بود:

1https://test-exchange.ir/asset-bridge/<bucket>/<object>
23https://s3.cloud-test.ir/<bucket>/<object>
4

این بخش نقطه چرخش اصلی در مسیر کشف آسیب‌پذیری بود.

Bucket متعلق به صرافی اجازه List یا Write ناشناس نمی‌داد؛ اما مسیر روی دامنه صرافی هر Bucket دیگری را نیز قبول می‌کرد.

در این مرحله سناریوی جدیدی شکل گرفت:

اگر روی همین سرویس یک Bucket قابل‌نوشتن پیدا کنم، می‌توانم فایل خودم را داخل آن قرار دهم و سپس همان فایل را از دامنه اصلی صرافی نمایش دهم.

جست‌وجوی Bucketهای دیگر روی Endpoint

یک مانع وجود داشت: سرویس ابری موردنظر امکان ساخت آزادانه Bucket جدید را در اختیار عموم قرار نمی‌داد.

بنابراین به‌جای ساخت یک Bucket، خود Endpoint را بررسی کردم تا Bucketهایی را پیدا کنم که از قبل وجود داشتند و احتمالاً تنظیمات دسترسی مناسبی نداشتند. برای بررسی نام‌های احتمالی Bucketها، از Wordlist و درخواست‌های ناشناس استفاده کردم.

برای این کار، با AWS CLI و بدون هیچ اعتبارنامه‌ای، برای هر نام احتمالی تلاش کردم محتوای Bucket را به‌صورت ناشناس فهرست کنم:

1aws s3 ls s3://<bucket-name>/ \
2  --no-sign-request \
3  --endpoint-url https://s3.cloud-test.ir
4

پاسخ این درخواست نشان می‌داد که آیا Bucketی با آن نام وجود دارد و در صورت وجود، آیا دسترسی ناشناس برای فهرست‌کردن یا خواندن محتوای آن باز است یا نه.

در این مرحله هدفم پاسخ‌دادن به چند سؤال بود:

1• آیا Bucket وجود دارد؟
2• آیا دریافت Object از آن برای کاربر ناشناس مجاز است؟
3• آیا فهرست محتوا قابل‌مشاهده است؟
4• آیا امکان نوشتن فایل بدون اعتبارنامه وجود دارد؟
5

با بررسی نام‌های مختلف به تعدادی Bucket موجود روی Endpoint رسیدم.

پیداکردن یک Bucket قابل نوشتن

در میان Bucketهای پیدا‌شده، یکی از آن‌ها پیکربندی دسترسی اشتباهی داشت و آپلود ناشناس فایل را می‌پذیرفت.

این Bucket ارتباطی با صرافی نداشت و متعلق به مجموعه دیگری روی همان سرویس Object Storage بود. برای تأیید دسترسی، یک فایل آزمایشی بی‌خطر با نام تصادفی در آن قرار دادم:

1aws s3 cp test-file.txt s3://<writable-bucket>/test-file.txt \
2  --no-sign-request \
3  --endpoint-url https://s3.cloud-test.ir
4

آپلود بدون هیچ اعتبارنامه موفق بود.

سپس همان فایل را مستقیماً از Endpoint سرویس درخواست کردم و فایل در دسترس قرار داشت.

در این مرحله دو رفتار مستقل در کنار هم قرار گرفته بودند:

۱. روی سرویس Object Storage یک Bucket متعلق به مجموعه‌ای دیگر وجود داشت که امکان آپلود ناشناس فایل را می‌داد؛

۲. مسیر موجود روی دامنه صرافی می‌توانست فایل‌های Bucketهای مختلف همان سرویس را روی Origin اصلی صرافی نمایش دهد.

بنابراین فایل کنترل‌شده توسط من می‌توانست از آدرسی شبیه زیر نمایش داده شود:

1https://test-exchange.ir/asset-bridge/<writable-bucket>/<uploaded-file>
2

از دید مرورگر، فایل از دامنه اصلی صرافی دریافت می‌شد؛ حتی اگر در واقع داخل Bucket متعلق به مجموعه دیگری قرار داشت.

آپلود فایل و اجرای JavaScript

برای اثبات اثر آسیب‌پذیری، یک فایل SVG حاوی JavaScript آزمایشی داخل Bucket قابل‌نوشتن قرار دادم.

نمونه ساده PoC به شکل زیر بود:

1<?xml version="1.0" encoding="UTF-8"?>
2<svg xmlns="http://www.w3.org/2000/svg">
3  <script>
4    alert(document.domain);
5  </script>
6</svg>
7

سپس فایل را از طریق مسیر موجود روی دامنه صرافی فراخوانی کردم:

1https://test-exchange.ir/asset-bridge/<writable-bucket>/test.svg
2

JavaScript فایل با موفقیت اجرا شد و مقدار document.domain دامنه صرافی را نمایش داد.

Payload داخل Object Storage ذخیره شده بود و هر بار که URL باز می‌شد، دوباره اجرا می‌شد. بنابراین امکان اجرای Stored XSS روی Origin اصلی صرافی وجود داشت.

از Stored XSS تا Account Takeover

پس از تأیید اجرای JavaScript روی Origin اصلی، Cookieهای نشست را بررسی کردم.

در این مرحله خوش‌شانس بودم؛ Cookie نشست به‌گونه‌ای تنظیم شده بود که JavaScript می‌توانست مقدار آن را از طریق document.cookie بخواند.

به‌عبارت دیگر، پرچم HttpOnly برای Cookie حساس نشست فعال نشده بود.

از طریق JavaScript اجراشده روی دامنه صرافی، مقدار Cookie نشست قابل‌دریافت بود.

در محیط آزمایشی، Cookie به مرورگر دیگری منتقل و نشست کاربر بازسازی شد. حساب بدون نیاز به واردکردن دوباره رمز عبور و بدون عبور مجدد از مرحله احراز هویت دومرحله‌ای در دسترس قرار گرفت.

به این ترتیب زنجیره آسیب‌پذیری به Account Takeover کامل رسید.

زنجیره کامل آسیب‌پذیری

مشاهده نامی شبیه Bucket در مسیر تصویر

بررسی Response Headers

درخواست Object نامعتبر و دریافت پاسخ خالی

بررسی زیردامنه‌های مجموعه

بررسی ارائه‌دهندگان ابری داخلی

پیداکردن Bucket هم‌نام با پاسخ AccessDenied

شکست سناریوی List، Write و Deface مستقیم

مقایسه یک Object از سایت و سرویس ابری

اثبات ارتباط مسیر سایت با Object Storage

Google Dork روی Endpoint سرویس ابری

پیداکردن Object عمومی از Bucket دیگر

اثبات قبول‌شدن نام Bucketهای دیگر در مسیر

بررسی Bucketهای موجود روی Endpoint

پیداکردن Bucket دارای Anonymous Write

آپلود فایل SVG

نمایش فایل از Origin اصلی صرافی

اجرای Stored XSS

خواندن Cookie نشست فاقد HttpOnly

بازسازی نشست در مرورگر دیگر

Full Account Takeover

ریشه اصلی آسیب‌پذیری

Bucket قابل‌نوشتنی که در این زنجیره استفاده شد متعلق به صرافی نبود. بنابراین وجود آن را نمی‌توان ریشه آسیب‌پذیری صرافی در نظر گرفت.

آن Bucket فقط یکی از پیش‌نیازهای لازم برای تکمیل سناریو بود.

مشکل اصلی در معماری صرافی از ترکیب موارد زیر ایجاد شده بود.

۱. دریافت نام Bucket از مسیر کاربر

مسیر موجود روی دامنه صرافی نام Bucket را مستقیماً از URL دریافت می‌کرد:

1/asset-bridge/<bucket>/<object>
2

این مقدار فقط به Bucketهای متعلق به صرافی محدود نشده بود و می‌شد نام Bucketهای دیگر موجود روی همان Endpoint را جایگزین کرد.

در نتیجه، مسیر صرافی به یک واسط عمومی برای دریافت فایل از Bucketهای مختلف سرویس ابری تبدیل شده بود.

۲. نمایش محتوای Bucketهای دیگر از Origin اصلی صرافی

محتوایی که در یک Bucket خارج از کنترل صرافی قرار داشت، از دامنه اصلی صرافی به کاربر تحویل داده می‌شد.

مرورگر فقط URL نهایی را مشاهده می‌کرد و محتوا را متعلق به Origin زیر در نظر می‌گرفت:

1https://test-exchange.ir
2

بنابراین مهاجم می‌توانست محتوایی را که در زیرساختی خارج از کنترل صرافی ذخیره شده بود، در Context امنیتی دامنه اصلی صرافی اجرا کند.

این رفتار اصلی‌ترین ضعف در زنجیره بود.

۳. قابل‌خواندن‌بودن Cookie نشست توسط JavaScript

پس از اجرای JavaScript روی Origin اصلی، Cookie نشست از طریق document.cookie قابل‌دسترسی بود.

نبود HttpOnly باعث شد امکان اجرای کد روی دامنه اصلی مستقیماً به سرقت نشست و تصاحب کامل حساب تبدیل شود.

تاثیر آسیب‌پذیری

نتیجه نهایی این زنجیره، تصاحب کامل حساب کاربری بود.

مهاجم می‌توانست یک فایل کنترل‌شده را در Bucket قابل‌نوشتن قرار دهد و آن را از طریق URL متعلق به دامنه اصلی صرافی در اختیار قربانی قرار دهد.

پس از بازشدن URL، JavaScript در Origin اصلی صرافی اجرا می‌شد و به Cookie نشست دسترسی پیدا می‌کرد.

با انتقال Cookie به مرورگر دیگری، نشست قربانی بدون نیاز به رمز عبور یا کد احراز هویت دومرحله‌ای بازسازی می‌شد.

در یک صرافی ارز دیجیتال، تصاحب حساب می‌تواند دسترسی به بخش‌هایی مانند موارد زیر را فراهم کند:

1• اطلاعات هویتی و KYC؛
2• تاریخچه معاملات؛
3• اطلاعات مالی حساب؛
4• تنظیمات امنیتی؛
5• آدرس‌های برداشت؛
6• عملیات حساس قابل‌انجام با نشست فعال.
7

همچنین Payload برای آماده‌سازی به حساب کاربری روی صرافی نیاز نداشت و حمله می‌توانست به‌صورت Pre-auth آماده شود.

با توجه به امکان تصاحب کامل حساب، شدت نهایی آسیب‌پذیری در محدوده Critical قرار گرفت.

خط زمانی و نتیجه گزارش

بخشی از نام‌ها، Endpointها، Bucketها و مسیرهای قابل‌شناسایی در نسخه عمومی مقاله تغییر داده شده‌اند تا امکان شناسایی زیرساخت واقعی یا سوءاستفاده مجدد از آن وجود نداشته باشد.

جمع‌بندی

این آسیب‌پذیری از یک نشانه قطعی شروع نشد.

در ابتدا فقط نامی شبیه Bucket را در آدرس یک تصویر دیدم. بررسی هدرهای پاسخ اطلاعاتی درباره زیرساخت ارائه نکرد و درخواست یک فایل نامعتبر نیز فقط صفحه خالی برگرداند.

پس از آن، زیردامنه‌های مجموعه را بررسی کردم تا مشخص شود آیا سرویس Object Storage اختصاصی دارند یا خیر. وقتی به نتیجه‌ای نرسیدم، چند ارائه‌دهنده ابری داخلی را بررسی کردم.

روی یکی از سرویس‌ها، Bucketی با همان نام پیدا شد؛ اما پاسخ AccessDenied اولین سناریوی من برای مشاهده فهرست فایل‌ها، نوشتن روی Bucket و تغییر تصویر صفحه اصلی را متوقف کرد.

ارتباط Bucket با صرافی تنها زمانی اثبات شد که یک Object مشخص را از هر دو مسیر دریافت و مقایسه کردم.

در ادامه، با استفاده از Google Dork یک Object عمومی متعلق به Bucket دیگری روی همان Endpoint پیدا کردم. با قراردادن نام همان Bucket و Object در مسیر صرافی، مشخص شد مسیر فقط به Bucket اصلی محدود نیست.

پس از بررسی Bucketهای موجود روی Endpoint، یک Bucket متعلق به مجموعه‌ای دیگر پیدا شد که امکان آپلود ناشناس فایل را می‌داد.

وجود آن Bucket به‌تنهایی ارتباطی با صرافی نداشت؛ اما مسیر صرافی امکان نمایش فایل‌هایش را از Origin اصلی فراهم می‌کرد.

یک فایل SVG آزمایشی در Bucket قرار دادم و آن را از دامنه صرافی باز کردم. JavaScript با موفقیت روی Origin اصلی اجرا شد.

در مرحله بعد خوش‌شانس بودم که Cookie نشست فاقد HttpOnly بود. مقدار Cookie از طریق JavaScript خوانده شد و با انتقال آن به مرورگر دیگر، نشست قربانی بدون نیاز به رمز عبور یا احراز هویت دومرحله‌ای بازسازی شد.

مهم‌ترین درس این گزارش همان چیزی بود که از ابتدا باعث انتخاب عنوان شد:

آسیب‌پذیری‌های بحرانی همیشه پشت یک باگ پیچیده پنهان نشده‌اند؛ گاهی باید همان جزئیات کوچک و به‌ظاهر بی‌اهمیت را دنبال کرد.

نام یک پوشه، یک پاسخ AccessDenied، یک مسیر قابل‌تغییر و یک Bucket فراموش‌شده، هیچ‌کدام به‌تنهایی یک آسیب‌پذیری Critical نبودند.

اما وقتی کنار یکدیگر قرار گرفتند، همان «زیرِ بغلِ مار» به مسیری مستقیم برای Account Takeover تبدیل شد.