Prototype Pollution آسیب پذیری Javascript

Prototype Pollution آسیب پذیری Javascript

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

اولین کسی باشید که به این سؤال پاسخ می‌دهید

آسیب پذیری Prototype pollution یک آسیب پذیری موجود در جاوااسکریپت است که به واسطه ساختار ارث بری پیاده شده در این زبان یعنی Prototype-based inheritance به وجود آمده است. در این زبان، برخلاف زبان هایی مانند Java یا C++ نیازی به تعریف کردن کلاس و ... برای ایجاد یک شیء نداریم و کافی است از ساختار {} برای تعریف کردن ویژگی ها(Property) در یک Object استفاده کنیم. نمونه ای از تعریف یک شیء را در مثال زیر می توانید مشاهده کنید:

const obj1 = {
prop1: 111,
prop2: "this is an string!",
}

کد بالا یک شیء با نام obj1 را تعریف می کند که دو ویژگی با نام های prop1 و prop2 دارد اما این دو مورد تنها ویژگی های این شیء نیستند. برای مثال اگر خروجی اجرای ()obj1.toString در کنسول را مشاهده کنیم با عبارت [object Object] برابر است و از Object.prototype برگرفته شده است. در واقع شیء Object پدر تمامی اشیاء در جاوااسکریپت است. هر شیء در جاوااسکریپت دارای یک Prototype است که در صورت مقداردهی نکردن آن، مقداری برابر با Object.prototype دارد. تصویر زیر خروجی اجرای Object.prototype در کنسول Devtools مرورگر را نمایش می دهد.

 Prototype Pollution آسیب پذیری Javascript

برای فهمیدن اینکه چه شیء ای Prototype یک شیء دیگر است کافی است که مقدار __Proto__ آن را بررسی کنیم یا خروجی متود ()Object.getPrototypeOf را برای آن ببینیم. به عنوان مثال برای شیء obj1 که قبلاً تعریف کردیم چون Prototype خاصی تعریف نکرده بودیم باید خروجی این دو مورد برابر با Object.prototype باشد که اثبات این موضوع را در تصویر زیر می توانید مشاهده کنید.

 Prototype Pollution آسیب پذیری Javascript

زمانی که می خواهیم به یک ویژگی از یک شیء دسترسی داشته باشیم جاوااسکریپت ابتدا بررسی می کند که آیا خود شیء چنین ویژگی دارد یا نه و در صورتی که داشته باشد آن را باز می گرداند. اما در صورتی که این ویژگی را نداشته باشد جاوااسکریپت Prototype آن را بررسی می کند و در صورت وجود ویژگی در Prototype آن را بر می گرداند و در صورتی که Prototype هم این ویژگی را نداشت Prototype خود آن Prototype را بررسی می کند و به همین ترتیب ادامه می دهد تا جایی که با یک مقدار null برخورد کند. به این موضوع در جاوااسکریپت Prototype Chain گفته می شود.
پیمایش کردن Prototype Chain توسط جاوااسکریپت یک اثر بسیار مهم دارد. اگر شما بتوانید یک ویژگی در Object.prototype اضافه کنید و در واقع آن را آلوده کنید در واقع به تمامی شیء های موجود در جاوااسکریپت این ویژگی اضافه خواهد شد. برای مثال نمونه زیر را در نظر بگیرید:

const user = { userid: 123 };
if (user.admin) {
    console.log('You are an admin')
}

در نگاه اول به نظر می رسد که نتوانیم شرط را true کنیم چون شیء user به صورت پیشفرض ویژگی با نام admin ندارد. اما اگر بتوانیم Object.prototype را آلوده کنیم و برای آن یک ویژگی admin تعریف کنیم که true باشد می توانیم این شرط را دور بزنیم. بنابراین با تغییر قطعه کد بالا به قطعه کد زیر و اضافه شدن ویژگی admin به Object.prototype عبارت «You are an admin» در کنسول چاپ خواهد شد.

Object.prototype.admin = true;
const user = { userid: 123 };
if (user.admin) {
    console.log('You are an admin'); // this will execute
}


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

https://playcode.io/683626/

 Prototype Pollution آسیب پذیری Javascript

چگونه Prototype Pollution ممکن است اتفاق بیفتد؟

در بیشتر مواقع نقطه شروع این آسیب پذیری عملیات ادغام (merge) چند شیء است که می توانید نمونه ای از آن را در مثال زیر مشاهده کنید. توجه داشته باشید که توابع ادغام بیان شده در مثال های زیر فرضی هستند و در کنسول مرورگر قابل اجرا نیستند.

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
merge(obj1, obj2) // returns { a: 1, b: 2, c: 3, d: 4}

خروجی اجرای کد بالا یک شیء است که ویژگی های هر دو شیء obj1 و obj2 را دارد. همانطور که در مثال زیر قابل مشاهده است در بعضی از موارد، عملیات ادغام به صورت recursive انجام می شود.

const obj1 = {
  a: {
          b: 1,
          c: 2,
       }
};

const obj2 = {
  a: {
           d: 3
       }
};

recursiveMerge(obj1, obj2); // returns { a: { b: 1, c: 2, d: 3 } }

روند طی شده در مثال بالا به صورت زیر است:

1. بررسی همه ویژگی های موجود درobj2 و بررسی تکراری نبودن آن ها با obj1.
2. اگر یکی از ویژگی ها تکراری بود آن را با نمونه تکراری ادغام می کنیم.
3. اگر یک ویژگی تکراری نبود آن را از obj2به obj1 کپی می کنیم.

در نمونه واقعی اگر کاربر بتواند اشیائی که با هم ادغام می شوند را کنترل کند می تواند از خروجی تابع JSON.parse برای آن ها استفاده کند. دلیل این موضوع، تفاوت رفتار تابع JSON.parse نسبت به رفتار عادی جاوااسکریپت با ویژگی __proto__ است. این تابع با ویژگی __proto__ به صورت یک ویژگی عادی برخورد می کند و مدل پردازشی پیشفرض برای prototype را در جاوا اسکریپت کنار می گذارد. نمونه ای از این موضوع را در تصویر زیر مشاهده می کنید و برای درک عملی تفاوت دو روش ذکر شده می توانید از لینک زیر استفاده کنید.

https://playcode.io/683664/

 Prototype Pollution آسیب پذیری Javascript

همانگونه که در تصویر بالا مشاهده می کنید obj1 با استفاده از مدل عادی درجاوااسکریپت و obj2 با استفاده از پردازش json توسط تابع JSON.parse ایجاد شده است. تفاوت خروجی مقدار __proto__ برای دو حالت نشان دهنده این است که تابع JSON.parse توانایی کنار گذاشتن مدل عادی موجود در جاوااسکریپت را دارد. برای درک نحوه خطرناک شدن این تابع فرض کنید که دو شیء مانند مثال زیر داشته باشیم:

obj1={}
obj2=JSON.parse('{"__proto__":{"x":1}}')

روند اجرای یک تابع ادغام Recursive روی این دو شیء به صورت زیر خواهد بود:
1. بررسی تمامی ویژگی های موجود برای obj2 که خروجی آن تنها ویژگی __proto__ خواهد بود.
2. بررسی وجود __proto__ در obj1 که در این شئ موجود است.
3. بررسی همه ویژگی ها در __obj2.__proto که تنها ویژگی موجود در آن x است.

4. ایجاد obj1.__proto__.x که مقدار آن برابر با obj2.__proto__.x است.

شیء __obj1.__proto با توجه به Prototype Chaining در جاوااسکریپت به شیء Object.prototype اشاره می کند. بنابراین اضافه کردن شیء x به __obj1.__proto برابر با انجام موفق Prototype Pollution است. این آسیب پذیری تاکنون در بسیاری از کتابخانه های معروف جاوااسکریپت از جمله jQuery کشف شده است.

تاکنون بیشتر مقالات منتشر شده، این آسیب پذیری را در سمت سرور و روی NodeJS بررسی کرده اند و از آن برای رسیدن به اجرای کد از راه دور (Remote Code Execution) استفاده کرده اند. در ادامه به بررسی این آسیب پذیری در سمت کاربر خواهیم پرداخت و مشاهده خواهیم کرد که چگونه با استفاده از این آسیب پذیری می توان HTML Sanitizer ها را دور زد.


HTML Sanitizer چیست؟

HTML Sanitizer ها همانطور که از نامشان مشخص است کتابخانه هایی هستند که کار پاک سازی کدهای HTML از قطعه کدهای مخرب و خطرناک جاوااسکریپت را انجام می دهند. برای درک بهتر تصور کنید که کدی مانند زیر داشته باشیم:

<h1>Header</h1>This is <b>some</b> <i>HTML</i><script>alert(1)</script>

همانطور که مشاهده می کنید یکی از معروف ترین قطعه کدهای مخرب برای تشخیص آسیب پذیری XSS یعنی <script>alert(1)</script> در این کد وجود دارد. حال اگر این کد را به یک HTML Sanitizer بدهیم خروجی آن عبارت زیر خواهد بود:

<h1>Header</h1>This is <b>some</b> <i>HTML</i>

این کتابخانه ها دو روش برای ذخیره سازی المان های مجاز دارند. روش اول ذخیره سازی المان ها در قالب یک آرایه است که مثالی از آن را نمونه زیر مشاهده می کنید:

const ALLOWED_ELEMENTS = ["h1", "i", "b", "div"]


برای بررسی مجاز بودن استفاده از یک المان کافی است آن را با استفاده از قطعه کد (ALLOWED_ELEMENTS.includes(element در مثال بالا چک کنیم. در این حالت چون ما نمی توانیم به این آرایه، اشیاء دیگری اضافه کنیم یا طول آرایه و یا محتوای یکی از اعضای آرایه را تغییر دهیم، در عمل Prototype Pollution دچار مشکل می شود و قابل انجام نیست.

 Prototype Pollution آسیب پذیری Javascript

برای نمونه تغییرات مثال تصویر بالا را در Object.prototype در نظر بگیرید. این تغییرات منجر به افزایش طول آرایه به 10 و تغییر محتوای عضو 0 ام این آرایه به مقدار test می شود. بعد از اجرای این نمونه ها همانطور که مشاهده می کنید اتفاق خاصی در آرایه ALLOWED_ELEMENTS نمی افتد و همچنان ALLOWED_ELEMENTS.length مقدار 4 و [ALLOWED_ELEMENTS[0 مقدار "h1" را خروجی خواهد داد.
روش دوم برای برای ذخیره سازی المان های مجاز، استفاده از یک شیء مانند مثال زیر است.

const ALLOWED_ELEMENTS = {
"h1": true,
"i": true,
"b": true,
"div" :true
}

در این روش برای بررسی مجاز بودن یکی از المان های HTML، وجود آن را در این شیء بررسی می کنند که این موضوع باعث می شود تا بتوانیم با اجرای قطعه کد زیر، مجوز اجرای کد در داخل المان script را دریافت کنیم.

Object.prototype.script = true;

نمونه ای از بررسی این مثال را در تصویر زیر می تواند مشاهده کنید.

 Prototype Pollution آسیب پذیری Javascript

بنابراین یاد گرفتیم که مناسب ترین حالت برای ایجاد Prototype Pollution زمانی است که المان های مجاز در قالب ویژگی های یک شیء ذخیره شده باشد. بدترین حالت زمانی است که ذخیره سازی المان های مجاز در یک آرایه انجام شده باشد که هیچ اجازه ای برای دستکاری Prototype به ما داده نمی شود.

در این مقاله سه مورد از معروف ترین Sanitizer های معروف مورد بررسی قرار گرفته است که عبارتند از:
Sanitize-html با 800 هزار دانلود در هفته
Xss با 770 هزار دانلود در هفته
Dompurify با 544 هزار دانلود در هفته


بررسی Sanitize-html

نمونه ای از اجرای این ابزار روی یک قطعه کد آلوده را در تصویر زیر می توانید مشاهده کنید.

 Prototype Pollution آسیب پذیری Javascript

در ادامه قطعه کد زیر را که بخشی از کد منبع این Sanitizer است در نظر بگیرید:

sanitizeHtml.defaults = {
allowedTags: ['h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'abbr', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe'],
disallowedTagsMode: 'discard',
allowedAttributes: {
a: ['href', 'name', 'target'],
// We don't currently allow img itself by default, but this
// would make sense if we did. You could add srcset here,
// and if you do the URL is checked for safety
img: ['src']
},
// Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemesByTag: {},
allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
allowProtocolRelative: true,
enforceHtmlBoundary: false 
};

همانطور که در کد بالا مشاهده می کنید allowedTags یک آرایه است که مشخص کننده المان های مجاز برای استفاده است و به دلیل ماهیت آرایه بودن آن، با توجه به توضیحات قبلی نمی توانیم از آن برای Prototype Pollution استفاده کنیم. نکته قابل توجه در این ویژگی، مجاز بودن المان iframe است.
چند خط پایین تر در کد منبع، allowedAttributes را مشاهده می کنیم که یک شیء از نوع map است و ممکن است بتوانیم با اضافه کردن ویژگی onload به آن بتوانیم با استفاده از iframe کد جاوااسکریپت دلخواهمان را اجرا کنیم. در ادامه، بخش دیگری از کد sanitize-html را مشاهده می کنید که در قسمت هایی از آن تعدادی شیء برای داشتن ویژگی هایی خاص با استفاده از تابع has چک می شوند (تابع has یک تابع برای کلاس map است که می تواند وجود یا عدم وجود ویژگی ها در یک شیء map را بررسی کند). همانطور که در کد زیر مشاهده می کنید تعدادی شرط با همدیگر OR شده اند.

// check allowedAttributesMap for the element and attribute and modify the value
// as necessary if there are specific values defined.
var passedAllowedAttributesMapCheck = false;
if (!allowedAttributesMap ||
(has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1) ||
(allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1) ||
(has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
(allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
passedAllowedAttributesMapCheck = true;

تمرکز ما روی قسمت هایی از کد بالا است که allowedAttributesMap را مورد بررسی قرار می دهد. در واقع این قسمت در حال بررسی این است که آیا یک attribute برای استفاده در یک تگ یا تمامی تگ ها مجاز است یا خیر. در ادامه نحوه تعریف تابع has را در قطعه کد زیر مشاهده می کنیم.

// Avoid false positives with .__proto__, .hasOwnProperty, etc.
function has(obj, key) {
return ({}).hasOwnProperty.call(obj, key);
}

در واقع کد بالا نشان دهنده این است که درخواست ها به تابع has دچار Prototype Pollution نمی شوند. اما جالب تر این است که تابع has برای فیلتر کردن به صورت دقیق استفاده نشده است و همانطور که قبلا اشاره شد با توجه به این که شرط های موجود در کد با هم OR شده اند در عمل می توانیم از قطعه کد زیر برای Prototype Pollution استفاده کنیم.

Object.prototype['*'] = ['onload']

حال اگر ورودی قبلی را پس از اجرای این قطعه کد، با sanitize-html بررسی کنیم قسمت خطرناک کد حذف نمی شود و می تواند منجر به اجرای کد دلخواه ما شود. نمونه ای از این اتفاق را می تواند در تصویر زیر مشاهده کنید.

 Prototype Pollution آسیب پذیری Javascript

بررسی XSS

در این کتابخانه پاک سازی کد مانند مثال زیر انجام می شود:

 Prototype Pollution آسیب پذیری Javascript

تابع filterXSS می تواند یک پارامتر دوم با نام options دریافت کند .کد نوشته شده برای پردازش options به نوعی بهترین حالت برای اجرای Prototype Pollution محسوب می شود. برای درک بیشتر به کد زیر توجه کنید:

options.whiteList = options.whiteList || DEFAULT.whiteList;
options.onTag = options.onTag || DEFAULT.onTag;
options.onTagAttr = options.onTagAttr || DEFAULT.onTagAttr;
options.onIgnoreTag = options.onIgnoreTag || DEFAULT.onIgnoreTag;
options.onIgnoreTagAttr = options.onIgnoreTagAttr || DEFAULT.onIgnoreTagAttr;
options.safeAttrValue = options.safeAttrValue || DEFAULT.safeAttrValue;
options.escapeHtml = options.escapeHtml || DEFAULT.escapeHtml;

در کد بالا تمامی ویژگی های موجود در options.propertyName می توانند آلوده شوند. مناسب ترین این ویژگی ها whitelist است که فرمتی مانند مثال زیر دارد:

a: ["target", "href", "title"],
abbr: ["title"],
address: [],
area: ["shape", "coords", "href", "alt"],
article: [],

با توجه به موارد بالا راحت ترین روش بهره کشی، ایجاد ویژگی whitelist به صورتی است که تگ img را با attribute های src و onerror تعریف کرده باشد. در تصویر زیر فرایند انجام این کار را مشاهده می کنید که کاملا نتیجه داده است و عبارت خطرناک حذف نشده است.

 Prototype Pollution آسیب پذیری Javascript

بررسی dompurify

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

 Prototype Pollution آسیب پذیری Javascript

این کتابخانه با استفاده از پارامتر دومی که در تابع sanitize می پذیرد می تواند تنظیماتی را دریافت و اعمال کند. الگوی مورد استفاده در کد نوشته شده برای این کتابخانه آن را آسیب پذیر به Prototype Pollution می کند. قسمتی از کد مربوط به این بخش را در تصویر زیر مشاهده می کنید.

/* Set configuration parameters */
ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS;
ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR;

در جاوااسکریپت با استفاده از اپراتور in می توان کل Prototype Chain را پیمایش کرد. بنابراین خروجی عبارت ALLOWED_ATTR' in cfg' در صورتی که ویژگی که ALLOWED_ATTR به آن اشاره می کند در Object.prototype وجود داشته باشد، معادل true است. این کتابخانه به صورت پیشفرض المان img را مجاز در نظر گرفته است و برای استفاده از آن، تنها کافی است تا attribute های src و onerror را اضافه کنیم. مثالی از اجرای آلوده سازی را در تصویر زیر مشاهده می کنید.

 Prototype Pollution آسیب پذیری Javascript

ابزارهایی برای شناسایی Prototype Pollution

در حالت کلی برای شناسایی این آسیب پذیری یکی از بهترین راه ها استفاده از Regular Expression ها است اما چون ممکن است property ها و ... اسامی متفاوتی داشته باشند یک روش واحد برای همه کدهای نوشته شده وجود ندارد. برای توسعه بهتر این روش می توان با توجه به کد، یک سری نشانگر را در داخل کد منبع یافت که با استفاده از آن ها ویژگی های همه Object ها را به Object.prototype اضافه کرد و سپس هر کدام از ویژگی ها که مورد دسترسی قرار بگیرد معلوم می شود که بعدا با استفاده از آن می توانیم Prototype Pollution انجام دهیم. این روش چندین عیب دیگر نیز دارد اما مهم ترین عیب آن وابسته بودن به کد منبع و نداشتن قابلیت استفاده عمومی است.


روشی بهتر برای شکار این آسیب پذیری تعریف یک تابع است که دسترسی به هر ویژگی، به یک درخواست به این تابع تبدیل شود و سپس این تابع مشخصات هر ویژگی که از Object.prototype خوانده شده را در کنسول چاپ کند. نمونه ای از این تبدیل می تواند تبدیل عبارت (if(cfg.ADD_ATTR به (('if($_GET_PROP(cfg, 'ADD_ATTR باشد که GET_PROP_$ تابع مورد نظر ما است و به شکل زیر تعریف می شود.

window.$_SHOULD_LOG = true;
window.$_IGNORED_PROPS = new Set([]);
function $_GET_PROP(obj, prop) {
if (window.$_SHOULD_LOG && !window.$_IGNORED_PROPS.has(prop) && obj instanceof Object && typeof obj === 'object' && !(prop in obj)) {
console.group(`obj[${JSON.stringify(prop)}]`);
console.trace();
console.groupEnd();
}
return obj[prop];
}

این روش به صورت یک ابزار آماده در گیت هاب قرار داده شده است که از طریق لینک زیر در دسترس است.

https://github.com/securitum/research/tree/master/r2020_prototype-pollution

نمونه ای از اجرای این ابزار برای یک اسکریپت ساده و خروجی که برای آن تولید می کند را در کلیپ زیر می توانید مشاهده کنید.

حال با قرار دادن خروجی این اسکریپت در کنسول مرورگر خود می توانید هنگام اجرای کد، قسمت های آسیب پذیر به Prototype Pollution را شکار کنید. 

نتیجه گیری

در این مقاله با هم یاد گرفتیم که آسیب پذیری Prototype Pollution چیست و چقدر می تواند خطرناک باشد و فهمیدیم این آسیب پذیری علاوه بر سمت سرور در سمت کاربر نیز می تواند ایجاد شود. سپس با هم دیدیم که چگونه می توان از این آسیب پذیری برای دور زدن کتابخانه های HTML Sanitizer استفاده کرد و در نهایت یک روش خوب برای پیدا کردن این آسیب پذیری معرفی شد و یک ابزار ویژه نیز برای تشخیص آن ارائه گردید. در پست های بعدی همراه ما باشید تا با هم نمونه سمت سرور این آسیب پذیری را هم بررسی کرده و چندین نمونه خطرناک آن را تحلیل کنیم.

منبع