ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java 模板注入(FreeMarker) · 01 · Java 基础

Java 模板注入(FreeMarker) · 01 · Java 基础 1. JVM、JRE、JDK名称全称是什么类比JVMJava Virtual Machine执行 Java 字节码的虚拟机发动机JREJava Runtime EnvironmentJVM 标准类库能运行Java 程序发动机 油箱JDKJava Development KitJRE 编译器javac等开发工具发动机 油箱 工具箱关键点Java 源码文件是.java先被javac编译成字节码.class再由 JVM 执行。字节码不是机器码所以一次编译到处运行只要目标机器有对应平台的 JVM。Java 和 JavaScript 有什么关系几乎没有关系只是名字像。Java 是编译型、静态类型JavaScript 是解释型、动态类型。别混。唯一的联系是当年 Netscape 为了蹭 Java 热度才把 LiveScript 改名成 JavaScript。为什么安全学习要懂 JVM因为 FreeMarker 的 payload 最终是在 JVM 里执行的 Java 方法调用。理解类class、对象object、方法method、类加载器classloader你才能理解freemarker.template.utility.Execute?new()到底 new 出了个什么东西为什么它能执行系统命令。2. 第一个程序编译与运行// 文件名必须叫 Hello.javapublic 类名要和文件名一致 public class Hello { public static void main(String[] args) { System.out.println(Hello, FreeMarker!); } }javac Hello.java # 生成 Hello.class java Hello # 运行输出 Hello, FreeMarker!逐词解释public static void main(String[] args)部分含义public访问权限公开JVM 能调用static静态不需要 new 出对象就能调用void没有返回值main约定的入口方法名String[] args命令行参数数组SSTI Server-Side Template Injection服务器端模板注入网站后台使用模板引擎FreeMarker / Thymeleaf / Velocity / Jinja2把用户输入直接当成模板代码解析执行攻击者写模板语法就能在服务器执行代码。正常安全写法模板固定写在服务端文件里用户只传变量数据不能改模板。SSTI 漏洞写法用户输入内容 → 直接交给模板引擎当成模板代码渲染。这些和 SSTI 有关系吗有。后面 payload 里会反复出现调用某个对象的某个方法。main就是一个最典型的方法。Java 里几乎所有行为都是对象.方法(参数)的形式。不过模板里的ex(id)是FreeMarker 的可调用对象写法不是 Java 语法ex是一个实现了TemplateMethodModel接口的对象FreeMarker 在求值时会去调用它的exec(...)方法参数是id。注意__call__是Python的魔术方法Java 没有这个概念Java 对象默认不能像函数那样obj(args)调用是 FreeMarker 提供了这套可调用对象机制。3. 变量、类型与字符串int a 10; // 整数 double b 3.14; // 小数 boolean c true; // 布尔 String s hello; // 字符串注意 String 首字母大写是类 String name world; String greeting Hello, name !; // 字符串拼接用 System.out.println(greeting); // Hello, world!要点Java 是强类型语言变量声明时要写类型或var推断。String是不可变的immutable拼接会生成新字符串不改原字符串。字符串拼接Hello, name !这个动作就是 SSTI 里用户输入被拼进模板的 Java 版本。为什么拼接这么危险因为模板引擎把模板文本当成代码。如果代码是h1Hello, name !/h1而name由攻击者控制那么攻击者就能往模板代码里塞任意指令。4. 类、对象、方法、构造器4.1 类class是模板对象object是实例public class Dog { String name; // 字段属性 ​ public Dog(String name) { // 构造器new 的时候调用 this.name name; } ​ public String bark() { // 方法 return name says woof; } } ​ Dog d new Dog(旺财); // 创建对象实例化 System.out.println(d.bark()); // 调用方法 旺财 says woof概念说明安全意义类蓝图如Dogpayload 里写的xxx.yyy.ClassName就是类名对象类的实例new Dog(...)?new()就是用某个类创建对象方法对象能做的事bark()RCE 就是调用某个危险方法构造器创建对象时执行的代码?new(参数)的参数会传给构造器this当前对象自身反射里常用this无关先了解即可4.2static与实例public class MathUtil { public static int add(int x, int y) { // 静态方法类.方法() return x y; } public int sub(int x, int y) { // 实例方法对象.方法() return x - y; } } ​ int r1 MathUtil.add(3, 4); // 静态直接用类名 MathUtil m new MathUtil(); int r2 m.sub(10, 3); // 实例先 new 再调用为什么 payload 里有的方法能用、有的不能用真正的原因不是静态 vs 实例而是2.3.30 的默认成员访问策略DefaultMemberAccessPolicy拉黑了一批危险方法。例如${list?api.getClass().forName(java.lang.Runtime)}会失败是因为Class.forName在黑名单里而不是因为它是静态方法。靶场对照实测/vuln/api默认策略下getClassLoader、forName都被拦而/vuln/api-unsafe成员策略换成允许一切下静态方法照样能调——Class.forName(...)、System.getProperty(...)都实测可用。所以判断一个方法能不能用看它是否被成员策略拉黑而不是看它是静态还是实例。4.3 继承与接口interface Animal { String sound(); } ​ class Cat implements Animal { public String sound() { return meow; } }extends继承类implements实现接口。接口只定义能做什么不定义怎么做。为什么接口对 SSTI 重要因为 FreeMarker 的?new只能实例化实现了freemarker.template.TemplateModel接口的类。freemarker.template.utility.Execute就实现了TemplateMethodModelExTemplateModel 的子接口所以能被?new出来并调用。而java.lang.ProcessBuilder没有实现 TemplateModel所以java.lang.ProcessBuilder?new()会报错Class java.lang.ProcessBuilder does not implement freemarker.template.TemplateModel靶场实测。4.4 铁律Java 是大小写敏感的语言Java区分大小写case-sensitivename、Name、NAME是三个完全不同的东西。这跟 SQL关键字通常不区分大小写不一样。int value 1; int Value 2; // 合法这是另一个变量 int VALUE 3; // 合法又是另一个变量这对 SSTI 有什么影响payload 里的类名、方法名必须精确匹配大小写。freemarker.template.utility.Execute写成execute、EXECUTE、ExEcUtE都会ClassNotFoundExceptionRuntime.getRuntime()写成runtime、GetRuntime同样失败。大小写就是 payload 的一部分。4.5class、Class、CLASS到底谁是谁写法是什么用途例子class关键字全小写用来定义一个类public class Dog {}Class一个类java.lang.Class表示某个类的元信息是反射的入口obj.getClass()、String.classCLASS不是Java 的类或关键字在 Java 里通常只是常量命名约定的全大写写法static final int MAX_SIZE 10;.class编译产物 / 类字面量编译后生成Dog.classDog.class表示 Dog 的Class对象javac产出.classDog.classClassLoader一个类首字母大写负责按名字加载类cl.loadClass(java.lang.Runtime)Class和class傻傻分不清class小写是定义类的关键字Class大写是代表类的类反射的起点。可以理解为Class是描述class的对象。例class Dog {}定义了狗这个概念Dog.class或dog.getClass()拿到的是描述 Dog 这个类的Class对象。那CLASS全大写呢Java 里没有CLASS这个类或关键字。全大写在 Java 里通常是常量的命名约定见 4.6例如public static final int MAX_RETRY 3;。如果你在别处看到CLASS它多半是某个变量/常量名、某个框架的缩写、或别的语言如 C 的宏#define CLASS。在 Java 里它没有特殊含义也不会被当成Class。拿到一个Class对象的三种方式都很重要Class? c1 hello.getClass(); // 1) 从对象实例拿任何对象都有 Class? c2 String.class; // 2) 类字面量编译期就确定 Class? c3 Class.forName(java.lang.String); // 3) 按类名字符串加载反射静态方法方式何时用攻击视角obj.getClass()已有对象想知道它是什么类实例方法默认策略允许Object.getClass在白名单类名.class写代码时已知类编译期固定payload 里基本用不到Class.forName(类名)运行时按字符串加载最常用于攻击但它是静态方法默认策略/包装器常常不暴露Class?里的?是什么那是 Java 的泛型通配符表示某种未知类型的Class。初学阶段先把它当成占位符即可不影响理解反射。另外要记住Object是所有类的父类所以任何对象都能调用getClass()、equals()、toString()、hashCode()。getClass()和getClassLoader()有什么关系getClass()返回的是Class对象这是什么类getClassLoader()返回的是ClassLoader是谁把这个类加载进来的。攻击链里常见顺序是对象 → getClass() → getClassLoader() → loadClass(危险类) → ...。但注意核心类如ArrayList的getClassLoader()返回nullBootstrap 加载要换成应用自己的对象才有值。4.6 Java 命名约定一眼看出这是类还是变量Java 社区有一套约定不是语法强制但不遵守会被吐槽类别约定例子记忆点类 / 接口PascalCase大驼峰Dog、ClassLoader、Execute首字母大写方法 / 变量camelCase小驼峰getName、userName首字母小写常量UPPER_SNAKE_CASEMAX_SIZE、DEFAULT_TIMEOUT全大写 下划线包名全小写点分隔freemarker.template.utility反域名全小写类字面量类名.classString.class拿Class对象这解释了 payload 为什么长这样freemarker.template.utility.Execute └──────── 全小写包名 ────────┘ └ PascalCase类名freemarker.template.utility全小写 → 是包名Execute首字母大写 → 是类名整串 包名 类名 全限定类名。一句话记忆大写开头多是类小写开头多是变量/方法全大写多是常量。4.7 全限定类名FQN与import全限定类名Fully Qualified Name, FQN 包名 . 类名例如java.lang.Runtime、freemarker.template.utility.Execute。在 Java源码里可以先用import引入之后只写类名import java.lang.Runtime; // 之后可以直接写 Runtime但在模板 / 反射里通常必须写全限定类名因为模板不知道你的源码import了什么。这就是 payload 里为什么总是写一长串的原因。为什么 payload 里类名是freemarker.template.utility.Execute这种带点的一长串那是全限定类名 包名 类名。点号分隔的是包路径最后一段是类名。类似文件系统的目录 文件名freemarker/template/utility/目录下的Execute。4.8 容易混淆的同名不同物清单容易混的写法区别classvsClass关键字 vs 元类反射CLASSJava 里不存在全大写一般是常量名StringvsstringString是类string不是关键字可当普通变量名ObjectvsobjectObject是所有类的父类object只是普通变量名SystemvssystemSystem是java.lang.System含out.printlnsystem无特殊含义RuntimevsruntimeRuntime是类getRuntime().exec()runtime只是变量名ClassLoadervsclassloaderClassLoader是类classloader只是变量名nullvsNullnull是空引用关键字Null无特殊含义thisvsThisthis是当前对象关键字This无特殊含义newvsNewnew是创建对象的关键字New无特殊含义.javavs.class.java是源码.class是编译后的字节码也是类字面量后缀看到Class、String、Runtime、ClassLoader这种首字母大写的怎么快速判断基本可以认定它们是类/接口。首字母大写 PascalCase 是 Java 类的命名约定。反过来getRuntime、loadClass、exec这种首字母小写的是方法。5. 反射Reflection反射 程序在运行时看到类和方法并动态调用它们。正常写代码是写死调用obj.bark()。反射是用字符串找到方法再调用先拿到bark这个Method对象再invoke。5.1 最核心的四个类类代表什么常用方法java.lang.Class一个类的元信息getName()、getMethod()、getConstructor()、forName()静态java.lang.reflect.Method一个方法invoke(对象, 参数...)java.lang.reflect.Constructor一个构造器newInstance(参数...)java.lang.ClassLoader类加载器能按名字加载类loadClass(类名)5.2 反射小例子// 1) 拿到一个对象的 Class Object obj hello; Class? cls obj.getClass(); // class java.lang.String System.out.println(cls.getName()); ​ // 2) 按名字找到方法 Method m cls.getMethod(toUpperCase); // 找无参的 toUpperCase Object result m.invoke(obj); // 在 obj 上调用它 HELLO ​ // 3) 按类名加载类Class.forName 是静态方法 Class? rt Class.forName(java.lang.Runtime); Method getRuntime rt.getMethod(getRuntime); Object runtime getRuntime.invoke(null); // 静态方法传 null Method exec rt.getMethod(exec, String.class); Process p (Process) exec.invoke(runtime, id); // 执行系统命令这段代码就是 FreeMarker SSTI RCE 的人类可读版。靶场里/vuln/api-unsafe的 payload 就是把这段代码翻译成了 FreeMarker 模板语法。5.3 反射为什么是逃逸的关键很多安全框架会做字符串黑名单比如禁止出现Runtime、exec。反射可以用字符串拼接绕开静态检测Class.forName(java.lang.Run time);或者用类加载器动态加载ClassLoader cl ...; Class? c cl.loadClass(java.lang.Runtime);那防护不是没法做了不是。正确做法不是黑名单字符串而是根本不让用户输入进模板治本在引擎层限制能调用哪些类/哪些方法如TemplateClassResolver、MemberAccessPolicy见 05 篇。这两层都不依赖猜 payload 长什么样所以反射也绕不过去。6. 类加载器ClassLoaderJVM 启动时类是被按需加载的。ClassLoader就是干这个的ClassLoader cl MyClass.class.getClassLoader(); // 拿到加载本类的加载器 Class? c cl.loadClass(java.lang.Runtime); // 按名字加载类java.util.ArrayList这类 JDK 核心类由Bootstrap ClassLoader加载getClassLoader()返回null。你自己项目里的类由AppClassLoader加载getClassLoader()返回非 null。为什么这个细节重要在靶场/vuln/api里list?api.getClass().getClassLoader()一开始返回 null导致链子断了——因为ArrayList是核心类。换成应用自己的对象bean后getClassLoader()返回 AppClassLoader链子就通了。实战里数据模型往往是应用自己的 POJO所以这条链非常现实。7. 异常Exceptiontry { risky(); } catch (Exception e) { e.printStackTrace(); // 打印调用栈 }异常信息尤其是调用栈 stack trace对安全人员极其宝贵能暴露框架版本、类路径、甚至源码行号。靶场的错误页就把完整堆栈返回了——这是很多真实系统的通病也是信息泄露点。为什么靶场故意返回报错为了让你直观看到模板处理失败在哪里、为什么失败。真实系统若返回详细堆栈攻击者就能据此调整 payload若只返回白页就只能盲打。与类/反射相关的常见异常利用时经常撞见异常含义常见原因ClassNotFoundException按名字找不到类类名拼错、大小写错、类不在 classpathNoClassDefFoundError类存在但依赖缺失例如JythonRuntime找不到org.python.*ClassCastException类型转换失败把 A 当 B 用IllegalAccessException没权限访问成员反射调用了不可访问的方法/构造器InvocationTargetException被反射调用的方法内部抛异常Method.invoke包了一层真正异常在getCause()NoSuchMethodException找不到指定方法方法名/参数类型不匹配这些异常对渗透有什么用报错信息会告诉你差在哪一步是类名不对ClassNotFoundException、还是依赖没引入NoClassDefFoundError、还是方法签名不匹配NoSuchMethodException。顺着报错调整 payload比盲试高效得多——这也是为什么详细报错本身就是一个信息泄露点。8. Java Web 应用长什么样只看需要的部分一个典型请求的处理流程简化浏览器 --HTTP-- Web 服务器(Tomcat/nginx...) -- Servlet/Controller | request.getParameter(name) -- 用户输入 | 业务逻辑 / 模板渲染 | response 返回 HTML用原生 Servlet 的伪代码protected void doGet(HttpServletRequest req, HttpServletResponse resp) { String name req.getParameter(name); // 1. 取用户输入 MapString,Object model new HashMap(); model.put(name, name); // 2. 放进数据模型 Template t cfg.getTemplate(hello.ftl); // 3. 加载模板文件 t.process(model, resp.getWriter()); // 4. 渲染模板 数据 }注意上面这种写法是安全的用户输入只是数据。危险写法是把用户输入直接拼进模板文本String tpl Hello, name !; // 用户输入进了模板 Template t new Template(x, new StringReader(tpl), cfg); // 变成代码 t.process(model, out);request.getParameter在 FreeMarker 里怎么体现在靶场里就是 URL 参数例如/vuln/direct?tpl...里的tpl。真实系统里它可能来自URL 参数、POST 表单、JSON 字段、Cookie、Header、数据库、上传的模板文件……9. 数据模型data-model到底是什么FreeMarker 渲染时接受一个数据模型本质是一个MapString, ObjectMapString,Object model new HashMap(); model.put(name, world); // 模板里就能用 ${name} model.put(list, List.of(a,b));模板里${name}取的是数据${name?upper_case}是数据 内建函数。只要用户输入只出现在数据位置它就永远是数据不会变成代码。这是理解 SSTI 的分水岭。10. 本篇小结Java 代码 类 对象 方法调用.java编译成.class由 JVM 执行。Java 大小写敏感class关键字、Class元类、CLASS不存在通常只是常量名是三回事类名 PascalCase、方法/变量 camelCase、常量 UPPER_SNAKE_CASE。String拼接是用户输入进模板的 Java 原型。反射能在运行时按名字找类、找方法、调用方法——这是 RCE 的底层机制。?new只能实例化实现了TemplateModel的类如Execute、ObjectConstructor。ClassLoader决定能不能动态加载某个类核心类的类加载器是 null。Web 应用从request.getParameter拿用户输入输入进数据模型安全进模板文本危险。
返回列表