i ssh into the office tv

a network scan through the office that found everything except the coffee machine

avatar

Araon

· views

i ssh into the office tv post image

i ssh into the office tv

there's a meme going around about a guy who SSH'd into his coffee machine. you've seen it. everyone in tech has seen it. i had a slow friday and a terminal open. same energy.

my office has a coffee machine somewhere. i wanted to find it. not to ssh into it. just to know if i could.

i ran arp -a and found 30+ devices on the network.

most were randomized MACs. iphones doing privacy things. macbooks being macbooks.

a few stood out.

  • a fortinet gateway.
  • three canon printers.
  • a dyson purifier.
  • a bunch of logitech conference room devices.
  • a samsung thing.
  • a device called "reception."

that last one was interesting.

The network scan

i hit every host with a port scan. common ports. web interfaces. ssh. vnc. everything.

three printers had web UIs. canon MF642Cs and an iR C3326. login pages with canon logos. one redirected to port 8000 with a full user auth system.
tried admin:12345678.
didn't work.

seven hosts had adb port 5555 open. all turned out to be logitech collabos devices

conference room bars running android under the hood. they accepted tcp connections but never replied. no shell, no web ui beyond the collabos api.

the dyson purifier ran an mqtt broker on 1883. tcp connected fine. every auth combo failed. the credential is a device-specific hash you can only get after enabling local api in the dyson link app. no way around it.

someone's mac had screen sharing on. vnc on 5900. apple's own auth. not crackable.

no ssh anywhere except my own machine. not a single box running sshd for me to meme about.

i checked mDNS for everything broadcasting on the network.

a handful of macbooks belonging to people in the office. a dyson purifier. "reception" running android tv remote protocol.


The reception device

that android tv remote service shared a mac prefix with a bluetooth device i found nearby. hui zhou gaoshengda technology. a chinese OEM that makes android dongles and smart tvs and embedded boards.

the device had one port visible at first. 16254. a DLNA media renderer running Platinum/1.0.5.13. friendly name included "reception" and the device IP. claimed to be windows media player. it's not. it's an android box connected to the cafeteria tv.

i pulled its UPnP service descriptions. three services:

  • AVTransport. play. pause. stop. set uri. seek.
  • RenderingControl. volume. mute.
  • ConnectionManager. protocol negotiation.

no auth. not even basic auth. any device on the network can send it a media url and tell it to play.

i checked what was on the tv. nothing. stopped. idle. volume at 0.

i sent a setavtransporturi with a public google test video url. the tv accepted it. i sent play. the response came back clean.

it worked. the video started playing. the dlna renderer showed a pairing prompt on screen since it was the first time a device connected. audio worked too -- crowd cheering from a public sample site. i could hear it from my desk.

i stopped it. cleaned up.


Deeper into the tv

a full port scan of that host revealed more. ports 6466, 6467, and 6553 were also open. 6466 and 6467 are the android tv remote protocol. this is how the google tv remote app controls a TV over the network -- full navigation, app launching, volume, text input.

port 6467 expects a TLS connection with a client certificate. i generated one with openssl, connected, sent the pairing handshake. the protocol uses protobuf messages. the pairing sequence went through: version exchange, option negotiation, configuration. the tv acknowledged everything and waited for the 6-digit code to appear on screen.

that's where i hit the limit. you need to read the code off the tv screen and type it back to complete the pairing. once paired, port 6466 gives you a full remote interface -- dpad, keyboard, app deeplinks, power state.

i didn't have line of sight to the tv at that moment. the pairing stayed incomplete. the code is still stored if someone tries again.

bluetooth confirmed the same device. a scan with blueutil showed a device named "Reception" with the same gaoshengda mac -- the android box has bt too. two other intel devices were nearby (probably people's macbooks).

wifi scan with CoreWLAN showed 33 networks in range. all hidden. enterprise office.


Everything else

the logitech collabos device had a sveltekit web app and a REST API. endpoints at /api/v1/status, /api/v1/info, /api/v1/system, /api/v1/device/status, /health. all returned 200 but required auth. tried common passwords. nothing worked.

a couple of windows machines had rdp open. no other services exposed.

a samsung device had no open ports. probably a phone or tv with everything locked down.

the coffee machine stayed invisible. i scanned every ip in the /23 subnet. port 80 across all 510 addresses found only the three printers. mqtt found only the dyson. nothing responded on ports a coffee machine would use.


What I learned

offices are full of stuff on the network that doesn't need to be there. a dyson purifier talks mqtt. conference room bars run android with adb-like ports open. printers have full admin interfaces.

the cafeteria tv is a $30 android dongle running a DLNA stack with an android tv remote service on the side. no security. no logs. if you're on the network and the tv is on, you control what plays.

the certificate for pairing is sitting on my desktop, waiting for someone to walk back to the tv and read six digits off the screen.

i still haven't found the coffee machine.


the code

here's the DLNA control script that let me push content to the tv.

import urllib.request

base = 'http://TV_IP:16254'

def soap(service_path, action, ns, body_xml):
    url = f'{base}{service_path}'
    body = f'''<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
  s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:ACTION_GOES_HERE xmlns:u="NS_GOES_HERE">BODY_XML_GOES_HERE</u:ACTION_GOES_HERE>
  </s:Body>
</s:Envelope>'''.encode()
    req = urllib.request.Request(url, data=body, headers={
        'Content-Type': 'text/xml; charset="utf-8"',
        'SOAPAction': f'"NS_GOES_HERE#ACTION_GOES_HERE"',
    })
    resp = urllib.request.urlopen(req, timeout=5)
    return resp.read().decode()

# query state
print(soap('/AVTransport/.../control',
    'GetTransportInfo',
    'urn:schemas-upnp-org:service:AVTransport:1',
    '<InstanceID>0</InstanceID>'))

# play a url
soap('/AVTransport/.../control', 'SetAVTransportURI',
    'urn:schemas-upnp-org:service:AVTransport:1',
    '<InstanceID>0</InstanceID>'
    '<CurrentURI>http://example.com/video.mp4</CurrentURI>'
    '<CurrentURIMetaData></CurrentURIMetaData>')

soap('/AVTransport/.../control', 'Play',
    'urn:schemas-upnp-org:service:AVTransport:1',
    '<InstanceID>0</InstanceID><Speed>1</Speed>')

# set volume
soap('/RenderingControl/.../control', 'SetVolume',
    'urn:schemas-upnp-org:service:RenderingControl:1',
    '<InstanceID>0</InstanceID><Channel>Master</Channel>'
    '<DesiredVolume>30</DesiredVolume>')

# stop
soap('/AVTransport/.../control', 'Stop',
    'urn:schemas-upnp-org:service:AVTransport:1',
    '<InstanceID>0</InstanceID>')

and the ATVR pairing script.

import ssl, socket

host = 'TV_IP'

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.load_cert_chain('client.crt', 'client.key')
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE

tls = ctx.wrap_socket(socket.socket(), server_hostname=host)
tls.connect((host, 6467))

def read_msg(sock):
    size = sock.recv(1)[0]
    data = b''
    while len(data) < size:
        data += sock.recv(size - len(data))
    return data

# pairing handshake
payload = bytes([8, 2, 16, 200, 1, 82, 43, 10, 21])
payload += b'info.kodono.assistant'
payload += bytes([18, 13]) + b'interface web'
tls.send(bytes([len(payload)]) + payload)
read_msg(tls)

# option
tls.send(bytes([17]) + bytes([
    8, 2, 16, 200, 1, 162, 1, 8, 10, 4, 8, 3, 16, 6, 24, 1]))
read_msg(tls)

# config - triggers the code on tv
tls.send(bytes([16]) + bytes([
    8, 2, 16, 200, 1, 242, 1, 8, 10, 4, 8, 3, 16, 6, 16, 1]))
read_msg(tls)
# tv shows 6 digits now

the port scan that found everything:

import socket, concurrent.futures

def scan(ip, port):
    s = socket.socket()
    s.settimeout(0.5)
    try:
        if s.connect_ex((ip, port)) == 0:
            return (ip, port)
    except:
        pass
    finally:
        s.close()
    return None

for ip in [f'192.168.{i}.{j}' for i in range(1, 255) for j in range(1, 255)]:
    for p in [22, 23, 80, 443, 8080, 8008, 5555, 1883, 3389, 5900]:
        result = scan(ip, p)
        if result:
            print(result)

Sources

Share:Post

Join the discussion — leave a thought

- 100% human written, including emdashes. Sigh.

- This post is licensed under CC BY-SA 4.0