What the heck is TCP port 18800

Reverse engineering a hidden api from Amazon Music client

There will be an suspicious open TCP port 18800 if you have Amazon Music client installed:

➜  ~ sudo tcpview
Password:
Proto Local address Remote address Status PID Program name
tcp 0.0.0.0:18800 - LISTEN 35050 Amazon Music Helper

Windows? The same.

People have been complaining since 2014: https://chriscarey.com/blog/2014/10/08/how-to-stop-amazon-music-helper-from-running-in-the-background-osx/

So what exactly does it do?


From the quick inspection, we know that the port belongs to /Applications/Amazon Music.app/Contents/MacOS/Amazon Music Helper

Now break on accept and nc 127.1 18800 to see what happens:

Process 35050 stopped
* thread #6, stop reason = breakpoint 1.1
frame #0: 0x0000000104855af0 Amazon Music Helper` boost::asio::detail::socket_ops::accept(int, sockaddr*, unsigned long*, boost::system::error_code&)
Amazon Music Helper`boost::asio::detail::socket_ops::accept:
-> 0x104855af0 <+0>: push rbp
(lldb) bt
* thread #6, stop reason = breakpoint 1.1
* frame #0: 0x0000000104855af0 Amazon Music Helper` boost::asio::detail::socket_ops::accept(int, sockaddr*, unsigned long*, boost::system::error_code&)
...
frame #5: 0x0000000104813fe5 Amazon Music Helper` boost::asio::detail::task_io_service::run(boost::system::error_code&) + 165
frame #6: 0x000000010480daea Amazon Music Helper` Morpho::HttpDispatcher::startListening(int) + 1418
frame #7: 0x0000000104897a4c Amazon Music Helper` boost::(anonymous namespace)::thread_proxy(void*) + 156

Huh, it’s an HTTP server? But curl fails.

➜  ~ curl localhost:18800/test
curl: (52) Empty reply from server

Sometimes this happens when the server requires SSL and you just send the request in plain text. So let’s try different payload:

➜  ~ curl https://localhost:18800/test -k
denied

Now it works.


Thanks to the unstripped symbols. Quick analyze shows lots of shared codebase between the Windows client and Mac, but MSVC has moved all symbols to pdb so it’s much harder to read.

So Morpho is the codename.

There are three request handlers:

  • Morpho::CrossDomainHandler (^/crossdomain[.]xml$)
  • Morpho::LaunchHandler (^/morpho)
  • Morpho::SystemHandler (^/.+)

Each handler has a method requireOrigin to require additional check for incoming requests:

The check is implemented in sym.Morpho::HttpDispatcher::getOriginMatchString_HttpRequest

Too many code screenshot? No worries, the simplified pseudo code will be like:

regex = RegExp("http(s)?.*[.]amazon[.](com|co[.]uk|de|fr|it|es|co[.]jp|ca|in|com[.]au)(:[0-9]{1,4})?");
if (regex.match(request.headers["origin"]) && regex.match(request.headers["origin"]))
return true;
if (regex.match(request.headers["x-amzn-origin"]))
return true;
return false;

Since it accepts the custom http header x-amzn-origin, it’s easy for any third party website to fake the requirement. Besides, this so called RESTful server is exposed on all interfaces (0.0.0.0), so don’t you Shodan or ZoomEye fans get excited?

Go back to the handlers.

Morpho::SystemHandler

It accepts arbitrary pathname, but don’t care the parameters at all. It will response basic system information like following:

➜  ~ curl -H "x-amzn-origin: https://a.amazon.com" -k https://127.1:18800/morpho
{ "version":"7.0.3.1540", "device":"AMZN{pretty_long_uuid_here}", "osType":"osx", "osVersion":"10.14.2" }

Morpho::LaunchHandler

This one looks evil. It checks if pathname matches regex at symbol sym._anonymousnamespace_::kLaunchRegEx, which will be initialized to ^/((purchase|download|play|cplaunch).*)$

If so, it launches Amazon Music executable with the pathname as command line argument:

Addictionaly, if the http verb is POST, a slash and the request body will be added to the parameter. For example, the request

➜  ~ curl -v -k -H "x-amzn-origin: https://a.amazon.com" -XPOST "https://127.0.0.1:18800/play" --data "boy"

Results in

Executable module set to "/Applications/Amazon Music.app/Contents/MacOS/Amazon Music".
Architecture set to: x86_64h-apple-macosx.
(lldb) po [[NSProcessInfo processInfo] arguments]
<__NSArrayI 0x7ff256dfc010>(
/Applications/Amazon Music.app/Contents/MacOS/Amazon Music,
play/boy
)

It’s lucky that they have the pathname regex check, and they’ve used QProcess::startDetached(QString const&, QStringList const&) instead of its brother QProcess::startDetached(QString const&). Why?

The main executable is based on libCEF, which supports Chromium command flags as well. If I ever got a chance to feed arbitrary argument to it, it can be like:

➜ ~ /Applications/Amazon\ Music.app/Contents/MacOS/Amazon\ Music --no-sandbox --renderer-cmd-prefix="/Applications/Calculator.app/Contents/MacOS/Calculator"

This one if you prefer Windows:

"Amazon Music.exe" --no-sandbox --renderer-cmd-prefix="cmd /c calc"

Dear Amazon developers, you have been so closed to a wormable remote code execution bug. Fortunately it didn’t happen.


One more thing. Don’t you need a certificate to run https server? Let’s add a -v to check it.

* SSL connection using TLSv1.2 / AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
* subject: CN=www.amazonmusiclocal.com
* start date: Nov 12 00:00:00 2018 GMT
* expire date: Oct 18 12:00:00 2019 GMT
* issuer: C=US; O=Amazon; OU=Server CA 1B; CN=Amazon
* SSL certificate verify ok.
They have a independent domain that resolves to loopback
And it’s valid signed

So Amazon Music just made this domain www.amazonmusiclocal.com resolve to loopback. Since its signature is valid, they must have the private key delivered to the client. And here we go:

[0x100006e10]> afl~Morpho::HelperServer
0x100076f40 1 31 sym.Morpho::HelperServer::getCert
0x100076f60 1 31 sym.Morpho::HelperServer::getPrivKey
0x100077070 5 234 sym.Morpho::HelperServer::getAesKey
0x100077180 33 609 sym.Morpho::HelperServer::getAesIV

The key and certificate are hardcoded in this initializer, you can grab them by yourself.

pdf @sym.__GLOBAL__sub_I_helperServerData.cpp

Just few days ago I read an article telling that Spotify do the same as well:

Actually, we don’t even need DNS for this one. We can do the same as the embedded Spotify player does and send a request inside the victim’s browser to their local Spotify control server. We don’t even need to be Spotify. Authenticated requests between websites are fine. That’s something the internet just allows (with several extremely technical and complicated caveats).
What did Spotify Security say? That it’s a product decision and they’re fine with it. I tried to explain further but they confirmed, yes it’s a product decision and they’re fine with it. I’m also, to be fair, fine with posting the spotilocal.com certificate online. So I did. Well it’s removed now, so guess it wasn’t a product decision they wanted to keep WINKING EMOJI

There’s no actual exploitable bug found. But at least you can scan the LAN and pop them a Music Player, and now you have a trusted certificate for debugging web, LOL