JAVA安全之JAVA服务器安全漫谈_java安全漫谈-程序员宅基地

技术标签: hack  web安全  安全  wooyun  网络安全  

0x00 前言


本文主要针对JAVA服务器常见的危害较大的安全问题的成因与防护进行分析,主要为了交流和抛砖引玉。

0x01 任意文件下载


示例

以下为任意文件下载漏洞的示例。

DownloadAction为用于下载文件的servlet。

1

2

3

4

5

6

7

8

9

10

11

12

13

<code>#!html

<servlet>

    <description></description>

    <display-name>DownloadAction</display-name>

    <servlet-name>DownloadAction</servlet-name>

    <servlet-class>download.DownloadAction</servlet-class>

</servlet>

<servlet-mapping>

    <servlet-name>DownloadAction</servlet-name>

    <url-pattern>/DownloadAction</url-pattern>

</servlet-mapping>

</code>

在对应的download.DownloadAction类中,将HTTP请求中的filename参数作为待下载的文件名,从web应用根目录的download目录读取文件内容并返回,代码如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

<code>#!java

protected void doGet(HttpServletRequest request,

        HttpServletResponse response) throws ServletException, IOException {

    String rootPath = this.getServletContext().getRealPath("/");

    String filename = request.getParameter("filename");

    if (filename == null)

        filename = "";

    filename = filename.trim();

    InputStream inStream = null;

    byte[] b = new byte[1024];

    int len = 0;

    try {

        if (filename == null) {

            return;

        }

        // 读到流中

        // 本行代码未对文件名参数进行过滤,存在任意文件下载漏洞

        inStream = new FileInputStream(rootPath + "/download/" + filename);

        // 设置输出的格式

        response.reset();

        response.setContentType("application/x-msdownload");

        response.addHeader("Content-Disposition""attachment; filename=\""

                + filename + "\"");

        // 循环取出流中的数据

        while ((len = inStream.read(b)) > 0) {

            response.getOutputStream().write(b, 0, len);

        }

        response.getOutputStream().close();

        inStream.close();

    } catch (Exception e) {

        e.printStackTrace();

    }

}

</code>

使用DownloadAction下载web应用根目录中的“download/test.txt”文件如下图所示。

由于在DownloadAction类中没有对filename参数值进行检查,因此产生了任意文件下载漏洞。

使用DownloadAction下载web应用根目录中的“WEB-INF/web.xml”文件如下图所示。

原因分析

从上述示例可以看出,在JAVA web程序的下载文件相关的代码中,若不对HTTP请求中的待下载文件名进行检查,则有可能产生任意文件下载漏洞。

java.io.File对象有两个方法可以用于获取文件对象的路径,getAbsolutePath与getCanonicalPath。

查看JDK 1.6 API中上述两个方法的说明。

getAbsolutePath

返回此抽象路径名的绝对路径名字符串。

如果此抽象路径名已经是绝对路径名,则返回该路径名字符串,这与 getPath() 方法一样。如果此抽象路径名是空抽象路径名,则返回当前用户目录的路径名字符串,该目录由系统属性 user.dir 指定。否则,使用与系统有关的方式解析此路径名。在 UNIX 系统上,根据当前用户目录解析相对路径名,可使该路径名成为绝对路径名。在 Microsoft Windows 系统上,根据路径名指定的当前驱动器目录(如果有)解析相对路径名,可使该路径名成为绝对路径名;否则,可以根据当前用户目录解析它。

getCanonicalPath

返回此抽象路径名的规范路径名字符串。

规范路径名是绝对路径名,并且是惟一的。规范路径名的准确定义与系统有关。如有必要,此方法首先将路径名转换为绝对路径名,这与调用 getAbsolutePath() 方法的效果一样,然后用与系统相关的方式将它映射到其惟一路径名。这通常涉及到从路径名中移除多余的名称(比如 "." 和 "..")、解析符号连接(对于 UNIX 平台),以及将驱动器号转换为标准大小写形式(对于 Microsoft Windows 平台)。

每个表示现存文件或目录的路径名都有一个惟一的规范形式。每个表示不存在文件或目录的路径名也有一个惟一的规范形式。不存在文件或目录路径名的规范形式可能不同于创建文件或目录之后同一路径名的规范形式。同样,现存文件或目录路径名的规范形式可能不同于删除文件或目录之后同一路径名的规范形式。

使用以下代码在Windows环境测试上述两个方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

<code>#!java

public static void main(String[] args) {

    getFilePath("C:/Windows/System32/calc.exe");

    getFilePath("C:/Windows/System32/drivers/etc/../../notepad.exe");

}

private static void getFilePath(String filename) {

    File f = new File(filename);

    try {      

        System.out.println("getAbsolutePath: " + filename + " " + f.getAbsolutePath());

        System.out.println("getCanonicalPath: " + filename + " " + f.getCanonicalPath());

    } catch (Exception e) {

        e.printStackTrace();

    }

}

</code>

输出结果如下。

1

2

3

4

5

6

<code>#!bash

getAbsolutePath: C:/Windows/System32/calc.exe C:\Windows\System32\calc.exe

getCanonicalPath: C:/Windows/System32/calc.exe C:\Windows\System32\calc.exe

getAbsolutePath: C:/Windows/System32/drivers/etc/../../notepad.exe C:\Windows\System32\drivers\etc\..\..\notepad.exe

getCanonicalPath: **C:/Windows/System32/drivers/etc/../../notepad.exe C:\Windows\System32\notepad.exe**

</code>

使用以下代码在Linux环境测试上述两个方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

<code>#!java

public static void main(String[] args) {

    getFilePath("/etc/hosts");

    getFilePath("/etc/rc.d/init.d/../../hosts");

}

private static void getFilePath(String filename) {

    File f = new File(filename);

    try {      

        System.out.println("getAbsolutePath: " + filename + " " + f.getAbsolutePath());

        System.out.println("getCanonicalPath: " + filename + " " + f.getCanonicalPath());

    } catch (Exception e) {

        e.printStackTrace();

    }

}

</code>

输出结果如下。

1

2

3

4

5

6

<code>#!bash

getAbsolutePath: /etc/hosts /etc/hosts

getCanonicalPath: /etc/hosts /etc/hosts

getAbsolutePath: /etc/rc.d/init.d/../../hosts /etc/rc.d/init.d/../../hosts

getCanonicalPath: **/etc/rc.d/init.d/../../hosts /etc/hosts**

</code>

可以看出,当File对象的文件路径中包含特殊字符时,JAVA能够按照操作系统的规范对其进行相应的处理。在Windows与Linux环境中,..均代表上一级目录,因此使用..能够访问上一级目录,导致任意文件读取漏洞产生。

防护方法

可在处理下载的代码中对HTTP请求中的待下载文件参数进行过滤,防止出现..等特殊字符,但可能需要处理多种编码方式。

也可在生成File对象后,使用getCanonicalPath获取当前文件的真实路径,判断文件是否在允许下载的目录中,若发现文件不在允许下载的目录中,则拒绝下载。

0x02 恶意文件上传


当攻击者利用恶意文件上传漏洞时,通常会向服务器上传jsp木马并访问,可以直接控制服务器。

示例

以下为恶意文件上传的示例。

upload目录中的upload.jsp为处理文件上传的jsp文件,内容如下。

1

2

3

4

5

6

7

<code>#!html

<form name="form1" action="<%=request.getContextPath()%>/strutsUploadFileAction_signle.action"

    method="post" enctype="multipart/form-data"><input type="file" name="file4upload"

        size="30"> <br> <input type="submit"

        value="submit_signle" name="submit">

</form>

</code>

strutsUploadFileAction_signle为处理文件上传的struts的action,内容如下。

1

2

3

4

5

6

<code>#!html

<action name="strutsUploadFileAction_signle" method="upload_signle" class="strutsUploadFile">

    <result name="success">upload/success.jsp</result>

    <result name="fail">upload/fail.jsp</result>

</action>

</code>

strutsUploadFile为处理文件上传的Spring的bean,内容如下。

1

2

3

4

<code>#!html

<bean id="strutsUploadFile" class="strutsTest.StrutsUploadFileAction">

</bean>

</code>

strutsTest.StrutsUploadFileAction为处理文件上传的JAVA类,在其中会检查上传的文件名是否以“.jpg”结尾,代码如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

<code>#!java

// 注意,并不是指前端jsp上传过来的文件本身,而是文件上传过来存放在临时文件夹下面的文件

private File file4upload;

// 提交过来的file的名字

private String file4uploadFileName;

// 提交过来的file的MIME类型

private String file4uploadContentType;

public String upload_signle() throws Exception {

    return uploadCommon(file4upload, file4uploadFileName);

}

private String uploadCommon(File file, String fileName) throws Exception {

    boolean success = false;

    try {

        String newFileName = "";

        String webPath = ServletActionContext.getServletContext()

                .getRealPath("/");

        String allowedType = ".jpg";

        String fileName_new = fileName.toLowerCase();

        // 本行代码有判断文件类型是否为".jpg",但存在文件名截断问题

        if(fileName_new.length() - fileName_new.lastIndexOf(allowedType) != allowedType.length()) {

            file.delete();

            ActionContext.getContext().put("reason""file type is not: " + allowedType);

            return "fail";

        }

        newFileName = webPath + "uploadDir/" + fileName;

        File dest = new File(newFileName);

        if (dest.exists())

            dest.delete();

        success = file.renameTo(dest);

    } catch (Exception e) {

        success = false;

        e.printStackTrace();

        throw e;

    }

    return success ? "success" "fail";

}

</code>

打开upload.jsp,选择文件“a.jpg”进行上传。

使用fiddler抓包并拦截,将filename参数修改为“a.jsp#.jpg”后的HTTP请求数据如下。

使用十六进制形式查看HTTP请求数据如下。

将#对应的字节修改为0x00并发送HTTP请求数据。

完成文件上传后,查看保存上传文件的目录,可以看到文件上传成功,生成的文件为“a.jsp”。

原因分析

从上述示例中可以看出,在上传文件时产生了文件名截断的问题。

使用以下代码测试JAVA写文件的文件名截断问题,使用0x00至0xff间的字符作为文件名生成文件。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

<code>#!java

public static void main(String[] args) {

    String java_version = System.getProperty("java.version");

    new File(java_version).mkdirs();   

    String filename = "a.jsp#a.jpg";

    for(int i=0; i<=0xff; i++) {

        String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i);

        File f = new File(filename_replace);

        try {

            f.createNewFile();

        } catch (Exception e) {

            System.out.println("error: " + i);

            e.printStackTrace();

        }

    }

}  

</code>

Windows环境文件名截断问题测试

在Windows 7,64位环境,使用JDK1.5执行上述代码生成文件的结果如下。

可以看到使用JDK1.5执行时,除0x00外,冒号“:”(ASCII码十进制为58)也会产生文件名截断问题。

JDK1.6与JDK1.5执行结果相同。

JDK1.7也与JDK1.5执行结果相同。

JDK1.8与JDK1.5执行结果不同,仅有冒号会产生文件名截断问题,0x00不会产生文件名截断问题,可能是JDK1.8已修复该问题。

使用Procmon查看上述过程中java.exe进程执行的写文件操作。

JDK1.5、1.6、1.7的监控结果相同,监控结果如下。

JDK1.5~1.7,当文件名中包含0x00时,java.exe在执行写文件操作时,会将0x00及之后的字符串丢弃,使用0x00之前的字符串作为文件名写文件。

JDK1.5~1.7,当文件名包含冒号时,java.exe在执行写文件操作时,不会将冒号及之后的字符串丢弃。

JDK1.8的监控结果如下。

JDK1.8,当文件名中包含0x00时,java.exe不会执行写文件的操作。

与JDK1.5~1.7一样,JDK1.8当文件名包含冒号时,java.exe在执行写文件操作时,不会将冒号及之后的字符串丢弃。截图略。

虽然java.exe在写文件时不会将冒号及之后的字符串丢弃,但在Windows环境下仍然出现了文件名截断的问题。

在Windows中执行“echo 1>abc:123”命令,可以看到生成的文件名为“abc”,冒号及之后的字符串被丢弃,造成了文件名截断。这是Windows特性导致的,与JAVA无关。

Linux环境文件名截断问题测试

在Linux RedHat 6.4环境,使用JDK1.6执行上述代码生成文件的结果如下。

JDK1.6,文件名中包含0x00时同样出现了文件名截断问题(文件名中包含ASCII码为92的反斜杠“\”时,生成的文件会产生在子目录中,但不会导致文件类型的变化)。

综上所述,JDK1.5-1.7存在0x00导致的文件名截断问题,与操作系统无关。冒号在Windows环境会导致文件名截断问题,与JAVA无关。

使用File对象的getCanonicalPath方法获取JAVA在文件名中包含0x00至0xff的字符时,生成文件时的实际文件路径,代码如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

<code>#!java

public static void main(String[] args) {

    String java_version = System.getProperty("java.version");

    String filename = "a.jsp#a.jpg";

    for(int i=0; i<=0xff; i++) {

        String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i);

        File f = new File(filename_replace);

        try {      

            System.out.println("getCanonicalPath " + f.getCanonicalPath());

        } catch (Exception e) {

            System.out.println("error: " + i);

            e.printStackTrace();

        }

    }

}  

</code>

Windows环境执行getCanonicalPath方法的结果

在Windows 7,64位环境,使用JDK1.5~1.7执行上述代码使用getCanonicalPath方法获取文件实际路径的结果相同,结果如下。

JDK1.5执行getCanonicalPath方法的结果。

JDK1.6执行getCanonicalPath方法的结果。

JDK1.7执行getCanonicalPath方法的结果。

可以看到JDK1.5~1.7使用getCanonicalPath方法获取文件实际路径时,当文件名中包含0x00时,获取到的文件实际路径中0x00及之后的字符串已被丢弃。

在Windows 7,64位环境,使用JDK1.8执行getCanonicalPath方法的结果如下。

可以看到JDK1.8使用getCanonicalPath方法获取文件实际路径时,当文件名中包含0x00时,会出现java.io.IOException异常,异常信息为“Invalid file path”。

Linux环境执行getCanonicalPath方法的结果

在Linux RedHat 6.4环境,使用JDK1.6执行上述代码的结果与Windows环境相同,截图略。

防护方法

以下的防护方法可以根据实际需求进行组合,相互之间没有冲突。

无效的防护方法

使用String对象的endsWith方法无法判断出文件生成时的实际文件名,使用以下代码进行证明。

1

2

3

4

5

6

7

8

9

10

11

12

13

<code>#!java

public static void main(String[] args) {

    String java_version = System.getProperty("java.version");

    String filename = "a.jsp#a.jpg";

    for(int i=0; i<=0xff; i++) {

        String filename_replace = java_version + "/" + i + "-" + filename.replace('#', (char)i);

        if(filename_replace.endsWith(".jpg")) {

            System.out.println("yes: " + filename_replace);

        }

    }

}  

</code>

执行结果如下。

当文件名为“a.jsp[特定字符]a.jpg”形式时,无论[特定字符]是否为0x00,使用String对象的endsWith方法对文件名进行检测,均认为是以“.jpg”结尾。

针对0x00进行检测

当文件名中包含0x00时,使用String对象的indexOf(0)方法执行结果非-1,可以检测到0x00的存在。但需考虑不同编码情况下0x00的形式。

检测实际的文件名

使用File对象的getCanonicalPath方法获取上传文件的实际文件名,若检测到文件名的后缀不是允许的类型(0x00截断,小于JDK1.8),或出现java.io.IOException异常(0x00截断,JDK1.8),或包含冒号(Windows环境中需处理),则说明需要拒绝本次文件上传。

修改保存上传文件的目录

上述的防护思路是防止攻击者将jsp文件上传至服务器中,本防护思路是防止攻击者上传的jsp文件被编译为class文件。

当JAVA中间件收到访问web应用目录中的jsp文件请求时,会将对应的jsp文件编译为class文件并执行。若将保存上传文件的目录修改为非web应用目录,当JAVA中间件收到访问上传文件的请求时,即使被访问的文件为jsp文件,JAVA中间件也不会将jsp文件编译为成class文件并执行,可以防止攻击者利用上传jsp木马控制服务器。

将保存上传文件的目录修改为非web应用目录的操作很简单,将处理文件上传代码中保存文件的目录修改为非web应用目录即可。进行该修改后,还可以使用共享目录解决多实例应用上传文件的问题。

将保存上传文件的目录修改为非web应用目录后,会导致无法使用原有方式访问上传的文件(例如文件上传目录原本为web应用目录中的upload目录,可直接使用http://[IP]:[PORT]/xxx/upload/xxx进行访问。将upload目录移动到非web应用目录后,无法再使用原有URL访问上传的文件)。可通过以下两种方法解决。

使用Servlet/action/.do请求访问上传文件,可参考前文中的download.DownloadAction类。本方法的影响面较大,不推荐使用。

除上述方法外,还可使用filter拦截HTTP请求处理,当HTTP请求访问文件上传目录中的文件时,读取对应的文件内容并返回(例如原本上传目录为web应用目录中的upload目录,可直接使用http://[IP]:[PORT]/xxx/upload/xxx进行访问。将upload目录移动到非web应用目录后,对HTTP请求处理进行拦截,当请求以“/xxx/upload”开头时,从文件上传目录中读取对应的文件内容并返回)。本方法可使用原本的URL访问上传文件,影响面较小,推荐使用。示例代码如下。

在web.xml中使用filter拦截HTTP请求处理。

1

2

3

4

5

6

7

8

9

10

11

<code>#!html

<filter>

    <filter-name>testFilter</filter-name>

    <filter-class>test.TestFilter</filter-class>

</filter>

<filter-mapping>

    <filter-name>testFilter</filter-name>

    <url-pattern>/*</url-pattern>

</filter-mapping>

</code>

对应的test.TestFilter类代码如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

<code>#!java

private static String IF_MODIFIED_SINCE = "If-Modified-Since";

private static String LAST_MODIFIED = "Last-Modified";

private static String startFlag = "/testDownload/upload/";

private static String storePath = "C:/Users/Public";

public void doFilter(ServletRequest request, ServletResponse response,

        FilterChain chain) throws IOException, ServletException {

    HttpServletRequest httpRequest = (HttpServletRequest) request;

    // 获取浏览器访问的URL,形式如/test/upload/xxx.jpg

    String requestUrl = httpRequest.getRequestURI();

    System.out.println("requestUrl: " + requestUrl);

    if (requestUrl != null) {

        // 判断是否访问upload目录的文件,若是则从对应的存储目录读取并返回

        if (requestUrl.startsWith(startFlag)) {

            try {

                returnFileContent(requestUrl, (HttpServletRequest) request,

                        (HttpServletResponse) response);

            } catch (Exception e) {

                // TODO Auto-generated catch block

                e.printStackTrace();

            }

            return;

        }

    }

    chain.doFilter(request, response);

    return;

}

// 当访问web应用特定目录下的文件时,重定向到实际存储这些文件的目录

private void returnFileContent(String url, HttpServletRequest request,

        HttpServletResponse response) throws Exception {

    java.io.InputStream in = null;

    java.io.OutputStream outStream = null;

    try {

        response.setHeader("Content-Type""text/plain");// 若不返回text/plain类型,浏览器无法正常识别文件类型

        String filePath = url.substring(startFlag.length() - 1);// 获取被访问的文件的URL

        String filePath_decode = URLDecoder.decode(filePath, "UTF-8");// 经过url解码之后的文件URL

        // 生成最终访问的文件路径

        // StorePath形式如C:/xxx/xxx,filePath_decode开头有/

        String targetfile = storePath + filePath_decode;

        System.out.println("targetfile: " + targetfile);

        File f = new File(targetfile);

        if (!f.exists() || f.isDirectory()) {

            System.out.println("文件不存在: " + targetfile);

            response.sendError(HttpServletResponse.SC_NOT_FOUND);// 返回错误信息,显示统一错误页面

            return;

        }

        // 判断上送的HTTP头是否有If-Modified-Since字段

        String modified = request.getHeader(IF_MODIFIED_SINCE);

        //获取文件的修改时间

        String modified_file = getFileModifiedTime(f);

        if (modified != null) {

            // 上送的HTTP头有If-Modified-Since字段,判断与对应文件的修改时间是否相同

            if(modified.equals(modified_file)) {

                //上送的文件时间与文件实际修改时间相同,不需返回文件内容

                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);//返回304状态

                outStream = response.getOutputStream();

                outStream.close();

                outStream.flush();

                outStream = null;

                return;

            }

        }

        // 文件无缓存,或文件有修改,需要在返回的HTTP头中添加文件修改时间

        response.setHeader(LAST_MODIFIED, modified_file);

        // 读取文件内容

        in = new FileInputStream(f);

        outStream = response.getOutputStream();

        byte[] buf = new byte[1024];

        int bytes = 0;

        while ((bytes = in.read(buf)) != -1)

            outStream.write(buf, 0, bytes);

        in.close();

        outStream.close();

        outStream.flush();

        outStream = null;

    } catch (Throwable ex) {

        ex.printStackTrace();

    } finally {

        if (in != null) {

            in.close();

            in = null;

        }

        if (outStream != null) {

            outStream.close();

            outStream = null;

        }

    }

}

// 获取指定文件的修改时间

private String getFileModifiedTime(File file) {

    SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);

    sdf.setTimeZone(TimeZone.getTimeZone("GMT"));

    return sdf.format(file.lastModified());

}

</code>

上述示例代码中,保存上传文件的目录为“C:/Users/Public”,当HTTP请求以“/testDownload/upload/”开头时,说明需要访问上传文件。

上述修改方法接管了JAVA中间件对原本上传目录的静态资源的访问请求,导致浏览器的缓存机制不可用。为了保证浏览器的缓存机制可用,上述代码中进行了专门处理。当HTTP请求头中不包含“If-Modified-Since”参数时,或“If-Modified-Since”对应的文件修改时间小于实际文件修改时间时,将文件的内容返回给浏览器,并在返回的HTTP头中加入“Last-Modified”参数返回文件修改时间,使浏览器对该文件进行缓存。当HTTP请求头的“If-Modified-Since”对应的文件修改时间等于实际文件修改时间时,不返回文件内容,将返回的HTTP码设为304,告知浏览器访问的文件无修改,可使用缓存。

以下为上述代码的测试结果。

web应用的目录中无upload目录。

文件上传目录“C:/Users/Public”中有以下文件。

访问文本文件正常。

访问图片正常。

访问音频文件正常。

访问jsp文件只返回文件本身的内容,不会被编译成class文件并执行。

使用fiddler查看访问记录,浏览器缓存机制正常。

修改web应用目录权限

将文件上传目录移出web应用目录后,JAVA中间件在运行过程中,web应用目录及其中的文件一般不会被修改。可在JAVA中间件启动后,将web应用目录设为JAVA中间件不可写;当需要进行版本更新或维护时,停止JAVA中间件后,将web应用目录设为JAVA中间件可写。通过上述限制,可严格地防止web应用目录被上传jsp木马等恶意文件。

可将JAVA中间件使用a用户启动,将web应用的目录对应用户设为b用户,JAVA中间件启动后,将web应用的目录设为a用户只读。需要进行版本更新或维护时,停止JAVA中间件后,将web应用的目录设为a用户可读写。对于某些JAVA中间件在运行过程中可能需要进行写操作的文件或目录,可单独设置权限。可将对web应用的权限修改操作在JAVA中间件启停脚本中调用,减少操作复杂度。

Windows的权限设置较复杂且速度较慢,使用上述的防护方法时会比较麻烦。

0x03 SQL注入


PreparedStatement与Statement

众所周知,在JAVA中使用PreparedStatement替代Statement可以防止SQL注入。

在oracle数据库中进行以下测试。

首先创建测试用的数据库表并插入数据。

1

2

3

4

5

6

7

8

9

10

11

12

13

<code>#!sql

create table test_user

(

username varchar2(100),

pwd varchar2(100)

);

Insert into TEST_USER

   (USERNAME, PWD)

 Values

   ('aaa''bbb');

COMMIT;

</code>

使用以下JAVA代码进行测试。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

<code>#!java

private Connection conn = null;

public dbtest2(String url, String username, String password)

        throws ClassNotFoundException, SQLException {

    try {

        Class.forName("oracle.jdbc.driver.OracleDriver");

        conn = DriverManager.getConnection(url, username, password);

    } catch (Exception e) {

        // TODO: handle exception

        e.printStackTrace();

    }

}

public void closeDb() throws SQLException {

    conn.close();

}

public void executeStatement(String username, String pwd)

        throws SQLException {

    String sql = "SELECT * FROM TEST_USER where username='" + username

            "' and pwd='" pwd "'";

    System.out.println("executeStatement-sql: " + sql);

    java.sql.Statement stmt = conn.createStatement();

    ResultSet rs = stmt.executeQuery(sql);

    showResultSet(rs);

    stmt.close();

}

public void executePreparedStatement(String username, String pwd)

        throws SQLException {

    java.sql.PreparedStatement stmt = conn

            .prepareStatement("SELECT * FROM TEST_USER where username=? and pwd=?");

    stmt.setString(1, username);

    stmt.setString(2, pwd);

    ResultSet rs = stmt.executeQuery();

    showResultSet(rs);

    stmt.close();

}

public void showResultSet(ResultSet rs) throws SQLException {

    ResultSetMetaData meta = rs.getMetaData();

    StringBuffer sb = new StringBuffer();

    int colCount = meta.getColumnCount();

    for (int i = 1; i <= colCount; i++) {

        sb.append(meta.getColumnName(i)).append("[")

                .append(meta.getColumnTypeName(i)).append("]").append("\t");

    }

    while (rs.next()) {

        sb.append("\r\n");

        for (int i = 1; i <= colCount; i++) {

            sb.append(rs.getString(i)).append("\t");

        }

    }

    // 关闭ResultSet

    rs.close();

    System.out.println(sb.toString());

}

public static void main(String[] args) throws SQLException {

    try {

        dbtest2 db = new dbtest2(

                "jdbc:oracle:thin:@192.xxx.xxx.xxx:1521:xxx",

                "xxx""xxx");

        db.executeStatement("aaa""bbb");

        db.executeStatement("aaa""' or '2'='2");

        db.executePreparedStatement("aaa""bbb");

        db.executePreparedStatement("aaa""' or '2'='2");

        db.closeDb();

    } catch (ClassNotFoundException e) {

        // TODO Auto-generated catch block

        e.printStackTrace();

    }

}

</code>

执行结果如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

<code>#!bash

1 db.executeStatement("aaa""bbb");对应的结果 

executeStatement-sql: SELECT * FROM TEST_USER where username='aaa' and pwd='bbb'

USERNAME[VARCHAR2]  PWD[VARCHAR2] 

aaa bbb

2 db.executeStatement("aaa""' or '2'='2");对应的结果 

executeStatement-sql: SELECT * FROM TEST_USER where username='aaa' and pwd='' or '2'='2'

USERNAME[VARCHAR2]  PWD[VARCHAR2] 

aaa bbb

3 db.executePreparedStatement("aaa""bbb");对应的结果 

USERNAME[VARCHAR2]  PWD[VARCHAR2] 

aaa bbb

4 db.executePreparedStatement("aaa""' or '2'='2");对应的结果 

USERNAME[VARCHAR2]  PWD[VARCHAR2] 

</code>

可以看到使用Statement时,将查询参数设为“username='aaa' and pwd='bbb'”使用正常的查询条件能查询到对应的数据。将查询参数设为“username='aaa' and pwd='' or '2'='2'”能够利用SQL注入查询到对应的数据。

使用PreparedStatement时,使用正常的查询条件同样能查询到对应的数据,使用能使Statement产生SQL注入的查询条件无法再查询到数据。

使用Wireshark对刚才的数据库操作抓包并查看网络数据。

查找select语句对应的数据包如下。

db.executeStatement("aaa", "bbb");对应的数据包如下,可以看到查询语句未使用oracle绑定变量方式,使用正常查询条件查询到了数据。

db.executeStatement("aaa", "' or '2'='2");对应的数据包如下,可以看到查询语句未使用oracle绑定变量方式,利用SQL注入查询到了数据。

db.executePreparedStatement("aaa", "bbb");对应的数据包如下,可以看到查询语句使用了oracle绑定变量方式,使用正常查询条件查询到了数据。

db.executePreparedStatement("aaa", "' or '2'='2");对应的数据包如下,可以看到查询语句使用了oracle绑定变量方式,SQL注入未生效,无法查询到对应数据。

在JAVA中使用PreparedStatement访问oracle数据库时,除了能防止SQL注入外,还能使oracle服务器降低硬解析率,降低系统开销,减少内存碎片,提高执行效率。

刚才执行的sql语句在oracle的v$sql视图中产生的数据如下。

ibatis

当使用ibatis作为持久化框架时,也需要考虑SQL注入的问题。使用ibatis产生SQL注入主要是由于使用不规范。

$#

在ibatis中使用#时,与使用PreparedStatement的效果相同,不会产生SQL注入;在ibatis中使用$时,与使用Statement的效果相同,会产生SQL注入。

继续使用刚才的数据库表TEST_USER进行测试,再插入一条数据如下。

1

2

3

4

5

6

7

<code>#!sql

Insert into TEST_USER

   (USERNAME, PWD)

 Values

   ('123''456');

COMMIT;

</code>

将log4j中的数据库相关日志级别设为DEBUG。

1

2

3

4

5

6

7

8

9

10

<code>#!sql

log4j.logger.com.ibatis=DEBUG

log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG

log4j.logger.com.ibatis.common.jdbc.ScriptRunner=DEBUG

log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG

log4j.logger.java.sql.Connection=DEBUG

log4j.logger.java.sql.Statement=DEBUG

log4j.logger.java.sql.PreparedStatement=DEBUG

log4j.logger.java.sql.ResultSet=DEBUG

</code>

首先使用#与$测试执行判断条件为“=”的sql语句时的情况。

在ibatis对应的xml文件中配置了语句test_right与test_wrong如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!html

<select id="test_right" resultClass="java.util.HashMap"

    parameterClass="java.util.HashMap">

    select * from test_user where username = #username#

</select>

<select id="test_wrong" resultClass="java.util.HashMap"

    parameterClass="java.util.HashMap">

    select * from test_user where username = '$username$'

</select>

</code>

在JAVA代码中执行上述语句如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!java

HashMap hs = new HashMap();

hs.put("username""' or '1'='1");

List<Object> list1 = queryListSql("test_right",hs);

logger.info("test-list1: " + list1);

List<Object> list2 = queryListSql("test_wrong",hs);

logger.info("test-list2: " + list2);

</code>

log4j中执行test_right语句时的相关日志如下。

1

2

3

4

5

6

7

8

<code>#!bash

[DEBUG] Preparing Statement:    select * from test_user where username = ? 

[DEBUG] Executing Statement:    select * from test_user where username = ?   

[DEBUG] Parameters: [' or '1'='1] 

[DEBUG] Types: [java language=".lang.String"][/java

[DEBUG] ResultSet 

[INFO ] test-list1: [] 

</code>

log4j中执行test_wrong语句时的相关日志如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!bash

[DEBUG] Preparing Statement:    select * from test_user where username = '' or '1'='1' 

[DEBUG] Executing Statement:    select * from test_user where username = '' or '1'='1' 

[DEBUG] Parameters: [] 

[DEBUG] Types: [] 

[DEBUG] ResultSet 

[DEBUG] Header: [USERNAME, PWD] 

[DEBUG] Result: [aaa, bbb] 

[DEBUG] Result: [123, 456] 

[INFO ] test-list2: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}] 

</code>

可以看到使用#可以防止SQL注入,使用$会产生SQL注入。

执行test_right语句时产生的数据包如下。

执行test_wrong语句时产生的数据包如下。

like

在使用ibatis执行判断条件为“like”的操作时,较容易误用$导致产生SQL注入问题。

当需要使用like时,应用使用“xxx like '%' || #xxx# || '%'”,而不应使用“xxx like '%$xxx$%'”(以oracle数据库为例)。

使用以下代码进行验证测试。

在ibatis对应的xml文件中配置了语句test_like_right与test_like_wrong如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!html

<select id="test_like_right" resultClass="java.util.HashMap"

    parameterClass="java.util.HashMap">

    select * from test_user where username like '%' || #username# || '%'

</select>

<select id="test_like_wrong" resultClass="java.util.HashMap"

    parameterClass="java.util.HashMap">

    select * from test_user where username like '$username$'

</select>

</code>

在JAVA代码中执行上述语句如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!java

HashMap hs = new HashMap();

hs.put("username""' or '1'='1");

List<Object> list3 = queryListSql("test_like_right",hs);

logger.info("test-list3: " + list3);

List<Object> list4 = queryListSql("test_like_wrong",hs);

logger.info("test-list4: " + list4);

</code>

log4j中执行test_like_right语句时的相关日志如下。

1

2

3

4

5

6

7

8

<code>#!bash

[DEBUG] Preparing Statement:    select * from test_user where username like '%' || ? || '%' 

[DEBUG] Executing Statement:    select * from test_user where username like '%' || ? || '%' 

[DEBUG] Parameters: [' or '1'='1] 

[DEBUG] Types: [java language=".lang.String"][/java

[DEBUG] ResultSet 

[INFO ] test-list3: [] 

</code>

log4j中执行test_like_wrong语句时的相关日志如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!bash

[DEBUG] Preparing Statement:    select * from test_user where username like '' or '1'='1'  

[DEBUG] Executing Statement:    select * from test_user where username like '' or '1'='1' 

[DEBUG] Parameters: [] 

[DEBUG] Types: [] 

[DEBUG] ResultSet 

[DEBUG] Header: [USERNAME, PWD] 

[DEBUG] Result: [aaa, bbb] 

[DEBUG] Result: [123, 456] 

[INFO ] [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}] 

</code>

执行语句时test_like_right产生的数据包如下。

执行语句时test_like_wrong产生的数据包如下。

in

在使用ibatis处理判断条件为“in”的操作时,同样容易误用$导致SQL注入问题。

当需要使用in时,可使用以下方法。

java代码。

1

2

3

4

5

6

<code>#!java

String[] xxx_list = new String[] { "xx1","xx2"};

HashMap hs = new HashMap();

hs.put("xxx", xxx_list);

//hs为sql语句查询参数

</code>

xml中的语句配置。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<code>#!html

<select id="" resultClass="java.util.HashMap"

parameterClass="java.util.HashMap">

    ...

    <dynamic prepend=" and ">

        <isNotEmpty prepend=" and  " property="xxx">

            (xxx in

                <iterate open="(" close=")" conjunction="," property="xxx">#xxx[]#</iterate>

            )

        </isNotEmpty>

    </dynamic>

    ...

</select>

</code>

当需要使用in时,不应使用“in ('$xxx$')”。

在ibatis对应的xml文件中配置了语句test_in_right与test_in_wrong如下。

1

2

3

4

5

6

7

8

9

10

11

12

<code>#!html

<select id="test_in_right" resultClass="java.util.HashMap"

    parameterClass="java.util.HashMap">

    select * from test_user where username in

    <iterate open="(" close=")" conjunction="," property="username">#username[]#</iterate>

</select>

<select id="test_in_wrong" resultClass="java.util.HashMap"

    parameterClass="java.util.HashMap">

    select * from test_user where username in ('$username$')

</select>

</code>

在JAVA代码中执行上述语句如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<code>#!java

String[] username_list = new String[] { "') or ('1'='1"};

hs.put("username", username_list);

List<Object> list5 = queryListSql("test_in_right",hs);

logger.info("test-list5: " + list5);

HashMap hs = new HashMap();

hs.put("username""') or ('1'='1");

List<Object> list6 = queryListSql("test_in_wrong",hs);

logger.info("test-list6: " + list6);

</code>

log4j中执行test_in_right语句时的相关日志如下。

1

2

3

4

5

6

7

8

<code>#!bash

[DEBUG] Preparing Statement:    select * from test_user where username in   (?) 

[DEBUG] Executing Statement:    select * from test_user where username in   (?) 

[DEBUG] Parameters: [') or ('1'='1] 

[DEBUG] Types: [java language=".lang.String"][/java

[DEBUG] ResultSet 

[INFO ] test-list5: [] 

</code>

log4j中执行test_in_wrong语句时的相关日志如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!bash

[DEBUG] Preparing Statement:    select * from test_user where username in ('') or ('1'='1'

[DEBUG] Executing Statement:    select * from test_user where username in ('') or ('1'='1'

[DEBUG] Parameters: [] 

[DEBUG] Types: [] 

[DEBUG] ResultSet 

[DEBUG] Header: [USERNAME, PWD] 

[DEBUG] Result: [aaa, bbb] 

[DEBUG] Result: [123, 456] 

[INFO ] test-list6: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}] 

</code>

执行test_in_right语句时产生的数据包如下。

执行test_in_wrong语句时产生的数据包如下。

在ibatis中在执行包含like或in的语句时,使用#也是能正常查询到数据的。

在JAVA代码中使用正确的查询条件执行test_like_right与test_in_right语句如下。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<code>#!java

HashMap hs = new HashMap();

hs.put("username""aaa");

List<Object> list7 = queryListSql("test_like_right",hs);

logger.info("test-list7: " + list7);

String[] username_list2 = new String[] { "aaa","123"};

hs.put("username", username_list2);

List<Object> list8 = queryListSql("test_in_right",hs);

logger.info("test-list8: " + list8);

</code>

log4j中使用正确的查询条件执行test_like_right语句时的相关日志如下。

1

2

3

4

5

6

7

8

9

10

<code>#!bash

[DEBUG] Preparing Statement:    select * from test_user where username like '%' || ? || '%' 

[DEBUG] Executing Statement:    select * from test_user where username like '%' || ? || '%' 

[DEBUG] Parameters: [aaa] 

[DEBUG] Types: [java language=".lang.String"][/java

[DEBUG] ResultSet 

[DEBUG] Header: [USERNAME, PWD] 

[DEBUG] Result: [aaa, bbb] 

[INFO ] test-list7: [{PWD=bbb, USERNAME=aaa}] 

</code>

log4j中使用正确的查询条件执行test_in_right语句时的相关日志如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!bash

[DEBUG] Preparing Statement:    select * from test_user where username in   (?,?) 

[DEBUG] Executing Statement:    select * from test_user where username in   (?,?) 

[DEBUG] Parameters: [aaa, 123] 

[DEBUG] Types: [java 1="java.lang.String" language=".lang.String,"][/java

[DEBUG] ResultSet 

[DEBUG] Header: [USERNAME, PWD] 

[DEBUG] Result: [aaa, bbb] 

[DEBUG] Result: [123, 456] 

[INFO ] test-list8: [{PWD=bbb, USERNAME=aaa}, {PWD=456, USERNAME=123}] 

</code>

使用正确的查询条件执行test_like_right语句时产生的数据包如下。

使用正确的查询条件执行test_in_right语句时产生的数据包如下。

上述全部语句执行时在oracle的v$sql视图中产生的数据如下。

0x04 其他问题


错误页

在web.xml中定义error-page,防止当出现错误时暴露服务器信息。

示例如下。

1

2

3

4

5

6

7

8

9

10

11

<code>#!html

<error-page>

    <error-code>404</error-code>

    <location>xxx.jsp</location>

</error-page>

<error-page>

    <error-code>500</error-code>

    <location>xxx.jsp</location>

</error-page>

</code>

仅允许已登录用户的访问

当用户访问jsp或Servlet/action/.do时,需要判断当前用户是否已登录且具有相应权限,防止出现越权使用。

0x05 后记


以上为本人的一点总结,难免存在错误之处,大牛请轻喷。

本文章来源于乌云知识库,文章版权归乌云知识库!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/jd_cx/article/details/124396649

智能推荐

jmu-Java-07多线程-Runnable与匿名类_6-1 jmu-java-07多线程-runnable与匿名类-程序员宅基地

文章浏览阅读902次,点赞2次,收藏3次。题目详情:6-1 jmu-Java-07多线程-Runnable与匿名类 (10 分)在Main方法中启动一个线程t1,该线程输出3行信息:主线程名 线程t1的线程名 线程t1所实现的所有接口。提示:使用System.out.println(Arrays.toString(getClass().getInterfaces()));输出。注:本题也可使用Lambda表达式实现。裁判测试程序:public class Main { public static void_6-1 jmu-java-07多线程-runnable与匿名类

[论文阅读] (07) RAID2020 Cyber Threat Intelligence Modeling Based on Heterogeneous GCN_cyber threat prediction using dynamic heterogeneou-程序员宅基地

文章浏览阅读6.7k次,点赞7次,收藏15次。前一篇文章分享了生成对抗网络GAN的基础知识,包括什么是GAN、常用算法(CGAN、DCGAN、infoGAN、WGAN)、发展历程、预备知识,并通过Keras搭建最简答的手写数字图片生成案例。这篇文章将详细介绍北航老师发表在RAID 2020上的论文《Cyber Threat Intelligence Modeling Based on Heterogeneous Graph Convolutional Network》,基于异构图卷积网络的网络威胁情报建模。希望这篇文章对您有所帮助。_cyber threat prediction using dynamic heterogeneous graph learning

Python三次样条插值与MATLAB三次样条插值简单案例_splev函数matlab-程序员宅基地

文章浏览阅读7.3k次,点赞7次,收藏51次。1 三次样条插值早期工程师制图时,把富有弹性的细长木条(所谓样条)用压铁固定在样点上,在其他地方让它自由弯曲,然后沿木条画下曲线,成为样条曲线。设函数S(x)∈C2[a,b] ,且在每个小区间[xj, xj+1]上是三次多项式,其中a=x0<x1<...<xn=b 是给定节点,则称S(x)是节点x0,x1,...xn上的三次样条函数。若在节点xj上给定函数值yj=f(xj..._splev函数matlab

Hadoop集群搭建所遇到的一些问题_error org.apache.hadoop.security.token.delegation.-程序员宅基地

文章浏览阅读3.9k次,点赞3次,收藏8次。1.命令找不到环境变量配置的不正确.2.权限问题2.1 /home/hadoop目录权限2.1 创建完hadoop用后/home/hadoop目录权限问题./home/Hadoop2.2 秘钥文件权限1.如下图的提示,是当前hadoop用的公钥没有添加到认证文件中去解决方法:将node1节点上的hadoop用户的公钥添加到认证文件中[hadoop@node1 ~]$ chmod -R 755 .ssh/[hadoop@node1 ~]$ cd .ssh/[hadoop@node_error org.apache.hadoop.security.token.delegation.abstractdelegationtokensec

ModuleNotFoundError: No module named ‘ahocorasick‘,亲测100%有效_modulenotfounderror: no module named 'ahocorasick-程序员宅基地

文章浏览阅读2.3k次,点赞10次,收藏10次。ModuleNotFoundError: No module named ‘ahocorasick‘,安装:pip install pyahocorasick -i HTTPS://mirrors.aliyun.com/pypi/simple/下面是我在安装是遇到的要安装最近在搞自然语言处理的知识图谱但是在搭建系统的时候使用了一个AC自动机词过滤,就是去掉敏感词汇就比如这些敏感词汇,需要将他替换成为**等一些隐含词或者符号。这个有许多的方法,这里列举几个:- 1、AC自动机- 2、DFA_modulenotfounderror: no module named 'ahocorasick

随便推点

OpenGLES demo - 11. 透视投影变换_opengles 透视变换-程序员宅基地

文章浏览阅读1.7k次。这章介绍一下透视投影变换,一个物体经过了透视投影变换之后,就会有jindayuan_opengles 透视变换

大数取模运算Barrett reduction-程序员宅基地

文章浏览阅读1.3w次,点赞11次,收藏40次。Barrett reduction算法的证明和使用。作者刚做完了课程设计作业,闲来无事写篇文章。大数中的指数运算,需要对一个数进行取模,因为最大可能二进制下2048位的2048位次方,所以必须边做乘法边取模。乘法使用快速幂,如果底数位数是x,指数位数是e,底数足够大的话,复杂度取决于模数,模数是m位的话,复杂度是O(m*m*e)。程序里,大数的存储是2的32次方进制的,这里..._barrett reduction

MIT 线性代数导论 第二讲:矩阵消元_方程组\left\{ \begin{matrix}3x - 4y = k + 1\\2x + 3y -程序员宅基地

文章浏览阅读367次。第二讲的主要内容:线性方程组的消元法使用矩阵语言表示消元过程向量、矩阵乘的理解置换矩阵的概念初步逆矩阵的概念线性方程组的消元法例子:{x+2y+z=23x+8y+z=124y+z=2\left\{\begin{matrix}x+2y+z=2\\ 3x+8y+z=12\\ 4y+z=2\end{matrix}\right.⎩⎨⎧​x+2y+z=23x+8y+z=12..._方程组\left\{ \begin{matrix}3x - 4y = k + 1\\2x + 3y = 5\end{matrix}\right.{ 3x 4y=k+1 2x+3y=5 的解中x与y的值相等,则k等于( ) a -1 b -2 c -3 d -4 查看提示

macOS下配置环境变量/查看环境变量_mac查看环境变量-程序员宅基地

文章浏览阅读4.8w次,点赞27次,收藏98次。Mac OS 的配置文件清单a. /etc/profile b. /etc/paths c. ~/.bash_profile d. ~/.bash_login e. ~/.profile f. ~/.bashrc 终端如何查看java的安装目录?输入命令:/usr/libexec/java_home -V如何查看java的版本信息?输入命令:java -version如何查看Mac的全部环境变量输入命令 export,查看全部的环境变量如何设置系统环境变量?系统环境变量应该在_mac查看环境变量

table表格------一行隐藏和显示_让row里面的内容成一行显示超出隐藏-程序员宅基地

文章浏览阅读5k次。这个功能是table原生js操作一行表_让row里面的内容成一行显示超出隐藏

使用C#使用Windows的HID通信-程序员宅基地

文章浏览阅读2k次。Windows使用HID通信相对比较简单,HID都是通过PID、VID信息来查找连接的,相比于串口,几乎无变化,连接无需人工选择,十分方便,也不需要驱动。下面上实例,PID为0x003f,VID为0x04D8,支持发送接收数据显示到UI,使用C#来编写,调用的是windows api(create file、read file、write file)。本实例将HID接口分成3层,支持自动连接..._c# hid 指定pid和vid

推荐文章

热门文章

相关标签