3317 字
17 分钟
NodeJs基础与安全
2025-02-21

基础#

语法#

在Nodejs中,全局变量定义方法为

var PATH=...

局部变量则依旧使用 let xxx=...

全局变量#

名称描述示例
__filename当前模块的绝对路径(包括文件名)console.log(__filename); 输出当前文件的完整路径
__dirname当前模块的目录名(不包括文件名)console.log(__dirname); 输出当前文件所在目录的路径
exports当前模块的导出对象,用于暴露模块中的功能exports.sayHello = function() { console.log('Hello'); };
module当前模块的对象,包含模块的相关信息(如 exportsfilename 等)console.log(module); 输出模块的详细信息
require用于加载其他模块的函数const fs = require('fs'); 加载内建的 fs 模块
globalNode.js 的全局对象,类似浏览器中的 window,用于存储全局变量global.myVar = 'Hello'; console.log(myVar);
process提供关于当前 Node.js 进程的信息和控制方法console.log(process.argv); 输出命令行参数
Buffer用于处理二进制数据的类const buf = Buffer.from('Hello'); console.log(buf.toString());
setTimeout()设置一个延时执行的定时器函数setTimeout(() => { console.log('Hello'); }, 2000);
setInterval()设置一个定期执行的定时器函数setInterval(() => { console.log('Every second'); }, 1000);
clearTimeout()清除由 setTimeout() 设置的定时器const id = setTimeout(() => {}, 2000); clearTimeout(id);
clearInterval()清除由 setInterval() 设置的定时器const id = setInterval(() => {}, 1000); clearInterval(id);
console提供用于输出调试信息的对象,包含如 log()error()warn() 等方法console.log('Debug message');
require.main返回启动 Node.js 应用程序时的主模块,用于检查当前模块是否是主入口if (require.main === module) { console.log('Main module'); }

模块#

模块引用方法:

const module = require('module-name');

使用npm安装包时默认安装在当前项目下的 node_modules文件夹中,若要安装在全局下则需要使用 npm install -g ...

要证明程序运行在函数内,就使用 console.log(arguments),arguments参数只在函数体里面有,arguments.callee返回的是当前的函数是谁 arguments.callee+''把这个对象变为字符串,输出完整的函数内容

exports#

module.exports 是一个非常重要的概念,它用于将模块的功能暴露给外部,使得其他文件可以通过 require() 导入和使用

module.exports 的作用: 每个模块在 Node.js 中都有一个 exports 对象,module.exports 代表模块的导出内容。通过它,你可以指定当前模块对外提供的功能。默认情况下,module.exports 和 exports 是指向同一个对象,但你可以通过修改 module.exports 来完全控制模块导出的内容。

//module1.js
module.exports = {
    add: function (a, b) {
        return a + b;
    },
    substract: function (a, b) {
        return a - b;
    }
}
//这里使用匿名函数直接返回计算结果
var module1 = require('./module1.js');
console.log(module1.add(623,126));
console.log(module1.substract(623,126));
/* 输出
749
497

export工作原理: exports 本质上是 module.exports 的引用。也就是说,在模块的加载过程中,exports 和 module.exports 默认指向同一个对象。这就意味着,当你在 exports 上添加属性或方法时,module.exports 也会反映出这些变化。 将以上例子改写:

//module1.js
exports.add = function (a, b) {
    return a + b;
}

exports.substract = function (a, b) {
    return a - b;
}

它们两个是等价的

module.exportsexports的区别? 首先需要知道node中的引用是怎么样的: 值类型传递

function modifyValue(x) { x = 100; } let a = 10; modifyValue(a); console.log(a); // 输出: 10

引用类型传递

function modifyObject(obj) { obj.name = 'Charlie'; } 
let person = { name: 'Alice' }; 
modifyObject(person); c
onsole.log(person.name); 
// 输出: 'Charlie'

对于一个引用出的对象(exports)来说,=代表的是对引用对象的修改。对 exports 的修改会影响 module.exports,反之亦然。

如果对 exports直接赋值,则会丢失对 module.exports的引用:

exports = { name: 'Alice', sayHello: function() { console.log('Hello ' + this.name); } };
//这种不行🙅‍♂️

函数#

来自菜鸟教程

声明:

function greet(name) {
    console.log(`Hello, ${name}!`);
}

默认参数 在函数声明时为参数提供默认值。

function greet(name = 'Guest') {
    console.log(`Hello, ${name}!`);
}

greet(); // Hello, Guest!
greet('Alice'); // Hello, Alice!

剩余参数

function sum(...numbers) {  
    return numbers.reduce((accnum=> acc + num, 0);  
}  
  
console.log(sum(1234)); // 10

解构参数 从对象或数组中提取数据并将其赋值给变量

function getUserInfo({ name, age }) {
    console.log(`Name: ${name}, Age: ${age}`);
}

const user = { name: 'Alice', age: 30 };
getUserInfo(user); // Name: Alice, Age: 30

箭头函数:

const greet = (name) => {
    console.log(`Hello, ${name}!`);
};

例子:

const users = [  
    { id: 1, name: 'Alice' },  
    { id: 2, name: 'Bob' }  
];  
  
const names = users.map(user => user.name);  
console.log(names); // ['Alice', 'Bob']

这里的 user => user.name相当于

function(user) {
    return user.name;
}

也就是说箭头左边的是输入的参数

异步处理#

同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去; 异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其 他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

这里跳过使用回调函数实现异步处理的方法

Promises#

什么是 Promise?#

一个 Promise 是一个对象,表示异步操作的最终完成(或失败)及其结果值的表示。Promise 是一个占位符,它会在未来某个时刻被 resolve(成功完成)或 reject(失败)赋予一个值。

Promise 的三种状态#

  1. Pending(等待中):初始状态,表示异步操作还在进行中。
  2. Fulfilled(已完成):表示异步操作成功完成,并返回了一个结果值。
  3. Rejected(已拒绝):表示异步操作失败,返回了一个错误。

这三种状态的转换是不可逆的。Promise 在一旦完成或拒绝后,就不能再进入其他状态。

Promise 的基本用法#

创建一个 Promise 对象时,你需要提供一个执行器函数(executor function)。这个执行器函数有两个参数:resolve 和 reject,分别用于处理成功和失败的结果。

const myPromise = new Promise((resolve, reject) => {
  let success = true;

  if (success) {
    resolve("操作成功!");
  } else {
    reject("操作失败!");
  }
});

使用 then 和 catch#

一旦 Promise 被解决,你可以使用 then() 和 catch() 方法来处理结果。

  • then() 用于处理 Promise 成功的结果。
  • catch() 用于处理 Promise 失败的错误。
myPromise
  .then(result => {
    console.log(result);  // 如果 Promise 成功,输出: "操作成功!"
  })
  .catch(error => {
    console.error(error);  // 如果 Promise 失败,输出: "操作失败!"
  });

链式调用#

Promise 允许链式调用,即你可以在 then() 中返回一个新的 Promise,这样就能继续处理异步操作。

const myPromise = new Promise((resolve, reject) => {
  resolve("数据获取成功");
});

myPromise
  .then(result => {
    console.log(result); // 输出: "数据获取成功"
    return new Promise((resolve) => resolve("更多的数据"));
  })
  .then(result => {
    console.log(result); // 输出: "更多的数据"
  })
  .catch(error => {
    console.error("错误:", error);
  });

静态方法#

Promise.all()#

并行,接受多个promise,全部执行则解决

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 100, "foo"));

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);  // 输出: [3, 42, "foo"]
});

Promise.allSettled()#

Promise.allSettled() 方法会等待所有 Promise 完成,不论是成功还是失败,它都会返回每个 Promise 的结果。

const promise1 = Promise.resolve(3);
const promise2 = Promise.reject("错误");

Promise.allSettled([promise1, promise2]).then((results) => {
  console.log(results);
  // 输出: 
  // [
  //   { status: "fulfilled", value: 3 },
  //   { status: "rejected", reason: "错误" }
  // ]
});

Promise.race()#

Promise.race() 返回第一个完成(无论是成功或失败)的 Promise 的结果,其他 Promise 将被忽略

const promise1 = new Promise((resolve) => setTimeout(resolve, 500, "快速"));
const promise2 = new Promise((resolve) => setTimeout(resolve, 1000, "慢速"));

Promise.race([promise1, promise2]).then((value) => {
  console.log(value); // 输出: "快速"
});

Promise.resolve() & Promise.reject()#

  • Promise.resolve(value) 返回一个已解决的 Promise。
  • Promise.reject(reason) 返回一个已拒绝的 Promise。
Promise.resolve(42).then(value => console.log(value));  // 输出: 42
Promise.reject('错误').catch(error => console.log(error));  // 输出: "错误"

async/await#

async和await是 ES2017被引用的语法糖,底层依然是通过promises实现的,它可以使得异步函数看起来像同步函数一样编写

以下是异步请求API的一个例子:

function fetchdata(url) {
    return new Promise((resolve, reject) =>{
        fetch(url) //fetch会返回一个Promise
        .then(response => {
            if (!response.ok) {
                throw new Error(response.statusText);
            }
            return response.json();
        })
        .then(data => {
            resolve(data);
        })
        .catch(error => {
            error => reject(error);
        });
    });
}

async function getData() {
    try {
        console.log("Fetching data...");
        const data = await fetchdata("https://jsonplaceholder.typicode.com/todos/1");
        //这里的await用于等待fetchData的结果
        console.log("Data fetched:",data);

    }catch (error) {
        console.log("Error fetching data:", error);
    }
}

getData();

修改 getData()使用 Promise.all实现并行:

async function getData() {
    try {
        console.log("Fetching data...");
        const [data1, data2] = await Promise.all([
            fetchdata("https://jsonplaceholder.typicode.com/posts/1"),
            fetchdata("https://jsonplaceholder.typicode.com/posts/2")
        ])
        console.log("Data fetched:", data1, data2);

    }catch (error) {
        console.log("Error fetching data:", error);
    }
}

[[Media/3463ed5338e44aca89b591652c708bba_MD5.jpeg|Open: Pasted image 20250207165253.png]]

Nodejs服务器#

路由#

const http = require('http');

const server = http.createServer((req, res) => {
    const { url, method } = req;
    if (url === '/ping' && method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('pong');
    } else if (url === '/hello' && method === 'GET') {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Hello, World!');
    } else {
        res.writeHead(404, { 'Content-Type': 'text/plain' });
        res.end('Not Found');
    }
})

server.listen(8082, () => {
    console.log('Server is running on port 8082');
})

参数#

使用URL包

const myUrl = new URL("http://localhost:8888/start?foo=bar&hello=world");

// 提取路径名
console.log(myUrl.pathname); // 输出: /start

// 提取查询参数
console.log(myUrl.searchParams.get("foo"));   // 输出: bar
console.log(myUrl.searchParams.get("hello")); // 输出: world

NodeJs 安全#

Node.Js安全分析 - Boogiepop Doesn’t Laugh 写的太全了,直接看这个好了😭

RCE#

这里RCE一是获得JS命令执行,再是系统命令的执行

JS命令执行#

eval 可以直接运行JS命令字符串,和php的eval差不多:eval("console.log('orxiain')")

setInterval 这个函数的第一个参数接受的是方法,可以用箭头函数简化 setInterval(()=>console.log('hack'),2000) 每两秒运行一次给定函数

setTimeout setTimeout(()=>console.log('hack'), 2000); 两秒后运行一次

Function Function("console.log('HelloWolrd')")();

系统命令执行#

exec 使用 require获取 child_process模块,然后调用 exec实现 eval("require(child_process).exec('calc')")

spawn 使用条件是具备 {shell:true} require('child_process').spawn('calc',{shell:true});

这里如果没有 {shell:true}计算器依然可以正常弹出: 若没有 {shell:true},Nodejs会去找可执行的应用,而不是作为一个shell命令来执行,也就是说可以用来执行可执行文件,有 {shell:true}的话就会作为shell命令执行。

execFile 专门用来执行可执行文件的,配合上传打

execFile('calc', [], (error, stdout, stderr) => {
  if (error) {
    console.error(`执行出错: ${error}`);
    return;
  }
  console.log(`输出:\n${stdout}`);
});

Fork fork 是 child_process 模块中特别用于创建新的 Node.js 进程的函数。它基于 spawn 方法,专门用于启动一个新的 Node.js 脚本,并且在父子进程之间建立一个通信通道,可通过 send 方法进行消息传递。就是能执行指定的JS文件,也能配合上传打

const { fork } = require('child_process'); 
const child = fork('path/to/childScript.js');

childScript.js里赛exec就行了

弱类型比较#

搬了直接

代码输出说明
1 == '1'true数字 1 和字符串 '1' 进行弱类型比较时,字符串 '1' 会被强制转换为数字 1。
1 > '2'false数字 1 和字符串 '2' 比较时,字符串 '2' 会被转换为数字 2,因此 1 > 2 为 false
'1' < '2'true字符串比较基于 ASCII 码的字符顺序,字符 '1' 的 ASCII 值小于字符 '2'
111 > '3'true数字 111 和字符串 '3' 比较时,字符串 '3' 会被强制转换为数字 3,因此 111 > 3 为 true
'111' > '3'false字符串 '111' 和字符串 '3' 比较时,基于 ASCII 顺序 '111' 被认为小于 '3'
'asd' > 1false字符串 'asd' 与数字 1 比较时,字符串 'asd' 被转换为 NaN,导致比较结果为 false
[] == []false空数组与空数组进行比较时,它们是不同的对象,因此为 false
[] > []false空数组与空数组进行比较,空数组转换为 false,所以结果是 false
[6, 2] > [5]true数组比较时,比的是数组的第一个元素,6 > 5,所以结果为 true
[100, 2] < 'test'true数组 [100, 2] 会被转换为字符串 '100,2',与字符串 'test' 比较,'100,2' < 'test' 为 true
[1, 2] < '2'true数组 [1, 2] 会被转换为字符串 '1,2',与字符串 '2' 比较,'1,2' < '2' 为 true
[11, 16] < "10"false数组 [11, 16] 会被转换为字符串 '11,16',与字符串 '10' 比较,'11,16' > '10' 为 false
null == undefinedtruenull 与 undefined 进行弱类型比较时,它们相等。
null === undefinedfalsenull 和 undefined 在严格比较时不相等。
NaN == NaNfalseNaN 不等于任何值,包括它自己,因此 NaN == NaN 为 false
NaN === NaNfalseNaN 在严格比较时也不等于任何值,包括它自己。
  • 数字与数字字符串比较时:数字字符串会被强制转换为数字后进行比较。
  • 字符串与字符串比较:基于字符的 ASCII 码顺序。
  • 数组比较:空数组在与其他类型进行比较时,通常会被视为 false。数组会按第一个元素进行比较,且如果数组含有非数值型元素,会以字符串形式与其进行比较。
  • 特殊值
    • null 与 undefined 在 弱类型比较 下相等,但 严格比较 不相等。
    • NaN 在 弱类型比较 和 严格比较 下都不等于任何值,包括它自己。

其他#

javascript大小写特性#

来自文章 - Node.js 常见漏洞学习与总结 - 先知社区

在javascript中有几个特殊的字符需要记录一下

对于toUpperCase():

字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"

对于toLowerCase():

字符"K"经过toLowerCase处理后结果为"k"(这个K不是K)

在绕一些规则的时候就可以利用这几个特殊字符进行绕过

Node.Js原型链污染#

深入理解 JavaScript Prototype 污染攻击 | 离别歌 不再展开 ctfshow Nodejs的wp

Links#

NodeJs基础与安全
https://fuwari.vercel.app/posts/nodejs基础与安全/
作者
𝚘𝚛𝚡𝚒𝚊𝚒𝚗.
发布于
2025-02-21
许可协议
CC BY-NC-SA 4.0