FinalShell 已保存密码还原:Base64+DES 解密与批量导出
发布时间:2026/10/7 1:09:59 作者:尧图编辑部 阅读量:1,286

开场一条服务器密码忘了但 FinalShell 里还留着上周三晚上十一点多同事在群里发了条消息某台跳板机的 root 密码他改完忘了记现在本地 FinalShell 里还挂着记住密码能连上去但密码框里是一串圆圈复制不出来问我有没有办法把这串东西还原成明文。这事在运维圈里出现的频率比想象中高得多——不是黑客场景就是纯粹的自己把自己坑了改密码时随手一改改完忘了存浏览器/客户端里那个记住密码成了唯一的救命稻草。FinalShell 是很多人日常连服务器的主力工具它把连接信息包括密码一起存在本地方便下次一键登录。问题是这个密码不是明文存的直接打开配置文件看到的是一串 Base64 样子的乱码。这篇就专门聊FinalShell 已经保存的密码怎么一键找回先说清楚它存在哪、怎么存的再把还原的原理讲透然后给三套可以直接跑起来的方案从单条解密到批量导出成表格最后把我在实操中踩过的坑整理成速查表。需要先说清楚一个前提这类操作的对象只能是你自己有权限、自己保存过的连接凭据。找回自己的密码属于正常的账号自助行为但把同样的手法用在别人的机器、别人的配置上性质完全不一样。文中的方法、代码都请只在授权范围内使用这一点后面我会单独展开。我这套流程前后帮过七八个同事也覆盖 Windows、macOS、Linux 三种环境所以下文的路径、命令我都按三种系统分别写清楚你照着对号入座就行。零基础也能跑只要你愿意装一个 JDK。1. FinalShell 的凭据到底躺在哪个角落很多人第一步就卡住因为找不到配置文件。FinalShell 的存储位置跟版本、安装方式、操作系统都有关系我把它拆开讲。1.1 三个常见路径按系统对号入座老版本大致 3.x 早期习惯把数据放在安装目录下新版本4.x 及以后更规范放到了用户数据目录。如果你装过多个版本可能两处都有残留建议都看一眼。下面这张表是我实际验证过的位置操作系统版本形态典型路径Windows4.x新版C:\Users\你的用户名\AppData\Local\finalshell\connWindows3.x旧版/绿色版安装目录\conn比如D:\FinalShell\connmacOS4.x~/Library/FinalShell/conn或~/.finalshell/connLinux通用~/.finalshell/connWindows 上有个小技巧直接在文件资源管理器地址栏敲%LOCALAPPDATA%回车进去找finalshell文件夹比一层层点要快。macOS 和 Linux 上ls -la ~ | grep -i final先扫一遍隐藏目录基本能定位。如果两个位置都没有说明你用的是便携版且数据被挪到了别处可以用系统自带的文件搜索功能全盘搜一个文件名特征比如*_connect_config.json很快能揪出来。提示在动任何文件之前先把整个 conn 目录复制一份到别处做备份。后面不管是解密还是误操作备份都是唯一的退路。1.2 单个连接文件里都存了什么进入 conn 目录你会看到一堆以 UUID 命名的 json 文件通常形如xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx_connect_config.json一个文件对应一条连接。用任意文本编辑器打开系统自带的记事本就够别用会美化格式的高级工具里面大致是这些字段字段名含义是否加密host服务器地址否明文port端口否明文user_name登录用户名否明文password登录密码是secret_key私钥文件路径否passphrase私钥密码是name你给这条连接起的备注名否明文看出来了吗host、user_name、name这些全是透明的唯独password和passphrase被处理过。所以定位很简单找到你要还原的那条连接靠name或者host认把password字段那一长串值拷出来。注意name字段是中文时某些编辑器会显示成转义字符如\u6d4b\u8bd5别慌那只是 Unicode 转义不影响密码字段的复制。1.3 密码字段的两层封装决定了还原思路把password的值贴到随便一个在线 Base64 解码器里你会发现能解出二进制但不是可读文字。原因在于它做了两层处理先加密再把加密后的二进制做 Base64 编码变成字符串。Base64 只负责把二进制变成能塞进文本文件的字符它本身不提供任何保密性——这点很关键很多人以为看到 Base64 就等于加密了其实那只是装箱。真正的保密来自加密那一层。FinalShell 用的是对称加密里的 DES一种经典的块加密算法密钥藏在密文里。听起来有点自相矛盾——密钥跟着密文一起存那还保密吗这就要说到它的设计逻辑了下一节展开。先把结论摆出来正因为密钥是伴随密文存储的本地能用、能跑起来的机器就一定能把明文还原出来。这不是漏洞而是这类工具的通用权衡——它要防的是别人顺手看到文件内容而不是有备而来的本地取证。1.4 顺手确认一下你的版本和 jar 位置在动手写代码之前建议先确认一下安装目录。FinalShell 是 Java 写的安装目录下通常有lib文件夹里面是若干jar。这一步的意义在于如果社区流传的独立脚本在你的版本上解不出来最稳妥的兜底是直接调用它自带的解密类而调用前提是你得知道 jar 的名字和类路径。在安装目录下执行# macOS / Linux ls -l lib/ # Windows 用 dir dir lib列出 jar 之后可以用jar tf xxx.jar | grep -i des这种思路去找跟 DES 相关的类。不同版本类名不一样这里不给死结论——重点是你要养成以本地实际版本为准的习惯而不是无脑照抄网上的代码。2. 还原逻辑拆解为什么一套代码就能解出明文知其然也要知其所以然不然一旦报错你连从哪查都不知道。这一节把算法层面的逻辑讲清楚后面写代码就顺理成章了。2.1 Base64 那一层先拆箱再谈解密第一步永远是解码 Base64把字符串还原成原始字节数组。这一步几乎不会有歧义标准 Base64 编码Python 的base64.b64decode()、Java 的Base64.getDecoder().decode()都能搞定。唯一要注意的是有些值末尾带或那是标准填充不要手动删掉解码器认识它。如果遇到Illegal base64 character之类的报错八成是复制的时候多带了空格、换行或者少复制了尾巴上的字符。养成习惯复制完整字段值前后不要有多余空白。拆完箱之后你手上是一个字节数组长度通常是 8 的整数倍。为什么是 8因为 DES 的块大小就是 8 字节。2.2 前 8 字节当密钥的对称设计接下来是把字节数组切成两段前 8 个字节取出来单独放剩下的部分留给解密。切出来的这 8 个字节就是这次解密要用的 DES 密钥。DES 的密钥长度标准就是 8 字节64 位其中 8 位用于校验实际有效 56 位但接口上就是收 8 字节。所以从密文头部取 8 字节当密钥这件事长度上是刚好吻合的。剩余的字节用这个密钥做 DES 解密得到的就是明文密码。整个链条串起来是这样的Base64 字符串 → 解码成字节 → 前 8 字节当密钥 → 剩余字节用该密钥 DES 解密 → 去掉填充 → UTF-8 解码成明文。用生活化的类比它像是把一个保险箱的钥匙粘在保险箱底部。你拿到了保险箱配置文件弯腰一摸就拿到了钥匙前 8 字节打开它自然顺理成章。这种设计的目标是防止路过的人随手翻到密码在便利性和安全性之间选择了便利性。同样的思路也适用于passphrase字段——私钥密码的存储方式是一致的用同一套代码换个输入就能解。2.3 三种实现路线的取舍实际落地有三条路各有适用场景我用一张表把差异摊开路线做法优点缺点适合谁复用自带类写一小段 Java依赖 FinalShell 安装目录的 jar兼容性最好跟着官方算法走需要配 classpathjar 名要确认版本较新、脚本解不出来的人独立 Java 脚本单文件自己实现 Base64DES零依赖JDK 自带加解密库易传播算法细节要跟版本对齐大多数人首选Python 复现用 pycryptodome 复现同一套逻辑无需 JDK脚本短便于批量处理需要装第三方库填充处理要小心手上没 Java 环境、想批量的人我的建议是先试独立 Java 脚本不行再退回复用自带类。Python 版作为批量导出时的补充日常单条查询用不着。提示无论走哪条路都别盲信网上任何一份代码。加密算法的参数模式、填充方式在不同版本可能微调正确姿势是先跑一条你知道明文的密码做验证验证通过再批处理。3. 动手实操从单条到批量的完整落地前面讲原理这节直接上代码和命令。3.1 环境准备装 JDK、备份数据Java 路线需要 JDK。检查一下java -version javac -version两个都能打印版本号就行JDK 8 及以上都可以。如果提示找不到命令去装一个Windows 装完记得把bin目录加进 PATH。然后就是老规矩——备份。把整个 conn 目录复制一份# macOS / Linux cp -r ~/.finalshell/conn ~/conn_backup # WindowsPowerShell Copy-Item -Recurse $env:LOCALAPPDATA\finalshell\conn $env:USERPROFILE\conn_backup备份的意义不只是防误删还在于你可能会反复跑脚本试参数有备份就不怕原始文件被脚本意外写坏。3.2 方案A独立 Java 单文件脚本一次编译到处用新建一个文件DecodeFinalshell.java内容如下这是一份社区通用的实现思路我整理成了单文件版import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import java.security.SecureRandom; public class DecodeFinalshell { public static byte[] desDecode(byte[] data, byte[] key) throws Exception { SecureRandom sr new SecureRandom(); DESKeySpec dks new DESKeySpec(key); SecretKeyFactory keyFactory SecretKeyFactory.getInstance(DES); SecretKey securekey keyFactory.generateSecret(dks); Cipher cipher Cipher.getInstance(DES); cipher.init(Cipher.DECRYPT_MODE, securekey, sr); return cipher.doFinal(data); } public static String decodePass(String data) throws Exception { if (data null || data.isEmpty()) { return null; } byte[] buf Base64.getDecoder().decode(data); byte[] head new byte[8]; System.arraycopy(buf, 0, head, 0, 8); byte[] body new byte[buf.length - 8]; System.arraycopy(buf, 8, body, 0, body.length); byte[] plain desDecode(body, head); return new String(plain, UTF-8); } public static void main(String[] args) throws Exception { if (args.length 0) { System.out.println(用法: java DecodeFinalshell 密文字符串); return; } System.out.println(明文密码: decodePass(args[0])); } }编译和运行javac DecodeFinalshell.java java DecodeFinalshell 把配置文件里的password字段整段贴进来几个细节值得展开Cipher.getInstance(DES)这个写法不显式指定模式和填充Java 会套用默认值通常是DES/ECB/PKCS5Padding。而doFinal()在解密时会自动把 PKCS5 填充剥掉所以你拿到的是干净的明文字节不用手动处理尾巴。这是 Java 版比 Python 版省事的地方。new String(plain, UTF-8)一定要指定编码。有些老代码写成new String(bt)会跟随系统默认编码在 Windows 上解出中文密码就可能变乱码。密码里全是 ASCII 时看不出来一旦带中文或者特殊符号就暴露了。如果这条连接用的是私钥登录passphrase字段的解密方式完全一样把值贴进去就行。我用这套代码解过 20 多个连接包括中文密码、带#$符号的密码都能正常还原。3.3 方案BPython 版复现没装 JDK 也能用Python 版本更适合批量。先装依赖pip install pycryptodome脚本import base64 import sys from Crypto.Cipher import DES def decode_finalshell(data: str) - str: buf base64.b64decode(data) if len(buf) 8: raise ValueError(密文长度异常检查是否复制完整) head buf[:8] # 密钥 body buf[8:] # 密文体 cipher DES.new(head, DES.MODE_ECB) plain cipher.decrypt(body) # 手动剥离 PKCS5 填充 pad_len plain[-1] if 1 pad_len 8: plain plain[:-pad_len] return plain.decode(utf-8) if __name__ __main__: if len(sys.argv) 2: print(用法: python decode.py 密文) else: print(明文密码:, decode_finalshell(sys.argv[1]))Python 这里必须手动去填充因为pycryptodome的decrypt()不会帮你剥。判断填充的写法要注意plain[-1]是最后一个字节的整数值合法范围是 1 到 8。如果解出来的值不在这个范围说明密钥不对或者算法参数跟版本不匹配这时候不要硬着头皮[:-pad_len]会切出一堆乱码反而更难排查。我实测过一个坑某次因为密文复制时少了一个字符Base64 解码后长度不对切出来的密钥完全错位解出来是一堆不可读字符填充字节也是随机的。所以遇到乱码先怀疑输入再怀疑算法。3.4 方案C批量导出成一张对照表连接的密码一条条查太慢我一般直接批处理整个目录输出一张备注名—地址—用户名—密码的表格。下面这段 Python 做了三件事遍历 conn 目录里所有 json、解析出关键字段、解密密码并打印成表格。import base64 import json import os from Crypto.Cipher import DES def decode_finalshell(data: str) - str: if not data: return buf base64.b64decode(data) head, body buf[:8], buf[8:] cipher DES.new(head, DES.MODE_ECB) plain cipher.decrypt(body) pad_len plain[-1] if 1 pad_len 8: plain plain[:-pad_len] return plain.decode(utf-8, errorsreplace) conn_dir os.path.expanduser(~/.finalshell/conn) # 按你的实际路径改 rows [] for fname in os.listdir(conn_dir): if not fname.endswith(.json): continue path os.path.join(conn_dir, fname) try: with open(path, r, encodingutf-8, errorsignore) as f: cfg json.load(f) pwd_raw cfg.get(password, ) pwd decode_finalshell(pwd_raw) if pwd_raw else (无/已清空) rows.append(( cfg.get(name, ), f{cfg.get(host,)}:{cfg.get(port,)}, cfg.get(user_name, ), pwd, )) except Exception as e: rows.append((fname, 解析失败, , str(e))) print(f{备注名:20}{地址:28}{用户名:16}{密码}) print(- * 88) for r in rows: print(f{str(r[0]):20}{str(r[1]):28}{str(r[2]):16}{r[3]})运行前把conn_dir改成你自己的路径。输出的表格可以直接重定向到文件方便存档python export_all.py 连接清单.txt这里有个我强烈建议的做法导出的表格如果是明文密码用完立刻删。别留在桌面、别丢进网盘、更别提交到任何代码仓库。这一点比技术本身重要得多。4. 踩坑记录这些报错我基本都遇过一遍技术方案看起来简单实操里坑不少。这一节按现象—原因—处理的顺序整理最后附一张速查表。4.1 找不到 conn 目录或者目录是空的最常见的两种情况。一是版本差异路径不在你以为的地方尤其 Windows 上 3.x 和 4.x 的落点不同。二是你没真正用过保存密码功能——FinalShell 只有在勾选记住密码并成功连接之后才会把密码写进配置文件临时输入没勾选的是不落盘的。还有个小概率情况你装了多个用户账户数据在另一个账户的目录下。Windows 上确认一下当前登录名别找错人。排查顺序建议先确认功能是否真的保存过 → 再用文件搜索找*_connect_config.json→ 找到后确认字段里password是否有值。4.2 解出来是乱码或者只有半截能看乱码的根源通常有三个。第一是输入不完整复制密文时漏了头尾字符导致 Base64 解码结果长度不对切出来的密钥完全错位。第二是编码问题new String(bytes)没指定 UTF-8。第三是算法参数不匹配比如某些版本的填充方式有变化。排查办法先拿一条你百分百确定明文密码的连接做验证。如果这条能解出来说明代码没问题问题在你要解的那条上多半是复制不全。如果连确定的都解不出来那就是版本算法差异跳到 4.3。还有一个容易忽略的点别把user_name和password两个字段复制反了。用户名是明文直接解会报 Base64 解码错误——这反而是个良性报错至少告诉你复制错了。4.3 密钥方案对不上疑似版本差异这种情况不多但确实存在。当所有输入都正确、编码也指定了、还是解不出来时说明这个版本的加密细节跟通用实现有出入。这时候有两条退路一条是回到 1.4 节去安装目录的lib里找自带的解密相关类直接调用它。这是兼容性最好的方案因为算法跟程序本身完全一致不存在偏差。另一条是换用更早或更晚的 FinalShell 版本打开同一份配置再试——注意是打开看不是让你改配置。有些版本之间是向下兼容读取旧格式的。我个人的经验是绝大多数连接用独立脚本都能解出来遇到解不了的先怀疑自己复制错了真的处理不了再走自带类路线。花时间在版本考古上不如先做一次输入校验。4.4 高频问题速查表现象可能原因处理办法找不到配置文件路径随版本变化用文件搜索找*_connect_config.json配置文件里 password 为空当初没勾选记住密码无法还原只能重置服务器密码Base64 解码报错复制不完整/复制了明文字段重新完整复制确认字段没搞错解出乱码编码未指定 UTC-8 / 密钥错位指定 UTF-8检查密文长度填充字节异常算法参数与版本不符改用自带 jar 里的类中文密码变问号终端编码问题输出重定向到 UTF-8 文件再查看多条连接批量失败个别文件格式异常单条 try-catch 跳过不让整体中断注意如果你在服务器上改过密码但本地从没重新连接保存过那配置文件里存的是旧密码解出来也登录不上。这种情况就不是找回而是重置得走服务器侧的密码重置流程。4.5 两个不常被提到的小技巧第一个是时间戳辅助定位。conn 目录下的文件修改时间大致对应你最后一次编辑这条连接的时间。如果你要找的是最近改过密码的那台按时间排序比一个个打开快得多。Linux/macOS 用ls -ltWindows 资源管理器切到详细信息按修改日期排。第二个是备注名规范。如果你平时给连接起的名字是测试机1测试机2这种事后根本对不上是哪台。我后来改成项目-环境-用途-机房的格式比如支付-预发-订单服务-北京找回密码的时候一眼就能认出目标省下大量翻找时间。这个习惯的收益比任何脚本都大。5. 找回之后该做的事把凭据管理做扎实密码找回来只是止血真正该做的是让这种事不再发生。这一节聊三件我认为比技术更重要的事。5.1 先把边界划清楚这类操作的适用范围只有一条你自己拥有或已获明确授权的设备与凭据。找回自己保存过、后来忘记的登录密码属于正常的账号自助反过来读取他人机器的配置、还原他人的凭据无论技术上多容易都是不能碰的。我在团队里推过一个简单规矩涉及密码的操作在工单里留记录谁、什么时候、对哪台机器、做了什么。不是为了监控而是为了出问题时能追溯。这条规矩执行下来大家对哪些能做、哪些不能做的边界感明显更强。另外导出的明文清单属于敏感数据处理完立刻删除不要留在协作工具、聊天记录、临时目录里。删除时注意有些系统只是移到回收站得彻底清掉。5.2 本地配置文件的保护比想象中重要既然 conn 目录里的密码是可还原的那它本质上就等同于明文。这意味着任何能读到你用户目录的人或程序都可能拿到你的服务器凭据。几个可以立刻做的加固动作措施做法效果收紧目录权限Windows 只给当前用户完全控制Linux/macOSchmod 700 ~/.finalshell阻断同机其他账户读取全盘加密开启系统自带的磁盘加密设备丢失时配置不易被提取定期清理删掉不再使用的连接配置减少暴露面私钥加口令私钥登录时设置 passphrase多一道防线清屏习惯用完复制粘贴的密码后清剪贴板防止剪贴板被读取这几条都是成本极低但收益明显的。尤其是目录权限一条命令的事很多人从来没设过。5.3 长期方案找个真正的密码库FinalShell 存密码是为了连接方便它承担不了密码管理这个职责。我的建议是把凭据的唯一权威来源放到专业密码管理工具里FinalShell 里存的只当作连接便利两者定期对齐。具体做法我摸索出一套每台机器的密码生成后先进密码库再从库里复制粘贴到 FinalShell 的连接配置里保存。这样即使某天 FinalShell 的配置丢了、或者你换了机器凭据还在库里随时能查。反过来如果只在 FinalShell 里存本地配置一丢就真的什么都不知道了。密码本身的规则也值得说一句。别用项目名年份公司缩写123这种模式这类密码在同一个人的多个系统之间高度重复一旦一处泄露横向扩散的风险很大。用密码库生成随机强密码人只需要记住密码库的主密码这是目前个人和小团队最省心的实践。提示密码库的主密码一定要单独备份且不要和密码库文件放在同一台设备上。这是整个链条里唯一的单点。最后一点个人体会这套流程我前后用了两年多最开始是给自己救急后来慢慢变成团队里约定俗成的标准动作。中间最大的体会是真正花时间的从来不是写那几行解密代码而是前面找配置文件、后面确认哪条连接对应哪台机器。算法部分照着跑一遍就熟了反倒是命名规范权限设置备份意识这些不起眼的习惯决定了你下次遇到同类问题是从容还是抓瞎。还有一点想提醒网上流传的各类解密脚本质量参差不齐很多人直接复制过来跑不通就放弃了。其实只要理解前 8 字节当密钥、剩余部分做 DES 解密、最后剥填充并按 UTF-8 转字符串这一条主线报错的时候你自己就能判断是输入问题还是算法问题不用来回试。这也是我坚持把原理放在代码前面讲的原因。如果你手上正好有一个解不出来的连接别急着去找更复杂的工具先老老实实做三件事确认密文复制完整、确认代码用了 UTF-8、拿一条已知明文的做对照验证。这三步能解决九成以上的问题剩下的那一成再考虑换自带类。等你把这套走顺一遍顺便把连接的命名和目录权限也理顺了下次就再也不用半夜在群里喊人了。