获取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>

⚠️ 本文仅用于帮助用户找回自己的密码,请勿用于非法用途。

posted @ 2026-07-24 10:38  tangoboy  阅读(10)  评论(0)    收藏  举报