Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Goalake managed switch — web UI diagnostics

Recon/diagnostic tooling for the "Goalake 8 Port Gigabit Ethernet Managed Switch" (Amazon: B0D7PVBJ68), reverse-engineered from its own web UI. Device tested: 192.0.2.10, firmware 6.0.250111, model 8GE.

What we found

The device. "Goalake" is a house brand of a Shenzhen OEM (jingsongke.com). It's built on the same class of Realtek switch-SoC reference design/firmware that a bunch of other Amazon-brand switches share (Hasivo, MokerLink, Horaco, Sodola, etc — all "Easy Smart Managed" clones with near-identical web GUIs). Only TCP port 80 is exposed — no telnet/SSH/SNMP found in a scan of ~50 common ports (see portscan.sh). The HTTP server identifies as lwIP/1.3.1, a minimal embedded TCP/IP stack — this is not a general-purpose Linux box, it's a small RTOS-style firmware.

The API. The whole UI is a single-page app. Every action POSTs JSON to a numeric path that matches a callcmd id, e.g. POST /101 with body {"data": {"callcmd": 101}}. We pulled every authenticated HTML page and extracted every callcmd reference to build a full map of the switch's RPC surface — see CALLCMD_CATALOG in switch_client.py for all ~25 documented commands (port state, VLANs, STP, port mirroring, storm control, DHCP snooping, LLDP, MAC table, firmware upgrade, factory reset, config backup/restore, etc). IDs 110-113,119,121,122,125,128-130,134,135 are never referenced by the UI JS at all — see Unmapped IDs below.

Auth. POST /123 with {"password": "..."} sets a random per-login session cookie; every other call requires it. Two quirks worth knowing:

  • The device only allows one active session at a time. A second login attempt while a session is live doesn't get a clean "already logged in" error — the TCP connection is just silently dropped, which looks identical to a network fault. If a script's login keeps failing, that's the first thing to suspect (another browser tab, or a previous run that didn't log out).
  • A failed/unauthenticated call gets the same silent-connection-drop treatment rather than a proper 401/error JSON.
  • The embedded httpd appears to only handle one request at a time — hit it too fast and connections start dropping. Scripts here add small delays and retries.

Interesting/notable findings (not necessarily "bugs", but worth knowing):

  • Everything is plaintext HTTP — no HTTPS option at all. The login password and the "change password" (callcmd 120, Original_Key/ New_Key) both go over the wire in clear text.
  • callcmd 114 with no calldata returns the switch's live learned MAC address table — every device MAC currently known via that switch's ports. On the test unit this returned 70 entries, i.e. it's seeing traffic from a much larger network segment behind one port, not just a handful of directly-attached devices.
  • The config export (callcmd 201, "Backup Configuration" button) is a full JSON dump of the running config — no admin password/hash appears in it.
  • loop_detect (115) and link_agg/LACP (116) exist as API endpoints wired into the frontend urlMap, but both return "parameter error" when queried, and their menu entries are hidden/commented out in the dashboard HTML — these features look incomplete/disabled in this firmware build even though the plumbing exists.
  • callcmd 101/200 also return the device serial number and MAC address alongside the firmware version.

Unmapped IDs — now probed (probe_unmapped.py). We tested all 13 IDs not referenced by the UI, one at a time, diffing port/link state before and after each call and health-checking the device in between. None caused a reboot or any visible state change. Two different behaviors turned up:

  • 110,111,112,113,119,121,122,125,128,134,135 — the connection gets silently dropped, the same failure mode as an unauthenticated or unrecognized request. These IDs don't appear to be implemented at all.

  • 129 and 130 — these are implemented: they return a proper {"errmsg": "parameter error", "errcode": -1} JSON response (the same shape loop_detect/link_agg give). That means they're real, functioning server-side commands with no UI page anywhere calling them - genuine hidden/unused API surface.

    The language file (langs/en.js) has several strings with no matching page or menu item in this switch's UI at all: AI_Function_Config ("AI Function Configuration"), AI_AnAuto ("Port Watchdog"), AI_PortRestart ("Automatic Port Extension"), poeSet ("POE Setting"), Voltage/Power ("POE Voltage"/"POE Power"), and rebootPort/reboottext ("Port Reboot Setting") - plus a monitor ("Monitor") top-level menu label. This model has no PoE output (it's a PD/powered-device input, not PoE-out), so these are very likely leftover strings from a PoE sibling model in the same firmware/OEM family that shares this language pack and API - a solid theory for what 129/130 are gated behind.

    We tested this theory with a handful of low-risk, targeted guesses (all against port 1, which was link-down at the time, so nothing live would be disrupted): {"AI_AnAuto":[0,0,0,0,0,0,0,0]}, {"AI_PortRestart":[0,0,0,0,0,0,0,0]}, {"port":0}, {"AI_AnAuto":0,"AI_PortRestart":0}, and (after research below turned up the OEM's internal name for this feature) {"poedog":0} / {"poedog":[0,0,0,0,0,0,0,0]}. All attempts still returned the same parameter error with zero state change - the exact field names remain unconfirmed.

    We also checked for unlinked static files this switch might still serve even without a menu item pointing at them (candidate paths like page/Monitor/monitor.html, page/Port_Management/ai_function.html, page/System_Management/poe_set.html, plus a few leftover-icon guesses like img/poe.png) - none exist. Useful side-finding: this device answers any unrecognized GET path (not just failed/unauthenticated calls) by silently dropping the connection rather than a proper 404, so "connection dropped" vs "200 with content" is a reliable exists/doesn't-exist signal for this kind of path probing, as long as you pace requests (its httpd struggles with back-to-back connections).

    Public research (searching for a PoE-output sibling model's leaked firmware/web-UI source, GitHub code search for the exact language-file strings, and the OpenWrt "Hasivo switches" forum thread) found no concrete JSON schema or file path. It did turn up one solid new fact worth recording: Hasivo (a sibling brand on the same firmware family) calls this feature internally "poedog" (PoE-watchdog), per a ServeTheHome review comment - which also claims this "generic Realtek" web UI ships with hidden menus for changing branding/default settings across the whole rebadged product line, without further detail on how to reach them. We tried poedog as a calldata key (above) without success. GitHub code search for the literal strings AI_AnAuto/AI_PortRestart/poeSet/rebootPort returned nothing relevant, and the OpenWrt thread is entirely serial-console/u-boot content with no mention of the JSON API.

    Update - manufacturer manuals obtained. The manuals.plus pages are Cloudflare-gated against automated fetches (confirmed: a plain HTTP GET gets a bare 403 at Cloudflare's edge, before it even reaches the real server - a real browser passes the same challenge transparently). With saved copies of two manuals in hand (Goalake 10-Port Managed PoE Switch, and the 16-Port Gigabit Easy Smart Managed Switch - the non-PoE sibling closest to our actual unit), we now have real documentation instead of just language-file guesses:

    • The companion mobile app (not the web UI) has a documented, non-hypothetical "reboot individual ports or schedule reboots" feature, confirmed in both manuals - a POE Settings screen shows a per-port table with Extend and Timing toggle columns alongside the PoE on/off switch. This is a real, named feature in the shared OEM firmware family, not wired into our specific switch's web UI menu.
    • We retried 129 with array-shaped field-name guesses matching this codebase's established per-port-array convention (every other per-port setting we've mapped - AdminState, tx/rx, up/down, trust_port, flow - is an 8-element array, not a single port index; our earlier {"port":0} guess didn't match that pattern): {"reboot":[0]*8}, {"rebootPort":[0]*8}, {"port_reboot":[0]*8}, {"PortReboot":[0]*8}. Still parameter error every time.
    • While doing this, port 8's link dropped between two of our checks. We confirmed this wasn't us: a real reboot would zero every port's TX/RX counters, but ports 4/5/7 kept counting live traffic throughout, and the serial/MAC/firmware were unchanged - so this looks like a real-world cable/device disconnection on that one port, coincidentally timed, not something our (all rejected) guesses caused.

    Resolved. A community-maintained open-source Home Assistant integration for this exact device family, slydiman/sscpoe, independently implements the identical protocol we reverse-engineered (same numeric callcmd-as-URL-path scheme, same login/cookie mechanism, same JSON envelope) - strong external validation of everything in this README. Its GitHub issues include a firmware dump the maintainer extracted from a sibling PoE model (GPS204V3, firmware 6.0.24...), and that firmware's bundled web UI JS contains the actual, unobfuscated source for both commands:

    function set_PortWatchdog() {
        swcallDatas.data.callcmd = 130;
        var AI_Wdog = Number($("#AI_Wdog").val());
        updateValueAndSession("AI_Wdog", "AI_Wdog", AI_Wdog, "AI_Wdog");
    }
    function set_portAutoExtend() {
        swcallDatas.data.callcmd = 129;
        var AI_AnAuto = Number($("#AI_AnAuto").val());
        updateValueAndSession("AI_AnAuto", "AI_AnAuto", AI_AnAuto, "AI_AnAuto");
    }

    So: 129 = AI_AnAuto ("Automatic Port Extension" - a global on/off toggle, calldata {"AI_AnAuto": 0|1}), and 130 = AI_Wdog ("Port Watchdog" - calldata {"AI_Wdog": 0|1}). Both are simple global scalars, not per-port arrays - which is exactly why our earlier array-shaped guesses never landed.

    We retried both with this exact schema (value 0, for safety) and still got parameter error on our unit. "Port Watchdog" pings a connected device on PoE ports and power-cycles it if it stops responding; "Automatic Port Extension" is a PoE-range-extension mode. Both are meaningless without PoE output hardware, which this switch genuinely doesn't have (PD/input only, not PSE/output - see sys_info: model 8GE). The most likely explanation: the commands are registered in the shared firmware's dispatch table (hence a real JSON error instead of the dropped-connection "unregistered" behavior we saw on the other unmapped IDs), but a hardware-capability check rejects them before even validating our field - i.e. these two are genuine, fully-identified dead code on this specific non-PoE model, not a gap in our schema guess.

    This closes out the 129/130 investigation: their real names, purpose, and exact JSON schema are now known with certainty (not inferred), and we understand precisely why they don't work on this hardware.

    (A parallel search for a standalone firmware download for Goalake/Hasivo/MokerLink specifically came up empty, but did turn up real firmware files for other sibling OEM brands - Sodola, Horaco, GoodTop. Those turned out to be an older CGI-based firmware generation

    • web_maintenance.cgi, ClearSilver templates - with no callcmd JSON scheme at all, so a different branch of this OEM family's history, not useful for this particular question.)

Script for repeating this yourself, one ID at a time:

SWITCH_PASSWORD='...' python3 probe_unmapped.py --host <ip> --cmd 129

Session timeout. While probing, a script bug left a session open without logging out. Reasserting a fresh login then failed (silently dropped, same single-session symptom as before) for almost exactly 2 minutes before succeeding on its own — so that's the device's idle-session timeout when nobody explicitly logs out.

UDP scan (udpscan.py). Probed SNMP (161, with a real SNMPv1 GetRequest for sysDescr.0), TFTP (69), mDNS/SSDP/NTP/IKE, and the discovery ports used by TP-Link's Easy Smart (29808/29809) and Netgear ProSAFE (63321/63322) protocols, in case this OEM cloned one of those. Every port came back closed (ICMP port-unreachable) — no SNMP agent, no proprietary LAN discovery beacon, nothing on UDP at all. Combined with the earlier TCP sweep, TCP port 80 genuinely appears to be the switch's only exposed service.

Pre-authentication config reset via serial number. The login page's "RESET" button (callcmd 132, calldata {Snvr: <last 6 chars of serial>}) requires no login at all — it's called straight from the login screen. The result strings it uses (RestoreConfig_success/ RestoreConfig_fail, "Restore the configuration...") are distinct from the authenticated factory-reset's strings (resetSuccess/resetFail, "Reset to factory..."), confirming it's a separate, unauthenticated config-restore path — not just an alias for the same authenticated action. We did not trigger it (would actually reset the device), but the security model is worth being clear-eyed about: anyone who can reach the web UI on the LAN and knows/reads 6 characters of the device's serial number can reset its configuration without ever knowing the admin password. Brute-forcing that over the network isn't practical (a 6-char alphanumeric suffix is on the order of 10^9 combinations, and this device's httpd can barely sustain back-to-back requests without dropping connections), so the realistic exposure is anyone with a photo of the serial-number label — which, being a physical sticker, is a fairly standard "recovery code on the bottom of the device" trust model, not a remote vulnerability by itself. Worth knowing about if that label is ever photographed for a return, a resale listing, or a support ticket.

Serial-console debug backdoor (unverified on this unit). Community reverse-engineering of the same Realtek reference firmware family (see the "Hasivo switches" OpenWrt forum thread) documents a hidden debug prompt reachable via Ctrl+T/F4 at a serial-console login, password switchrtk, extracted from a defdb.ko kernel module — it drops into a menu with Diag/Engineering/Shell options that can pivot to a full Linux shell. This switch only exposes port 80 over the network (no telnet, and the UDP scan above found nothing either), so this would require physical access to a serial header on the board. Not tested here — noted for completeness if you ever open the case.

Files

  • switch_client.py — stdlib-only Python client (no pip deps). Handles login/logout, the JSON envelope, cookie/session handling, and retries around the device's fragile connection handling. CALLCMD_CATALOG is the full command reference.
  • enumerate.py — logs in and pulls every command confirmed safe to call with empty calldata (SAFE_GET_CMDS) into one JSON report. Never touches destructive/action commands.
  • portscan.sh — dependency-free bash TCP port sweep (/dev/tcp, no nmap needed).
  • udpscan.py — dependency-free UDP probe (SNMP GetRequest, common service ports, known vendor discovery-tool ports).
  • probe_unmapped.py — safely test one not-yet-understood callcmd ID at a time, diffing port/link state before and after.

Usage

./portscan.sh 192.0.2.10

# password via env var so it doesn't end up in shell history
SWITCH_PASSWORD='...' python3 enumerate.py --host 192.0.2.10 --out report.json

Or use the client directly:

from switch_client import SwitchClient
c = SwitchClient("192.0.2.10")
c.login("...")
print(c.get_detail())       # firmware version, serial, MAC, port counters
print(c.backup_config())    # full config export
c.logout()

Deliberately not automated

enumerate.py never calls: 102 (firmware upload), 104 (reboot), 105 (factory reset), 120 (change password), 132 (SN-based reset), or 201 with calldata (config restore/overwrite) — all state-changing or destructive. It also doesn't brute-force the unmapped ID gaps (see above). These are all one-line client.call(...) invocations if you deliberately want to exercise them, but they're not something to run automatically.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages