sshole

sshole

back in old unix your password hash just sat in /etc/passwd, right next to your username. in a file everyone could read.

在早期的 Unix 系统中,你的密码哈希值就直接存放在 /etc/passwd 文件里,紧挨着你的用户名,而且这是一个所有人都能读取的文件。

in 1980 bill joy put a * there for an account called daemon. nobody’s supposed to ever log in as it.

1980 年,Bill Joy 在一个名为 daemon 的账户密码字段中放入了一个 *。这个账户本就不应该被任何人登录。

a * isn’t a hash, so no password can match it. unix still does this today

* 并不是一个哈希值,因此没有任何密码能与之匹配。直到今天,Unix 系统依然沿用这种做法。

the next field is the real name. he wrote “the devil himself”

紧接着的字段是真实姓名。他写的是“魔鬼本人”。

ok so these days the hash lives in /etc/shadow and looks like $6$abc$k2P9…. the $6$ means sha512, abc is the salt, the rest is the actual hash

好了,如今哈希值存放在 /etc/shadow 中,格式看起来像 $6$abc$k2P9…。其中 $6$ 代表 sha512 算法,abc 是盐值(salt),剩下的部分则是真正的哈希值。

you type your password, sshd hashes it with that same salt and checks if it comes out identical. same = you’re in. that’s it

当你输入密码时,sshd 会用同样的盐值对其进行哈希处理,并检查结果是否完全一致。一致即代表登录成功,就是这么简单。

and if the field is just *? there’s nothing to match. type whatever you want, it’s never getting you in

如果该字段仅仅是一个 * 呢?那就没有任何东西可以匹配。无论你输入什么,都永远无法登录。

one more rule. if crypt() can’t do its job, it returns NULL. like if the salt asks for some algorithm the system doesn’t have, there’s nothing to compute, so you get NULL instead of a hash

还有一个规则:如果 crypt() 函数无法完成任务,它会返回 NULL。例如,如果盐值要求使用系统不支持的算法,由于无法计算,你得到的就会是 NULL 而不是哈希值。

except a lot of programs never check for NULL. they just compare whatever crypt() gives back with the stored field, hit the NULL, and crash

但问题在于,许多程序从不检查 NULL。它们只是将 crypt() 返回的任何结果与存储的字段进行比较,一旦遇到 NULL 就会崩溃。

so musl (the libc under alpine) goes ok fine, crypt() just won’t return NULL anymore. if it fails, it returns a single character instead. that character can never match a real password, so programs don’t crash, they just see a wrong password

因此,musl(Alpine 系统下的 libc 库)决定:好吧,crypt() 不再返回 NULL 了。如果失败,它会返回一个单字符。这个字符永远无法匹配真实的密码,所以程序不会崩溃,只会识别为密码错误。

now guess which character they picked for that

现在猜猜他们选了哪个字符?

yep. the star. again.

没错,又是星号。

“should not happen” gone, NULL gone, ”*” in

“不应发生”的情况消失了,NULL 消失了,* 取而代之。

then musl adds sha512 crypt. the thing is, sha512crypt gets slower the longer the password is, so someone could send one giant password and keep your server busy for a long time. so musl just caps it at 256 bytes and refuses anything longer

随后 musl 加入了 sha512 加密。问题在于,sha512crypt 的速度会随着密码长度增加而变慢,因此有人可能会发送一个超长密码,让你的服务器长时间处于忙碌状态。所以 musl 将长度限制在 256 字节,拒绝处理更长的密码。

twenty days after the ”*” commit btw

顺便提一下,这距离 * 的提交仅仅过了二十天。

so past 256 bytes crypt fails. and when crypt fails… it says ”*”.

所以,超过 256 字节时 crypt 会失败。而当 crypt 失败时……它返回的是 *。

literally in the comment the whole time

这在注释里写得清清楚楚。

CVE-2016-6210. ask sshd about a user that doesn’t exist and it says no instantly, cause there’s nothing to hash. real users take a few ms longer. so just time it and you know who exists

CVE-2016-6210 漏洞:向 sshd 查询一个不存在的用户,它会立即拒绝,因为没有东西可以哈希。而真实用户则需要多花几毫秒。所以只要测量时间,你就能知道谁是系统用户。

the fix is kinda funny. no real hash to check? sshd grabs some other account’s salt and hashes your password anyway, just to waste the same amount of time. still a no, just a slower no

修复方法有点滑稽。没有真实的哈希值可供检查?sshd 会抓取另一个账户的盐值并照样对你的密码进行哈希,只是为了浪费同样的时间。结果依然是拒绝,只是拒绝得慢了一些。

“for invalid users.” and also, it turns out, real users with a *.

“针对无效用户。”结果发现,对于带有 * 的真实用户也适用。

  1. turns out alpine’s docker images had no root password. at all. CVE-2019-5021. alpine fixes it by locking root with !

2019 年,人们发现 Alpine 的 Docker 镜像根本没有 root 密码。CVE-2019-5021。Alpine 通过用 ! 锁定 root 账户来修复此问题。

but without pam, ! also kills ssh keys. and cloud images log in with keys.

但在没有 PAM 的情况下,! 也会导致 SSH 密钥失效。而云镜像通常使用密钥登录。

  1. alpine 3.16 switches root to *. still locked, keys work again

2022 年,Alpine 3.16 将 root 密码改回了 *。账户依然被锁定,但密钥又可以正常工作了。

narrator: it did not.

旁白:其实并没有。

why did nobody notice? well, maybe someone did. maybe someone pasted the wrong thing into a password prompt, half a slack message or a whole ssh key, and it just let them in. you’ve never pasted the wrong thing into a password prompt?? amazing. a fresh box with no passwords set isn’t affected. a * in shadow needs a wrong try first. openssh tests on openbsd and glibc, musl doesn’t test sshd, and most distros go through pam, which never even calls crypt for a *.

为什么没人注意到?好吧,也许有人注意到了。也许有人在密码提示符中粘贴了错误的内容,比如半条 Slack 消息或完整的 SSH 密钥,然后系统就让他们进去了。你从来没在密码提示符里粘贴错过东西吗??太神奇了。一个没设置密码的新系统不受影响。shadow 文件中的 * 需要先进行一次错误的尝试才会触发。OpenSSH 在 OpenBSD 和 glibc 上进行测试,musl 不测试 sshd,而大多数发行版通过 PAM 运行,PAM 甚至从不会为 * 调用 crypt。

so as far as anyone knows, nobody noticed. as far as anyone knows.

所以据目前所知,没人注意到。据目前所知。

some people got really close though

不过有些人确实非常接近真相。

a debian dev asks glibc to copy musl’s ”*”. six years later debian moves to libxcrypt, which says “*0”, and closes the bug. one character away.

一位 Debian 开发者曾要求 glibc 复制 musl 的 *。六年之后,Debian 转向了 libxcrypt,后者返回的是 *0,并关闭了该漏洞报告。只差一个字符。

this thread is about busybox passwd breaking on a 257 character password. same ”*”, same limit. nobody says “sshd”

这个讨论串是关于 busybox 的 passwd 在处理 257 字符密码时崩溃的问题。同样的 *,同样的限制。没人提到“sshd”。

a security review fixes pick_salt(). the bug is four lines below it. four days before i found it.

一次安全审查修复了 pick_salt()。而漏洞就在它下方四行处。就在我发现它的四天前。

i was messing around with openssh, mostly the boring parts where it talks to libc, like the password code. people don’t really look at that stuff, they look at the crypto. so i built sshd every way i could think of (glibc, musl, pam, no pam, shadow, no shadow) and threw like 17,600 weird logins at it. weird hashes, broken accounts, passwords way too long

我当时在摆弄 OpenSSH,主要是那些与 libc 交互的枯燥部分,比如密码代码。人们通常不看这些,他们只关注加密算法。所以我用我能想到的所有方式(glibc、musl、PAM、无 PAM、shadow、无 shadow)构建了 sshd,并向它发起了约 17,600 次奇怪的登录尝试。包括奇怪的哈希值、损坏的账户以及过长的密码。

oh did i say “i”? sorry. i had agent swarms. a lot of them. let’s not get into it

哦,我说的是“我”吗?抱歉。我有一群代理集群。很多很多。我们就不细说了。

every build agreed on every login. except musl without pam. that one said yes to things it shouldn’t

每个构建版本在登录测试上都达成了一致,除了没有 PAM 的 musl。那个版本对一些本不该通过的登录请求说了“是”。

one debian box, one alpine box. type stuff. it all runs right here in your browser, nothing gets sent anywhere

一台 Debian 机器,一台 Alpine 机器。输入内容。一切都在你的浏览器中运行,没有任何数据被发送到别处。

crypt(pw, bob’s salt)

crypt(pw, bob’s salt)

on alpine’s official aws and azure images that’s the default alpine user, and it has passwordless doas. so it’s root

在 Alpine 官方的 AWS 和 Azure 镜像中,这是默认的 alpine 用户,并且它拥有免密码的 doas 权限。所以它就是 root。

on debian, ubuntu, fedora or anything else with glibc? going through pam? dropbear? openbsd? you’re fine

在 Debian、Ubuntu、Fedora 或任何其他使用 glibc 的系统上?通过 PAM?使用 Dropbear?OpenBSD?你很安全。

check github.com/oz9un/sshole

查看 github.com/oz9un/sshole

see a line like that? an account with no password just took a password

看到像那样的行了吗?一个没有密码的账户刚刚接受了一个密码。