Написать пост

Магия JavaScript: что можно сделать, используя лишь 6 символов?

Аватар Иван Бирюков

Обложка поста Магия JavaScript: что можно сделать, используя лишь 6 символов?

JavaScript — это одновременно странный и замечательный язык, который позволяет нам писать абсолютно бредовый код, являющийся валидным. Он пытается помогать нам, конвертируя величины в нужные типы в зависимости от того, как мы с ними работаем. 

Если мы прибавляем строку к чему-то, результат будет конвертирован в строку.

Если мы припишем слева от чего-то плюс или минус, будет произведена попытка конвертации строки в число.

Если на что-то навесить логическое отрицание, результат будет представлен как логическая величина.

Мы можем использовать это и творить магию, используя лишь эти символы: []()! и +. Если вы читаете статью не с мобильного устройства, то вы можете открыть консоль в браузере и выполнять весь код там.

Начнём с основ. То, что стоит запомнить:

  1. Префикс ! конвертирует в Boolean;
  2. Префикс + конвертирует в число;
  3. Сложение с [] конвертирует в строку.

Вот пример:

			![] === false
+[] === 0
[]+[] === ""
		

Кроме того, нужно знать, что символы из строки можно получать следующим образом:

			"hello"[0] === "h"
		

Также запомните, что числа можно получать сложением других чисел в строковом виде и конвертацией результата в числовой формат:

			+("1" + "1") === 11
		

Так, а теперь попробуем использовать рассмотренные свойства и получить букву a.

			![] === false
![]+[] === "false"
+!![] === 1
------------------------
(![]+[])[+!![]] === "a"  // same as "false"[1]
		

Забавно!

Таким образом, путём нехитрых махинаций мы можем получить все буквы из слов true и false. a, e, f, l, r, s, t, u. Хорошо, а можем ли мы взять буквы ещё где-то?

Ну, ещё есть undefined, который и можно получить, сделав что-то глупое, как [][[]]. Конвертация в строку даст нам буквы d, i и n.

			[][[]] + [] === "undefined"
		

Используя все эти буквы, можно составить слова fill, filter и find. Конечно, есть и другие доступные слова, но ценностью именно этих слов является то, что они — методы массивов. Это означает, что они являются частью объекта Array и их можно вызывать прямо для массивов-сущностей. Например, [2,1].sort().

Кроме того, нужно помнить, что свойства объекта в JS можно использовать как через точку, так и через квадратные скобки. Поскольку методы массива — это свойства объекта Array, мы можем вызывать их, используя скобки вместо точки.

То есть [2,1]["sort"]() — это то же самое, что и [2,1].sort().

Давайте посмотрим, что произойдёт, когда мы попробуем использовать один из доступных нам методов, не вызывая его.

			[]["fill"]
		

Это выдаст function fill() { [native code] }. Конвертируем заголовок метода в строку:

			[]["fill"]+[] === "function fill() { [native code] }"
		

И получим новые символы: c, o, v, (, ), {, [, ], },  .

Получив буквы c и o, можем составить слово constructor. constructor — это метод, который есть у всех JS-объектов, и он возвращает их функцию-конструктор.

Получим строковое представление всех конструкторов наших объектов:

			true["constructor"] + [] === "function Boolean() { [native code] }"  
0["constructor"] + []    === "function Number() { [native code] }"  
""["constructor"] + []   === "function String() { [native code] }"
[]["constructor"] + []   === "function Array() { [native code] }"
		

Из этих строк мы можем пополнить наш арсенал следующими символами: B, N, S, A, m, g, y.

Теперь мы можем составить "toString", функцию, которую можно использовать с квадратными скобками. Только на этот раз мы вызовем её.

			(10)["toString"]() === "10"
		

Но мы же уже можем конвертировать в строку всё что угодно, так как нам это поможет?

Что, если я скажу вам, что метод toString типа Number обладает секретным аргументом radix, который меняет основание возвращаемого числа перед конвертацией в строку. Смотрите:

			(12)["toString"](10) === "12" // base 10 - normal to us
(12)["toString"](2) === "1100" // base 2, or binary, for 12
(12)["toString"](8) === "14" // base 8 (octonary) for 12
(12)["toString"](16) === "c" // hex for 12
		

Но зачем останавливаться на 16? Максимум — это 36, чего хватает на все символы 0–9 и a–z. Теперь мы можем получить любую цифру или букву:

			(10)["toString"](36) === "a"
(35)["toString"](36) === "z"
		

Отлично! Но как же пунктуационные символы и заглавные буквы? Идём дальше!

В зависимости от того, где вы исполняете свой JS-код, он может получать, а может и не получать доступ к заранее определённым объектам и данным. Есть вероятность, что при запуске в браузере вы сможете получить доступ к обёрточным методам HTML.

Например, bold — это метод String, оборачивающий строку в тег <b>.

			"test"["bold"]() === "<b>test</b>"
		

Это даст нам символы <, > и /.

Возможно, вы слышали о функции escape. По сути, она конвертирует строку в URI-формат, который могут распознать браузеры. Если мы передадим ей пробел, то получим %20. Передадим < — получим %3C. Эта заглавная C очень важна для получения остальных символов.

Благодаря ей мы можем использовать функцию fromCharCode, возвращающую символ Юникода по данному десятичному представлению. Она является частью объекта String, который можно получить, вызвав конструктор любой строки.w

			""["constructor"]["fromCharCode"](65) === "A"
""["constructor"]["fromCharCode"](46) === "."
		

Для получения десятичных представлений символов можно использовать сайт Unicode lookup.

Хорошо, теперь мы можем написать что угодно в виде строки и выполнить любую функцию, которая принадлежит типам Array, String, Number, Boolean или Object, через их конструкторы. Немало для 6 символов. Но это не конец.

Чем является конструктор любой функции?

Ответ: function Function() { [native code] }, настоящий объект Function.

			[]["fill"]["constructor"] === Function
		

Используя его, мы можем передать строку кода, чтобы создать реальную функцию.

			Function("alert('test')");
		

Получим:

			Function anonymous() {  
    alert('test')
}
		

Что можно вызвать сразу же, просто добавив в конце (). Да, теперь мы можем исполнять строки кода!

ФУХ. Вот теперь всё!

Мы можем получить доступ к любому символу, объединять их в валидный код и исполнять его. Это значит, что JS является полным по Тьюрингу языком из 6 символов [, ], (, ), + и !.

Не верите? Запустите это в консоли:

Если вы читаете статью с мобильного устройства, код выше — это исполняемая функция alert("wtf").

Вот инструмент, который может автоматизировать конверсию, а вот так он переводит каждый символ.

Ну и как это может пригодиться?

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

Спасибо за внимание!

Следите за новыми постами
Следите за новыми постами по любимым темам
41К открытий41К показов