$mol_func_sandbox: взломай меня, если сможешь!.

от автора

Здравствуйте, меня зовут Дмитрий Карловский и я… хочу сыграть с вами в игру. Правила её очень просты, но их нарушение… приведёт вас к победе. Почувствуйте себя в роли хакера выбирающегося из JavaScript песочницы с целью прочитать куки, намайнить биткоины, сделать дефейс или ещё что-нибудь интересное.


https://sandbox.js.hyoo.ru/

А далее я расскажу как работает песочница и подкину несколько идей для взлома.

Как это работает

Итак, первым делом нам надо спрятать все глобальные переменные. Сделать это просто — достаточно замаскировать их одноимёнными локальными переменными:

for( let name in window ) {     context_default[ name ] = undefined }

Однако, многие свойства (например, window.constructor) являются неитерируеммыми. Поэтому необходимо итерироваться по всем пропертям объекта:

for( let name of Object.getOwnPropertyNames( window ) ) {     context_default[ name ] = undefined }

Но Object.getOwnPropertyNames возвращает лишь собственные свойства объекта, игнорируя всё, что он наследует от прототипа. Поэтому надо аналогичным образом пройтись по всей цепочке прототипов и собрать имена всех возможных свойств глобального объекта:

function clean( obj : object ) {      for( let name of Object.getOwnPropertyNames( obj ) ) {         context_default[ name ] = undefined     }      const proto = Object.getPrototypeOf( obj )     if( proto ) clean( proto )  } clean( win )

И всё бы хорошо, да только этот код падает, ибо в строгом режиме нельзя объявлять локальную переменную с именем eval:

'use strict' var eval // SyntaxError: Unexpected eval or arguments in strict mode

А вот использовать — пожалуйста:

'use strict' eval('document.cookie') // password=P@zzW0rd

Ну, ничего, благо глобальный eval можно просто удалить:

'use strict' delete window.eval eval('document.cookie') // ReferenceError: eval is not defined

А для надёжности лучше пройтись по всем собственным свойствам и всё поудалять:

for( const key of Object.getOwnPropertyNames( window ) ) delete window[ key ]

Зачем нам вообще строгий режим? Да потому что без него можно использовать arguments.callee.caller чтобы получить любую функцию выше по стеку и натворить дел:

function unsafe(){ console.log( arguments.callee.caller ) } function safe(){ unsafe() } safe() // ƒ safe(){ unsafe() }

Кроме того, в нестрогом режиме легко получить глобальный неймспейс просто взяв this при вызове функции не как метода:

function get_global() { return this } get_global() // window

Ладно, все глобальные переменные мы замаскировали. Но их значения всё ещё можно получить из примитивов языка. Например:

var Function = ( ()=>{} ).constructor var hack = new Function( 'return document.cookie' ) hack() // password=P@zzW0rd

Что делать? Удаляем небезопасные конструкторы:

Object.defineProperty( Function.prototype , 'constructor' , { value : undefined } )

Этого было бы достаточно для какого-то древнего яваскрипта, но сейчас у нас есть разные виды функций и каждый вариант следует обезопасить:

var Function = Function || ( function() {} ).constructor var AsyncFunction = AsyncFunction || ( async function() {} ).constructor var GeneratorFunction = GeneratorFunction || ( function*() {} ).constructor

В одной песочнице могут запускаться разные скрипты и будет не хорошо, если они смогут влиять на работоспособность друг друга, поэтому замораживаем все объекты, доступные через примитивы языка:

for( const Class of [     String , Number , BigInt , Boolean , Array , Object , Promise , Symbol , RegExp ,      Error , RangeError , ReferenceError , SyntaxError , TypeError ,     Function , AsyncFunction , GeneratorFunction , ] ) {     Object.freeze( Class )     Object.freeze( Class.prototype ) }

Ок, тотальное огораживание мы реализовали, но цена этому — жёсткое надругательство над рантаймом, что может сломать и наше собственное приложение. То есть нам нужен отдельный рантайм для песочницы, где можно творить любые непотребства. Получить его можно двумя способами: через скрытый фрейм или через веб-воркер.

Особенности воркера:

  • Полная изоляция памяти. Из воркера невозможно сломать рантайм основного приложения.
  • В воркер нельзя передать свои функции, что часто необходимо. Это ограничение можно частично обойти через реализацию RPC.
  • Воркер можно прибить по таймауту, если злодей напишет там бесконечный цикл.
  • Вся коммуникация строго асинхронная, что не очень быстро.

Особенности фрейма:

  • Можно передавать во фрейм любые объекты и функции, но по ссылкам можно случайно предоставить доступ к тому, к чему не стоило бы.
  • Бесконечный цикл в песочнице вешает всё приложение.
  • Вся коммуникация строго синхронная.

Реализация RPC для воркера — дело не хитрое, но его ограничения не всегда приемлемы. Поэтому рассмотрим вариант с фреймом.

Если в песочницу передать какой-либо объект из которого по ссылкам доступен хоть какой-то изменяемый объект, то из песочницы можно будет его поменять и сломать наше приложение:

numbers.toString = ()=> { throw 'lol' }

Но это ещё цветочки. Передача в во фрейм любой функции тут же откроет кулхацкеру настеж все двери:

var Function = random.constructor var hack = new Function( 'return document.cookie' ) hack() // password=P@zzW0rd

Ну ничего, прокси спешит на помощь:

const safe_derived = ( val : any ) : any => {      const proxy = new Proxy( val , {          get( val , field : any ) {             return safe_value( val[field] )         },          set() { return false },         defineProperty() { return false },         deleteProperty() { return false },         preventExtensions() { return false },          apply( val , host , args ) {             return safe_value( val.call( host , ... args ) )         },          construct( val , args ) {             return safe_value( new val( ... args ) )         },     }      return proxy })

То есть мы разрешаем обращаться к свойствам, вызывать функции и конструировать объекты, но запрещаем все инвазивные операции. Соблазнительно возвращаемые значения тоже заворачивать в такие прокси, но тогда можно будет добраться по ссылкам до объекта у которого есть какой-либо мутирующий метод и воспользоваться им:

config.__proto__.__defineGetter__( 'toString' , ()=> ()=> 'rofl' ) ({}).toString() // rofl

Поэтому все значения принудительно прогоняем через промежуточную сериализацию в JSON:

const SafeJSON = frame.contentWindow.JSON  const safe_value = ( val : any ) : any => {      const str = JSON.stringify( val )     if( !str ) return str      val = SafeJSON.parse( str )     return val  }

Таким образом из песочницы будут доступны только объекты и функции которые мы передали туда явно. Но порой нужно и неявно передавать некоторые объекты. Для них заведём whitelist в который будем автоматически добавлять все объекты, что заворачиваются в безопасный прокси, проходят обезвреживание или приходят из песочницы:

const whitelist = new WeakSet  const safe_derived = ( val : any ) : any => {     const proxy = ...     whitelist.add( proxy )     return proxy }  const safe_value = ( val : any ) : any => {      if( whitelist.has( val ) ) return val      const str = JSON.stringify( val )     if( !str ) return str      val = SafeJSON.parse( str )     whitelist.add( val )     return val }

И на случай, если разработчик по невнимательности предоставит доступ к какой-либо функции позволяющей интерпретировать строку как код, заведём ещё blacklist, с перечислением того, что в песочницу нельзя передавать ни при каких обстоятельствах:

const blacklist = new Set([     ( function() {} ).constructor ,     ( async function() {} ).constructor ,     ( function*() {} ).constructor ,     eval ,     setTimeout ,     setInterval , ])

В результате у нас получилась довольно безопасная песочница со следующими характеристиками:

  • Можно исполнять произвольный JS код.
  • Код исполняется синхронно и не требует делать все функции выше по стеку асинхронными.
  • Нельзя прочитать данные к которым не предоставлен доступ.
  • Нельзя изменить поведение использующего песочницу приложения.
  • Нельзя сломать работоспособность собственно песочницы.
  • Можно подвесить приложение бесконечным циклом.

Если есть идеи как это можно улучшить или хотите вступить в ТехноГильдиюпишите телеграммы.

Ссылочки

  • https://sandbox.js.hyoo.ru/ — онлайн песочница с примерами потенциально опасного кода.
  • https://calc.hyoo.ru/ — электронная таблица, позволяющая использовать в ячейках произвольный JS код.
  • https://t.me/mol_news — канал с новостями об экосистеме $mol и открытых проектах ТехноГильдии.

ссылка на оригинал статьи https://habr.com/ru/post/507586/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *