网络篇12 | SSH2的应用
解决的业务问题
在禁使用STP子模式的情况下,研发一套“平台”级的程序,实现与10万多台的Linux机器交互,包含但不限于通道连接、shell命令执行、文件传输等。
具体的业务需求为:获取Linux主机上的shadow文件、以及上传shell的基线检测脚本并将检查结果下载到系统中。
协议选定
SSH2、SSH1和Telnet都是网络协议,它们在远程通信和数据传输方面发挥着重要作用,但各自具有不同的特点和用途。以下是对这三个协议的详细解释:
SSH2(Secure Shell 2,目前基本用这个)
定义与特点:
SSH2(Secure Shell 2)是一种用于计算机网络的加密协议,旨在在不安全的网络中安全地传输数据。它是SSH协议的升级版本,提供了更强大的加密和认证机制。
SSH2协议通过加密通道来传输数据,防止数据在传输过程中被窃听、篡改或伪造。它采用了公钥加密、对称加密和消息认证码等多种加密技术,保障了数据的机密性、完整性和可靠性。
SSH2还提供了强大的身份认证机制,包括密码认证、公钥认证和基于密钥的认证等,确保了通信双方的身份合法性和安全性。
SSH2协议支持多种加密算法和密钥长度,可以根据实际需求选择合适的加密方式,提高了系统的灵活性和安全性。
应用场景:
SSH2协议被广泛用于远程登录和管理服务器、网络设备。
它也用于安全文件传输等场景,支持端口转发和X11转发等功能,可以实现安全的远程访问和数据传输。
SSH1(Secure Shell 1)
定义与特点:
SSH1是SSH协议的第一个版本,由芬兰赫尔辛基工业大学的研究员Tatu Ylönen设计。
它最初提出的目的是替代非安全的Telnet、rsh、rexec等远程Shell协议。
SSH1虽然在一定程度上提高了远程通信的安全性,但随着时间的推移,其安全性漏洞逐渐暴露,因此被SSH2所取代。
应用现状:
由于SSH1存在安全漏洞,目前已被SSH2广泛替代。然而,在一些旧系统或特定场景下,SSH1可能仍然被使用,但建议尽快升级到SSH2以提高安全性。
Telnet
定义与特点:
Telnet是一种网络协议,用于远程登录到另一台计算机并执行命令。
它可以在本地与远程计算机之间建立一个虚拟终端会话,使用户可以像在本地计算机上一样操作远程计算机。
使用Telnet连接远程计算机需要知道目标计算机的IP地址或域名,并开启远程登录服务。
安全隐患:
Telnet协议本身并不提供加密功能,因此数据传输过程中存在被窃听的风险。
这使得Telnet在安全性要求较高的场景下不再适用,而是被SSH等更安全的协议所取代。
代码实现
落地方案1:ganymed-ssh2
maven坐标
中央仓库最新版本262,2014年的版本,有点旧了。
<dependency>
<groupId>ch.ethz.ganymed</groupId>
<artifactId>ganymed-ssh2</artifactId>
<version>262</version>
</dependency>
为什么选型这个第三方的实现? 此处是基于mqcloud搜狐开源的中间件管理框架,他集成这种技术用来管理rocketmq的broker与ns的发布与运维管理。
关键源代码
技术效果验证
以下效果是本机环境,通过传入ip与账户,获取服务器上jdk的版本号功能(远程登录服务器,并执行获取jdk版本的shell命令:source /etc/profile;javap -version)
以下是通过类似xshell的客户端工具,root登录后获取jdk的版本号:
到此为止,你认为成功了吗?我们发到正式环境试一下吧。
由于正式环境OpenSSH版本比较高,所以直接报错了。(原因是算法不对等)
连接高版本OpenSSH报错分析
正式环境服务器的ssh版本,OpenSSH_9.6p1支持的算法
[myhome@paas-core-hu5-a-2 ~]$ ssh -V
OpenSSH_9.6p1, OpenSSL 1.1.1k FIPS 25 Mar 2021
[myhome@paas-core-hu5-a-2 ~]$ ssh -Q kex
diffie-hellman-group1-sha1
diffie-hellman-group14-sha1
diffie-hellman-group14-sha256
diffie-hellman-group16-sha512
diffie-hellman-group18-sha512
diffie-hellman-group-exchange-sha1
diffie-hellman-group-exchange-sha256
ecdh-sha2-nistp256
ecdh-sha2-nistp384
ecdh-sha2-nistp521
curve25519-sha256
curve25519-sha256@libssh.org
sntrup761x25519-sha512@openssh.com
[myhome@paas-core-hu5-a-2 ~]$ ssh -Q mac
hmac-sha1
hmac-sha1-96
hmac-sha2-256
hmac-sha2-512
hmac-md5
hmac-md5-96
umac-64@openssh.com
umac-128@openssh.com
hmac-sha1-etm@openssh.com
hmac-sha1-96-etm@openssh.com
hmac-sha2-256-etm@openssh.com
hmac-sha2-512-etm@openssh.com
hmac-md5-etm@openssh.com
hmac-md5-96-etm@openssh.com
umac-64-etm@openssh.com
umac-128-etm@openssh.com
[myhome@paas-core-hu5-a-2 ~]$ ssh -Q cipher
3des-cbc
aes128-cbc
aes192-cbc
aes256-cbc
aes128-ctr
aes192-ctr
aes256-ctr
aes128-gcm@openssh.com
aes256-gcm@openssh.com
chacha20-poly1305@openssh.com
开发环境服务器的ssh版本,OpenSSH_7.4p1支持的算法
[root@AC-SEC-01 ~]# ssh -V
OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017
[root@AC-SEC-01 ~]# ssh -Q kex
diffie-hellman-group1-sha1
diffie-hellman-group14-sha1
diffie-hellman-group14-sha256
diffie-hellman-group16-sha512
diffie-hellman-group18-sha512
diffie-hellman-group-exchange-sha1
diffie-hellman-group-exchange-sha256
ecdh-sha2-nistp256
ecdh-sha2-nistp384
ecdh-sha2-nistp521
curve25519-sha256
curve25519-sha256@libssh.org
gss-gex-sha1-
gss-group1-sha1-
gss-group14-sha1-
[root@AC-SEC-01 ~]# ssh -Q mac
hmac-sha1
hmac-sha1-96
hmac-sha2-256
hmac-sha2-512
hmac-md5
hmac-md5-96
hmac-ripemd160
hmac-ripemd160@openssh.com
umac-64@openssh.com
umac-128@openssh.com
hmac-sha1-etm@openssh.com
hmac-sha1-96-etm@openssh.com
hmac-sha2-256-etm@openssh.com
hmac-sha2-512-etm@openssh.com
hmac-md5-etm@openssh.com
hmac-md5-96-etm@openssh.com
hmac-ripemd160-etm@openssh.com
umac-64-etm@openssh.com
umac-128-etm@openssh.com
[root@AC-SEC-01 ~]# ssh -Q cipher
3des-cbc
blowfish-cbc
cast128-cbc
arcfour
arcfour128
arcfour256
aes128-cbc
aes192-cbc
aes256-cbc
rijndael-cbc@lysator.liu.se
aes128-ctr
aes192-ctr
aes256-ctr
aes128-gcm@openssh.com
aes256-gcm@openssh.com
chacha20-poly1305@openssh.com
服务器的生效配置 sudo cat /etc/ssh/sshd_config
MACs hmac-sha1
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521
Ciphers aes128-ctr,aes192-ctr,aes256-ctr
客户端算法 ganymed-ssh2 262版本(从源码中抓取出来)
MACs getMacList() "hmac-sha1-96", "hmac-sha1", "hmac-md5-96", "hmac-md5"
KexAlgorithms getDefaultClientKexAlgorithmList() "diffie-hellman-group-exchange-sha1", "diffie-hellman-group14-sha1", "diffie-hellman-group1-sha1" ecdh-sha2-nistp256
Ciphers aes128-ctr,aes192-ctr,aes256-ctr,blowfish-ctr,3des-ctr,3des-cbc
结论就是KexAlgorithms算法不匹配
因为客户端diffie系列算法不安全,所有高版本的OpenSSH服务端已经默认去除了这些算法,如果想用也是可以的,就是容易被攻击,比如直接在服务端的sshd_config的KexAlgorithms 最后一个算法加上客户端你想用的算法,比如, “diffie-hellman-group1-sha1”。
为了安全,我只能使用加密等级比较高的非对称椭圆算法,一个选择是在262版本上继续二开,加入高版本的算法支持,或者找找别人遇到类似的问题,他升级了此包。当然还有一条路,换第三方实现,我最终换成了jsch的实现。
落地方案2:jsch
maven坐标
<dependency>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId>
<version>0.2.19</version>
</dependency>
jsch客户端功能
session: 一个基础的会话通道,通常用来建立一个基础的交互式命令行会话。
shell: 提供了一个登录shell的交互环境,允许用户执行命令。
exec: 用于执行远程命令,不提供交互式shell。
x11: 允许X11图形应用通过SSH隧道进行转发。
auth-agent@openssh.com: 用于SSH认证代理转发,这样远程服务器可以使用本地机器的SSH密钥来访问其他系统。
direct-tcpip: 用于创建一个TCP/IP端口转发通道,允许通过SSH隧道转发TCP流量。
forwarded-tcpip: 类似于direct-tcpip,但是它是由远程服务器发起的连接请求到指定的地址和端口。
sftp: 用于安全文件传输协议(SFTP),允许文件传输。useWriteFlushWorkaround是一个配置选项,可能与某些SFTP服务器的兼容性有关。
subsystem: 允许运行注册了的子系统,如sftp子系统。
direct-streamlocal@openssh.com: 用于本地流套接字的直接连接,通常用于非TCP协议或特定于平台的服务。
不墨迹,直接上源码
Controller层(接口文件内容)
@Data
public class ScriptUploadRequest {
private String fileContent;
private String filePath;
private String fileName;
private String resourceHost;
private String bastionHost;
}
@Slf4j
@Api(value = "代理层通用接口", tags = {"代理层通用接口"})
@RestController
@RequestMapping("/api/xshell")
public class BasicLineController {
@Autowired
private basicLineService basicLineService;
@ApiOperation(value = "脚本上传")
@PostMapping("/script/upload")
public AjaxResult uploadScript(@RequestBody ScriptUploadRequest suRequest) {
if(ChannelCache.isNotEmpty(testResult))
return testResult;
return basicLineService.uploadScript(suRequest);
}
}
service 层(将文件落地,连接管道,发送sh命令,执行curl回调)
/**
* 上传脚本,接受应用系统推送过来的文件内容,将文本内容写入本地,并连上Linux通道,通过curl方式回调getfile接口抓取并写入到主机的本机上
* @param suRequest fileContent 脚本文件内容
* @param suRequest filePath 脚本文件在前置服务器上保存的目录
* @param suRequest fileName 脚本文件的文件名(很重要)
* @param suRequest resourceHost 主机的IP
* @param suRequest bastionHost 堡垒机的IP(没有的堡垒机的兄弟们,直接忽略此IP)
* **/
public AjaxResult uploadScript(ScriptUploadRequest suRequest) {
try {
// 获取请求参数
// MultipartFile file = suRequest.getFile(); //也可以传文件嘛
String fileContent = AESUtil.decryptStr(suRequest.getFileContent());
String filePath = suRequest.getFilePath();
String fileName = suRequest.getFileName();
String resourceHost = suRequest.getResourceHost();
String bastionHost = suRequest.getBastionHost();
ResHosts resHosts = ProxyUtils.getResHosts(proxyProperties, bastionHost, resourceHost);
String cacheKey = ProxyUtils.getCacheKey(resHosts);
// 1. 保存文件到本地
Path path = Paths.get(filePath, fileName);
Files.createDirectories(path.getParent());// 创建父目录(如果它们不存在)
Files.write(path, fileContent.getBytes());// 写入内容到文件,如果文件不存在则创建文件
ChannelShell channelShell = JSchUtils.remoteConnectShell(cacheKey, resHosts, proxyProperties, proxyProperties.getSocketOverwrite());
log.info("通道连接...cacheKey={},channel={}" , cacheKey, channelShell.isConnected());
Thread.sleep(2000);
if(!channelShell.isConnected()){
return AjaxResult.error("连接失败");
}
// 获取当前接口的URL与端口
String callUrl = JSchUtils.getCallUrl(proxyProperties, Constants.callGetFile);
String command1 = "mkdir -p "+Constants.dirScript;
String command2 = "curl --location "+callUrl+"'?filePath="+Constants.dirproxyscript+"&filename="+fileName+"' --header 'access_token: "+Constants.access_token+"' -o "+Constants.dirScript + File.separator + fileName;
String command3 = "chmod +x "+Constants.dirScript + File.separator + fileName;
String command = command1+"&&"+command2+"&&"+command3;
AjaxResult ajaxResult = JSchUtils.remoteExecuteShell(ProxyUtils.getCacheKey(resHosts), resHosts, command, proxyProperties);
log.info("指令执行(2)...cacheKey={},channel={},msg={}" , cacheKey, channelShell.isConnected(), ajaxResult.get(AjaxResult.MSG_TAG));
return ajaxResult;
} catch (IOException | InterruptedException e) {
log.error("连接失败", e);
return AjaxResult.error("连接失败"+e.getMessage());
}
}
JSchUtils管道连接的工具类是灵魂,就免费赠送大家了(画重点)
为了这里面的实现,花了3天没日没夜的各种姿势尝试,相信遇到一个技术卡点的技术爱好者,又缺少对方系统的各种信息,这个滋味应该很酸爽,放出来的目的也是为了让有相同近况的,少走一些弯路。
package com.why.proxy.app.util;
import com.why.app.entity.ResHosts;
import com.why.app.service.ChannelCache;
import com.why.ganymed.util.Result;
import com.why.ganymed.vo.AjaxResult;
import com.why.proxy.app.ProxyProperties;
import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.springframew