NodeJS基本概念

  1. 概念

    NodeJS是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,允许开发者在服务器端运行 JavaScript。它没有浏览器的 DOM 或 BOM,而是提供了与文件系统、网络请求、进程管理等相关的 API,主要用于构建服务器端应用程序

    注意:nodejs不是一门新的编程语言,nodejs是在服务端运行javascript的运行环境

  2. 使用文档

    https://www.w3cschool.cn/nodejs/

  3. 第三方库

    • express:Express是一个简洁而灵活的node.js Web应用框架
    • body-parser:node.js中间件,用于处理 JSON, Raw, Text和URL编码的数据。
    • cookie-parser:一个解析Cookie的工具。通过req.cookies可以取到传过来的cookie,并把它们转成对象。
    • multer:node.js中间件,用于处理 enctype=“multipart/form-data”(设置表单的MIME编码)的表单数据。
    • mysql:Node.js来连接MySQL专用库,并对数据库进行操作。

NodeJS的安装使用

  1. 安装

    在cmd中输入 node -v 确定自己是否已经安装过

    没有安装过的话,参考:

  2. 使用方法

    初始化项目并安装Express框架

    1
    2
    npm init -y  # 初始化 Node.js 项目,生成 package.json 文件
    npm install express # 安装 Express

    这会在你的项目中安装 Express 库,并在 node_modules 目录下存放依赖。

    在终端中输入以下命令来运行demo:

    1
    node app.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 引入 Express 框架
    const express = require('express');
    // 创建 Express 应用程序实例
    const app = express();

    // 处理 '/login' 路径的 GET 请求,返回简单的登录页面
    app.get('/login', function(req, res) {
    res.send('<hr>登录页面</hr>');
    });

    // 处理根路径的 GET 请求,发送名为 'sql.html' 的文件
    app.get('/', function(req, res) {
    // res.sendFile(__dirname + '/' + 'sql.html');
    res.send("hello word");
    });

    // 启动服务器,监听端口 3001
    const server = app.listen(3001, function() {
    console.log('Web 服务器已经启动,监听端口 3001!');
    });

基本语法

GET/POST请求

req.query :用于处理 URL 查询字符串参数**GET请求**

req.body :用于处理 **POST 请求**中的表单数据。

回调函数

  1. 如何理解回调函数

    彻底理解 Node.js 中的回调(Callback)函数

  2. 用法

    在JavaScript中,回调函数通常是作为参数传递给其他函数,然后由那个函数在适当的时机调用。这种方式使得代码更加灵活和可扩展。

    回调函数通常与异步操作一起使用,比如读取文件、发送网络请求或等待用户输入。由于这些操作可能需要一些时间来完成,所以通常会使用回调函数来在操作完成后执行某些任务。

  3. 基本语法

下面的延时执行函数中的匿名函数就是回调函数,表示2秒之后执行这个匿名函数

1
2
3
setTimeout(function() {
console.log('This is a delayed message.');
}, 2000); // 2秒后打印
  1. 如何判断目标函数是否使用了回调函数

    回调函数并不是指函数内部必须明确命名为 callback

    在 JavaScript 中,回调函数的核心概念是:它是作为参数传递给另一个函数,并且在该函数执行时被调用。所以,不管这个函数的名字是什么,只要它是被传递到其他函数中,并在那个函数内部执行,就可以算作回调函数

    比如上面的 setTimeout 函数就是一个回调函数,虽然它没有显式地命名为 callback,因为:

    • 它作为参数传递给了 setTimeout 函数。
    • 在指定的时间延迟后,setTimeout 会调用这个函数。

功能实现

用户登录

  1. 功能:前端输入用户名和密码,在数据库中匹配验证

  2. 环境

    下载解析GET/POST请求体的中间件 body-parser

    1
    npm i body-parser
  3. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    // 引入 Express 框架
    const express = require('express');
    const bodyParser = require('body-parser');
    const mysql = require('mysql');


    // 创建 Express 应用程序实例
    const app = express();
    //创建一个用于解析 URL 编码的 bodyParser 中间件实例
    const urlencodedParser = bodyParser.urlencoded({ extended: false });

    // 处理 '/login' 路径的 GET 请求,返回简单的登录页面
    app.get('/login', function(req, res) {
    // 从请求中获取用户名和密码
    const u = req.query.username;
    const p = req.query.password;

    console.log(u);
    console.log(p);

    // 检查用户名和密码是否为 admin 和 123456
    if (u === 'admin' && p === '123456') {
    res.send('欢迎进入后台管理页面');
    } else {
    res.send('登录用户或密码错误');
    }
    });

    // 处理 '/login' 路径的 POST 请求,使用 bodyParser 解析表单数据
    app.post('/login', urlencodedParser, function(req, res) {
    // 从请求中获取表单提交的用户名和密码
    const u = req.body.username;
    const p = req.body.password;

    // 输出获取到的用户名和密码,用于调试
    console.log(u);
    console.log(p);

    // 创建与 MySQL 数据库的连接
    var connection = mysql.createConnection({
    host : 'localhost',
    user : 'root',
    password : 'root',
    database : 'dome01'
    });

    // 建立数据库连接
    connection.connect();

    // 构建 SQL 查询,检查数据库中是否存在匹配的用户名和密码
    const sql = 'select * from admin where username="'+u+'" and password="'+p+'"';
    console.log(sql);

    // 执行 SQL 查询
    connection.query(sql, function(error, data){
    // 检查查询执行中是否存在错误
    if(error){
    console.log('数据库连接失败!');
    }

    try {
    // 检查用户名和密码是否匹配数据库中的数据
    if(u == data[0]['username'] && p == data[0]['password']){
    // 如果匹配,发送欢迎消息到前端
    res.send('欢迎进入后台管理页面');
    }
    } catch {
    // 捕获异常,如果没有匹配的数据或其他错误,发送错误消息到前端
    res.send('错误');
    };
    });
    })


    // 处理根路径的 GET 请求,发送名为 'sql.html' 的文件
    app.get('/', function(req, res) {
    res.sendFile(__dirname + '/' + 'sql.html');
    });

    // 启动服务器,监听端口 3001
    const server = app.listen(3001, function() {
    console.log('Web 服务器已经启动,监听端口 3001!');
    });

文件管理

  1. 功能:目录读取

  2. 环境

    下载相关文件管理依赖 fs

    1
    npm i fs
  3. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 引入文件系统和 Express 框架
    const fs = require('fs');
    const express = require('express');
    const app = express();

    // 处理 '/file' 路径的 GET 请求
    app.get('/file', function (req, res) {
    // 从请求中获取目录参数
    const dir = req.query.dir;
    console.log(dir);·

    // 调用文件管理函数,传递目录参数
    filemanage(dir);
    });

    // 启动 Express 应用监听在3000端口
    var server = app.listen(3000, function () {
    console.log('Web应用已启动在3000端口!');
    });

    // 文件管理函数,接收一个目录参数
    function filemanage(dir) {
    // 使用 fs.readdir 读取目录下的文件
    fs.readdir(dir, function (error, files) {
    // 打印目录中的文件列表
    console.log(files);
    });
    }

    调用文件管理函数,传递目录参数

命令执行

  1. 功能:执行系统命令、将字符串当作代码解析

  2. 环境

    下载子进程管理模块 child_process

    1
    npm i child_process
  3. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 引入child_process模块
    const rce = require('child_process');

    // 使用exec方法调用系统命令'notepad',打开记事本
    rce.exec('notepad');

    // 使用spawnSync方法调用系统命令'calc',打开计算器
    rce.spawnSync('calc');

    // 使用eval调用代码命令执行,将字符串当做代码解析
    // 请注意:避免在生产环境中使用eval,可能存在安全风险
    eval('require("child_process").exec("calc");');

原型链污染

什么是原型链

因为 JavaScript 没有类似 Java 中的 Class 概念,所以继承都是由原型链来实现的。JavaScript 中每个对象都有一个内部属性 [[Prototype]],这个属性指向另一个对象。通过这个原型对象,当前对象可以继承其上定义的属性和方法。

  1. 每个对象都有一个原型:当你创建一个对象时,它会有一个指向其构造函数的原型对象。

  2. 查找过程:当你访问一个对象的属性时,JavaScript 引擎首先会检查这个对象自身是否有该属性。如果没有,它会通过原型链查找,依次查找对象的原型及其原型的原型,直到找到该属性或者原型链的尽头(通常是 null)为止。

  3. 例子

    下面的代码中,foo.bar 输出为1而不是x,因为引擎会首先检查该对象自身是否有该属性。zoo作为继承自Object的对象,同样也继承了其bar值,为x。

    1
    2
    3
    4
    5
    6
    7
    8
    let foo = {bar:1}
    console.log(foo.bar) // 输出为1

    foo.__proto__.bar = 'x' // 修改原型
    console.log(foo.bar) // 输出为 1,因为查找顺序的原因

    let zoo = {}
    console.log(zoo.bar) // 输出为 'x',因为zoo的原型指向foo.__proto__,即最底层的对象Object
  4. 原型链结构图

    Object.prototype 是 JavaScript 中所有对象的原型对象,它是 所有对象的原型链 的终极基类。几乎在 JavaScript 中创建的所有对象,都会继承 Object.prototype 中的方法和属性。如果你访问一个对象的某个属性或方法时,JavaScript 会沿着原型链从当前对象开始查找,直到它找到 Object.prototype 为止。如果 Object.prototype 中没有该属性或方法,JavaScript 会返回 undefined

    以下面的图为例:person.__proto__ 是与 Person.prototype 等价的,而 Person.prototype.__proto__ 是指向 Object.prototype 的,再往下 Object.prototype.__proto__ 指向 null

    image-20250224210756528

原型链污染的原理

在 JavaScript 中,对象通过原型链继承属性和方法。如果攻击者能够通过某些方式修改对象的原型(即 prototype),构造函数和原型属性,就可能导致整个对象模型的行为发生异常。同时,所有继承了被污染原型的对象都会受到影响。原型链污染通常会导致拒绝服务、篡改程序执行流程、导致远程执行代码等漏洞。

原型链污染的利用

原型链污染配合RCE:

有原型链污染的前提之下,我们可以控制基类的成员,赋值为一串恶意代码,从而造成代码注入。

例如下面的代码:通过原型链bar属性执行命令打开计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建一个包含属性 bar 的对象 foo,并将 bar 设置为 1
let foo = {bar: 1};

// 输出 foo 对象的 bar 属性,预期输出为 1
console.log(foo.bar); // 输出: 1

// 修改 foo 对象的原型链上的 bar 属性,将其设置为执行命令 'require(\'child_process\').execSync(\'calc\');',从而调用计算器
foo.__proto__.bar = 'require(\'child_process\').execSync(\'calc\');';

// 输出 foo 对象的 bar 属性,预期输出仍为 1,因为直接属性优先于原型链上的属性
console.log(foo.bar); // 输出: 1

// 创建一个空对象 zoo
let zoo = {};

// 使用 eval 执行 zoo 对象的 bar 属性,由于 zoo 对象没有 bar 属性,会导致 ReferenceError
//调用计算机
console.log(eval(zoo.bar));

实际案例

因为原型链污染的条件苛刻,一般只在CTF比赛中作为考题出现,现实中比较少

  1. CTF

  2. yapi token注入漏洞

    yapi token注入漏洞