Здравствуйте, меня зовут Дмитрий Карловский и я.. хочу сыграть с вами в игру. Правила её очень просты, но их нарушение.. приведёт вас к победе. Почувствуйте себя в роли хакера выбирающегося из JavaScript песочницы с целью прочитать куки, намайнить биткоины, сделать дефейс или ещё что-нибудь интересное.
А далее я расскажу как работает песочница и подкину несколько идей для взлома.
Как это работает
Итак, первым делом нам надо спрятать все глобальные переменные. Сделать это просто - достаточно замаскировать их одноимёнными локальными переменными:
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
Зачем нам вообще строгий режим? Да потому что без него можно использовать 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
Этого было бы достаточно для какого-то древнего яваскрипта, но сейчас у нас есть разные виды функций и каждый вариант следует обезопасить:
var Function = Function || ( function() {} ).constructor
var AsyncFunction = AsyncFunction || ( async function() {} ).constructor
var GeneratorFunction = GeneratorFunction || ( function*() {} ).constructor
var AsyncGeneratorFunction = AsyncGeneratorFunction || ( async 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
})
То есть мы разрешаем обращаться к свойствам, вызывать функции и конструировать объекты, но запрещаем все инвазивные операции. Соблазнительно возвращаемые значения тоже заворачивать в такие прокси, но тогда можно будет добраться по ссылкам до объекта у которого есть какой-либо мутирующий метод и воспользоваться им:
Поэтому все значения принудительно прогоняем через промежуточную сериализацию в 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, с перечислением того, что в песочницу нельзя передавать ни при каких обстоятельствах:
Наконец, есть такая противная вещь как import(), которая является не функцией, а конструкцией языка, поэтому просто удалить её не получится, а натворить дел она позволяет:
Но запрос до сервера всё-равно пройдёт. Поэтому лучше воспользоваться более надёжным решением - прибить event-loop путём удаления фрейма, после получения из него всех нужных для запуска скриптов объектов:
Соответственно, любые асинхронные операции будут выдавать ошибку, но синхронные будут продолжать работать.
В результате у нас получилась довольно безопасная песочница со следующими характеристиками:
Можно исполнять произвольный JS код.
Код исполняется синхронно и не требует делать все функции выше по стеку асинхронными.
Нельзя прочитать данные к которым не предоставлен доступ.
Нельзя изменить поведение использующего песочницу приложения.
Нельзя сломать работоспособность собственно песочницы.
Можно подвесить приложение бесконечным циклом.
Но что делать с бесконечными циклами? Они достаточно легко детектируются. Такой код можно не пропускать ещё на этапе его ввода злоумышленником. А даже если такой код и пролезет, то можно детектировать его постфактум и удалять автоматически или вручную.
https://page.hyoo.ru/#!=8a1uve_9b2hs5
Здравствуйте, меня зовут Дмитрий Карловский и я.. хочу сыграть с вами в игру. Правила её очень просты, но их нарушение.. приведёт вас к победе. Почувствуйте себя в роли хакера выбирающегося из JavaScript песочницы с целью прочитать куки, намайнить биткоины, сделать дефейс или ещё что-нибудь интересное.
А далее я расскажу как работает песочница и подкину несколько идей для взлома.
Как это работает
Итак, первым делом нам надо спрятать все глобальные переменные. Сделать это просто - достаточно замаскировать их одноимёнными локальными переменными:
Однако, многие свойства (например,
window.constructor
) являются неитерируеммыми. Поэтому необходимо итерироваться по всем пропертям объекта:Но
Object.getOwnPropertyNames
возвращает лишь собственные свойства объекта, игнорируя всё, что он наследует от прототипа. Поэтому надо аналогичным образом пройтись по всей цепочке прототипов и собрать имена всех возможных свойств глобального объекта:И всё бы хорошо, да только этот код падает, ибо в строгом режиме нельзя объявлять локальную переменную с именем
eval
:А вот использовать - пожалуйста:
Ну, ничего, благо глобальный
eval
можно просто удалить:А для надёжности лучше пройтись по всем собственным свойствам и всё поудалять:
Зачем нам вообще строгий режим? Да потому что без него можно использовать
arguments.callee.caller
чтобы получить любую функцию выше по стеку и натворить дел:Кроме того, в нестрогом режиме легко получить глобальный неймспейс просто взяв
this
при вызове функции не как метода:Ладно, все глобальные переменные мы замаскировали. Но их значения всё ещё можно получить из примитивов языка. Например:
Что делать? Удаляем небезопасные конструкторы:
Этого было бы достаточно для какого-то древнего яваскрипта, но сейчас у нас есть разные виды функций и каждый вариант следует обезопасить:
В одной песочнице могут запускаться разные скрипты и будет не хорошо, если они смогут влиять на работоспособность друг друга, поэтому замораживаем все объекты, доступные через примитивы языка:
Ок, тотальное огораживание мы реализовали, но цена этому - жёсткое надругательство над рантаймом, что может сломать и наше собственное приложение. То есть нам нужен отдельный рантайм для песочницы, где можно творить любые непотребства. Получить его можно двумя способами: через скрытый фрейм или через веб-воркер.
Особенности воркера:
Особенности фрейма:
Реализация RPC для воркера - дело не хитрое, но его ограничения не всегда приемлемы. Поэтому рассмотрим вариант с фреймом.
Если в песочницу передать какой-либо объект из которого по ссылкам доступен хоть какой-то изменяемый объект, то из песочницы можно будет его поменять и сломать наше приложение:
Но это ещё цветочки. Передача в во фрейм любой функции тут же откроет кулхацкеру настеж все двери:
Ну ничего, прокси спешит на помощь:
То есть мы разрешаем обращаться к свойствам, вызывать функции и конструировать объекты, но запрещаем все инвазивные операции. Соблазнительно возвращаемые значения тоже заворачивать в такие прокси, но тогда можно будет добраться по ссылкам до объекта у которого есть какой-либо мутирующий метод и воспользоваться им:
Поэтому все значения принудительно прогоняем через промежуточную сериализацию в JSON:
Таким образом из песочницы будут доступны только объекты и функции которые мы передали туда явно. Но порой нужно и неявно передавать некоторые объекты. Для них заведём whitelist в который будем автоматически добавлять все объекты, что заворачиваются в безопасный прокси, проходят обезвреживание или приходят из песочницы:
И на случай, если разработчик по невнимательности предоставит доступ к какой-либо функции позволяющей интерпретировать строку как код, заведём ещё blacklist, с перечислением того, что в песочницу нельзя передавать ни при каких обстоятельствах:
Наконец, есть такая противная вещь как
import()
, которая является не функцией, а конструкцией языка, поэтому просто удалить её не получится, а натворить дел она позволяет:Мы могли бы с помощью атрибута
sandbox
у фрейма запретить исполнять скрипты загруженные с левого домена:Но запрос до сервера всё-равно пройдёт. Поэтому лучше воспользоваться более надёжным решением - прибить event-loop путём удаления фрейма, после получения из него всех нужных для запуска скриптов объектов:
Соответственно, любые асинхронные операции будут выдавать ошибку, но синхронные будут продолжать работать.
В результате у нас получилась довольно безопасная песочница со следующими характеристиками:
Но что делать с бесконечными циклами? Они достаточно легко детектируются. Такой код можно не пропускать ещё на этапе его ввода злоумышленником. А даже если такой код и пролезет, то можно детектировать его постфактум и удалять автоматически или вручную.
Если есть идеи как это можно улучшить или хотите вступить в ТехноГильдию - пишите телеграммы.
Ссылочки