[Перевод] Javascript-путешествие с шестью символами

fd0fbae92e7c4775b6bf06d758ee24ae.jpg


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


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


Если мы добавляем префикс «плюс» или «минус», то он допустит, что нам нужно числовое представление и сконвертирует строку в число, если сможет.


Если мы отрицаем что-то, то он сконвертирует это в булево значение.


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


Давайте начнем с простого. Вот главные правила:


  1. Префикс ! конвертирует в Boolean
  2. Префикс + конвертирует в Number
  3. Добавление [] конвертирует String

Вот они в действии:


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

Еще один важный момент, о котором стоить помнить — с помощью квадратных скобок можно получать конкретный символ (букву) из строки вот так:


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

Также помните, что можно брать несколько цифр и складывать их в строковом представлении, а потом конвертировать это обратно в число.


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

Хорошо, давайте попробуем скомбинировать эти трюки и получить букву a.


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

Прикольно!


Этой довольно простой комбинацией можно получить все буквы из слов true и falsea, e, f, l, r, s, t, u. Окей, можно ли получить буквы еще откуда-нибудь?


Ну, есть undefined, который можно получить с помощью странной ерунды вроде [][[]]. Сконвертируем его в строку используя одно из наших главных правил и получим дополнительные буквы d, i and n.


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

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


Важная особенность Javascript: свойства объекта доступны через точку (dot notation) или квадратные скобки (square bracket notation). Так как методы массива — это свойства самого объекта Array, можно вызвать эти методы с помощью квадратных скобок вместо точки.


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


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


[]["fill"]

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


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

Вот, теперь у нас есть дополнительные символы:  c, o, v, (, ), {, [, ], }, .


С помощью букв c и o мы теперь можем написать constructorconstructor — это метод, доступный во всех объектах Javascript, он просто возвращает функцию-конструктор.


Давайте получим строковое представление функции-конструктора для всех доступных нам сейчас объектов:


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" // основание 10
(12)["toString"](2) === "1100" // основание 2, бинарная система
(12)["toString"](8) === "14" // основание 8, восьмеричная система
(12)["toString"](16) === "c" // шестнадцатеричная система 12

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


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

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


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


Например,  bold — это метод строки, который добавляет теги для полужирности:


"test"["bold"]() === "test"

Отсюда можно достать <,  > и /.


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


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


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

Можно использовать Unicode lookup и с легкостью найти код для любого символа Юникода.


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


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


Ответ это function Function() { [native code] }, то есть сам объект Function.


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

Используя это можно передать строку кода и создать настоящую функцию.


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

Получаем:


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

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


Уфф! Все!


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


Хотите доказательств? Запустите этот код в консоли.


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


В чем польза?


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


Извините.


Но, надеюсь, вам понравилось это путешествие.


Источники:


  • https://en.wikipedia.org/wiki/JSFuck
  • https://esolangs.org/wiki/JSFuck
  • https://github.com/aemkei/jsfuck
  • http://patriciopalladino.com/blog/2012/08/09/non-alphanumeric-javascript.html

Комментарии (1)

  • 10 октября 2016 в 10:16 (комментарий был изменён)

    0

    Сразу же вспомнил про эту задачу https://ipsc.ksp.sk/2015/real/problems/m.html.
    Правда, оказалось, там были чуть другие 6 символов.

© Habrahabr.ru