macOS Stash Proxy Unauthorized Manipulation of System Network Preferences

Aug 14, 2024

CVE Number

CVE-2024-7457

Credits

Carlos Garrido of Pentraze Cybersecurity

ws.stash.app.mac.daemon.helper - Unauthorized Manipulation of System Network Preferences

Description

The ws.stash.app.mac.daemon.helper helper tool has a vulnerability where it fails to validate the client’s authenticity and privileges when attempting to perform an operation via XPC. As a result, it is possible to invoke multiple functions, allowing changes to system-wide network preferences as an unprivileged user, i.e., without the system.privilege.admin privilege. Likewise, there is no verification (code signing flags) process for the client attempting to connect with the helper tool. Consequently, a malicious actor can change proxy configurations (SOCKS, HTTP, HTTPS), enabling man-in-the-middle (MITM) attacks.

It is important to note that all of the following actions can be performed without having a license for the application, i.e., without being registered.

Authorization macOS

According to the official macOS documentation, the following workflow must be followed to ensure a secure authorization model:

  • Client Authorization:

Retrieved from Apple’s Authorization Services Programming Guide.

  • Helper Tool Authorization:

Retrieved from Apple’s Authorization Services Programming Guide.

According to Apple, the workflow should be: The client performs pre-authorization, i.e., attempts to obtain the necessary privileges (e.g., system.preference.admin) to perform operations like changing the system proxy settings. This is done via the AuthorizationCreate() and AuthorizationCopyRights() calls using the flags kAuthorizationFlagInteractionAllowed (allowing the security agent to prompt the user for credentials if necessary), kAuthorizationFlagPreAuthorize, and kAuthorizationFlagExtendRights. Pre-authorization is not mandatory but is a significant step as it prevents a client without the necessary privileges from calling the helper tool and consuming system resources unnecessarily.

In the case of factored applications, the client (main application) proceeds to create an external reference (AuthorizationExternalForm) (via AuthorizationMakeExternalForm()) for the authorization session previously created with AuthorizationCreate(). By creating an external reference (a 12-byte HANDLE), it is possible to pass it to the server (helper tool) via XPC, which can then authorize the client and perform the requested operation. In this scenario, there are two approaches:

  1. The client performs pre-authorization, and on the server side (helper tool), it is validated via AuthorizationCopyRights() if the privilege was granted.

  2. If the client did not perform pre-authorization, the server side should attempt to authorize the client via AuthorizationCopyRights() as well.

The difference between approach 1 and 2 is that in the first case, the authorization is done before contacting the server. In both cases, it is validated whether the client’s authorization reference possesses the privilege.

ws.stash.app.mac.daemon.helper authorization

If we validate how the Stash application performs authorization, we see that the server (ws.stash.app.mac.daemon.helper), when initializing an instance of the ProxySettingTool class, calls the localAuth function:

id [ProxySettingTool init](struct ProxySettingTool* self, SEL sel)
{
    struct ProxySettingTool* super = self
    struct objc_class_t* superRef_ProxySettingTool_1 = superRef_ProxySettingTool
    id result = _objc_msgSendSuper2(&super, op: "init")

    if (result != 0)
        [ProxySettingTool localAuth](self: result, sel: "localAuth")
    return result
}

When analyzing the localAuth() function, the server creates a null authorization session via AuthorizationCreate() and then grants itself the system.preference.admin privilege by calling AuthorizationCopyRights():

void [ProxySettingTool localAuth](struct ProxySettingTool* self, SEL sel)

        if (_AuthorizationCreate(__nullable: nullptr) == 0)
            int128_t var_48_1 = data_1003da170
            int128_t var_58 = data_1003da160.o
            int32_t rights = 1
            int128_t* var_28_1 = &var_58
            _AuthorizationCopyRights(authorization: -[ProxySettingTool authRef](self, sel: "authRef"),
 &rights, __nullable: nullptr)
  • What is the problem with this approach?

The server assumes the entire authorization process. Therefore, it does not matter at all what the client (main application) attempts to do on the system (at the network level) or its privileges (it could even be an unprivileged user), as it is the server (root)that authd (the authorization daemon) will authorize. Since a Privileged Helper Tool runs as root, root can typically obtain nearly any privilege without even needing to authenticate, as shown in the following definition of the system.privilege.admin rule:

As we can see in the .plist, there are two ways to obtain this privilege:

  1. Being root (see the allow-root key), without authentication.

  2. Being a member of the admin group and authenticating (see the authenticate-user and group keys).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>allow-root</key>
	<true/>
	<key>authenticate-user</key>
	<true/>
	<key>class</key>
	<string>user</string>
	<key>comment</key>
	<string>Used by AuthorizationExecuteWithPrivileges(...).
		AuthorizationExecuteWithPrivileges() is used by programs requesting
		to run a tool as root (e.g., some installers).</string>
	<key>created</key>
	<real>716069213.93575501</real>
	<key>group</key>
	<string>admin</string>
	<key>modified</key>
	<real>716069213.93575501</real>
	<key>session-owner</key>
	<false/>
	<key>shared</key>
	<false/>
	<key>timeout</key>
	<integer>300</integer>
	<key>tries</key>
	<integer>10000</integer>
	<key>version</key>
	<integer>0</integer>
</dict>
</plist>

Client validation - shouldAcceptNewConnection

Although Stash does not correctly implement the authorization model, this security issue alone is not sufficient to exploit and/or abuse the application. If the server correctly validates (code signing flags) a client attempting to connect to it, it would not be possible to perform arbitrary operations on the system's network preferences.

The shouldAcceptNewConnection: method determine whether or not the connection will be accepted:

char [ProxyHelper listener:shouldAcceptNewConnection:](struct ProxyHelper* self, SEL sel, id listener, id shouldAcceptNewConnection)
{

        id obj = _objc_retain(obj: listener)
        id rax = _objc_retain(obj: shouldAcceptNewConnection)
        id obj_1 = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: _OBJC_CLASS_$_NSXPCInterface, cmd: "interfaceWithProtocol:", protoRef_ProxyHelperProtocol))
        _objc_msgSend(self: rax, cmd: "setExportedInterface:", obj_1)
        _objc_release(obj: obj_1)
        _objc_msgSend(self: rax, cmd: "setExportedObject:", self)
        void var_40
        _objc_initWeak(location: &var_40, val: rax)
        void var_38
        _objc_initWeak(location: &var_38, val: self)
        int64_t (* const var_78)() = __NSConcreteStackBlock
        int64_t var_70 = 0xc2000000
        int64_t (* var_68)(void* arg1) = sub_100004707
        void* const var_60 = &data_1003da1b0
        void var_58
        _objc_copyWeak(to: &var_58, from: &var_38)
        void var_50
        _objc_copyWeak(to: &var_50, from: &var_40)
        _objc_msgSend(self: rax, cmd: "setInvalidationHandler:", &var_78)
        id obj_2 = _objc_retainAutoreleasedReturnValue(obj: -[ProxyHelper connections](self, sel: "connections"))
        _objc_msgSend(self: obj_2, cmd: "addObject:", rax)
        _objc_release(obj: obj_2)
        _objc_msgSend(self: rax, cmd: "resume")
       <SNIP>
        return 1
}
char [ProxyHelper listener:shouldAcceptNewConnection:](struct ProxyHelper* self, SEL sel,
id listener, id shouldAcceptNewConnection)
{

        id obj = _objc_retain(obj: listener)
        id rax = _objc_retain(obj: shouldAcceptNewConnection)
        id obj_1 = _objc_retainAutoreleasedReturnValue(obj: _objc_msgSend(self: _OBJC_CLASS_$_NSXPCInterface,
cmd: "interfaceWithProtocol:", protoRef_ProxyHelperProtocol))
        _objc_msgSend(self: rax, cmd: "setExportedInterface:", obj_1)
        _objc_release(obj: obj_1)
        _objc_msgSend(self: rax, cmd: "setExportedObject:", self)
        .
	.
	<SNIP>
        _objc_msgSend(self: rax, cmd: "resume")
       <SNIP>
        return 1
}

Unfortunately, there is no client verification, which means that the connection will be always accepted as the function always returns 1 (YES).

Any unauthorized client can establish a valid connection to the server and call the various functions it exposes via the ProxyHelperProtocol interface.

If we validate the signature as well as the entitlements of ws.stash.app.mac.daemon.helper, we will see the following:

Executable=/Library/PrivilegedHelperTools/ws.stash.app.mac.daemon.helper
Identifier=ws.stash.app.mac.daemon.helper
Format=Mach-O universal (x86_64 arm64)
CodeDirectory v=20500 size=39946 flags=0x10000(runtime) hashes=1237+7 location=embedded
Signature size=9064
Authority=Developer ID Application: Stash Networks Limited (B36787XSBG)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=Apr 3, 2024 at 5:46:26 AM
Info.plist entries=5
TeamIdentifier=B36787XSBG
Runtime Version=14.4.0
Sealed Resources=none
Internal requirements count=1 size=224
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.application-identifier</key><string>B36787XSBG.ws.stash.app.mac.daemon.helper</string></dict></plist>

As we can see, the server was signed with runtime, which means it enjoys the same benefits as binaries protected by SIP (rootless). Additionally, we observe that it does not have any dangerous entitlements and that the following certificate was used: Stash Networks Limited (B36787XSBG). Similarly, since the server is signed and has runtime enabled, it is necessary to validate that the client meets these same conditions before accepting a connection.

As we will see later in the remediation section of this report, the Bundle Identifier, version, certificate (Team ID) with which the client was signed, and the entitlements should be validated. In this way, we prevent a malicious client from establishing a successful connection with the server and performing unauthorized privileged operations.

ProxyHelperProtocol - Methods List

The interface exposes a total of 9 methods. Not all of them can be used to directly impact the system, but some can, such as: enableProxyWithPort:socksPort:pac:filterInterface:ignoreList:error:, prepareTunDevice:enableIPv6:reply: and restoreProxyWithCurrentPort:socksPort:info:filterInterface:error:.


    struct objc_method_t method_getVersion: = 
        {
            char* name = sel_getVersion: {"getVersion:"}
            char* types = selTypes_closeTunDevice: {"v24@0:8@?16"}
            void* imp = 0x0
        }

    struct objc_method_t method_getCurrentProxySetting: = 
        {
             char* name = sel_getCurrentProxySetting: {"getCurrentProxySetting:"}
             char* types = selTypes_closeTunDevice: {"v24@0:8@?16"}
             void* imp = 0x0
        }

    struct objc_method_t method_getCurrentDNSSetting: = 
        {
             char* name = sel_getCurrentDNSSetting: {"getCurrentDNSSetting:"}
             char* types = selTypes_closeTunDevice: {"v24@0:8@?16"}
             void* imp = 0x0
        }

    struct objc_method_t method_enableProxyWithPort:socksPort:pac:filterInterface:ignoreList:error: = 
        {
            char* name = sel_enableProxyWithPort:socksPort:pac:filterInterface:ignoreList:error: {"enableProxyWithPort:socksPort:pa…"}
            char* types = selTypes_enableProxyWithPort:socksPort:pac:filterInterface:ignoreList:error: {"v52@0:8i16i20@24c32@36@?44"}
        void* imp = 0x0
        }

    struct objc_method_t method_disableProxyWithFilterInterface:reply: = 
        {
            char* name = sel_disableProxyWithFilterInterface:reply: {"disableProxyWithFilterInterface:…"}
            char* types = selTypes_disableProxyWithFilterInterface:reply: {"v28@0:8c16@?20"}
             void* imp = 0x0
        }

    struct objc_method_t method_restoreProxyWithCurrentPort:socksPort:info:filterInterface:error: = 
        {
            char* name = sel_restoreProxyWithCurrentPort:socksPort:info:filterInterface:error: {"restoreProxyWithCurrentPort:sock…"}
            char* types = selTypes_restoreProxyWithCurrentPort:socksPort:info:filterInterface:error: {"v44@0:8i16i20@24c32@?36"}
            void* imp = 0x0
        }

    struct objc_method_t method_restoreDNSWithInfo:filterInterface:error: = 
        {
            char* name = sel_restoreDNSWithInfo:filterInterface:error: {"restoreDNSWithInfo:filterInterfa…"}
            char* types = selTypes_restoreDNSWithInfo:filterInterface:error: {"v36@0:8@16c24@?28"}
            void* imp = 0x0
        }

    struct objc_method_t method_prepareTunDevice:enableIPv6:reply: = 
        {
            char* name = sel_prepareTunDevice:enableIPv6:reply: {"prepareTunDevice:enableIPv6:repl…"}
            char* types = selTypes_prepareTunDevice:enableIPv6:reply: {"v32@0:8i16c20@?24"}
            void* imp = 0x0
        }

    struct objc_method_t method_closeTunDevice: = 
        {
            char* name = sel_closeTunDevice: {"closeTunDevice:"}
            char* types = selTypes_closeTunDevice: {"v24@0:8@?16"}
            void* imp = 0x0
        }

ws.stash.app.mac.daemon.helper Exploitation - Putting the Pieces Together:

Important: To carry out this attack/exploitation, it is not necessary to have a license or be registered with the application, as explained earlier, the attacker can invoke various functions without being halted by an authorization and/or validation process.

In the following exploitation scenario, the enableProxyWithPort:socksPort:pac:filterInterface:ignoreList:error: function will be invoked from the context of a non-privileged user.

It is not necessary for the Stash Main application to be running to perform this attack as the ws.stash.app.mac.daemon.helper server runs on-demand via launchctl; however, if it is not, the exploit must be kept running when applying the configuration. Upon pressing any key, the attack will end, and the network configuration will revert to its original state.

houdini@Garridos-MacBook-Air /tmp % id
uid=1337(houdini) gid=20(staff) 
groups=20(staff),12(everyone),61(localaccounts),702(com.apple.sharepoint.group.2),100(_lpoperator),701(com.apple.sharepoint.group.1)
houdini@Garridos-MacBook-Air /tmp % ./StashExp 
2024-08-04 01:44:33.526 StashExp[3423:144051] [+] macOS Stash Network Tool Exploit: 

2024-08-04 01:44:33.528 StashExp[3423:144051] [+] Establishing and resuming connection [_agentConnection] with target service name: 
`ws.stash.app.mac.daemon.helper`
2024-08-04 01:44:33.529 StashExp[3423:144051] [+] Remote Object: <__NSXPCInterfaceProxy_ProxyHelperProtocol: 0x600001594140>
2024-08-04 01:44:33.529 StashExp[3423:144051] [+] Remote Connection: <NSXPCConnection: 0x600000794000> connection to service named 
ws.stash.app.mac.daemon.helper
2024-08-04 01:44:33.529 StashExp[3423:144051] [+] Obtaining version information by calling `getVersion:` method
2024-08-04 01:44:33.529 StashExp[3423:144051] [+] Calling `enableProxyWithPort:socksPort:pacUrl:filterInterface:ignoreList:` 
method...
2024-08-04 01:44:33.530 StashExp[3423:144051] [+] Please press any key to end the attack...
2024-08-04 01:44:33.530 StashExp[3423:144052] [+] Version Information: 2.4.8
2024-08-04 01:44:33.744 StashExp[3423:144052] [+] Last Error: (null)

2024-08-04 01:44:37.189 StashExp[3423:144051] [+] Done!

Recommendations:

  • Follow Apple’s step-by-step guide/documentation on how to implement a secure authorization scheme in your factored application. From the main application (client), perform pre-authorization. This ensures that only authorized users (members of the admin group or root) can perform privileged operations.

  • The client process verification in the shouldAcceptNewConnection call should ensure the following:

  1. The connecting process is signed by Apple.
  2. The connecting process is signed by your team ID (B36787XSBG).
  3. The connecting process is identified by your bundle ID.
  4. The connecting process has a minimum software version where the fix has been implemented or is hardened against injection attacks.

To identify the client, use the audit_token instead of the PID, as the latter is vulnerable to PID reuse attacks.

Additionally, the client allowed to connect must be compiled with a hardened runtime or library validation and must not possess the following entitlements:

  • com.apple.security.cs.allow-dyld-environment-variables
  • com.apple.security.cs.disable-library-validation
  • com.apple.security.get-task-allow

These entitlements would permit another process to inject code into the app, enabling it to communicate with the helper tool.

Furthermore, the connecting client must be identified by the audit token, not by PID (process ID).

Leverage new APIs for automatic code signing verification before accepting a connection:

  • [NSXPCConnection setCodeSigningRequirement:] (available since macOS 13.0)

  • xpc_connection_set_peer_code_signing_requirement (available since macOS 12.0)

References:

¿Ver el sitio en español?