این اولین بانتیِ PlayStation من و اولین متن فارسیِ من درباره امنیت است. متون فارسی در سایت خودم به دلیل راست به چپ بودن درست نمایش داده نمیشدند، این شد که یک تِمِ Hugo را دستکاری کردم.
نوشتن در مورد امنیت برای من به زبان فارسی سختِ. خودم را “خارجی” حساب نمیکنم ولی امنیت و اصطلاحاتش را به زبان انگلیسی یاد گرفته ام و زبانِ کار روزمرهام هست. در مقابل مثلاً در زمینه “کلیله و دمنه” به انگلیسی نمیتوانم صحبت کنم.
اگر مشکل زبان انگلیسی ندارید (خواندن گزارش زبان سطح بالایی لازم ندارد)، پیشنهاد من خواندن متن اصلی باگ در سایت هکروان است:
مقدمات
این گزارش ترکیبی از سه باگ منطقی مختلف است. هر کدام از این باگها در یک محدوده تحقیقاتی مختلف قرار دارند. برای یادگرفتن این باگ ها شاید لازم باشد که موارد زیر را بدانید. در خیلی از موارد توضیحات مفصلی ندادم چون نمیشود همه چیز را در گزارش توضیح داد.
- نزدیک یک سال قبل من ارائه ای در مورد این باگ ها (این باگ هنوز disclose نشده بود) در Appsec village کنفرانس DEF CON 2020 داشتم. در آن یکی از باگ های مشابه خودم برای برنامه Attack Surface Analyzer مایکروسافت را بررسی کردم. ویدئو و اسلاید این ارائه در لینک زیر هستند:
- اطلاعات مربوط به هک برنامه های فریمورک الکترون
- پیدا کردن سرورهای localhost و ترافیک برنامه های ویندوز
- باگ های Tavis Ormandy از Google Project Zero درباره سرورهای localhost
خلاصه
برنامه PlayStation Now نگارش 11.0.2 دارای آسیب پذیری “اجرای کد از راه دور” یا Remote Code Execution (RCE) است. هر وب سایتی که در کامپیوتری که برنامه را اجرا میکند باز شود میتواند از طریق یک websocket روی آن ماشین کد اجرا کند.
- سرور websocket که روی پورت 1235 اجرا میشود هدر origin ریکوئست ها را چک
نمیکند. این به وب سایتهایی که روی ماشین باز شدهاند اجازه میدهد که مستقیم
به این سرور متصل شوند و داده بفرستند.
- websocket ها توسط Same-Origin Policy کنترل نمیشوند. یک وب سایت باز شده در مرورگر میتواند یک ارتباط websocket با هر سرور در دسترس ایجاد کند.
- برنامه psnow یک برنامه Electron به نام AGL را اجرا میکند. با فرستادن داده به
سرور websocket، میتوان به AGL دستور داد تا هر URL را بارگزاری کند.
- در نتیجه، هر وب سایت باز شده در ماشین میتواند به AGL دستور دهد تا هر URL که میخواهد را باز کند.
- همچنین میتوان توسط دستور
setUrlDefaultBrowser
به AGL دستور داد که یک executable را اجرا کند (مثلاً calc).
- در برنامه AGL فیلد
nodeIntegration
فعال است. یعنی کد جاواسکریپت وب سایت باز شده در AGL میتواند از کتابخانههای Node استفاده کند.
با سر هم کردن این سه باگ میتوانیم به RCE برسیم.
اصل ماجرا
برنامه PlayStation Now یک برنامه برای استریم بازیهای پلی استیشن روی ویندوز است (البته این برنامه روی کنسول PS4 هم وجود دارد اما اینجا در مورد آن صحبت نمیکنیم). این برنامه دو بخش اصلی دارد. QAS و AGL.
برنامه QAS
این برنامه اصلی بر پایه فریمورک Qt 5 (کیوت یا cute تلفظ میشود). اسم اصلی این برنامه psnowlauncher.exe است. بعد از اجرا دو کار انجام میدهد:
- برنامه دیگری به نام AGL.exe را اجرا میکند.
- یک سرور websocket در آدرس
localhost:1235
میسازد.
برنامه AGL
برنامه AGL بر پایه فریمورک Electron است. این برنامه
توسط QAS و به صورت زیر با یک سوییچ url
اجرا میشود.
"C:\Program Files (x86)\PlayStationNow\agl\agl.exe"
--url=https://psnow.playstation.com/app/1.10.43/105/00d3603f8/
بعد از اجرای برنامه، وب سایتی که با سوییچ url
مشخص شده در برنامه باز میشود.
فریمورک Electron
هم Qt و هم Electron از فریمورک های پرطرفدار ساخت برنامه های دسکتاپ هستند. فریمورک الکترون بر پایه مرورگر متن باز Chromium ساخته شده است. مرورگرهای مشهوری مانند Chrome, Edge و Brave هم از آن استفاده میکنند. به خلاصه، هر صفحه یک برنامه الکترون یک تب مرورگر است که در آن یک صفحه وب نمایش داده میشود.
یکی از اصلی ترین نکات هنگام بررسی یک برنامه الکترون بررسی فیلد nodeIntegration
است. در صورت فعال بودن (به صورت پیش فرض غیرفعال است) کد جاوااسکریپت داخل مرورگر
میتواند از کتابخانههای Node استفاده کند. من از دو روش برای چک کردن این فیلد
استفاده میکنم:
- بازکردن کد برنامه و static analysis: کد جاوااسکریپت برنامه Electron معمولاً
در یک فایل به نام
app.asar
است که میتوان آن را به راحتی باز و مشاهده کرد. بعد از باز کردن کد برنامه سرچ کنیدnodeIntegration
و ببینید که آیا به مقدار true سِت شده است یا خیر. - بازکردن یک وب سایت در برنامه که مثلاً ماشین حساب را اجرا میکند.
برای اطلاعات بیشتر به این رفرنس که در قسمت مقدمات معرفی کردم مراجعه کنید.
مشکل اول: nodeIntegration
برای تست این فیلد من از روش دوم استفاده کردم. یک صفحه با کد زیر را در یک s3
bucket (سطل؟) ذخیره کردم. این کد ماشین حساب ویندوز را از طریق ماژول child_process
اجرا میکند.
<html>
<head>
<title>This should pop calc on Windows</title>
</head>
<body>
<script>
require('child_process')
.exec('calc')
</script>
</body>
</html>
سپس برنامه را دستی با سویچ url
اجرا کردم.
"C:\Program Files (x86)\PlayStationNow\agl\agl.exe"
--url=https://[redacted].s3.us-east-1.amazonaws.com/node.html
ماشین حساب ویندوز اجرا شد و معلوم که مقدار آن فیلد سِت شده است.
تا اینجا کار خارق العاده ای انجام ندادم، روی کامپیوتر خودم دستی کد اجرا کردهام. به قول این بلاگ، هنوز در سمت دیگر “دریچه” (یعنی روی کامپیوتر خودم) هستم.
مشکل دوم: سرور websocket
همانطور که بالا دیدیم برنامه AGL روی پورت 1235 یک سرور websocket میسازد. قدم بعدی پراکسی کردن برنامه توسط Burp بود. برنامه های بر پایه مرورگر Chromium معمولاً از تنظیمات پراکسی ویندوز استفاده میکنند.
پراکسی کردن ترافیک برنامه های دسکتاپ
پراکسی کردن ترافیک این برنامه ساده بود ولی همیشه اینگونه نیست. این بحث مفصلی است و من تا امروز 18 بلاگ درباره آن نوشتهام و هنوز جا برای تحقیق زیاد دارد
بعد از پراکسی کردن این برنامه ها ترافیک زیادی در Burp دیدم. یکی از مشکلات پراکسی کردن با تنظیمات ویندوز این است که بسیاری از برنامه های دیگر و سرویس های ویندوز نیز ترافیک خود را به پراکسی میفرستند. برای این کار میتوانید دامنههای بیربط را به TLS Passthrough اضافه کنید تا Burp آنها را پراکسی نکند. برای اطلاعات بیشتر بلاگ زیر را بخوانید:
ترافیک برنامهها را توسط user-Agent
شان تشخیص دادم. هر دو برنامه کلمه
gkApollo
را در user-agent
داشتند. پس بقیه ترافیک به درد من نمیخورد. برای
تشخیص ترافیک این دو برنامه از هم به کلمات دیگر در user-agent
دقت کردم:
- ترافیک برنامه QAS که بر پایه Qt است این کلمه را دارد
QtWebEngine/5.5.1
. - ترافیک برنامه AGL که بر پایه الکترون است این دو را دارد
Electron/1.4.16
وplaystation-now/0.0.0
.
توسط یک افزونه به نام Request Highlighter در Burp ترافیک این دو برنامه را زرد و آبی کردم که تشخیص آنها از هم راحتتر باشد.
پروتکل پیام های websocket
پس از پراکسی کردن در Burp ترافیک سرور websocket را میبینیم. پیامهای پروتکل این
سرور بسیار ساده بودند و به نظر میرسید توسط JSON.stringify
جاوااسکریپت ایجاد شدهاند.
.پیام ها به این شکل بودند:
{
"command": "isMicConnected",
"params": {},
"source": "AGL",
"target": "QAS"
}
- فرمان یا
command
که مشخص میکند این دستور باید چه کاری انجام دهد. - پارامترهای دستور.
- بخشهای سوم و چهارم مبدا و مقصد پیام هستند. در واقع مبدا پیام مهم نیست و فقط مقصد مهم است.
برای دیدن اینکه چه دستورهایی در برنامه وجود دارند دو کار کردم:
- برنامه را اجرا کردم و چند کار داخل برنامه انجام دادم سپس ترافیک داخل Burp را دیدم.
- کد برنامه الکترون را باز کردم و اسم چند دستور که در Burp دیده بودم را سرچ کردم تا به بخش commandHandler رسیدم. در این بخش همه دستورات سرور وجود داشتند.
دو دستور مهم هستند: setUrl
و setUrlDefaultBrowser
.
setUrl
به برنامه میگوید که کدام وب سایت را باز کند. پیام این دستور به این شکل است:
{
"command": "setUrl",
"params": {
"url": "https://psnow.playstation.com/app/1.10.43/105/00d3603f8/"
},
"source": "AGL",
"target": "QAS"
}
setUrlDefaultBrowser
یک فایل را به کمک سیستم عامل باز میکند. مثلاَ اگر ورودی
آن یک آدرس وب سایت باشد، آن را در مرورگر باز میکند و یک فایل ورد در برنامه آفیس
(اگر نصب شده باشد) باز میشود. چند روز بعد از گزارش باگ به من به یاد یکی از باگ
های Tavis Ormandy افتادم که با استفاده از این دستور به RCE دست یافته بود.
اگر یک فایل اجرایی را با این دستور باز کنیم، ویندوز خود آن فایل را اجرا میکند. پس دستور زیر ماشین حساب ویندوز را اجرا میکند.
{
"command": "setUrl",
"params": {
"url": "file:///c:/windows/system32/calc.exe"
},
"source": "AGL",
"target": "QAS"
}
با استفاده از این دستور دیگر نیاز به nodeIntegration
هم نداریم. اما این دستور
یک محدودیت بزرگ دارد. فقط میتوانیم فایل اجرا کنیم و نمیتوانیم به فایل اجرایی
پارامتر و یا سوییچ بفرستیم.
مشکل سوم: اجرای کد روی ماشین توسط دستورات websocket
در Burp دیدم که برنامه AGL (الکترون) به QAS میگوید که یک وب سایت را باز کند. در ابتدا من به مبدا و مقصد دقت نکردم و بیخیال باگ شدم چون بازکردن سایت در QAS فایدهای برای من نداشت. اما چند ساعت بعد وقتی کار دیگری انجام میدادم یادم آمد که چرا مقصد را AGL (برنامه الکترون) نگذارم؟ با فرستادن پیام پایین وب سایتی که در بخش اول ساخته بودم (ماشین حساب ویندوز را اجرا میکرد) در برنامه الکترون باز کردم و ماشین حساب ویندوز اجرا شد.
{
"command": "setUrl",
"params": {
"url": "https://example.net"
},
"source": "AGL",
"target": "QAS"
}
بعد از این کد یک برنامه چت بر پایه websocket را دستکاری کردم و در سطل (؟) s3 دیگری قرار دادم. بعد از باز کردن این وب سایت در مرورگر روی کامپیوتری که برنامه psnow در حال اجرا بود میتوانستم با سرور websocket صحبت کنم و به آن دستور بفرستم.
امنیت websocket
اولین مرحله در ایجاد ارتباط با یک سرور websocket، فرستادن یک ریکوئست HTTP با تعدادی هِدِر (header) خاص است. در حالت عادی Same-Origin Policy یا SOP مرورگر به اسکریپتهای یک اریجین اجازه نمیدهد تا با اریجین دیگر ارتباط برقرار کنند (بحث SOP مفصلتر از این است که اینجا مطرح کنم ولی حتماً در مورد آن بخوانید چون یکی از مهمترین بخشهای امنیت مرورگر و وب است). websocket ها شامل SOP نمیشوند. یعنی هر وب سایت میتواند با هر سرور websocket ارتباط برقرار کند.
حل این مشکلات
تا اینجا سه باگ را بررسی کردیم و فهمیدیم که اگر برنامه PlayStation Now را اجرا کنیم. وب سایتی که در مرورگر کامپیوتر ما باز شده است میتواند روی ماشین ما کد اجرا کند. این اصلاً خوب نیست.
یکی از مهمترین وظایف من به عنوان یک مهندس امنیت نرم افزار، حل مشکلات امنیتی است. راحتترین روش حل این مشکل چک کردن هدر origin در اولین قدم ایجاد ارتباط websocket است. این هدر فقط توسط مرورگر میتواند اضافه شود1. اولین ریکوئست ایجاد یک websocket شکلی مشابه این دارد:
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== - مقدار این هدر معمولاً مهم نیست
Sec-WebSocket-Version: 13
Origin: whatever.com
چون نمیتوان به مکانیزم SOP برای فیلتر کردن درخواست های websocket اتکا کرد باید
به صورت دستی هدر origin را بررسی کنیم و مواردی که نمیخواهیم (مثلاً هر چه که از
playstation.com.*
نیست) را رد کنیم.
دومین مشکل این است که سرور روی همه IP های دستگاه در حال شنیدن است. به عبارت دیگر روی 0.0.0.0 بایند شده. این یعنی هر کسی که بتواند به پورت 1235 دستگاه از خارج دسترسی داشته باشد میتواند به آن وصل شود. در دنیای واقعی این مشکل آنقدر ترسناک نیست زیرا اکثر مودم و روترها تنها اجازه دسترسی مستقیم به پورت را در شبکه داخلی میدهند. برای حل این مشکل باید سرور روی localhost بایند شود.
چی یاد گرفتیم؟
متاسفانه در این باگ مرزهای علم را جابجا نکردم و دانش جدیدی تولید نشد. خودم هم چیز جدیدی یاد نگرفتم. اما امیدوارم خواننده چند نکته یادگرفته باشد. اگر بازخورد یا پیشنهادی دارید، پیدا کردن من در اینترنت بسیار راحت است.
به این هدرها forbidden header میگویند. ↩︎