0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

パスワードを257文字打つだけでroot権限まで。*sshole、なんで?

0
Last updated at Posted at 2026-11-07

ポケモン金銀で、ボックスを切り替えるレポートの最中に電源を切ると、預けたはずのポケモンが手元にも残る。子どものころ、あれで増やした人は多いと思います。

ボックスに書き込む処理も、手持ちから消す処理も、単体ではどこも壊れていません。壊れていたのは処理と処理の「継ぎ目」でした。

2つの箱に同じドット絵の生き物が1体ずつ入り、間を白いひびが走る。下の進捗バーは途中で止まっている

2026年11月、これとよく似た形のバグが OpenSSH で見つかりました。名前は *sshole。「256文字を超えるパスワードを打つと、ログインできないはずのアカウントに入れてしまう」というものです。X では「Linuxに致命的な脆弱性」という見出しで回っていました。

最初に聞きたくなるのは「なんでこんなバグが?」だと思います。

先に安心材料:すべての Linux の話ではありません

見出しだけ読むと Linux 全体が危なそうに見えますが、対象はかなり狭いです。

鍵になるのは C ライブラリです。ファイル操作や文字列の比較、パスワードのハッシュ化など、Linux 上のほぼすべてのプログラムが共通で使う部品集です。Linux では主に2系統あり、Ubuntu や Debian、RHEL は glibc、Alpine Linux や OpenWrt は musl を使っています。今回の穴が開くのは musl の上だけです。

そのうえで、次の条件が全部揃ったときだけ起きます。

条件 よくある環境
C ライブラリが musl Alpine Linux、OpenWrt など
OpenSSH を PAM なしで動かしている Alpine の openssh-server は PAM なし(PAM 版は openssh-server-pam という別パッケージ)
PasswordAuthentication が有効 鍵認証だけなら対象外
パスワード欄が * のアカウントがある ログイン禁止のアカウント
パスワードハッシュを持つ別のアカウントがある 普通にパスワードを設定したユーザー

コラム:条件表のわかりにくい行

1. C ライブラリが musl
同じ crypt() という名前の関数でも、glibc と musl では中身が別物です。失敗したときに "*" を返すのは musl のほうです。Docker で FROM alpine と書けば、中は musl になります。

2. OpenSSH を PAM なしで動かしている
PAM は、Linux のログイン確認を一手に引き受ける窓口です。Ubuntu などの sshd は「この人を通していい?」と PAM に聞くだけで、自分ではパスワードを比べません。PAM なしの sshd は、自分で crypt() を呼んで /etc/shadow の値と文字列で比べます。今回の穴はこの「自分で比べる」経路にあるので、PAM ありならこの経路を通りません。

3. パスワード欄が * のアカウントがある
/etc/shadow は1行1アカウントで、: 区切りの2番目がパスワードのハッシュです。ここが * のアカウントは、パスワードでのログインを禁止されています。システム用のアカウントや、鍵認証専用のユーザーによく使われる書き方です。今回入れてしまうのは、このアカウントです。

4. パスワードハッシュを持つ別のアカウントがある
本文の「なんで?③」で説明する、OpenSSH が計算方式を借りる相手です。$6$... のような正しいハッシュを持つアカウントが1つでもあると、条件が揃います。

逆に、次の環境は対象外です。

  • Ubuntu、Debian、RHEL などの glibc 系(PAM を通るので経路が違う)
  • PasswordAuthentication no にして鍵認証だけで運用しているサーバー
  • sshd を動かしていないコンテナ(Alpine のコンテナの大半はこれ)

root への直接ログインは、OpenSSH の標準設定で止まります。PermitRootLogin prohibit-password が、root のパスワードログインを許さないからです。報道で「root を奪える」とされているのは、sudo や doas を使えるアカウントに入れた場合の話です。

対処は OpenSSH 10.6p2 への更新です。すぐ更新できないなら、パスワード認証を切って sshd を再起動すれば塞がります。

ここからが本題の「なんで?」です。

なんで?① musl は親切のつもりで "*" を返していた

パスワードの照合には、昔から crypt() という関数を使います。入力したパスワードを、保存されているハッシュと同じ方式で計算し直して、文字列として比べる仕組みです。

musl の crypt() には、入力の長さに上限があります。SHA-256 / SHA-512 方式では256バイトです。とても長い入力で計算させ続ける DoS を防ぐための、まっとうな制限です。

上限を超えたらどうするか。musl のソースの冒頭コメントにはこう書いてあります。

/*
 * public domain sha512 crypt implementation
 * ...
 * on error "*" is returned.
 */

失敗したときに返すのは NULL でなく "*" です。これは親切な設計です。呼び出し側が NULL チェックを忘れていてもクラッシュしません。しかも "*" は、どんな正しいハッシュとも一致しません。比べても必ず「不一致」になる、安全側の値のはずでした。

この記事では、あえて「親切」という言葉を使います。ここから出てくる部品は、どれも誰かがよかれと思って作ったものです。一つひとつを見ても、手抜きや悪意は見当たりません。それでも穴が開いた、というのがこのバグの一番おもしろいところです。

なんで?② shadow の * も同じ親切だった

一方、/etc/shadow のパスワード欄にも * が入ることがあります。これは昔からの「パスワードでログインさせない」という印です。システム用のアカウントや鍵認証専用のユーザーに使われます。

なぜ * なのか。理由は musl とまったく同じで、どんな正しいハッシュとも一致しない文字列だからです。crypt() がどんな入力から計算しても * 1文字にはならないので、照合は必ず失敗します。

並べるとこうなります。

値 意味 選ばれた理由
musl の crypt() "*" 計算に失敗した どのハッシュとも一致しないから
/etc/shadow * ログイン禁止 どのハッシュとも一致しないから

どちらも「ハッシュとは絶対に一致しない値」として選ばれています。誰も想定していなかったのは、この2つどうしを比べる場面でした。

なんで?③ 2016年の良かれと思った変更が、2つを出会わせた

もうひとつの部品が OpenSSH です。

ログインに失敗したとき、ユーザーが存在しない場合だけ応答が速いとします。すると時間を測るだけで、「このユーザー名は存在する」と推測されてしまいます。そこで OpenSSH は、存在しないユーザーやログイン禁止のユーザーでも、わざと crypt() を走らせるようになりました。応答時間を揃えるためです。The Register の報道によれば、この対策は2016年からです。

ただ、保存値が * 1文字だと、ハッシュ計算に使う salt(計算方式の指定)として使えません。そこで OpenSSH は、他のアカウントの正しいハッシュから方式を借りて計算します。ソースのコメントにも「salt がなければタイミング対策のための偽の計算だ」と書いてあります。

/*
 * If we don't have a salt we are encrypting a fake password for
 * for timing purposes.  Pick an appropriate salt.
 */

ここまで揃うと、継ぎ目がつながります。

アスタリスク(*)は、ギリシャ語で「小さな星」という意味です。musl の "*" と shadow の *。別々の場所で生まれた2つの星が、OpenSSH という天の川を渡って出会ってしまいました。七夕なら年に一度ですが、こちらは10年かかっています。

紺の夜空に縦に流れる天の川。左右に小さな星が1つずつあり、川の真ん中に大きな星が強く光っている。両端に笹のシルエット

修正前の OpenSSH の判定は、要約すると次の1行でした。

return encrypted_password != NULL &&
    strcmp(encrypted_password, pw_password) == 0;

musl は NULL を返さないので、1つ目のチェックは通ります。2つ目の strcmp は "*" と "*" を比べるので一致します。「失敗」と「禁止」が、同じ1文字で書かれていました。

冒頭の条件表にあった「パスワードハッシュを持つ別のアカウント」は、この「方式を借りる」部分から来ています。

修正と教訓:「無効」どうしを等しいとみなさない

OpenSSH 10.6p2 の修正(コミット 4353fdc)は小さなものです。保存されている値が4文字未満なら、一致とはみなさない条件が1行足されました。

-	return encrypted_password != NULL &&
+	ret = encrypted_password != NULL &&
+	    strlen(pw_password) >= MIN_PASSWORD_HASH_LEN &&
 	    strcmp(encrypted_password, pw_password) == 0;

MIN_PASSWORD_HASH_LEN は 4 です。* や ! のような印は、比較の前に弾かれます。

コミットメッセージにはこう書かれています。「根本原因は OpenSSH の中にあるとは考えていないが、回避できる立場にある」。どの部品が悪いとも言い切れないので、継ぎ目に一番近い OpenSSH が蓋をしました。

glibc 系のディストリでは、libxcrypt という実装がよく使われます。そのマニュアルは、失敗時の戻り値をこう定めています。

This string will be shorter than 13 characters, will begin with a *, and will not compare equal to setting.

失敗の印が比較相手と一致しないことまで、仕様で約束しています。

この「無効な値どうしは等しくない」という考え方には、有名な先例があります。浮動小数点の NaN です。NaN == NaN が false になるのは、「計算できなかった」どうしを等しいと判定させないためです。

自分のコードに持ち帰るなら、次の3点だと思います。

  • エラーを表す値("*"、-1、空文字、null)を、正常な値と同じ型・同じ比較で扱わない
  • 番兵の値を選ぶとき、別の層が同じ文字を別の意味で使っていないか確認する
  • 比較の前に、「どちらかが失敗の印なら不一致」と先に判定する

まとめ

*sshole は、次の3つがそれぞれ正しく動いた結果でした。

  1. 長すぎる入力を断った musl
  2. ログイン禁止に * を使う shadow の慣習
  3. ユーザー名を推測させないために、禁止アカウントでも計算を走らせた OpenSSH

どれも「一致しないはずの値」を選んでいたのに、その2つが同じ文字でした。電源切り増殖と同じく、部品の中には穴がなく、穴は継ぎ目にありました。

Alpine で sshd を動かし、パスワード認証を有効にしている方は要注意です。10.6p2 への更新か、パスワード認証の停止をおすすめします。それ以外の Linux は、慌てなくて大丈夫です。

あなたのコードで返している -1 や空文字は、どこか別の層で「正しい値」として読まれていないでしょうか。似た衝突を見たことがあれば、コメントで教えてください。

X(@kenimo49)でも、こういう「えっ、なんで?」な技術の話を発信しています。よければフォローしてください。Qiita のフォローでも、新しい記事の通知が届きます。

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?