Skip to content

Commit bd0a7d6

Browse files
Ar3hcursoragent
andcommitted
fix: make FakeMySQL capture dir writable under Docker bind mounts
Entrypoint chowns chains-config cache (and related runtime dirs) for appuser before dropping privileges, so LOCAL INFILE captures no longer fail with AccessDeniedException on root-owned empty volume mounts. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e7550ab commit bd0a7d6

5 files changed

Lines changed: 80 additions & 5 deletions

File tree

Dockerfile

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
FROM eclipse-temurin:8u432-b06-jdk-jammy
22

3-
RUN addgroup --system appgroup && adduser --system appuser --ingroup appgroup
3+
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
44

55
WORKDIR /chains
66

77
COPY --chown=appuser:appgroup java-chains.jar /chains/java-chains.jar
88
COPY --chown=appuser:appgroup chains-config/ /chains/chains-config/
99

10-
USER appuser
10+
# Runtime-writable paths used by FakeMySQL captures / plugins / presets.
11+
# Bind mounts may overlay chains-config; entrypoint then fixes ownership.
12+
RUN mkdir -p /chains/chains-config/cache/fake-server-files \
13+
/chains/chains-config/plugins \
14+
/chains/chains-config/presets \
15+
/chains/chains-config/plugin-data \
16+
&& chown -R appuser:appgroup /chains
17+
18+
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
19+
RUN chmod 755 /usr/local/bin/docker-entrypoint.sh
1120

1221
EXPOSE 8011 58080 50389 50388 13999 3308 11527 50000
1322

23+
# Entrypoint starts as root to repair bind-mount permissions, then drops to appuser.
24+
ENTRYPOINT ["docker-entrypoint.sh"]
1425
CMD ["java", "-jar", "-Xms512m", "-Xmx2g", "-XX:+UseG1GC", "/chains/java-chains.jar"]

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ Docs: https://java-chains.github.io/en/docs/guide
2727
### Docker Compose
2828

2929
```bash
30-
# optional: place chains-config from the release tarball next to docker-compose.yml
30+
# recommended: place chains-config from the release tarball next to docker-compose.yml
31+
# (an empty ./chains-config mount still starts, but hides image-baked config/plugins)
3132
docker compose up -d
3233
# startup banner prints credentials on the "Auth" line
3334
docker logs -f java-chains | grep -i auth
3435
```
3536

3637
Open `http://your-ip:8011`
3738

39+
**FakeMySQL read files:** `FakeMySQLReadPayload` asks the **victim JDBC client** to send a path (e.g. `/etc/passwd` on the victim). Chains does **not** open that path inside the container. Captured bytes are written under `chains-config/cache/fake-server-files/` (browse in Studio → FakeMySQL Files). The image entrypoint ensures that cache dir is writable by the non-root `appuser` even when the bind mount was created as root.
40+
3841
### Docker run
3942

4043
```bash

README.zh-cn.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@
2727
### Docker Compose
2828

2929
```bash
30-
# 可选:把 release 包里的 chains-config 放到本目录旁(与 docker-compose.yml 同级)
30+
# 推荐:把 release 包里的 chains-config 放到本目录旁(与 docker-compose.yml 同级)
31+
# (空的 ./chains-config 挂载仍可启动,但会遮住镜像内置的配置/插件)
3132
docker compose up -d
3233
# 启动横幅里账号密码在 "Auth" 行
3334
docker logs -f java-chains | grep -i auth
3435
```
3536

3637
打开 `http://your-ip:8011`
3738

39+
**FakeMySQL 读文件:** `FakeMySQLReadPayload`**受害端 JDBC 客户端**去读你配置的路径(例如受害者机器上的 `/etc/passwd`),**不是**在容器内打开该路径。读回内容落在 `chains-config/cache/fake-server-files/`(Studio → FakeMySQL 文件)。镜像 entrypoint 会保证非 root 的 `appuser` 即使在 root 创建的 bind mount 下也能写入该缓存目录。
40+
3841
### Docker run
3942

4043
```bash

docker-compose.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,19 @@ services:
2121
# 可选:启动时查询公网 IP(会访问第三方 IP 回显服务)
2222
CHAINS_SHOW_PUBLIC_IP: ${CHAINS_SHOW_PUBLIC_IP:-false}
2323
volumes:
24-
# 持久化配置 / 插件 / third-libs / presets
24+
# Persist config / plugins / presets / FakeMySQL captures.
25+
# Prefer copying chains-config from the release tarball next to this file.
26+
# An empty host dir still works for writes (entrypoint chowns it for appuser),
27+
# but hides the image's baked-in chains-config — copy from the tarball for plugins.
2528
- ./chains-config:/chains/chains-config
29+
# Optional: mount a host path you want available *inside* the container for
30+
# other tooling. FakeMySQLReadPayload does NOT read host/container files itself —
31+
# the victim JDBC client reads the path you configure (e.g. /etc/passwd on the victim).
32+
# Captures land in chains-config/cache/fake-server-files/.
33+
# - /path/on/host:/chains/host-files:ro
34+
# Optional: run as a specific host UID/GID instead of image appuser.
35+
# Entrypoint skips chown when not root; ensure the volume is writable by that user.
36+
# user: "1000:1000"
2637
networks:
2738
- java-chains-net
2839

docker-entrypoint.sh

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/bin/sh
2+
# Ensure bind-mounted chains-config is writable by appuser, then drop privileges.
3+
# FakeMySQL LOCAL INFILE captures are persisted under:
4+
# chains-config/cache/fake-server-files/
5+
# Empty host mounts (./chains-config created as root:root) otherwise yield
6+
# AccessDeniedException and appear as "cannot read files" in the UI.
7+
set -eu
8+
9+
APP_USER="${CHAINS_APP_USER:-appuser}"
10+
APP_GROUP="${CHAINS_APP_GROUP:-appgroup}"
11+
CONFIG_DIR="${CHAINS_CONFIG_DIR:-/chains/chains-config}"
12+
13+
ensure_dir_writable_by_app() {
14+
dir="$1"
15+
mkdir -p "$dir"
16+
if ! runuser -u "$APP_USER" -- test -w "$dir" 2>/dev/null; then
17+
chown "$APP_USER:$APP_GROUP" "$dir" || true
18+
fi
19+
}
20+
21+
if [ "$(id -u)" = "0" ]; then
22+
mkdir -p "$CONFIG_DIR"
23+
# Host bind mounts often create the volume root as root:root (mode 755).
24+
# Chown the directory inode (not a recursive tree walk) so appuser can create
25+
# cache/plugin/preset children without rewriting user plugin contents.
26+
if ! runuser -u "$APP_USER" -- test -w "$CONFIG_DIR" 2>/dev/null; then
27+
chown "$APP_USER:$APP_GROUP" "$CONFIG_DIR" || true
28+
fi
29+
30+
ensure_dir_writable_by_app "$CONFIG_DIR/cache"
31+
ensure_dir_writable_by_app "$CONFIG_DIR/cache/fake-server-files"
32+
ensure_dir_writable_by_app "$CONFIG_DIR/plugins"
33+
ensure_dir_writable_by_app "$CONFIG_DIR/presets"
34+
ensure_dir_writable_by_app "$CONFIG_DIR/plugin-data"
35+
36+
# Previous root-owned captures under cache/ would still be unreadable; reclaim cache only.
37+
if [ -d "$CONFIG_DIR/cache" ]; then
38+
chown -R "$APP_USER:$APP_GROUP" "$CONFIG_DIR/cache" 2>/dev/null || true
39+
fi
40+
41+
if command -v setpriv >/dev/null 2>&1; then
42+
exec setpriv --reuid="$APP_USER" --regid="$APP_GROUP" --init-groups -- "$@"
43+
fi
44+
exec runuser -u "$APP_USER" -- "$@"
45+
fi
46+
47+
exec "$@"

0 commit comments

Comments
 (0)