در این پست میخوایم آسیبپذیری Race Condition رو تشریح کنیم، اول بگیم که این آسیبپذیری چیه و یه مثال در دنیای واقعی ازش بگیم، بعد بریم یک اسکریپت PHP از گیتهاب که آسیبپذیری Race Condition داره رو کلون و اکسپلویت کنیم.
آسیبپذیری Race Condition کلیتش به این صورت هست که چندتا پروسه همزمان فرستاده میشن بر روی یک منبع به اشتراک گذاشته شده و همزمان پروسه ها اجرا میشن، دقت کنید که این آسیبپذیری بر اثر درست اعتبار سنجی نکردن یکسری ورودیها به وجود میاد.
بیاید اول یه مثال داخل زندگی واقعی بزنیم
شما فرض کنید وقتی یک در برای ورود یک موتور باز میشه اما چهارتا موتور همزمان سمت اون در میرن، بصورت رندوم یا شانسی هست که دقیقا کدوم موتورها وارد بشن یا نشن اما در هرصورت بجای ورود یک موتور دو یا چند موتور وارد میشه حالا شما فرض کنید که عملیاتی مثل شستن موتور میخواد انجام بشه در آخر بجای شستن یک موتور اون تعداد موتوری که وارد شدن شسته میشن و عملیات روی چندتا بصورت همزمان انجام میشه نه یکی. این دقیقا مثالی هست که داخل دنیای اپلیکیشن ها هم وجود داره.
اما بریم سراغ مثال واقعی که برو روی سیستم عامل کالی لینوکس تست شده.
سرویس mysql رو فعال میکنیم با دستور
root@root:~# service mysql start
سپس به mysql shell میریم
root@root:~# mysql
و دستورات زیر رو به ترتیب داخل mysql shell میزنیم
MariaDB [(none)]> create database test character set utf8 collate utf8_general_ci;
MariaDB [(none)]> grant all privileges on test.* to test@'localhost' identified by 'test@123';
MariaDB [(none)]> flush privileges;
MariaDB [(none)]> use test;
MariaDB [test]> create table bank_accounts(uid int auto_increment primary key,ucode varchar(10) not null,balance int(11) not null default 0,uname varchar(50) not null);
MariaDB [test]> insert into bank_accounts(ucode,uname,balance) values ('BANK000001','User 1',20000),('BANK000002','User 2',5500),('BANK000003','User 3',8700);
من یه توضح خلاصه میدم که چیکار کردیم، دیتابیسی با نام test ساختیم، دسترسیهای لازم رو بهش دادیم، یک جدول به نام bank_accounts ساختیم و یک سری ستون به اسمهای uid,ucode,balance,uname ساختیم و دیتاهایی درون هرکدوم ریختیم.
خب این از این، حالا بریم اسکریپت رو از گیتهاب کلون کنیم و تحلیل و سپس اکسپلویت.
ابتدا سرویس apache2 را استارت میزنیم
root@root:~# service apache2 start
به مسیر زیر میریم
root@root:~# cd /var/www/html
و پروژه خودمون رو از داخل گیتهاب کلون میکنیم و اسمش رو rc میزاریم
root@root:/var/www/html# git clone https://github.com/doantranhoang/php-race-condition-example.git rc
حالا وارد پوشه rc میشیم
root@root:/var/www/html# cd rc
اگر ls بگیریم و لیست فایل هارو ببینیم میبنید که فایل هایی به اسمهای:
poc.php
withdraw_normal.py
withdraw_race.py
وجود داره. فایل poc.php فایل آسیبپذیر ما هست. فایل withdraw_normal.py اسکریپت پایتونی هست که ما درخواست های معمولی به poc.php میدیم که ببینیم چطور عمل میکنه. اما فایل withdraw_race.py اسکرپیتی هست که ما از طریقش آسیبپذیری رو اکسپلویت میکنیم. خب بریم سراغ شرح یکسری نکات بعد هم اکسپلویت poc.php. اگر دستور
MariaDB [test]> select * from bank_accounts;
داخل mysql shell و دیتابیسی که قبلا ایجاد کردیم بزنیم خروجی زیر رو میبینید
بریم ببینیم poc.php درحالت عادی چطور ورودی میگیره و چطور با جدول بالا ارتباط برقرار میکنه. نحوه ورودی به این شکل هست
http://127.0.0.1/rc/poc.php?user_code=BANK000001&amount=100
بیاید یکبار اینو داخل مروگر کپی کنیم و اجراش کنیم. خب حالا اگه جدول رو نگاه کنیم
مببینم که از مقدار 20000 دلار اومده 19900 دقت کنید این 100 دلار که کم شده طبق پارامتر amount برای BANK000001 اعمال شده.
خب دستور زیر رو داخل mysql shell بزنید که دلار ما به همون مقدار 20000 برگرده
MariaDB [test]> update bank_accounts set balance = 20000 where uid = 1;
حالا ما میایم همین درخواست رو به صورت کاملا عادی با اسکریپت withdraw_normal.py به تعداد 128 بار میدیم و به ازای هربار درخواست تعداد 100 دلار از 20000 دلار کم میشه
اما دقت کنید که باید در این اسکریپت ادرس زیر رو داخل متغیر url_test ست کنیم.
http://127.0.0.1/rc/poc.php?user_code=BANK000001&amount=100
خب حالا اسکریپت رو اجرا میکنیم
root@root:/var/www/html/rc# python withdraw_normal.py
حال اگر جدول رو نگاه کنیم میبینم که بله 7200 دلار باقیمانده.
خب این تست عادی بود که با اسکریپت 128 درخواست به صورت عادی فرستادیم و با هربار درخواست 100 دلار کم میشد که در نهایت شد 7200 و اگر اون بخشی که با رنگ قرمر مشخص کردم رو نگاه کنید روند عادی همینو نشون میده که به ترتیب و دونه دونه درخواست ها انجام شدن.
خب دوباره این دستور رو داخل mysql shell بزنید تا دلار ما بشه همون 20000
MariaDB [test]> update bank_accounts set balance = 20000 where uid = 1;
بریم سراغ اکسپلویتش که اسکریپت withdraw_race.py میشه، دقت کنید که همون ست کردنی که برای فایل قبل کردیم اینجا هم باید کنیم.
این فایل میاد 128 درخواست همزمان به poc.php میفرسته و خودتون میبیند که تفاوتش با اسکریپت قبل چیه. دقت کنید که فایل قبل میومد به واسطه یه حلقه for و به ترتیب درخواستو میداد اما این فایل همزمان کل 128 درخواست رو میده و بعضی درخواست ها باهم پردازش میشن و همین میشه آسیب پذیری RC 🙂
خب اسکریپت رو اجرا کنیم
root@root:/var/www/html/rc# python withdraw_race.py
حالا اگر بعد اجرا اسکریپت جدول رو نگا کنیم به این شکل هست
دقت کنید که باید 7200 تا باقی میموند
20000 - 128*100 = 7200
اما جالب اینجاس که هر 128 درخواست ما به ازای هر یک درخواست 100 دلار انتقال به حساب دیگه یا حالا هرچی انجام شده و در آخرم 9200 تا مونده در صورتی که باید 7200 تا میموند.اگر بازم بخش قرمز رنگی که مشخص کردم رو نگاه کنید میبیند که با عکس قبل کلی فرق داره و درخواست ها همزمان پردازش شدن.
فقط دقت کنید که اگه شما تست کنید ممکنه بجای 9200 یه عدد دیگه نشون بده، کلا این کار رندومه و اصلا حد مشخصی نداره که بگیم هر دفعه عددش تکراریه، نه اینطور نیست شما تست کنید ممکنه باقیمانده 8400 بشه یا یه عدد دیگه. امیدوارم تحقیق کنید و علتشو بدونید چون واقعا اگه اینو هم توضیح بدم پست سنگین میشه برای بعضی دوستان.
من نخواستم خیلی ریز بشم روی آسیب پذیری و یکسری چیزا های دیگه رو تشریح کنم،فقط ساده و قابل فهم اونو بیان کردم،در پست های بعد بهتر بیانش میکنیم.
موفق باشید
منابع:
http://blog.hoangdoan.io/2015/11/php-race-condition-vulnerability-example.html
https://github.com/doantranhoang/php-race-condition-example
[…] را خودتان هم برای تست پیاده سازی کنید، میتوانید پست Race Condition را مطالعه کنید و ببینید چطور میشود mysql را آماده و […]
، درود ، برای جلوگیری از این باگ باید چه کار کرد ؟ آیا استفاده از آبجکت های thread safe و همچنین اعمال lock های synchronize بر روی این نوع متد ها میتونه کافی باشه ؟