• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Находим уязвимость в js криптосейфе для localStorage браузера.

О чем написать еще?


  • Можно выбрать несколько вариантов.

byt3r0se

HDD-drive
Пользователь
Регистрация
19.04.2020
Сообщения
30
Реакции
15
Депозит
100
Всем привет, сам сейф и задача по нему, были в одном ctf.

Заранее кто не знаком с js, особо впечатлительные С-кодеры, не падайте, от того что происходит в коде, в С все по другому.))))(Если что С и Rust > other code lang.)

Мы имеем простой HTML, в нем реализован «JS Safe», который хранит данные в localStorage браузера.
Мы не извлечем от туда данные, так как они храняться локально.
Но этот сейф, открываеться только по пaролю владельца. Как мы видим это тут.

HTML:
<title>JS safe v2.0 - the leading localStorage based safe solution with high grade JS anti-debug technology</title>
<!--
Advertisement:
Looking for a hand-crafted, browser based virtual safe to store your most
interesting secrets? Look no further, you have found it. You can order your own
by sending a mail to js_safe@example.com. When ordering, please specify the
password you'd like to use to open and close the safe. We'll hand craft a
unique safe just for you, that only works with your password of choice.
-->

Перевод:
HTML:
<title>JS safe v2.0 - ведущее localStorage на основе безопасного решения, с высококлассной JS анти-отладочной технологией</title>
<!--
Реклама:
В поисках созданного вручную, виртуального сейфа на основе браузера для хранения ваших самых
интересных секретов? Не ищите дальше, вы нашли это. Вы можете заказать свой собственный,
отправив письмо по адресу js_safe@example.com. При заказе, пожалуйста, укажите
пароль, который вы хотите использовать, чтобы открыть и закрыть сейф. Мы сделаем вручную
уникальный сейф только для вас, который работает только с вашим выброным паролем.
-->

На странице мы видим, анимацию куба и input для пароля.
1.cleaned.png

Наша точка входа, onchange обработчик который при изминение <input>, выполняет func open_safe().

<input id="keyhole" autofocus onchange="open_safe()" placeholder="?">

Иследуем функцию open_safe().

JavaScript:
function open_safe() {
  keyhole.disabled = true;
  password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
  if (!password || !x(password[1])) return document.body.className = 'denied';
  document.body.className = 'granted';
  password = Array.from(password[1]).map(c => c.charCodeAt());
  encrypted = JSON.parse(localStorage.content || '');
  content.value = encrypted.map((c,i) => c ^ password[i % password.length]).map(String.fromCharCode).join('')
}

Строка блокирует инпут для дальнейшего ввода.
keyhole.disabled = true;
Сразу уберем ее, чтоб могли иследовать, без перезагрузки.

Видим эту строчку:
password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);

Переменной password присваеваится результат exec().
Метод exec() выполняет поиск сопоставления регулярного выражения в указанной строке.
Возвращает массив с результатами или null.

В нашем случае он вернул null.

Сейф разблокируется, если пароль соответствует регулярному выражению /^CTF{([0-9a-zA-Z_@!?-]+)}$/ и вызов функции x(password[1]) будет true.
if (!password || !x(password[1])) return document.body.className = 'denied';

Мы проваливаемся в if и возвращаемся из open_safe не доходя до вызова x(password[1]).
Остальная часть кода просто расшифровывает паролем, зашифрованные данные, которых у нас нет, поскольку они в localStorage владельца сейфа.

При вводе любой строки которая подходит под регулярку к примеру "CTF{byt3r0se}", будет вызов x(password[1])
password[1] это "byt3r0se" из строки "CTF{byt3r0se}"
Если отладка не будет открыта F12 то мы провалимся в другую петлю в функции с(), но об этом далее. Которая преведет к утечке памяти.

Идем дальше функция x(). Начинеться интересное.

JavaScript:
function x(х){
    ord = Function.prototype.call.bind(''.charCodeAt);// при вызове ord возвращает числовое значение Юникода.
    chr = String.fromCharCode; // при вызове chr вернет строку, созданную из последовательности значений единиц кода UTF-16.
    str = String;// конструктор строк
    function h(s) {
      //функция, которая преобразует произвольную непустую строку в строку длиной 4 символа
      //каждый из которых имеет код <256.
      for (i = 0; i != s.length; i++) {
          a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521;
          b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521;
      }
      return chr(b>>8) + chr(b&0xFF) + chr(a>>8) + chr(a&0xFF);
    }
    function c(a, b, c){
      // xor соединяет строки a и b вместе, циклически изменяя b по мере необходимости до длины a (третий аргумент
      //никогда не передается этой функции и представляет собой простой метод запутывания
      for (i = 0; i != a.length; i++)
          c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])));
      return c
    }
    for (a=0; a!=1000; a++) debugger; //петля которая будет вызывать дебагер при открытой отладке
    x = h(str(x));
    source = /Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤¢dÈ9&òªћ#³1᧨/;
    source.toString = function() {
      return c(source,x);
    };
    try {
      console.log('debug', source);
      with (source)
        return eval('eval(c(source,x))');
    } catch (e) {
    }
    // неявно return undefined (который falsе)
}

В функц х() нас при открытой отладочной панели, будет ждать петля для раздражения, так как она будет запускать дебаг, и чтоб пройти цикл надо проходить его в ручную но мы уберем его вдальнейшем.
for (a=0; a!=1000; a++) debugger; //запустит дебагер браузера для раздражения в петле
Присваиваем пременной a=1000 в консоле или scope и минуем петлю, главное присвоить когда уже сделали шаг a++,
так как если перепишим переменной a, значение которой 0 до того как сделаем инкримент по завершению прохода,
а будет 1001 а это не соответсвует условию выхода из цикла a!=1000 так и будем в лупе.


3.cleaned.png

Далее вызов h() которая с помошь побитовых операций возвращает в переменую x строку длиной 4 символа каждый из которых имеет код символа меньше 256.
Получаеться х это аргумент функции х(), вызывая h() мы х приводим к строке это бесмысленно, входной аргумент x фунции х() строку приводить к строке,
В h() мы видим что аргумент s который мы передали х при вызове вовсе не наша строка, а текстовое предстовление кода нашей функции. Как так?
4.cleaned.png

Дело в том что аргумнт х в функции х() это переменая из алфавита кирилицы х, а остальные х в коде латиница которая и есть код функции х() которую приводит к строк перед вызовом h()
5.cleaned.png

Результат функции h() зависит от тела функции х(), таким образом если мы изменим код функции х() выходной ключ будет другим.
Мы его просто просто статически в пишем в переменую х в дальнейшем без вызова h() так как мы его уже знаем, тем самым сможем спокойно менять тело функции х().
6.cleaned.png

source красивая регулярка,
source = /Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤¢dÈ9&òªћ#³1᧨/;
Переопределили метод объекта source, который возвращает строковое представление объекта.
source.toString = function() { return c(source,x);};

Зашли в try и при вызове console.log('debug', source); мы передаем source который console.log приводит к строке используя нами переопределенный метод toString который возвращает вызов c(source,x).

В функции с() опять петля так как i != a.length будет true постоянно, a.length == undefined соответственно не будет
равно i которое number. это потому-что c(source,x) мы передали source объкт а его длинна undefined.
Удалим эту анти отладочную карусель.

Далее with - ищет unqualified имя у нас это source, исследуя цепочку областей видимости, связанную
с выполнением скрипта или функции, сожержащих это имя. Оператор 'with' добавляет данный объект в начало цепочки областей видимости в ходе исследования тела его оператора. Если имя используемое в теле соответствует свойству в цепочке областей видимости, тогда имя привязывается к свойству и объекту, содержащему это свойство.

В нашем случае source станет свойство source однойменной переменной source.
Так source.source возврашает нам строку регулярки без слешей.
"Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤¢dÈ9&òªћ#³1᧨"
Теперь получатся source = source.source

Далее мы попадаем в eval вызывает код из строки, где будет еще один eval который вызовет c(), она соединяет строки a и b вместе, циклически изменяя b по мере необходимости до длины a, она возврашает строку.
Строку результат с() выполнит второй eval.
Вот что за строку нам вернула с(). "х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х))//᧢" теперь ее выполняет второй eval

7.cleaned.png

Но как мы узнаем ключ?
Взглянем на строку которую возвращает c(source,x)
"х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х))//᧢"
Взглянем на переменные x в начале и конце строки их код 1093, кририлица значит тут проверяеться наша переменная обозначенная русской буквой х, которая аргумет функции х(), значение которой "byt3r0se".
И опять вызов с() с новым аргументом и с новым ключом так как в h() передастся аргумент с нашим паролем, а не как в первом случае с телом функции.

И при сравнение с нашим паролем х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х)) будет false.

Перепиши фунцию х()

JavaScript:
function x(х){
    ord = Function.prototype.call.bind(''.charCodeAt);// при вызове ord возвращает числовое значение Юникода.
    chr = String.fromCharCode; // при вызове chr вернет строку, созданную из последовательности значений единиц кода UTF-16.
    str = String;// конструктор строк
    function h(s) {
      for (i = 0; i != s.length; i++) {
        a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521;
        b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521;
      }
      return chr(b>>8) + chr(b&0xFF) + chr(a>>8) + chr(a&0xFF);
    }
    function c(a, b, c){
        for (i = 0; i != a.length; i++)
          c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])));
        console.log(c);
        return c
    }
    a=2714;
    x ='↵↵↵↵';//тут не коректно отображаеться в пред скрине видно значение х
    source = "Ӈ#7ùª9¨M¤À.áÔ¥6¦¨¹.ÿÓÂ.Ö£JºÓ¹WþÊmãÖÚG¤¢dÈ9&òªћ#³1᧨";
    return eval('eval(c(source,x))');
}

Изменения: переменая а та самая
for (a = 0; a != 1000; a++)
debugger ;
Она нам нужна для работы в h() так как мы убрали первый вызов h() где используеться a=1000, после выхода из h() она будет a=2714
Переменая x та самая
x = h(str(x));
h() возвращает ключ из 4 символов, при дифолтном теле функции ключ всегда один и тот же.

Теперь нам надо найти значение при котором х == c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ', h(х)) будет тру

Взглянем на h() она возвращает ключ в диапозоне 0-0xff длиной 4 байта.

В c(), результат функции зависит от второго аргумента b(который есть h(х)), так как первый нам уже известен. И так, нам нужно найти четыре числа a,b,c,d в диапазоне 0-0xff для нашего ключа b[a,b,c,d].

Мы знаем пароль состоит из буквенно-цифровых символов и знаков препинания из регулярярки /^CTF{( [0-9a-zA-Z_@!?-]+ )}$/

c() xor это значение Unicode для символов a с теми, что в b, но мы знаем, какой индекс b будет использоваться для поиска c получаеться для b[0] будут c[0] c[4] c[8] c[12] и тд
Для с = a^b[i%4] получаеться шаг 4 для каждого индекса b.

Получаеться мы можем сбрутить для каждого индекса b значение c с шагом для с[i+4] которое подойдет под regex.

Теперь напиши скрипт который сбрутит нам возможные пароли:

JavaScript:
ord = Function.prototype.call.bind(''.charCodeAt);
chr = String.fromCharCode;
str = String;
regBrut = /^[0-9a-zA-Z_@!?-]+$/;
arrBrut = [];
function test(a, b, index) {
    c = '';
    for (i = index; i < a.length; i += 4)
    c = c + chr(ord(str(a[i])) ^ b[i % b.length]);
    return c;
}
for (let iG= 0; iG < 4; iG++) {
    for (let i = 0; i <= 0xff; i++) {
    let array = [0, 0, 0, 0];
    array[iG] = i;

    let out = test('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ', array, iG);
    let valid = regBrut.exec(out);
    if (valid) {
        arrBrut.push([iG, out]);
    }
    }
}

function cat(x) {
    result = '';
    for (let i = 0; i < x[0].length; i++) {
        x.forEach(el => {
            if (i<el.length) {
                result+=el[i];
            }
        });
    }
    return result;
}

let aL= arrBrut.length;
let loc=[];
let double = 0;

for (let i = 0,k=0; i < aL; i++,k++) {
    if (double!=0 && i==double-1) {
        i=double;
        double=0;
    }

    if (i<aL-2 && arrBrut[i][0]==arrBrut[i+1][0]) {
        double = i+1;
    }
    loc[k]=arrBrut[i][1];

    if (k==3) {
        console.log(`CTF{${cat(loc)}}`);
        if (double!=0) {
            i=0;
            k=0;
        }
    }
}

Получилось 2 результата:

CTF{_BN37!-vR951N!-h5!-ATEI-NXTiabnt-HD3Ukg_}
CTF{_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_}

8.cleaned.png

Думаю понятно какой из них тру пароль.

Теперь мы открыли криптосейф.

9.cleaned.png

На этом все.

Кто хочет оказать поддержку.

bircoin
grin
monero
other
Писать в лс, для получения реквизитов.
 
Последнее редактирование:

И в чем прикол? Статьи которые мы заслужили... панимаю...
 
Ничего не понял, но очень интересно.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх