讲解前吐槽为什么考的全是cve,要是我没记录cve的话真的要现场挖吗(bushi)
所有题目附件在文章末尾,可以直接下载,jdbc的题目jar包因为过大所以使用了分块压缩
附件里提供了题目的完整环境,在webstorm里打开文件夹即可。
题目仅提供了两个附件:app.js与package.json。
先审计app.js,发现了一个递归合并属性的merge函数:

递归合并就要考虑是否有原型链污染了,虽然其在遇到__proto__会跳过,但是我们可以通过consturctor.prototype的链子进行绕过。
检查该函数在哪里使用:

可以看到其在修改密码时毫无过滤的使用了merge函数来进行合并。

后面的/sandbox很显然是一个沙箱逃逸,但是需要admin权限,因此思路很清晰:利用原型链污染进行提权,进行后续的沙箱逃逸


可以看到新注册的用户已经变为了admin。

可以看到其使用vm2进行沙箱代码执行,检查vm2版本

该版本存在一个今年的cve高危漏洞:CVE-2026-22709
JavaScript 中的一个关键特性是:async 函数总是返回一个原生的 globalPromise 对象,而不是 vm2 制造的 localPromise。
因此,攻击者只需在沙箱代码中定义一个 async 函数,就能获取一个未被“清理”的 globalPromise 对象。
我们可以让这个globalPromise被reject,并调用未被“清理”的 .catch 方法。这个 .catch 回调将在沙箱外部的宿主环境中执行。
注意:函数体内变量(如 require)的解析规则:遵循 JavaScript 的静态作用域(lexical scoping),即在定义该函数的位置向上查找作用域链,因此如果你在catch里直接写require,虽然是外部宿主环境,但是仅匹配定义位置是否有require,没有就会报错并不执行,这样的外带就不行了
因此我们需要想办法让这个globalPromise获取到一个外部对象,通过这个外部对象来获取外部的Function,进而获取到process与require,完成后续RCE。
我们可以很快速的想到:在这个globalPromise里制造异常,会不会就能获取到一个外部对象呢?很可惜,vm2沙箱内得到的Error对象都是经过其包装过的代理Error,其 constructor 指向的是沙箱内的 Function,无法进行逃逸......吗?
通过以上构造,当访问 error.stack 时,V8 引擎内部会调用 error.name.toString()。但 Symbol 不能隐式转为字符串,会抛出 TypeError,被catch捕获到e上。关键在于:这个 TypeError 是在 vm2 的 host 上下文中创建的,而非沙箱内部。换句话讲,这个TypeError没有被沙箱包装代理。
通过e.constructor.constructor,我们就能获取到最顶上的函数构造器,进而利用其获取到process,利用process获取到require并引入child_process模块,进行后续的代码执行。

执行成功!
在idea里新建一个maven项目,将pom.xml、ctf-challenge.jar放进项目里并同步maven库即可,使用的jdk为jdk17。
该题目是一道AWD题,本篇wp仅针对attack阶段,不过我把利用的漏洞点说了你们也就知道哪里需要防了。
题目提供了三个附件:pom.xml、start.sh与Webconfig.java。
先分析一下pom.xml,发现了一个静态路径穿越漏洞:

这个漏洞需要在应用使用webflux来提供静态资源,且在提供静态资源时,明确使用了FileSystemResource来指定文件系统中的位置时才能被利用。有一段代码示例:
再看提供的Webconfig.java源码内容:

可以说是完全一致,由此我们确定了其存在路径穿越漏洞。
springboot在处理客户端发来的静态路径访问时会进行预处理,检查路径是否存在../这样路径穿越的问题,但是由于在检查../时代码逻辑存在缺陷,导致攻击者可以构造一个设计好的payload,使其包含../的同时不被认定为“包含了../”路径,进而实现路径穿越。
用于windows:/static/%5c/%5c/../../flag.txt
用于windows与linux:/static/%2f/%2f/../../flag.txt
(将flag.txt替换为你实际要访问的文件内容)
在对/static/.....发起一个Get请求后,springboot框架会调用spring-webflux-6.0.2.jar里的PathResourceLookupFunction类的apply方法进行路径解析,这里我们打好断点准备调试

提前在D盘根目录下放置一个111.txt文件

启动调试,访问/static/%5c/%5c/../../111.txt

可以看到确实走到我们的断点了

我们的目标就是走到这个判断语句的里面,也就是要保证isInvalidPath函数最终返回false。
跟进这个函数看看具体做了些什么操作

前面简单的检验了有没有敏感路径,去除了开头的绝对路径,检查有没有使用file://或者http://或者url://等协议
重点在下面,if (path.contains("..") && StringUtils.cleanPath(path).contains("../")),前面不用说肯定会返回true,重点是后面,我们要想办法让他返回false,这样就可以绕过非法性检查
继续跟进:

这里会将我们的url进行标准化处理(反斜杠转为斜杠,因为在前面的代码里使用/////写法会被合并为/)(使用第二个payload也完全可以),并且处理掉了开头的两个/,然后准备做元素分割

重点在这里!该处代码逻辑处理///这种写法会得到空元素,其会影响后续处理该段path的代码,并在最后得到一个不含..的版本(这里具体是怎么绕过的大家可以自行去调试源码逻辑)

最后返回的字符串就是"/"+"/1.txt"。很显然没有../,因此在StringUtils.cleanPath(path).contains("../")里会判定为false,进而绕过了非法判断,从而放过了我们的路径穿越payload,访问到了111.txt文件

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2天前
被Fulucky0编辑
,原因: