Four critical vulnerabilities have been found in OMI, one of Azure’s most common but least-known software agents, and deployed on a large portion of Linux VMs in Azure. It has proven to be very easy to exploit vulnerabilities. It allows attackers to remotely execute arbitrary code within the network with a single request and escalate to root privileges.
- CVE-2021-38647 – Unauthenticated RCE as root
- CVE-2021-38648 – Privilege Escalation vulnerability
- CVE-2021-38645 – Privilege Escalation vulnerability
- CVE-2021-38649 – Privilege Escalation vulnerability
We will try to explain all the technical details in the following sections.
First of all, what is the OMI that causes these vulnerabilities?
OMI is the UNIX/Linux equivalent of Windows' WMI. It allows users to manage configurations and gather statistics in remote and local environments. It is widely used in Azure, especially Open Management Suite (OMS), Azure Insights, Azure Automation, and more because of the ease of use and abstraction that OMI provides.
Why is the OMI attack surface of interest to attackers?
The OMI agent runs as root with elevated privileges. Any user can communicate using a UNIX socket or sometimes using an HTTP API when configured to allow external use. As a result, OMI represents a potential attack surface for an attacker where external users or low-privileged users allow remote code execution or elevation of privileges on target machines.
Note that in scenarios where OMI ports (5986/5985/1270) are accessible over the internet to allow remote management, this vulnerability could also be exploited by attackers to gain initial access to the target Azure environment and then move laterally within it. Therefore, an exposed HTTPS port is a holy grail for malicious attackers. With a simple exploit, as shown in the diagram below, they can access new targets, execute commands with the highest privileges, and possibly propagate to new target machines.
CVE-2021-38647 Remote Code Execution – Remove Authentication Header and Become Root!
It's like a way of exploiting a vulnerability straight out of the 90s. However, it is the RCE vulnerability that took place in 2021 and is a lesson that affects millions of endpoints.
With a single package, an attacker can become a root user on a remote machine by simply removing the authentication header.
How Can It Be So Simple?
This vulnerability allows remote takeover when OMI exposes the HTTPS management port externally (5986/5985/1270). This is actually the default configuration when installed standalone and in Azure configuration management or System Center Operations Manager (SCOM).
Fortunately other Azure services (like Log Analytics) don't expose this port and therefore scope is limited to local privilege escalation.
The diagram below shows the unexpected behavior of OMI when a command execution request is issued without an authorization header.
- It streams normally with valid password in the Authentication header, sends an HTTP request to the remote OMI instance, and transmits the login information in the authorization header.
- Getting an authorization error when passing an Invalid Authentication header, fails as expected if omicli passes an invalid header.
- The vulnerability can be exploited when passing a command without the Authentication header. The OMI server trusts the request and enables perfect RCE even without an Authentication header.
There are approximately 15,707 online devices that are vulnerable and have not yet updated.
More Technical Details!
OMI has a front-end-back-end architecture. Omiengine receives an authentication request from the client, sends a new authentication request with omic, the omiserver records the user's authentication result information. uid and gid' forward response back to user.
Let's look at the authentication logic inside the _ProcessReceivedMessage function:
static Protocol_CallbackResult _ProcessReceivedMessage(
ProtocolSocket* handler)
{
…
BinProtocolNotification* binMsg = (BinProtocolNotification*) msg;
if (binMsg->type == BinNotificationConnectRequest)
{
// forward to server
uid_t uid = INVALID_ID;
gid_t gid = INVALID_ID;
Sock s = binMsg->forwardSock;
Sock forwardSock = handler->base.sock;
// Note that we are storing (socket, ProtocolSocket*) here
r = _ProtocolSocketTrackerAddElement(forwardSock, handler); <— (1)
if(MI_RESULT_OK != r)
{
trace_TrackerHashMapError();
return PRT_RETURN_FALSE;
}
DEBUG_ASSERT(s_socketFile != NULL);
DEBUG_ASSERT(s_secretString != NULL);
/* If system supports connection-based auth, use it for
implicit auth */
if (0 != GetUIDByConnection((int)handler->base.sock, &uid, &gid))
{
uid = binMsg->uid;
gid = binMsg->gid;
}
/* Create connector socket */
{
if (!handler->engineBatch)
{
handler->engineBatch = Batch_New(BATCH_MAX_PAGES);
if (!handler->engineBatch)
{
return PRT_RETURN_FALSE;
}
}
ProtocolSocketAndBase *newSocketAndBase = Batch_GetClear(handler->engineBatch, sizeof(ProtocolSocketAndBase));
if (!newSocketAndBase)
{
trace_BatchAllocFailed();
return PRT_RETURN_FALSE;
}
r = _ProtocolSocketAndBase_New_Server_Connection(newSocketAndBase, protocolBase->selector, NULL, &s); <— (2)
if( r != MI_RESULT_OK )
{
trace_FailedNewServerConnection();
return PRT_RETURN_FALSE;
}
handler->clientAuthState = PRT_AUTH_WAIT_CONNECTION_RESPONSE;
handler = &newSocketAndBase->protocolSocket;
newSocketAndBase->internalProtocolBase.forwardRequests = MI_TRUE;
// Note that we are storing (socket, ProtocolSocketAndBase*) here
r = _ProtocolSocketTrackerAddElement(s, newSocketAndBase); <— (3)
if(MI_RESULT_OK != r)
{
trace_TrackerHashMapError();
return PRT_RETURN_FALSE;
}
}
handler->clientAuthState = PRT_AUTH_WAIT_CONNECTION_RESPONSE;
if (_SendAuthRequest(handler, binMsg->user, binMsg->password, NULL, forwardSock, uid, gid) ) <— (4)
{
ret = PRT_CONTINUE;
}
}
….
}
Let's review the logic; (1) first, omiengine registers the client's socket in a connection hash map, using the port number as the key. (2) Next, omiengine establishes a new connection with the omiserver. (3) and saves it to the same tracker hash map. (4) Then the verification request is sent to the server for verification.
Now let's see how the same function handles the server response::
static Protocol_CallbackResult _ProcessReceivedMessage(
ProtocolSocket* handler)
{
…
// forward to client
Sock s = binMsg->forwardSock; <— (1.1)
Sock forwardSock = INVALID_SOCK;
ProtocolSocket *newHandler = _ProtocolSocketTrackerGetElement(s); <— (1.2)
if (newHandler == NULL)
{
trace_TrackerHashMapError();
return PRT_RETURN_FALSE;
}
if (binMsg->result == MI_RESULT_OK || binMsg->result == MI_RESULT_ACCESS_DENIED)
{
if (binMsg->result == MI_RESULT_OK)
{
newHandler->clientAuthState = PRT_AUTH_OK; <— (2)
newHandler->authInfo.uid = binMsg->uid;
newHandler->authInfo.gid = binMsg->gid;
trace_ClientCredentialsVerfied(newHandler);
}
ProtocolSocketAndBase *socketAndBase = _ProtocolSocketTrackerGetElement(handler->base.sock); <— (3)
if (socketAndBase == NULL)
{
trace_TrackerHashMapError();
return PRT_RETURN_FALSE;
}
r = _ProtocolSocketTrackerRemoveElement(handler->base.sock);
if(MI_RESULT_OK != r)
{
trace_TrackerHashMapError();
return PRT_RETURN_FALSE;
}
r = _ProtocolSocketTrackerRemoveElement(s);
if(MI_RESULT_OK != r)
{
trace_TrackerHashMapError();
return PRT_RETURN_FALSE;
}
// close socket to server
trace_EngineClosingSocket(handler, handler->base.sock);
….
}
}
The _ProcessReceivedMessage function forwards an incoming request to the client and server alike, without any server authentication. (1.1) The client's socket ID is taken from the response. (1.2) is taken from the hash map. If the socket is not found in the hash-map, the authentication fails. (2) Then the authentication response is parsed and the authentication information is set accordingly. From now on, every command that leaves this client socket is executed with binMsg->uid and binMsg->gid. Then (3) the server takes the socket from the hash map. Otherwise, the authentication process will fail.
Vulnerability Exploitation
The exploit creates a Python script that sends messages directly over the omiengine UNIX socket.
Python Algorithm
First business logic;
- Submit an authentication request with fake credentials
- Submit another request
- Send command id >> /tmp/win
Second business logic;
- Submit an authentication request
- Get authentication successful response with uid=0, gid=0 for authentication request initiated in first business logic. (You can manipulate the post body using Burp Suite.)
- After a certain number of iterations, our code will be executed as root.
OMI PATCH?
Minimal patch required; Simply initialize the Auth-Header described in the OMI GitHub repository with an invalid value.
KVKK, ISO 27001, Bilgi ve İletişim Güvenliği Rehberi, ISO 27701, Bilgi Güvenliği, Siber Güvenlik ve Bilgi Teknolojileri konularında destek ve teklif almak için lütfen