Difficulty: Medium

AWS keys were found exposed in a git repository on the webserver.
Using those keys, the AWS command line was exploited to access cloud functions and retrieve a secret.
That secret was then used to exploit the site for code execution with SSTI and gain an initial shell.
Finally, a backup script was abused to escalate to root and capture the flag.

Nmap

The nmap scan revealed three open ports:

obraz

Port 80 - Website

obraz

As can be seen on nmap scan this website contains .git directory.
It can be dumped to kali using git-dumper tool.

https://github.com/arthaud/git-dumper

obraz

Directory contains source code:
obraz

Good thing to start with when enumerating .git directory is to look at git commits.
It can be done with git log command:

obraz

With git diff we can compare previous commits and look at the changes done.
One that turned out to be intresting was:

git diff c622771686bd74c16ece91193d29f85b5f9ffa91 7cf92a7a09e523c1c667d13847c9ba22464412f3

obraz

It exposed AWS secret key:

  • aws_access_key_id=’AQLA5M37BDN6FJP76TDC’
  • aws_secret_access_key=’OsK0o/glWwcjk2U3vVEowkvq5t4EiIreB+WdFo1A’

I will also add cloud.epsilon.htb to /etc/hosts.
Now I’ll analyze source code and after that we can try to use aws key.
I’ll paste the code here:

#!/usr/bin/python3

import jwt
from flask import *

app = Flask(__name__)
secret = '<secret_key>'

def verify_jwt(token,key):
        try:
                username=jwt.decode(token,key,algorithms=['HS256',])['username']
                if username:
                        return True
                else:
                        return False
        except:
                return False

@app.route("/", methods=["GET","POST"])
def index():
        if request.method=="POST":
                if request.form['username']=="admin" and request.form['password']=="admin":
                        res = make_response()
                        username=request.form['username']
                        token=jwt.encode({"username":"admin"},secret,algorithm="HS256")
                        res.set_cookie("auth",token)
                        res.headers['location']='/home'
                        return res,302
                else:
                        return render_template('index.html')
        else:
                return render_template('index.html')

@app.route("/home")
def home():
        if verify_jwt(request.cookies.get('auth'),secret):
                return render_template('home.html')
        else:
                return redirect('/',code=302)

@app.route("/track",methods=["GET","POST"])
def track():
        if request.method=="POST":
                if verify_jwt(request.cookies.get('auth'),secret):
                        return render_template('track.html',message=True)
                else:
                        return redirect('/',code=302)
        else:
                return render_template('track.html')

@app.route('/order',methods=["GET","POST"])
def order():
        if verify_jwt(request.cookies.get('auth'),secret):
                if request.method=="POST":
                        costume=request.form["costume"]
                        message = '''
                        Your order of "{}" has been placed successfully.
                        '''.format(costume)
                        tmpl=render_template_string(message,costume=costume)
                        return render_template('order.html',message=tmpl)
                else:
                        return render_template('order.html')
        else:
                return redirect('/',code=302)
app.run(debug='true')

There are a few routes but every one of them calls verify_jwt() function.
This function look like this:

def verify_jwt(token,key):
        try:
                username=jwt.decode(token,key,algorithms=['HS256',])['username']
                if username:
                        return True
                else:
                        return False
        except:
                return False

I tried logging in to the website on port 5000 with admin:admin, but it didn’t work.
The code must have changed since then.
Another thing that came from reading the code is the possibility of SSTI vulnerability in /order.

@app.route('/order',methods=["GET","POST"])
def order():
        if verify_jwt(request.cookies.get('auth'),secret):
                if request.method=="POST":
                        costume=request.form["costume"]
                        message = '''
                        Your order of "{}" has been placed successfully.
                        '''.format(costume)
                        tmpl=render_template_string(message,costume=costume)
                        return render_template('order.html',message=tmpl)
                else:
                        return render_template('order.html')
        else:
                return redirect('/',code=302)

It takes “costume” parameter as user input and passes it to render_template_string which is a dangerous function.

AWS command line tool - exploitation

I will install awscli to talk with the server, then configure secrets that we previously found in .git directory:
obraz

We will start with listing functions:

./aws lambda list-functions --profile exploit --endpoint-url http://cloud.epsilon.htb 

obraz

There is one lambda function called “costume_shop_v1”.
To get more info about this function we can run:

./aws lambda get-function --function-name costume_shop_v1 --endpoint-url http://cloud.epsilon.htb --profile exploit

obraz

We now know the location of the source code.
We can go to this url and download source code:

http://cloud.epsilon.htb/2015-03-31/functions/costume_shop_v1/code

obraz

Let’s unzip it and view:
obraz

obraz

It exposed a secret:

secret='RrXCv`mrNe!K!4+5`wYq'

It was mentioed before in website source code.
We can encode this secret as JWT token, and use it to authenticate to the website.

obraz

Now we have cookie value but we don’t know the cookie name.
To get cookie name we can go back to the website code:

cat server.py

[...]
token=jwt.encode({"username":"admin"},secret,algorithm="HS256")
   res.set_cookie("auth",token)
[...]

Cookie is called auth, now go to website on port 5000.
Press f12 and add a cookie named auth and paste JWT token as value.

obraz

Port 5000 - Exploiting SSTI

Now with cookie set we can access /order directory.
obraz

We can now try to exploit previously identified SSTI vulnerability.
Server-Side Template Injection (SSTI) is a type of security vulnerability that occurs when user input is improperly handled within a server-side template engine.
We can now catch a request with burp and then manipulate costume parameter which wouldn’t be possible directly on the website.

obraz

Basic payload to test for SSTI is , if it returns as 49 it means we have SSTI working.

obraz

Now we can try to achieve code execution with this payload:

``

It worked, let’s now encode a reverse shell payload:

obraz

And now run place it into SSTI payload and start a listener.

obraz

We got a connection back!

obraz

Priv Esc

We can retrieve a flag.

obraz

First thing that came to my mind was to check current application code to look for credentials.

obraz

4d_09@fhgRTdws2

Unfortunately it is not reused, there is no second user.

There are also two new ports open 4566 and 38047, I discovered it with netstat -nvlp command.

obraz

I’ll leave them for now and proceed to look for cron jobs with pspy64.

https://github.com/DominicBreuker/pspy/releases/tag/v1.2.1

obraz

We successfully found some cron jobs running:

obraz

The one that is intresting to us is:

  • /bin/bash /usr/bin/backup.sh
  • /usr/bin/tar -cvf /opt/backups/923048034.tar /var/www/app/

Let’s take a look at this script:

obraz

The second-to-last line of code contains a security risk.
It uses tar command with -h option, meaning it will follow symlinks.

We need to wait for checksum being created and then we can create a symlink on it to any file:

obraz

There is only 5 second window to overwrite checksum file.
Now it will get archived into a .tar file which we can view without extracting it:

tar -xOf 518221472.tar opt/backups/checksum

obraz

It works, now we will try to retrieve root’s ssh key:

ln -sf /root/.ssh/id_rsa /opt/backups/checksum

obraz

And now we can view it:

obraz

With the key we can connect via ssh:

obraz

Lastly we will retrieve a flag:

obraz

Thank you for reading!


<
Previous Post
HTB Olympus (Medium) - Writeup
>
Next Post
HTB Noter (Medium) - Writeup