SSuRFing da web web easy

Category

Web

Chall Author

wuurrd

Stats

Total Solves: 86 Players

Final Points: 100p

Description

The flag is luckily only available from inside the server, so it's totally safe.

Solution

I was provided the files: main.py, flag.txt and a Dockerfile. Let's look at the source:


import threading
from fastapi.responses import RedirectResponse
import uvicorn
import socket
import ipaddress

import aiohttp
from fastapi import FastAPI, HTTPException
from urllib.parse import urlparse

app = FastAPI()
local_app = FastAPI()


class SSRFError(Exception):
    pass


@local_app.get("/flag")
def get_flag() -> str:
    return open("flag.txt").read()


async def assert_url_safe(url: str):
    hostname = urlparse(url).hostname
    if not hostname:
        return
    ip = socket.gethostbyname(hostname)
    try:
        i = ipaddress.ip_address(ip)
        if i.is_private:
            raise SSRFError(f"Host {hostname} is not safe")
    except ValueError:
        raise SSRFError(f"Unknown Host {hostname}")


@app.get("/")
def index():
    return RedirectResponse("/docs")


@app.post("/scrape", response_model=str)
async def scrape(url: str) -> str:
    try:
        await assert_url_safe(url)
        timeout = aiohttp.ClientTimeout(
            total=5, connect=5, sock_read=5, sock_connect=5, ceil_threshold=5
        )
        async with aiohttp.ClientSession() as session:
            async with session.get(url, timeout=timeout) as response:
                try:
                    data = await response.text()
                except:
                    data = ""
                if not response.ok:
                    raise HTTPException(
                        status_code=response.status,
                        detail=f"STATUS={response.status} {data}",
                    )
                return data
    except SSRFError as e:
        raise HTTPException(status_code=403, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


def main():
    threading.Thread(
        target=uvicorn.run,
        kwargs={
            "app": local_app,
            "port": 1337,
        },
    ).start()
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=4000,
    )


if __name__ == "__main__":
    main()
                    

Its a simple backend server with two main functions. assert_url_safe & scrape. The assert_url_safe function basically just checks the url we provide to the /scrape route. It does this using i.is_private which is defined in the python3.12/ipadress native library.


    def is_private(self):
        """``True`` if the address is defined as not globally reachable by
        iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
        (for IPv6) with the following exceptions:

        * ``is_private`` is ``False`` for ``100.64.0.0/10``
        * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
            semantics of the underlying IPv4 addresses and the following condition holds
            (see :attr:`IPv6Address.ipv4_mapped`)::

                address.is_private == address.ipv4_mapped.is_private

        ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
        IPv4 range where they are both ``False``.
        """
        return (
            any(self in net for net in self._constants._private_networks)
            and all(self not in net for net in self._constants._private_networks_exceptions)
        )
                    

Now that we understand the source we can think of an exploitation plan!

We do know that flag.txt is in the server's localhost:3000/flag.txt. So our goal is to trick the server into scraping the flag from itself locally. Problem is that because of the is.private check, we cannot make it go to a local IP.

My first idea was to rewrite the URL in such a way that it bypasses the checks, like for example rewriting it into IPv6, but reading the native library's source, this is out of the box.

But what we can do is make it go to, for example, https://google.com/. We control the page's functions and thus we control what the aiohttp client does.

This means that we can do a simple redirect. Basically, for example, making the server go to https://alanoo.dev/code.php. The aiohttp client will thus execute the PHP code, which we will write to redirect it to:

localhost:3000/flag.txt

Now I only needed to write some simple php code and get the flag!

Final Payload


<?php
header("Location: http://localhost:3000/flag.txt");
die();

# https://skibidi.wtf/code.php
    

Flag

flag{wait_redirects_are_a_thing_too???}

alanoo.dev

My dev page :).


By alanoo, 2025-04-14