获取FinalShell忘记的ssh密码
服务器太多,密码忘了?别慌——FinalShell 导出的配置文件里就藏着你的明文密码,一招把它揪出来。
场景
干运维的谁还没个几十台服务器?时间一长,当初设的密码早就忘得干干净净。
偏偏 FinalShell 里还存着,能连上,但密码是 ••••••••。想用别的终端连?想分享给同事?傻眼了。
其实 FinalShell 导出连接时会把密码一起导出——只不过被加密了。只要解密一下,明文就到手了。
一分钟找回密码
FinalShell 里右键连接 → 导出,会生成一个 *_connect_config.json 文件:
{
"host": "192.168.110.18",
"port": 22,
"user_name": "root",
"password": "YgxhbkVaLiQRT3dqxADSog==",
"name": "my-server"
}
password 字段这串乱码就是加密后的密码。把它粘贴到下面的在线工具里,点解密,明文立刻出来。
🔗 FinalShell 密码在线解密 (文章底部)
纯浏览器端运行,密码不会上传到任何服务器,放心使用。
加密原理(不想看可跳过)
当然,总有些好奇的同学想知道 FinalShell 到底是怎么加密的。下面展开讲讲。
整体结构
password 字段的值是一段 Base64,解码后分两部分:
[ head (8 字节) ][ 密文 (N 字节) ]
解密流程:
Base64 解码 → head[0:8] 派生 DES 密钥 → DES-ECB 解密密文 → 去 PKCS5 填充 → UTF-8 明文
加密用的不是 AES 而是 DES,密钥不是直接存的,而是通过 java.util.Random 这个伪随机数生成器从头部的 8 个字节"算"出来的。
密钥是怎么算出来的
这是整个加密最骚的地方——FinalShell 用 Java 的随机数生成器来派生密钥。
分四步:
第一步:用 head[5] 生成初始种子
long ks = 3680984568597093857L
/ (long)(new Random((long)head[5])).nextInt(127);
head[5] 这个字节作为 Random 的种子,调用一次 nextInt(127)(范围 0~126),然后用一个魔数除以它得到 ks。
第二步:用 head[0] 决定跳过次数
Random random = new Random(ks);
for (int i = 0; i < (head[0] & 0xff); i++) {
random.nextLong(); // 最多跳过 255 次
}
long n = random.nextLong();
第三步:混入 head 的其余字节,组装 64 字节数据
Random r2 = new Random(n);
long[] ld = {
head[4], r2.nextLong(), head[7], head[3],
r2.nextLong(), head[1], random.nextLong(), head[2]
};
// 大端序写入 DataOutputStream → 64 字节
第四步:MD5 → 取前 8 字节 → DES 密钥
byte[] desKey = Arrays.copyOf(md5(ldBytes), 8);
一张图总结:
head[5] ──→ Random ──→ nextInt(127) ──→ ks
│
head[0] ──→ 跳过 t 次 ←── Random(ks) ←───┘
│
├──→ nextLong() ──→ n ──→ Random(n) ──→ nextLong()
│ │
head[1..7] ─────┴──────────────────────────────────────────→ 组装 8 个 long
│
64 bytes → MD5 → DES Key(前8字节)
DES 解密
拿到 8 字节密钥后就是标准操作了:
Cipher cipher = Cipher.getInstance("DES"); // 等同于 DES/ECB/PKCS5Padding
cipher.init(Cipher.DECRYPT_MODE, desKey);
byte[] plainBytes = cipher.doFinal(encryptedData);
String password = new String(plainBytes, "UTF-8");
复刻 Java Random 的正确姿势
如果在其他语言里实现解密,最大的坑不是 DES(标准算法哪都有),而是 精确复刻 java.util.Random。
Java Random 是一个 48-bit 线性同余生成器(LCG):
seedₙ₊₁ = (seedₙ × 0x5DEECE66D + 0xB) mod 2⁴⁸
几个容易翻车的地方:
坑一:nextLong() 用 + 不是 |
// ✅ 正确
return ((long) next(32) << 32) + next(32);
// ❌ 错误——很多人习惯用 | 拼接高低位
return ((long) next(32) << 32) | (next(32) & 0xFFFFFFFFL);
next(32) 返回的是 有符号 int(-2³¹ ~ 2³¹-1)。当它为负数时,+ 会产生借位影响高 32 位,而 | 不会。差之毫厘,谬以千里。
坑二:next(32) 是有符号的,不是无符号
next(32) 范围是 [-2147483648, 2147483647],不是 [0, 4294967295]。
坑三:Java 的 byte 是有符号的
(long) head[5] // byte 0xFF 会变成 -1L,不是 255L
在 JS/Python/Go 里要手动处理:b > 127 ? b - 256 : b。
JavaScript 核心代码
贴一下关键部分,完整源码见 GitHub。
class JavaRandom {
constructor(seed) {
this.seed = (BigInt.asIntN(64, BigInt(seed)) ^ 0x5DEECE66Dn) & ((1n << 48n) - 1n);
}
_next(bits) {
this.seed = (this.seed * 0x5DEECE66Dn + 0xBn) & ((1n << 48n) - 1n);
const raw = this.seed >> (48n - BigInt(bits));
return bits === 32 ? Number(BigInt.asIntN(32, raw)) : Number(raw);
}
nextInt(bound) { /* ... */ }
nextLong() {
return (BigInt(this._next(32)) << 32n) + BigInt(this._next(32)); // 注意是 +
}
}
function decryptFinalShell(base64Password) {
const bytes = Uint8Array.from(atob(base64Password), c => c.charCodeAt(0));
const head = bytes.slice(0, 8);
const data = bytes.slice(8);
const desKey = ranDomKey(head).slice(0, 8); // DES 只用前 8 字节
const plain = desEcbDecrypt(data, desKey);
// 去掉 PKCS5 填充 → UTF-8 解码 → 明文
return new TextDecoder().decode(plain.slice(0, plain.length - plain[plain.length - 1]));
}
总结
| 组件 | 算法 | 要点 |
|---|---|---|
| 编码 | Base64 | 标准 |
| 密钥派生 | Java Random LCG + MD5 | 8 字节 head → 64 字节 → MD5 → DES Key |
| 加密 | DES-ECB + PKCS5Padding | 标准对称加密 |
| 明文 | UTF-8 |
FinalShell 的密码保护说实话就是层窗户纸——DES 本身早已不安全,密钥派生依赖的 java.util.Random 也完全不具有密码学强度。只要复刻对了 Random 的行为,解密就是几行代码的事。
所以如果你忘了服务器的密码,不妨翻翻 FinalShell 的导出文件。你的密码就在那里,等着你解密。
📦 在线解密工具(点击下面⬇️按钮,纯前端实现):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FinalShell 密码解密</title>
<style>
:root {
--bg: #1a1d23;
--card: #21252b;
--border: #333842;
--text: #abb2bf;
--accent: #61afef;
--success: #98c379;
--error: #e06c75;
--input-bg: #282c34;
--radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 600px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 32px;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.icon { text-align: center; font-size: 36px; margin-bottom: 8px; }
h1 {
text-align: center;
font-size: 20px;
font-weight: 600;
color: #e5e7eb;
margin-bottom: 4px;
}
.subtitle {
text-align: center;
font-size: 13px;
color: #5c6370;
margin-bottom: 24px;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
color: #7d8799;
margin-bottom: 6px;
}
textarea {
width: 100%;
height: 80px;
padding: 12px 14px;
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: #e5e7eb;
font-size: 14px;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
resize: vertical;
outline: none;
transition: border-color 0.2s;
}
textarea:focus { border-color: var(--accent); }
textarea::placeholder { color: #4a5058; }
.btn {
display: block;
width: 100%;
margin-top: 14px;
padding: 12px;
background: var(--accent);
color: #1a1d23;
border: none;
border-radius: var(--radius);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s, transform 0.1s;
}
.btn:hover { opacity: 0.9; }
.btn:active { transform: scale(0.98); }
.result {
margin-top: 20px;
padding: 14px;
border-radius: var(--radius);
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 16px;
word-break: break-all;
display: none;
}
.result.show { display: block; }
.result.success {
background: #1e3320;
border: 1px solid #2d4a30;
color: var(--success);
}
.result.error {
background: #331e20;
border: 1px solid #4a2d30;
color: var(--error);
}
.result-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 6px;
opacity: 0.7;
}
.copy-btn {
display: none;
margin-top: 8px;
padding: 6px 14px;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
}
.copy-btn:hover { background: #2c313a; }
.copy-btn.show { display: inline-block; }
.hint {
margin-top: 16px;
padding: 12px;
background: #1e2229;
border-radius: var(--radius);
font-size: 12px;
color: #5c6370;
line-height: 1.6;
}
.hint code {
background: #2c313a;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
color: #7d8799;
}
.hint a { color: var(--accent); text-decoration: none; }
.hint a:hover { text-decoration: underline; }
.divider {
border: none;
border-top: 1px solid var(--border);
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="icon">🔐</div>
<h1>FinalShell 密码解密</h1>
<p class="subtitle">粘贴 FinalShell 导出的 <code>_connect_config.json</code> 中 <code>password</code> 字段值</p>
<label for="encPwd">加密密码 (Base64)</label>
<textarea id="encPwd" autofocus></textarea>
<button class="btn" id="decryptBtn" onclick="doDecrypt()">解 密</button>
<div class="result" id="result">
<div class="result-label">解密结果</div>
<span id="resultText"></span>
</div>
<button class="copy-btn" id="copyBtn" onclick="copyResult()">📋 复制密码</button>
<hr class="divider">
<div class="hint">
<strong>💡 说明</strong><br>
本工具解密 FinalShell 导出的 <code>_connect_config.json</code> 中
<code>password</code> 字段。算法:Base64 → DES-ECB → UTF-8。<br>
纯浏览器端运行,密码不会上传到任何服务器。
</div>
</div>
<script src="https://files.cnblogs.com/files/tangoboy/fs-decrypt.js?t=1784790864"></script>
<script>
var currentPassword = '';
function doDecrypt() {
var input = document.getElementById('encPwd').value.trim();
var resultDiv = document.getElementById('result');
var resultText = document.getElementById('resultText');
var copyBtn = document.getElementById('copyBtn');
resultDiv.classList.remove('success', 'error', 'show');
copyBtn.classList.remove('show');
currentPassword = '';
if (!input) {
resultDiv.classList.add('error', 'show');
resultText.textContent = '请输入加密密码';
return;
}
try {
var plain = decryptFinalShell(input);
if (plain === null || plain === '') {
throw new Error('解密结果为空');
}
currentPassword = plain;
resultDiv.classList.add('success', 'show');
resultText.textContent = plain;
copyBtn.classList.add('show');
} catch (e) {
resultDiv.classList.add('error', 'show');
resultText.textContent = '解密失败: ' + e.message;
}
}
function copyResult() {
if (!currentPassword) return;
navigator.clipboard.writeText(currentPassword).then(function() {
var btn = document.getElementById('copyBtn');
var orig = btn.textContent;
btn.textContent = '✅ 已复制!';
setTimeout(function() { btn.textContent = orig; }, 1500);
});
}
// 回车解密
document.getElementById('encPwd').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
doDecrypt();
}
});
</script>
</body>
</html>
⚠️ 本文仅用于帮助用户找回自己的密码,请勿用于非法用途。
浙公网安备 33010602011771号