In November 2019, Slack announced the release of a network overlay tool, called Nebula. Basically, Nebula is a piece of software that acts as a kind of VPN between different types of hosts and a "lighthouse" server that ties them all together and implements rules and IP addressing. From Slack:
Nebula is a scalable overlay networking tool with a focus on performance, simplicity and security. It lets you seamlessly connect computers anywhere in the world. Nebula is portable, and runs on Linux, OSX, and Windows.
At the end of January 2020, I took a look at the software to determine how it could be used to manage resources on multiple cloud providers, lab environments, and my home network. Off the bat, I found it easy to work with an definitly saw the use case for it - no need for complex VPN server setups, no need to purchase additional hardware - everything just connected nicely and acted as I defined in the yaml files.
But then I noticed something odd - every now and again, when starting the client, it would hang for a moment. I noticed this was inconsistent on differnet hosts, so I decided to take as look at what it was doing under the hood. Here's an excerpt from the tun_darwin.go file:
Notice anything strange? exec.Command calls the "ifconfig" command with three arguments. In our case, running on Mac OS, ifconfig is stored in /sbin/ - but the command doesn't specify that. See, Bash-based shells (including zsh on MacOS) run commands by searching for them in a specific order. Each time you run a command without an absolute path (lets use the example "foo"), the shell searches for "foo" as a shell function, then as a builtin, then looks in the directories listed in the environmental variable $PATH, in order of appearance. The last part is important; if your PATH variable looks like this (find this with 'env')...
...then your command execution order is /usr/local/bin -> /usr/bin -> /bin -> /usr/sbin/ -> /sbin. Bash will only execute the first command it finds - meaning if "foo" exists in /usr/local/bin and another version exits in /sbin/, the version in /usr/local/bin will fire.
As a standard user, we probably can't modify any of these directories. However, we can change the order in which they appear - and even prepend our own directories to the variable. See where this is going? If Nebula calls "ifconfig," and the shell searches the path in order, and we can add a directory we can write to in front of the valid location of ifconfig...we can control what file or script Nebula is actually executing. So, lets do that.
First, we create the file in a directory we can write to. All users can typically write to /tmp, so lets create the file /tmp/ifconfig and add the following:
The breakdown here: First, we tell the file its going to execute a bash script. On line 3, we add a command to pass the three arguments that Nebula is passing over to /sbin/ifconfig, to ensure Nebula gets a valid connection after our exploit. But sandwiched between the two, we're creating a reverse TCP bash shell out to 10.0.0.1 on port 443 - which Nebula will execute and send to the background before finishing its connection routine. Make the command executable with "chmod +x /tmp/ifconfig"
Now we have to change our path. Because /sbin/ifconfig lives in /sbin, which is the final entry in our PATH variable, lets tack /tmp to the front of PATH:
This changes PATH to /tmp:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin/ - meaning when our shell finds our malicious "ifconfig" script in /tmp, it executes it and gives us a reverse shell.
Now, what does this do for us? On the surface not a lot, because Nebula has to be executed as root to make changes to the system's network. But thats also the ace up our sleeve - if we're able to make these changes and wait for the user or user's admin to launch Nebula with sudo or as root, we gain remote shell as root, for some pretty serious privilege escalation. Nebula must also be restarted to make changes to certain firewall rules - so if you can social engineer your way to a user restarting the program (again, with sudo) you can get your root shell. Here it is in action:
Vulnerable MacOS Client
Attacker Listener Server
The fix here is simple, and an oversight many developers fall victim to: just use absolute paths. By modifying the line above to...
...we effectively kill this technique by giving Nebula an absolute path to ifconfig.
All in all, I found six instances of the relative path command execution in both the tun_darwin.go and tun_windows.go files. I reported the issue to Slack on January 28th via the HackerOne platform with the following timeline:
- Jan 28, 2020: Initial Report to Slack
- Jan 30, 2020: Acknowledgement from Slack
- Jan 30, 2020: Provided additional information to Slack
- Jan 31, 2020: Acknowledgement from Slack
- Feb 4, 2020: Requested Update from Slack
- Feb 7, 2020: Requested Update from Slack
- Feb 10, 2020: Acknowledgement from Slack
- Feb 14, 2020: Slack responded that they could not reproduce the issue, requested more info
- Feb 17, 2020: Sent additional info, screenshot, and walkthrough video to Slack
- Feb 19, 2020: Acknowledgement from Slack
- Feb 19, 2020: Traiged
- Feb 21, 2020: Issue resolved in Github commit 191: "use absolute paths on darwin and windows (#191)"
- Feb 21, 2020: Verified fix was in place and worked
- Feb 21, 2020: Requested timeline for bounty and disclosure
- Feb 25, 2020: Awarded $750 Bounty.
- Feb 25, 2020: Requested Disclosure, automatically granted 30 day disclosure timeline
- Apr 1, 2020: Disclosed
While I reported this bug as a High, Slack reduced it to a Medium severity with the following explanation: "While the impact here is high the vulnerability requires a high degree of access and other preconditions that are tough to achieve which impacts the overall risk." While I don't entirely agree with that statement, I understand their viewpoint.
In closing, developers - remember to always use absolute paths or native syscalls in your applications, and always consider how much need there is to call an external command from within your application. Thanks to Slack for a quick fix after triage, and for the bounty!