oss-sec mailing list archives

Re: transmission: rpc session-id mechanism design flaw results in RCE


From: Marcus Meissner <meissner () suse de>
Date: Mon, 15 Jan 2018 17:33:15 +0100

Hi,

Mitre assigned CVE-2018-5702.

Ciao, Marcus


On Thu, Jan 11, 2018 at 10:47:38AM -0800, Tavis Ormandy wrote:
Hello, the transmission bittorrent client uses a client/server
architecture, the user interface is the client and a daemon runs in the
background managing the downloading, seeding, etc.

Clients interact with the daemon using JSON RPC requests to a web server
listening on port 9091. The daemon will only accept requests from localhost
by default, but it's common to configure NAS devices to accept remote
clients.

A sample RPC session looks like this:

$ curl -sI http://localhost:9091/transmission/rpc
HTTP/1.1 409 Conflict
Server: Transmission
X-Transmission-Session-Id: JL641xTn2h53UsN6bVa0kJjRBLA6oX1Ayl06AJwuhHvSgE6H
Date: Wed, 29 Nov 2017 21:37:41 GMT

$ curl -H 'X-Transmission-Session-Id:
JL641xTn2h53UsN6bVa0kJjRBLA6oX1Ayl06AJwuhHvSgE6H'  -d
'{"method":"session-set","arguments":{"download-dir":"/home/user"}}' -si
http://localhost:9091/transmission/rpc
HTTP/1.1 200 OK
Server: Transmission
Content-Type: application/json; charset=UTF-8
Date: Wed, 29 Nov 2017 21:38:57 GMT
Content-Length: 36

{"arguments":{},"result":"success"}

As with all HTTP RPC schemes like this, any website can send requests to
the daemon listening on localhost with XMLHttpRequest(), but the theory is
they will be ignored because clients must prove they can read and set a
specific header, X-Transmission-Session-Id.

Unfortunately, this design doesn't work because of an attack called "DNS
rebinding". Any website can simply create a dns name that they are
authorized to communicate with, and then make it resolve to localhost.

The attack works like this:

1. A user visits http://attacker.com, which has an <iframe> to a subdomain
the attacker controls.
2. The attacker configures their DNS server to respond alternately with
127.0.0.1 and 123.123.123.123 (an address they control) with a very low TTL.
3. When the browser resolves to 123.123.123.123, they serve HTML that waits
for the DNS entry to expire (or force it to expire by flooding the cache
with lookups), then they have permission to read and set headers.

I have a domain I use for testing dns rebinding called rbndr.us, you can
use this page to generate hostnames (source code is here:
https://github.com/taviso/rbndr):

https://lock.cmpxchg8b.com/rebinder.html

Here I want to alternate between 127.0.0.1 and 199.241.29.227, so I use
7f000001.c7f11de3.rbndr.us:

$ host 7f000001.c7f11de3.rbndr.us
7f000001.c7f11de3.rbndr.us has address 127.0.0.1
$ host 7f000001.c7f11de3.rbndr.us
7f000001.c7f11de3.rbndr.us has address 199.241.29.227
$ host 7f000001.c7f11de3.rbndr.us
7f000001.c7f11de3.rbndr.us has address 127.0.0.1

Here you can see the resolution alternates between the two addresses I want
(note that depending on caching it might take a while to switch, the TTL is
set to minimum but some servers round up).

I just wait for the cached response to expire, and then POST commands to
the server.

Exploitation is simple, you could set script-torrent-done-enabled and run
any command, or set download-dir to /home/user/ and then upload a torrent
for ".bashrc".

Here is my (simple) demo, it's slow, but could be made very fast:

http://lock.cmpxchg8b.com/Asoquu3e.html

I've verified it works on Chrome and Firefox on Windows and Linux (I tried
Fedora and Ubuntu), I expect other platforms and browsers are affected. There
are screenshots of how the attack is supposed to look on the bug report
here:

https://github.com/transmission/transmission/pull/468

Tavis.

From a82c9d154613a5f99394029c28a01148749735af Mon Sep 17 00:00:00 2001
From: Tavis Ormandy <taviso () gmail com>
Date: Fri, 1 Dec 2017 12:49:25 -0800
Subject: [PATCH] mitigate dns rebinding attacks

---
 libtransmission/quark.c        |   2 +
 libtransmission/quark.h        |   2 +
 libtransmission/rpc-server.c   | 116 +++++++++++++++++++++++++++++++++++++----
 libtransmission/rpc-server.h   |   4 ++
 libtransmission/session.c      |   2 +
 libtransmission/transmission.h |   1 +
 libtransmission/web.c          |   3 ++
 7 files changed, 121 insertions(+), 9 deletions(-)

diff --git a/libtransmission/quark.c b/libtransmission/quark.c
index 861050057..87ed9f3ef 100644
--- a/libtransmission/quark.c
+++ b/libtransmission/quark.c
@@ -296,6 +296,8 @@ static struct tr_key_struct const my_static[] =
     { "rpc-version-minimum", 19 },
     { "rpc-whitelist", 13 },
     { "rpc-whitelist-enabled", 21 },
+    { "rpc-host-whitelist", 18 },
+    { "rpc-host-whitelist-enabled", 26 },
     { "scrape", 6 },
     { "scrape-paused-torrents-enabled", 30 },
     { "scrapeState", 11 },
diff --git a/libtransmission/quark.h b/libtransmission/quark.h
index d40ab75fa..d82c7051b 100644
--- a/libtransmission/quark.h
+++ b/libtransmission/quark.h
@@ -298,6 +298,8 @@ enum
     TR_KEY_rpc_version_minimum,
     TR_KEY_rpc_whitelist,
     TR_KEY_rpc_whitelist_enabled,
+    TR_KEY_rpc_host_whitelist,
+    TR_KEY_rpc_host_whitelist_enabled,
     TR_KEY_scrape,
     TR_KEY_scrape_paused_torrents_enabled,
     TR_KEY_scrapeState,
diff --git a/libtransmission/rpc-server.c b/libtransmission/rpc-server.c
index 7c78f92ac..a33f07e10 100644
--- a/libtransmission/rpc-server.c
+++ b/libtransmission/rpc-server.c
@@ -51,6 +51,7 @@ struct tr_rpc_server
     bool isEnabled;
     bool isPasswordEnabled;
     bool isWhitelistEnabled;
+    bool isHostWhitelistEnabled;
     tr_port port;
     char* url;
     struct tr_address bindAddress;
@@ -62,6 +63,7 @@ struct tr_rpc_server
     char* password;
     char* whitelistStr;
     tr_list* whitelist;
+    tr_list* hostWhitelist;
     int loginattempts;
 
     bool isStreamInitialized;
@@ -547,6 +549,47 @@ static bool isAddressAllowed(tr_rpc_server const* server, char const* address)
     return false;
 }
 
+static bool isHostnameAllowed(tr_rpc_server const* server, struct evhttp_request* req)
+{
+    const char *host = evhttp_find_header(req->input_headers, "Host");
+    char *hostname;
+
+    // If password auth is enabled, any hostname is permitted.
+    if (server->isPasswordEnabled)
+        return true;
+
+    // No host header, invalid request.
+    if (!host)
+        return false;
+
+    // Host header might include the port.
+    hostname = tr_strndup(host, strcspn(host, ":"));
+
+    // localhost or ipaddress is always acceptable.
+    if (strcmp(hostname, "localhost") == 0
+     || strcmp(hostname, "localhost.") == 0
+     || tr_addressIsIP(hostname))
+    {
+        tr_free(hostname);
+        return true;
+    }
+
+    // Otherwise, hostname must be whitelisted.
+    if (server->isHostWhitelistEnabled)
+    {
+        for (tr_list* l = server->hostWhitelist; l != NULL; l = l->next) {
+            if (tr_wildmat(hostname, l->data))
+            {
+                tr_free(hostname);
+                return true;
+            }
+        }
+    }
+
+    tr_free(hostname);
+    return false;
+}
+
 static bool test_session_id(struct tr_rpc_server* server, struct evhttp_request* req)
 {
     char const* ours = get_current_session_id(server);
@@ -636,6 +679,22 @@ static void handle_request(struct evhttp_request* req, void* arg)
 
 #ifdef REQUIRE_SESSION_ID
 
+        else if (!isHostnameAllowed(server, req))
+        {
+            char* tmp = tr_strdup_printf(
+                "<p>Transmission received your request, but the hostname was unrecognized.</p>"
+                "<p>To fix this, choose one of the following options:"
+                "<ul>"
+                "<li>Enable password authentication, then any hostname is allowed.</li>"
+                "<li>Add the hostname you want to use to the whitelist in settings.</li>"
+                "</ul></p>"
+                "<p>If you're editing settings.json, see the 'rpc-host-whitelist' and 'rpc-host-whitelist-enabled' 
entries.</p>"
+                "<p>This requirement has been added to help prevent "
+                "<a href=\"https://en.wikipedia.org/wiki/DNS_rebinding\";>DNS Rebinding</a> "
+                "attacks.</p>");
+            send_simple_response(req, 421, tmp);
+            tr_free(tmp);
+        }
         else if (!test_session_id(server, req))
         {
             char const* sessionId = get_current_session_id(server);
@@ -647,7 +706,7 @@ static void handle_request(struct evhttp_request* req, void* arg)
                 "<li> When you get this 409 error message, resend your request with the updated header"
                 "</ol></p>"
                 "<p>This requirement has been added to help prevent "
-                "<a href=\"http://en.wikipedia.org/wiki/Cross-site_request_forgery\";>CSRF</a> "
+                "<a href=\"https://en.wikipedia.org/wiki/Cross-site_request_forgery\";>CSRF</a> "
                 "attacks.</p>"
                 "<p><code>%s: %s</code></p>",
                 TR_RPC_SESSION_ID_HEADER, sessionId);
@@ -844,17 +903,12 @@ char const* tr_rpcGetUrl(tr_rpc_server const* server)
     return server->url != NULL ? server->url : "";
 }
 
-void tr_rpcSetWhitelist(tr_rpc_server* server, char const* whitelistStr)
+static void tr_rpcSetList(char const* whitelistStr, tr_list** list)
 {
     void* tmp;
 
-    /* keep the string */
-    tmp = server->whitelistStr;
-    server->whitelistStr = tr_strdup(whitelistStr);
-    tr_free(tmp);
-
     /* clear out the old whitelist entries */
-    while ((tmp = tr_list_pop_front(&server->whitelist)) != NULL)
+    while ((tmp = tr_list_pop_front(list)) != NULL)
     {
         tr_free(tmp);
     }
@@ -866,7 +920,7 @@ void tr_rpcSetWhitelist(tr_rpc_server* server, char const* whitelistStr)
         size_t const len = strcspn(walk, delimiters);
         char* token = tr_strndup(walk, len);
 
-        tr_list_append(&server->whitelist, token);
+        tr_list_append(list, token);
 
         if (strcspn(token, "+-") < len)
         {
@@ -889,6 +943,23 @@ void tr_rpcSetWhitelist(tr_rpc_server* server, char const* whitelistStr)
     }
 }
 
+void tr_rpcSetHostWhitelist(tr_rpc_server* server, char const* whitelistStr)
+{
+    tr_rpcSetList(whitelistStr, &server->hostWhitelist);
+}
+
+void tr_rpcSetWhitelist(tr_rpc_server* server, char const* whitelistStr)
+{
+    void* tmp;
+
+    /* keep the string */
+    tmp = server->whitelistStr;
+    server->whitelistStr = tr_strdup(whitelistStr);
+    tr_free(tmp);
+
+    tr_rpcSetList(whitelistStr, &server->whitelist);
+}
+
 char const* tr_rpcGetWhitelist(tr_rpc_server const* server)
 {
     return server->whitelistStr != NULL ? server->whitelistStr : "";
@@ -904,6 +975,11 @@ bool tr_rpcGetWhitelistEnabled(tr_rpc_server const* server)
     return server->isWhitelistEnabled;
 }
 
+void tr_rpcSetHostWhitelistEnabled(tr_rpc_server* server, bool isEnabled)
+{
+    server->isHostWhitelistEnabled = isEnabled;
+}
+
 /****
 *****  PASSWORD
 ****/
@@ -1054,6 +1130,28 @@ tr_rpc_server* tr_rpcInit(tr_session* session, tr_variant* settings)
         tr_rpcSetWhitelistEnabled(s, boolVal);
     }
 
+    key = TR_KEY_rpc_host_whitelist_enabled;
+
+    if (!tr_variantDictFindBool(settings, key, &boolVal))
+    {
+        missing_settings_key(key);
+    }
+    else
+    {
+        tr_rpcSetHostWhitelistEnabled(s, boolVal);
+    }
+
+    key = TR_KEY_rpc_host_whitelist;
+
+    if (!tr_variantDictFindStr(settings, key, &str, NULL) && str != NULL)
+    {
+        missing_settings_key(key);
+    }
+    else
+    {
+        tr_rpcSetHostWhitelist(s, str);
+    }
+
     key = TR_KEY_rpc_authentication_required;
 
     if (!tr_variantDictFindBool(settings, key, &boolVal))
diff --git a/libtransmission/rpc-server.h b/libtransmission/rpc-server.h
index 46e8a871f..ad1eb5204 100644
--- a/libtransmission/rpc-server.h
+++ b/libtransmission/rpc-server.h
@@ -42,6 +42,10 @@ void tr_rpcSetWhitelist(tr_rpc_server* server, char const* whitelist);
 
 char const* tr_rpcGetWhitelist(tr_rpc_server const* server);
 
+void tr_rpcSetHostWhitelistEnabled(tr_rpc_server* server, bool isEnabled);
+
+void tr_rpcSetHostWhitelist(tr_rpc_server* server, char const* whitelist);
+
 void tr_rpcSetPassword(tr_rpc_server* server, char const* password);
 
 char const* tr_rpcGetPassword(tr_rpc_server const* server);
diff --git a/libtransmission/session.c b/libtransmission/session.c
index 86d054f7f..9a0b7c104 100644
--- a/libtransmission/session.c
+++ b/libtransmission/session.c
@@ -371,6 +371,8 @@ void tr_sessionGetDefaultSettings(tr_variant* d)
     tr_variantDictAddStr(d, TR_KEY_rpc_username, "");
     tr_variantDictAddStr(d, TR_KEY_rpc_whitelist, TR_DEFAULT_RPC_WHITELIST);
     tr_variantDictAddBool(d, TR_KEY_rpc_whitelist_enabled, true);
+    tr_variantDictAddStr(d, TR_KEY_rpc_host_whitelist, TR_DEFAULT_RPC_HOST_WHITELIST);
+    tr_variantDictAddBool(d, TR_KEY_rpc_host_whitelist_enabled, true);
     tr_variantDictAddInt(d, TR_KEY_rpc_port, atoi(TR_DEFAULT_RPC_PORT_STR));
     tr_variantDictAddStr(d, TR_KEY_rpc_url, TR_DEFAULT_RPC_URL_STR);
     tr_variantDictAddBool(d, TR_KEY_scrape_paused_torrents_enabled, true);
diff --git a/libtransmission/transmission.h b/libtransmission/transmission.h
index ac1871adb..08a24eca4 100644
--- a/libtransmission/transmission.h
+++ b/libtransmission/transmission.h
@@ -109,6 +109,7 @@ char const* tr_getDefaultDownloadDir(void);
 #define TR_DEFAULT_BIND_ADDRESS_IPV4 "0.0.0.0"
 #define TR_DEFAULT_BIND_ADDRESS_IPV6 "::"
 #define TR_DEFAULT_RPC_WHITELIST "127.0.0.1,::1"
+#define TR_DEFAULT_RPC_HOST_WHITELIST ""
 #define TR_DEFAULT_RPC_PORT_STR "9091"
 #define TR_DEFAULT_RPC_URL_STR "/transmission/"
 #define TR_DEFAULT_PEER_PORT_STR "51413"
diff --git a/libtransmission/web.c b/libtransmission/web.c
index cca888b66..a57c85457 100644
--- a/libtransmission/web.c
+++ b/libtransmission/web.c
@@ -678,6 +678,9 @@ char const* tr_webGetResponseStr(long code)
     case 417:
         return "Expectation Failed";
 
+    case 421:
+        return "Misdirected Request";
+
     case 500:
         return "Internal Server Error";
 
-- 
2.15.0.531.g2ccb3012c9-goog



-- 
Marcus Meissner,SUSE LINUX GmbH; Maxfeldstrasse 5; D-90409 Nuernberg; Zi. 3.1-33,+49-911-740 
53-432,,serv=loki,mail=wotan,type=real <meissner () suse de>


Current thread: