邮轮上免费上网,节省170美元。
Getting free internet on a cruise, saving $170

原始链接: https://angad.me/blog/2025/getting-free-cruise-internet/

这个十几岁的少年发现了一个公主邮轮MedallionNet网络系统中的漏洞,可以绕过预设的15分钟下载时间限制,免费使用互联网。这个漏洞利用了系统依赖MAC地址的会话机制。他使用运行OpenWRT的旅行路由器和一个Python脚本自动化了这个过程。脚本会更改路由器的MAC地址,登录MedallionNet门户网站,并反复请求免费的15分钟互联网会话。 脚本使用Python的`requests`库来自动化MedallionNet门户网站中通常需要手动输入的API调用。通过连接移动电源,路由器可以保持连接,在船上提供持续的互联网访问。脚本会持续通过ping `example.com`来检查网络连接,并在需要时更新会话,确保不间断的服务,即使偶尔出现短暂的断网也能继续工作。虽然维护得不好,但运行效果良好。

这个Hacker News帖子讨论了一篇博客文章,文章讲述了如何在公主邮轮上免费使用互联网,节省了170美元。原发帖人(OP),一名高中生,详细描述了他们是如何绕过邮轮的互联网限制的。 评论者分享了他们在酒店、机场和移动网络上类似的免费互联网利用经验,通常涉及MAC地址更改或利用开放端口,例如53端口。一些人回忆起过去使用IP over DNS隧道技术。 讨论还涉及邮轮互联网的经济学,包括关于Starlink能否降低成本以及邮轮公司为何收费如此之高的推测。一些人认为Starlink的定价是基于市场需求,而不仅仅是带宽成本。另一些人指出,邮轮上的大量使用可能会使Starlink过载。 伦理问题也随之出现,一些人称这种黑客行为为“窃取服务”,而另一些人则认为这针对贪婪公司的无受害者犯罪。一些用户警告OP,公主邮轮可能会向他们收费或禁止他们上船。
相关文章

原文

Picture this, you’re a teenager in the middle of the ocean on a cruise ship. All is good, except you’re lacking your lifeblood: internet. You could pay $170 for seven days of throttled internet on a single device, and perhaps split it via a travel router or hotspot, but that still seems less than ideal.

I’ve been travelling Europe with family and am currently on Princess Cruises’ Sun Princess cruise ship. For the past two days, the ship has mostly been in port, so I was able to use a cellular connection. This quickly became not feasible as we got further from land, where there was no coverage. At around the same time, I wanted to download the Princess Cruises app on my phone, and realized that it would give me a one-time 15-minute internet connection. I activated it, and quickly realized that it didn’t limit you to just the Play Store or App Store: all websites could be accessed. I soon realized that this “one-time” download was MAC-address dependent and thus, could be bypassed by switching MAC addresses. However, doing so logs you out of the MedallionNet portal, which is required to get the 15-minutes of free internet.

This means that in order to get just 15 minutes of internet, the following is required:

  1. Change MAC address
  2. Login to MedallionNet with date-of-birth and room number, binding the MAC address to your identity
  3. Send a request with your booking ID activating 15 minutes of free internet, intended to be used for downloading the Princess app
    • Not completely sure, but it did initially seem that to activate unrestricted access to the internet, you would need to send a simple HTTP to play.google.com, although I don’t think this is needed.

This process, on the surface, seems extremely arduous. But if this could be automated, it would suddenly become a much more viable proposition. After looking at the fetch requests the MedallionNet portal was sending to login and activate the free sessions, I realized that it shouldn’t be too hard to automate.

Conveniently, my family also brought a travel router running OpenWRT as we were planning on purchasing the throttled single-device plan (which is ~$170 for the entire cruise) and using the router as a hotspot so we could connect multiple devices to the internet. This router (a GL.iNet) allows you to change the MAC address via the admin portal, needing only a single POST request. This meant that if I could string together the API requests to change the MAC address (after getting a token from the router login endpoint), login to MedallionNet, and request the free internet session, I would have free internet.

I first tried copying the requests as cURL commands via DevTools into Notepad and using a local LLM to vibe-code a simple bash file. Realizing this wasn’t going to work, I began work on a Python script instead. I converted the cURL commands to requests via Copilot (yes, I used LLMs to an extent when building this) and started chaining together the requests.

The only issues I faced that took some time to overcome were figuring out how to repeat the requests when needed and being resistant to unexpected HTTP errors. For the former, I initially tried repeating it on an interval (first through a while True loop and later via shell scripting in the container) but later realized it was much easier by checking if internet access expired by sending a request to example.com and checking if it fails. For the latter, I used while True loops to allow the requests to be retried and executed break when the requests succeeded. The only other issue, that still exists, is that occasionally, the connection will drop out while the session is refreshed, although this seems to happen less than every 15 minutes and only lasts for a minute or two.

The container running in Docker Desktop, refreshing the session when needed

The container running in Docker Desktop, refreshing the session when needed

After comparing speeds with another guy I met on the cruise (who also happens to be a high-school programmer), who had the highest-level MedallionNet plan, there seems to be no additional throttling (7+ Mbps). Even better, after connecting my power bank’s integrated USB-C cable to the router, I’m able to move around the ship for hours. So far, I’ve been using the router for nearly ~7 hours and my 10,000 mAh power bank is still at 42% battery. In fact, I’m writing this very post while connected to the router.

The script

Here’s the code that I’ve been using and a sample .env, although I have a repository with the Dockerfile and compose file as well. Also, I’m aware the code isn’t pretty, I mostly just wrote it as a proof-of-concept that turned out to work amazingly well. I also doubt I’ll update the code after my cruise ends, since it won’t really be possible to test it. If you try to use this later, and it’s broken, you can go to DevTools, go to the network tab, go to “Fetch/XHR”, and then reload, which should allow you to reverse-engineer the API if you want to try to fix the script.

FIRST_NAME=YourFirstName
LAST_NAME=YourLastName
DOB=YYYY-MM-DD
BOOKING_ID=YourBookingID
ROOM_NUMBER=YourRoomNumber
PASSWORD=YourPassword
# Not doing uv's script syntax

import time
import requests
from rich.pretty import pprint

import os
from dotenv import load_dotenv
import random
from netaddr import EUI, mac_unix_expanded

load_dotenv()

# ONLY_RUN_ONCE = False

FIRST_NAME = os.getenv("FIRST_NAME")
LAST_NAME = os.getenv("LAST_NAME")
DOB = os.getenv("DOB")
BOOKING_ID = os.getenv("BOOKING_ID")
PASSWORD = os.getenv("PASSWORD")
ROOM_NUMBER = os.getenv("ROOM_NUMBER")

USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"

def app_access() -> bool:
    url = "https://mnet.su.ocean.com/captiveportal/api/v1/pcaAppDownload/usr/appValidAccess"
    headers = {
        "accept": "application/json",
        "accept-language": "en",
        "apicaller": "3",
        "content-type": "application/json",
        "origin": "https://mnet.su.ocean.com",
        "priority": "u=1, i", 
        "referer": "https://mnet.su.ocean.com/MednetWifiWeb/plan",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
        "sec-gpc": "1",
        "user-agent": USER_AGENT
    }
    data = {
        "bookingId": BOOKING_ID
    }
    response = requests.post(url, headers=headers, json=data)
    try:
        pprint(response.json())
        if response.json()["isTimeRemaining"] is False:
            return True
    except Exception as e:
        pprint({"error getting internet access via app download method": str(e)})
        return 
        
    # Make a simple request to https://play.google.com or apple app store
    time.sleep(5)  # Small delay since I don't think it gives access immediately
    try:
        url = "https://apps.apple.com/us/app/princess-cruises/id6469049279"
        # url = "https://play.google.com"
        headers = {
            "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
            "accept-language": "en-US,en;q=0.5",
            "user-agent": USER_AGENT
        }
        response = requests.get(url)
        pprint(
            response.status_code,
            )
    except requests.exceptions.ConnectionError:
        pprint("Couldn't GET app store url, should be fine though")
    return False
def login_user(ship_code: str):
    url = "https://mnet.su.ocean.com/captiveportal/api/v1/loginUser"
    headers = {
        "accept": "application/json",
        "accept-language": "en",
        "apicaller": "3",
        "content-type": "application/json",
        "origin": "https://mnet.su.ocean.com",
        "priority": "u=1, i",
        "referer": "https://mnet.su.ocean.com/MednetWifiWeb/login",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
        "sec-gpc": "1",
        "user-agent":  USER_AGENT
    }
    data = {
        "cabinNumber": ROOM_NUMBER,
        "dob": DOB,
        "clickfromOceanConcierge": False,
        "shipCode": ship_code,
        "tokenType": 1
    }
    response = requests.post(url, headers=headers, json=data)
    # pprint(response.json())

def random_mac_even_first_byte() -> str:
    # Generate a random MAC with even first byte
    first_byte = random.randint(0, 255) & 0xFE
    mac_bytes = [first_byte] + [random.randint(0, 255) for _ in range(5)]
    mac = EUI(':'.join(f"{b:02x}" for b in mac_bytes))
    mac.dialect = mac_unix_expanded
    return str(mac)

def edit_mac(admin_token):
    url = "http://192.168.8.1/cgi-bin/api/router/mac/clone"
    new_mac = random_mac_even_first_byte()  # Use the correct function
    headers = {
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Accept-Language": "en-US,en;q=0.8",
        "Authorization": admin_token,
        "Connection": "keep-alive",
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        "Origin": "http://192.168.8.1",
        "Referer": "http://192.168.8.1/",
        "Sec-GPC": "1",
        # "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
        "User-Agent": USER_AGENT,
        "X-Requested-With": "XMLHttpRequest"
    }
    cookies = {
        "Admin-Token": admin_token
    }
    data = {
        "newmac": new_mac
    }
    response = requests.post(url, headers=headers, cookies=cookies, data=data, verify=False)
    pprint({"new_mac": new_mac, "response": response.json()})
    return response.json()

def login_router(password):
    """
    Logs into the router and returns the Admin-Token.
    """
    url = "http://192.168.8.1/cgi-bin/api/router/login"
    headers = {
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Accept-Language": "en-US,en;q=0.8",
        "Authorization": "undefined",
        "Connection": "keep-alive",
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        "Origin": "http://192.168.8.1",
        "Referer": "http://192.168.8.1/",
        "Sec-GPC": "1",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
        "X-Requested-With": "XMLHttpRequest"
    }
    data = {
        "pwd": password
    }
    response = requests.post(url, headers=headers, data=data, verify=False)
    # pprint({"status_code": response.status_code})
    # pprint({"headers": dict(response.headers)})
    # pprint({"cookies": response.cookies.get_dict()})
    try:
        token = response.json().get("token")
        # pprint({"login_router_response": response.json(), "Admin-Token": token})
        return token
    except Exception as e:
        pprint({"error": str(e)})
        return None

def can_connect_to_example() -> bool:
    try:
        requests.get("https://example.com", timeout=5)
        return True
    except requests.RequestException:
        return False
    
def get_ship_code() -> str:
    # https://mnet.su.ocean.com/captiveportal/api/v1/shipcodes/ship
    # {"shipCodesMapping":[{"shipCode":"SU","shipName":"Sun Princess"}]}
    url = "https://mnet.su.ocean.com/captiveportal/api/v1/shipcodes/ship"
    headers = {
        "accept": "application/json",
        "accept-language": "en",
        "content-type": "application/json",
        "user-agent": USER_AGENT
    }
    response = requests.get(url, headers=headers)
    try:
        ship_code = response.json()["shipCodesMapping"][0]["shipCode"]
        return ship_code
    except Exception as e:
        pprint({"error getting ship code, defaulting to SU for Sun Princess": str(e)})
        return "SU"

def connect_to_internet(ship_code: str):
    while True: # Loop until we get internet access
        while True: # Loop until we get any response from the app internet request
            while True: # Loop until we successfully login
                print("Logging in")
                try:
                    login_user(ship_code=ship_code)
                except requests.exceptions.ConnectionError:
                    print("Couldn't connect to login page, sleeping for 2 seconds and retrying")
                    time.sleep(2)
                    continue
                else:
                    break
            print("Requesting app internet")
            try:
                # Don't regen mac unless this returns true
                print("Randomizing MAC due to session expiration") 
                regen_mac = app_access()
            except requests.exceptions.ConnectionError:
                print("Couldn't get app internet, sleeping for 2 seconds and retrying")
                time.sleep(2)
                continue
            else:
                break
        if regen_mac:
            edit_mac(login_router(PASSWORD))
            # time.sleep(3)
        else:
            break

def main():
    print("hi")
    ship_code = get_ship_code()
    while True:
        if can_connect_to_example():
            print("Internet access detected (can connect to example.com). Waiting 15 seconds before checking again.")
            time.sleep(15)
            continue
        else:
            print("Cannot connect to example.com, attempting to activate a 15 minute free cruise internet session")
        connect_to_internet(ship_code=ship_code)
        print(f"Finished at {time.strftime('%H:%M:%S')}, but sleeping for 30 seconds before verifying internet access")
        time.sleep(30) # Sleep for 30 seconds before checking again since it might take a while for internet access to be granted

if __name__ == "__main__":
    main()
联系我们 contact @ memedata.com