Alpineの鍵専用アカウントに、パスワードを知らない相手がログインできる「sshole」|OpenSSH 10.6p2の修正と、影響する条件を確かめるスクリプト

OpenSSH 10.6p2がAlpineなどのパスワード認証の回避を修正

OpenSSHの開発チームは2026年11月5日、10.6p2を公開し、PAM(OSの認証の仕組み)を使わずに動かす一部のLinuxで起きるパスワード認証の回避を修正しました。同じ日にAlpine Linuxも勧告を出し、対象のサーバーでは、SSHのポートへ届く攻撃者がパスワードを知らなくてもログインできると説明しています。OpenSSHとAlpineが報告者として名前を挙げるÖzgün Kültekin氏は、この問題をsshole(サイトのドメインは sshhole.com)と名付けて公開しました。CVE番号はCVE-2026-123158です。CVEレコードによると、CVSSの基本値は8.1(High)です。CISAの2026年11月5日時点の評価では、悪用の状況は再現コードの公開(poc)の段階で、その後に変わることがあります。

入られるのは、鍵でしかログインしないつもりのアカウントです。Linuxは利用者のパスワードのハッシュを /etc/shadow の2番目の欄(以下、パスワード欄)に保存します。この欄が * のアカウントはどのパスワードとも一致しないはずですが、軽量なCライブラリのmuslを使う環境では一致してしまいます。muslを使う代表がAlpine Linuxで、Kültekin氏はほかにOpenWrt、Buildroot、Yoctoも挙げています。この3つは手元で試していません。

この記事は、Alpineなどmuslの環境でsshdを動かしている人に向けたものです。外へ出られないDockerのネットワークに修正前と修正後のパッケージを用意して、どの条件で入られるのかを確かめました。自分のサーバーを調べるスクリプト、修正版の一覧、更新できないときの設定、ログの調べ方をまとめます。

記事の要点

Alpineでsshdを動かしているなら、今日更新する

2026年11月7日時点の情報です。検証はAlpine 3.24とDebian 12のコンテナで行いました。

入られるアカウント

パスワード欄が *

鍵専用の利用者や、クラウドイメージの alpine 利用者です。別の利用者のパスワード欄が $5$ か $6$ で始まり、ほかの条件もそろうと入られます。

手元の結果

修正前は成立、修正後は拒否

Alpine 3.24 の 10.3_p1-r1 でログインが成立し、10.3_p1-r2 では拒否されました。

今日やること

更新してsshdを再起動

更新できなければ PasswordAuthentication no にします。そのあとログを確かめます。

5つの条件がそろうと、パスワードを知らなくてもログインされる

Alpineの勧告とOpenSSHの告知を合わせると、次の条件がすべてそろったときに、攻撃者はパスワードを知らなくてもログインできます。対象のAlpineは、勧告によると、修正前のパッケージを入れた3.16から3.24までとedgeです。

1. muslの上でsshdを動かしている

Alpine Linuxが代表です。OpenSSHの開発者は、glibcのLinuxにこの問題は存在しないと説明しています。Kültekin氏はDebian、Ubuntu、Fedoraを対象外の例に挙げています。

2. PAMを使っていない

Alpineの標準の openssh-server はPAMなしで作られています。openssh-server-pam は UsePAM no のときに当たります。設定ファイルで UsePAM yes にしていなければ既定の no になるので、PAM版を入れただけでは防げません。手元でも、PAM版の既定の設定でログインが成立しました。

3. パスワード認証が有効になっている

OpenSSHの既定は PasswordAuthentication yes です。

4. パスワード欄が * のアカウントがある

鍵専用の利用者、ディレクトリサービスの利用者、公式クラウドイメージの alpine 利用者です。root も * ですが、既定の PermitRootLogin prohibit-password がパスワードでのログインを断ります。

5. 別のアカウントのパスワード欄が $5$ か $6$ で始まる

$5$ はSHA-256、$6$ はSHA-512でハッシュにしたパスワードの印です。該当するアカウントが1つあれば足ります。

原因は、パスワードの照合の組み合わせにあります。sshdはパスワードを crypt() という関数でハッシュにして、保存された値と比べます。* のアカウントには塩(ハッシュの材料)がないので、別のアカウントの $5$ か $6$ の塩を借ります。muslの crypt() は、長すぎる文字列を渡すと失敗し、失敗の印として * を返します。その結果、計算結果の * と保存された * が一致します。OpenSSHの修正は、* のような短い値を比較の対象から外すものです。OpenSSHの開発者は、根本の原因はOpenSSHの外にあり、sshdの側で回避できる問題だと説明しています。同じメールで、対象のシステムではすぐにパスワード認証を切るかSSHを止めるよう呼びかけています。

条件を変えて確かめた結果

勧告の条件どおりに動くのかを、使い捨てのコンテナで確かめました。サーバーのコンテナと接続元のコンテナを、外へ出られないネットワークにつなぎ、鍵専用のアカウントへの接続を条件ごとに試しています。

変えた条件 長い文字列でのログイン
修正前の 10.3_p1-r1、既定の設定で、パスワード欄が * のアカウントログイン成立
修正後の 10.3_p1-r2 に替える拒否
パスワード欄が $6$ のアカウントをなくす拒否
PasswordAuthentication no拒否
PAM版(openssh-server-pam)で UsePAM yes拒否
PAM版で UsePAM noログイン成立
Debian 12(glibc、OpenSSH 9.2p1)で UsePAM no、パスワード欄が $6$ のアカウントあり拒否
接続先がパスワード欄 ! のアカウント拒否
接続先がパスワード欄 $6$ のアカウント拒否
接続先が root(既定の PermitRootLogin prohibit-password)拒否
接続先が root(PermitRootLogin yes)ログイン成立

macOS 27.0.1のDocker 29.5.3、alpine:3.24.4、2026年11月7日。同じ接続元から続けて試すため PerSourcePenalties=no で起動しました(Debianの9.2p1にはこの設定がないので付けていません)。接続先を書いていない行は、パスワード欄が * のアカウントへの結果です。正しいパスワードでのログインが成立することも、対照として確かめています(PasswordAuthentication no の行と、パスワード欄が $6$ のアカウントをなくした行を除く)。

修正版に替えるか、パスワード認証を切れば、ログインは止まりました。パスワード欄が ! のアカウントはロックされた状態で、同じ条件でも入られません。ただし、鍵でのログインもできなくなるので、* を ! に変える方法は回避策に向きません。root は既定の設定で守られ、PermitRootLogin yes にしているサーバーでは入られます。

接続できる利用者が doas で管理者になれる場合は、root の設定にかかわらず管理者の権限まで届きます。Alpineの勧告は、公式のtiny-cloudイメージ(3.18.11〜3.24.3)の alpine 利用者をその例に挙げています。この利用者はパスワードなしで doas を使えます。イメージは起動した時点では安全で、管理者がコンソール用のパスワードを設定するなどして $6$ のハッシュが生まれると条件がそろいます。cloud-initで設定するイメージは PasswordAuthentication no になるので対象外です。

自分のサーバーを調べるスクリプト

次のスクリプトは、5つの条件とパッケージの版を調べて、すべて当たるかを表示します。読むだけで設定は変えません。/etc/shadow を読むので root で実行します。最後に、パスワード欄が * のアカウントへのパスワードログインがログにあれば表示します。判定が「当たらない」でも、条件はあとから利用者を足すだけで変わるので、修正版でなければ更新します。

#!/bin/sh
# OpenSSH の sshole(CVE-2026-123158)の条件を調べる。読むだけで、設定は変えない。
[ "$(id -u)" = 0 ] || { echo "root で実行してください"; exit 1; }
miss=0
for b in /usr/sbin/sshd /usr/sbin/sshd.pam /usr/local/sbin/sshd; do [ -x "$b" ] && sshd=$b; done
[ -z "$sshd" ] && { echo "sshd: 見つからない"; exit 0; }
cfg=$("$sshd" -T 2>&1) || { echo "sshd -T に失敗: $cfg"; exit 1; }

# 1. musl か
if ls /lib/ld-musl-*.so.1 >/dev/null 2>&1; then echo "1. musl: はい"; else echo "1. musl: いいえ"; miss=1; fi

# 2. PAM を使っていないか
pam=$(echo "$cfg" | awk '$1=="usepam"{print $2}')
if [ "$pam" = yes ]; then echo "2. PAM: 使っている"; miss=1; else echo "2. PAM: 使っていない"; fi

# 3. パスワード認証が有効か(Match の中の設定は sshd -T の全体の値に出ない)
pa=$(echo "$cfg" | awk '$1=="passwordauthentication"{print $2}')
echo "3. PasswordAuthentication: $pa"
[ "$pa" = no ] && miss=1

# 4. パスワード欄が * のアカウント(root はパスワードでのログインを許す設定のときだけ数える)
stars=$(awk -F: '$2=="*"{print $1}' /etc/shadow /etc/passwd | sort -u | tr '\n' ' ')
prl=$(echo "$cfg" | awk '$1=="permitrootlogin"{print $2}')
targets=$(for u in $stars; do [ "$u" != root ] || [ "$prl" = yes ] && echo "$u"; done | tr '\n' ' ')
echo "4. パスワード欄が * のアカウント: ${targets:-なし}(PermitRootLogin $prl)"
[ -z "$targets" ] && miss=1

# 5. パスワード欄が $5$ か $6$ で始まるアカウント
hashes=$(awk -F: '$2 ~ /^\$[56]\$/{n++} END{print n+0}' /etc/shadow)
echo "5. パスワード欄が \$5\$ か \$6\$ で始まるアカウント: ${hashes} 件"
[ "$hashes" = 0 ] && miss=1

# 6. Alpine の修正版か
if [ -f /etc/alpine-release ]; then
  case $(cut -d. -f1,2 /etc/alpine-release) in
    3.24) fixed=10.3_p1-r2 ;; 3.23) fixed=10.2_p1-r1 ;; 3.22) fixed=10.0_p1-r11 ;;
    3.21) fixed=9.9_p2-r1 ;; 3.20) fixed=9.7_p1-r6 ;; 3.19) fixed=9.6_p1-r3 ;;
    3.18) fixed=9.3_p2-r4 ;; 3.17) fixed=9.1_p1-r7 ;; 3.16) fixed=9.0_p1-r6 ;;
    *) fixed=10.6_p2-r0 ;;
  esac
  cur=$(apk info -v 2>/dev/null | sed -n 's/^openssh-server-common-\([0-9]\)/\1/p' | head -1)
  echo "6. openssh: ${cur:-不明}(修正版 ${fixed})"
  case "$(apk version -t "$cur" "$fixed" 2>/dev/null)" in '='|'>') miss=1 ;; esac
else
  echo "6. Alpine 以外: $("$sshd" -V 2>&1 | head -1)"
fi

if [ "$miss" = 0 ]; then echo "判定: すべての条件に当たる。すぐに更新する"
else echo "判定: 今は当たらない条件がある。修正版でなければ更新する"; fi

# 記録: パスワード欄が * のアカウントへのパスワードログイン
logs=$(ls /var/log/messages* 2>/dev/null)
if [ -z "$logs" ]; then
  echo "ログ: /var/log/messages がない(別の場所を調べる)"
else
  found=0
  for u in $stars; do
    for f in $logs; do
      case "$f" in *.gz) zcat "$f" ;; *) cat "$f" ;; esac | grep "Accepted password for $u " && found=1
    done
  done
  [ "$found" = 0 ] && echo "ログ: 残っている範囲に該当なし(調べたファイル: $(echo $logs))"
fi

修正前の 10.3_p1-r1 を入れたコンテナでは、次のように表示されました。修正版、UsePAM yes、PasswordAuthentication no、Debian 12 の各コンテナでは「今は当たらない条件がある」と表示され、root 以外で実行すると止まることも確かめています。条件の調べ方は、Kültekin氏が公開しているcheck.sh(MITライセンス)を参考にしました。

1. musl: はい
2. PAM: 使っていない
3. PasswordAuthentication: yes
4. パスワード欄が * のアカウント: keyonly (PermitRootLogin prohibit-password)
5. パスワード欄が $5$ か $6$ で始まるアカウント: 1 件
6. openssh: 10.3_p1-r1(修正版 10.3_p1-r2)
判定: すべての条件に当たる。すぐに更新する
ログ: 残っている範囲に該当なし(調べたファイル: /var/log/messages)

このスクリプトで見えないものが2つあります。LDAPなどのディレクトリサービスの利用者は /etc/shadow に現れませんが、勧告では対象とされています。Match で一部の接続だけパスワード認証を許している場合は、全体の値が no と表示されるので、sshd -T -C user=利用者名,host=接続元の名前,addr=接続元のIP で確かめます。圧縮されたログ(.gz)はスクリプトが zcat で読みます。別の場所に保存している場合は、そちらも調べます。

修正版へ更新してsshdを再起動する

Alpineでは次のコマンドで更新し、sshdを再起動します。apk upgrade -a はopenssh以外のパッケージも更新します。修正を含むリリースは3.21.10、3.22.8、3.23.8、3.24.4です。サポートの終わった3.21と、それ以前の3.16までにも修正版のパッケージが公開されています。3.15以前は勧告の表にないので、新しいリリースへ移すか、パスワード認証を切ります。

apk upgrade -U -a
rc-service sshd restart
Alpineのブランチ 修正された openssh の版
edge10.6_p2-r0
3.2410.3_p1-r2
3.2310.2_p1-r1
3.2210.0_p1-r11
3.219.9_p2-r1
3.209.7_p1-r6
3.199.6_p1-r3
3.189.3_p2-r4
3.179.1_p1-r7
3.169.0_p1-r6

出典: Alpine Linuxの勧告(2026年11月5日)。

Alpineは古い版へ修正を移しているので、10.6より小さい版番号でも修正版があります。sshd -V の表示は修正の前後でどちらも OpenSSH_10.3p1 で変わらないので、パッケージの版で確かめます。更新のあとはもう一度スクリプトを実行し、6の版が修正版以上であることを確かめます。

Alpineの勧告は、パッケージを更新しても動いているsshdは置き換わらないので、再起動が必要だと書いています。手元の3.24では、動かしたままの修正前のsshdへ 10.3_p1-r2 を入れたあと、新しい接続は拒否されました。接続ごとに起動する sshd-session が新しいファイルになるためと考えられます。試したのはこの版だけで、待ち受けているプロセスは古いままなので、勧告のとおり再起動します。

コンテナでsshdを動かしているときは、キャッシュを使わずにイメージを作り直し、動いているコンテナを入れ替えます。手元では、この方法で 10.3_p1-r2 が入りました。sshdを動かさないコンテナは、勧告で対象外とされています。OpenWrt、Buildroot、Yoctoで openssh を使っているときは、それぞれの配布元の更新を確かめます。

docker build --pull --no-cache -t イメージ名 .
docker run --rm イメージ名 apk info -v | grep '^openssh-server-common-[0-9]'

更新できないときはパスワード認証を切る

すぐに更新できないサーバーでは、パスワード認証を切ります。パスワードでログインしている利用者も締め出されるので、全員が鍵へ移れることを先に確かめます。作業の前に、自分の鍵でログインできることを確かめ、新しいログインを試すまで今の接続を閉じないでください。次のコマンドは勧告に書かれたもので、root で実行します。PAM版だけを入れたサーバーには sshd コマンドがないので、最後の2行は sshd.pam に置き換えます。

mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
sed -i.bak -e '1i PasswordAuthentication no' \
  -e 's/^\(\s*PasswordAuthentication[^y#]*\)yes/\1no/i' \
  /etc/ssh/sshd_config
sshd -t && rc-service sshd restart
sshd -T | grep -i passwordauthentication

最後に passwordauthentication no と表示されれば設定が効いています。手元では、再起動の部分を手で行って同じ手順を試し、表示と接続の拒否を確かめました。元に戻すときは、00-no-passwords.conf を消して、/etc/ssh/sshd_config.bak を元の場所へ戻し、sshdを再起動します。

ログインされた跡をログで探す

Alpineの勧告は、/var/log/messages に、パスワードのないアカウントへの Accepted password for の行がないかを調べるよう勧めています。手元で再現したときの記録は次のとおりです。パスワード欄が * のアカウントで、この行が出るのは本来おかしなことです。

Nov  6 22:53:21 lab-log auth.info sshd-session[73]: Accepted password for keyonly from 172.20.0.2 port 47694 ssh2

上のスクリプトの最後の部分が、この行を探します。「該当なし」は、残っているログの範囲に限った結果です。BusyBoxのsyslogdは既定で200KBごとにログを入れ替え、1世代だけ残すので、古い記録は消えていることがあります。5つの条件がそろっていた時期があるサーバーで、この行が見つかったときや、その時期のログが残っていなくて入られていないと確かめられないときは、勧告のとおり、侵害されたものとして扱います。doas の使用の跡、最近作られた利用者とファイル、authorized_keys の変更を確かめ、必要に応じて、鍵とパスワードの交換やサーバーの作り直しまで検討します。

Alpineの運用者は更新と再起動を今日のうちに

順番としては、まず更新してsshdを再起動し、スクリプトで条件を調べ、ログを確かめます。スクリプトの判定にかかわらず、Alpineでsshdを動かしているなら更新します。更新のあとは6の行が修正版になるので、判定は「今は当たらない」に変わります。更新前に入られた可能性は、1から5の行がすべて当たるかと、ログで判断します。パスワードでログインしないサーバーは、更新のあとも PasswordAuthentication no にしておくと、パスワードの照合に関わる欠陥の影響を受けなくなります。

開発者のメールは、報告の翌日に公開した理由として、AIエージェントが同じ欠陥を数時間で見つけ直す例を見てきたことを挙げています。OpenSSHの告知は、修正を早く届けるために、当面はリリースの頻度を上げるとも書いています。Kültekin氏も、glibcとmusl、PAMの有無などを変えて作ったsshdに、AIエージェントを使って約17,600回の変則的なログインを試して見つけたと説明しています。

参考資料