D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Open Closed Redirect
This article is about the vulnerability, which blackhat sells, and whitehat deletes. Why? Because it is useless to us. As you can see the vulnerability is open redirect, but in most cases it is not accepted by security platforms, because it requires user interaction, or as we say “phishing”. So I am drinking my coffee as always and someone messaged me on forum that they want to buy an open redirect for X company and the company X is big enough for vulnerabilities to be there, obviously I had to reject the offer, because it is not ethical. I try not to cross the line, I love researching and that’s kinda enough for now. Anyways, that guy gave me an idea to search for open redirects in organization Y. This is another article explaining the automation, easy and medium (another easy way) ways to do it.Open Redirect
Open Redirect is a vulnerability that occurs when a web application is manipulated to redirect a user to a different destination, typically an untrusted and potentially malicious external site. Open Redirect Vulnerabilities exploit the trust a user has in a given website, to transport the user to harmful locations. By trust I mean the domain, the user sees that there is a trusted link, “xss.is/?redirectUrl=xss[.]bz” - in this example case which is xss.is and when the user clicks on it, it redirects to an untrusted website, xss[.]bz. This type of vulnerability is a result of improper validation / lack of filtering in handling user-supplied URLs. When a web application accepts unvalidated input that defines the destination of a redirection hackers can substitute their own malicious URL.Situation in our world
Open Redirect Vulnerabilities are often overlooked even though they pose a significant threat due to their indirect nature and the ease with which attackers can exploit them. Sadly vulnerability disclosure programs pay attention to direct attack methods (SQL/XSS and etc) rather than to vulnerabilities like Content Spoofing/Open-Redirection and etc. Obviously as a result they get hacked by simple phishing attacks.How does open redirect work?
Open Redirect occurs when an application takes a user-supplied URL and uses it to redirect the user to a new page without proper validation. So it can be a parameter which is sent in POST request, or a parameter in URL in the GET request. If you find an open redirect in a POST request, it won’t be even looked at by the companies, obviously depending on the case, but mostly will be ignored. But in GET request, it will be at least looked at. The process is usually like this:User Input: A user clicks on a link that is supposed to redirect them within the same service (website). This link, however, has a parameter that is user-controllable. Let’s say redirectURL parameter.
System Check: The web application fails to sanitize this input. As a result, it does not distinguish between internal and external destinations. So the value of the parameter becomes a malicious website.
Exploiting: The application executes the redirection based on the user-supplied input and the user gets redirected to a malicious website.
There are no specific kinds of open redirects, but if I had to divide them, there would be 3 kinds based on my experience.
- Unauthenticated Redirects: These are the redirects which don’t require any authentication. So the vulnerability is actually unauthenticated.
- Authenticated Redirects: As you can understand, these are the redirects that require authentication OR happen in the authentication process. Great example for this can be OAuth service which allows redirection and it can be used by hackers to steal tokens.
- Third-Party services: Your website may be integrated with 3rd party services which have this vulnerability or “functionality”. A good example would be URL shortener plugins, you add an url shortener plugin for your forum and attackers use it to phish your users.
CVE-2023-5375
I will try to explain it as easy as possible, let’s before checking the code, check the exploit itselfКод: Скопировать в буфер обмена
http://127.0.0.1:8080/project/switch/1?targetPath=https://google.com
We can take note to ourselves when things get complicated, but this cases is simple as is most open redirect cases. Vulnerable parameter is “targetPath”
Vulnerable code:
Код: Скопировать в буфер обмена
Код:
// 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);
}
Fixed version of the code:
Код: Скопировать в буфер обмена
Код:
// 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;
}
}
New isLocalUrl method is designed to check whether a given URL is local to the application. This method performs two main checks, It first checks if the URL starts with a forward slash (/), indicating that it's a relative path and thus local. Second check is that it then checks if the URL is absolute, but starts with the same scheme and host as the application, ensuring that the redirect stays within the same domain. And obviously the redirect logic is changed, the targetPath is now validated using the isLocalUrl method. This ensures that the application only redirects to URLs that are either relative paths or absolute URLs belonging to the same domain.
Something from myself:
I am not a PHP developer, I analyzed with google + AI, as I understand they used $request->getSchemeAndHttpHost() method to get the scheme and host of the application. As I understand method identifies the host (domain) from the “Host” header, so if you let’s say send the request to “xss.is” with host header of “xss.bz”, it technically should result in error, but if it doesn’t, then you can abuse the method. AGAIN, I am not a developer and I don’t have much of a coding experience, so I may be wrong, but I think it is a possible way to abuse the method. Also another way to abuse this method is through payloads like this ‘/\/google.com/’, it will pass the first check for sure, but I have no idea how it will be processed further.
As I understand this case is the second kind of open redirect, Authenticated Redirect, which is exploitable AFTER the authentication.
CVE-2023-35948
There is an open redirect vulnerability in Novu, when users try to sign in with github.Let’s check the Github Login first:
Код: Скопировать в буфер обмена
Код:
@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);
}
Код: Скопировать в буфер обмена
Код:
if (redirectUrl && redirectUrl.startsWith('http://localhost:')) {
url = redirectUrl;
}
The payload used for the attack here is:
https://<novu_api>/v1/auth/github/callback?code=&state={"source":"web","redirectUrl":"http://localhost
So when attacker used @, instead of redirecting to the original server, it redirected to attacker’s server:
http://localhost
I may be wrong, but as I understand, the attacker's server in this case is the main domain and localhost
https://localhost:pass@evil.com/
When you click this link, you will be redirected to the evil.com. Why? Because the browser interprets the part after @ (evil.com) as the actual destination and redirects you there. This is a security measure to prevent phishing where a user might be tricked into thinking they are accessing a safe localhost link, while they are actually being redirected to a harmful external site. So the security measure done by the browser here becomes a threat itself, which is sooo ironic xD
Fixed code:
Код: Скопировать в буфер обмена
if (redirectUrl && redirectUrl.startsWith('http://localhost:') && !redirectUrl.includes('@')) {
Now in this code, the url must start with localhost, can’t be null and shouldn’t include ‘@’.
This is also the second kind mixed with a bit of a third kind of open redirects, in which the vulnerability is WHILE authenticating.
CVE-2023-0748
In this case the vulnerable code is everywhere, so let’s start with one of the exploits:https://mainnet.demo.btcpayserver.o...alse&requireConfirm=true&returnUrl=//evil.com
The vulnerable part is the “returnUrl” parameter, now let’s check how it is in code:
Код: Скопировать в буфер обмена
<a href="@Model.ReturnUrl" class="btn btn-primary btn-lg mt-3 px-5 order-3" id="proceed" rel="noreferrer noopener">Done</a>
I have absolutely no experience with C#, let the noob-ing begin. We can see that problem probably is in @Model.ReturnUrl, "Model" is the object that is being passed from the controller to the view. It contains the information that needs to be processed, which in our case is the URL. ReturnUrl is a variable within the "Model" object. It holds a value that represents a return URL, which redirects users back to a specific page. The problem here is that there is no sanitization at all.
Fixed code is this:
Код: Скопировать в буфер обмена
<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 is a method provided by the Url object that ensures that a URL is a local URL. Model.ReturnUrl, refers to a variable named ReturnUrl within the Model object, as I said before. Now the question here is how the EnsureLocal method works. Here is the main code which was created to remediate this vulnerability:
Код: Скопировать в буфер обмена
Код:
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;
}
Basically then this method is used everywhere to prevent open redirect vulnerabilities.
Organization Y
From what we see, open redirect vulnerabilities usually can be identified through parameters like redirectUrl and etc. So let’s think about the basic automation steps:1. Website must be scraped and values of parameters must be removed
2. Removed values must be replaced with url to which we want redirect to happen
3. Tool must check, whether redirect did happen or not, it can be done through header called “Location”
Easy way
Easiest way to this is to use nuclei, it identifies open redirects with ease and there are a lot of plugins for CVEs that have open redirects within them, so using it is actually the easiest way to identify these vulnerabilities. Let’s check out some 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"
Код: Скопировать в буфер обмена
Код:
- 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)"
Another easy way
Nuclei scans for ready plugins, which is bad if you are planning to use nuclei on some product where you aim to find 0day. I prefer you to use scraper, which will get all parameters needed and then use those parameters to check for open redirect vulnerabilities. You can use burp pro’s crawler or any other open source tool like bablo which is in contest. Usage of these tools are simple and have the same purpose, to crawl. After that you can write a tool which will replace parameters with some url and check whether the redirect goes to that url through the header.Код: Скопировать в буфер обмена
Код:
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)
}
}
Image [1]