Перевод статьи «A Javascript journey with only six characters»
JavaScript — это одновременно странный и замечательный язык, который позволяет нам писать абсолютно бредовый код, являющийся валидным. Он пытается помогать нам, конвертируя величины в нужные типы в зависимости от того, как мы с ними работаем.
Если мы прибавляем строку к чему-то, результат будет конвертирован в строку.
Если мы припишем слева от чего-то плюс или минус, будет произведена попытка конвертации строки в число.
Если на что-то навесить логическое отрицание, результат будет представлен как логическая величина.
Мы можем использовать это и творить магию, используя лишь эти символы: [
, ]
, (
, )
, !
и +
. Если вы читаете статью не с мобильного устройства, то вы можете открыть консоль в браузере и выполнять весь код там.
Начнём с основ. То, что стоит запомнить:
- Префикс
!
конвертирует в Boolean; - Префикс
+
конвертирует в число; - Сложение с
[]
конвертирует в строку.
Вот пример:
![] === 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 в свои страницы, используя только эти символы, но такие атаки очень редки. Можно подумать про обфускацию, но, будем честны, есть методы получше.
Спасибо за внимание!