总结常见漏洞的代码审计方法

本文作者:Pamela@涂鸦智能安全实验室

前言

这篇文章主要是总结一下在安全工作中常见漏洞的代码审计方法,以及修复方案,希望能对初学代码审计小伙伴们有所帮助,笔芯️

代码审计的思路

通常做代码审计都是检查敏感函数的参数,然后回溯变量,判断变量是否可控并且没有经过严格的过滤,这是一个逆向追踪的过程。而代码审计并非这一种手段,还可以先找出哪些文件在接收外部传入的参数,然后跟踪变量的传递过程,观察是否有变量传入到高危函数里面,或者传递的过程中是否有代码逻辑漏洞,这是一种正向追踪的方式,这样的挖掘方式比逆向追踪挖掘得更全。还有一种方式是直接挖掘功能点漏洞,根据自身的经验判断该类应用通常在哪些功能中会出现漏洞,直接全篇阅读该功能代码。

常见漏洞的代码审计

1. HTTP响应头截断

漏洞描述

HTTP响应截断是由于应用程序未对用户提交的数据进行严格过滤,当用户恶意提交包含 CR(回车,即URL编码%0d或\r)和 LF(换行符,即URL编码%0a或\n)的HTTP请求,服务器可能会创建两个 HTTP 响应,攻击者可以控制第二个响应并加载攻击。 攻击者可控制响应的内容构造XSS攻击,其中响应中包含恶意的JavaScript或其它代码在用户的浏览器中执行,也有可能让用户重定向到攻击者控制的Web内容或在用户的主机上执行恶意操作。

审计方法

检查对响应头字段是否进行安全处理。

如果未对响应头进行任何安全处理,则为确认:

//未对响应头做任何安全处理,审计时为确认
String data;
if (data != null){
	response.addHeader("Location", "/author.jsp?lang=" + data);
}

再举一例:

//同样的未对响应头做任何安全处理,审计时为确认
String author = request.getParameter(AUTHOR_PARAMETER);
// ...
Cookie cookie = new Cookie("author", author);
response.addCookie(cookie);

如果对响应头做了响应的安全处理,则为误报:

//使用Refenence类对环境变量值进行编码,剔除特殊字符,为误报
if (data != null){
	String decode = Reference.decode(data);
	response.addHeader("Location", "/author.jsp?lang=" + decode);
}

修复方案:

1、对用户的输入进行合理验证,对特殊字符(如<、>、’、”等)等进行编码。

2、创建一份安全字符白名单,只接受完全由这些受认可的字符组成的输入出现在 HTTP 响应头文件中。 3、使用源代码静态分析工具,进行自动化的检测,可以有效的发现源代码中的 HTTP 响应截断问题。

2. 硬编码问题

漏洞描述

硬编码问题,是指将敏感数据(包括口令和加密密钥,部分账号的密码以及其他敏感信息等)硬编码在程序中。

审计步骤

1、看扫描出的硬编码是否为常规单词(或通读代码查看是否有硬编码敏感文件)

2、如果是常规单词,则为误报,如:

//fipAddress为硬编码
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

再举一例:

//SECRET_PASSWORD为硬编码
private String SECRET_PASSWORD = "No fear in my heart!"; 
Properties props = new Properties(); 
props.put(Context.SECURITY_CREDENTIALS, "password");

3、如果是随机字符串,则为确认或待确认,如:

//1546272000000为硬编码
byte[] sr = hBaseClient.buildRowKey(devId, "1546272000000");

再举一例:

//qafgshw1900wxxxx为硬编码
private String accessKeyId = "qafgshw1900wxxxx";

4、追踪key值,如果key为硬编码则为确认,如:

//key值为硬编码
byte[] key = {1, 2, 3, 4, 5, 6, 7, 8};
SecretKeySpec spec = new SecretKeySpec(key, "AES");
Cipher aes = Cipher.getInstance("AES");
aes.init(Cipher.ENCRYPT_MODE, spec);
return aesCipher.doFinal(secretData);

追踪key值,如果追踪不到或者为安全形式,则为误报,如

//无法再继续追踪key值,为误报
public static byte[] encryptByPrivateKey(byte[] data, String key) throws Exception {
    //key为密钥
    byte[] keyBytes = decryptBASE64(key);
    PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
    Key privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
    Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
    cipher.init(Cipher.ENCRYPT_MODE, privateKey);
    return cipher.doFinal(data);
}

再来一栗:

//存储密钥。KeyStore.getInstance("PKCS12")为密钥库,为误报
try{
    KeyStore keyStore = KeyStore.getInstance("PKCS12");
    keyStore.load(null, null);

    KeyGenerator keyGen = KeyGenerator.getInstance("AES");
    keyGen.init(128);
    Key key = keyGen.generateKey();
    keyStore.setKeyEntry("secret", key, "password".toCharArray(), null);

    keyStore.store(new FileOutputStream("output.p12"), "password".toCharArray());
} catch (Exception ex){
    ex.printStackTrace();
}

修复方案

推荐使用配置文件或者通过配置中心来下发这些敏感配置,密码和密钥应存储在单独的加密配置文件或密钥库中。

3. SQL注入

漏洞描述

注入攻击的本质,是程序把用户输入的数据当做代码执行。这里有两个关键条件:

第一是用户能够控制输入;

第二是用户输入的数据被拼接到要执行的代码中从而被执行。

sql 注入漏洞则是程序将用户输入数据拼接到了 sql 语句中,从而攻击者即可构造、改变 sql 语义从而进行攻击。

漏洞举例

(1) 直接通过拼接 sql

@RequestMapping("/SqlInjection/{id}")
public ModelAndView SqlInjectTest(@PathVariable String id){
String mysqldriver = "com.mysql.jdbc.Driver";
String mysqlurl = 
"jdbc:mysql://127.0.0.1:3306/test?user=root&password=123456&useUnicode=true&c
haracterEncoding=utf8&autoReconnect=true";#直接通过拼接 sql
String sql = "select * from user where id=" + id;
ModelAndView mav = new ModelAndView("test2"); 
try{
Class.forName(mysqldriver);
Connection conn = DriverManager.getConnection(mysqlurl);
PreparedStatement pstt = conn.prepareStatement(sql);
ResultSet rs = pstt.executeQuery();

再来一例:

//没有做任何其他安全处理措施
stmt = conn.createStatement();
rs = stmt.executeQuery("select * from user where username = '" + username+"' and password='"+password+"'");

(2) 预编译使用有误

漏洞举例:

//只使用了占位符
@RequestMapping("/SqlInjection/{id}")
public ModelAndView SqlInjectTest(@PathVariable String id){
String mysqldriver = "com.mysql.jdbc.Driver";
String mysqlurl = 
"jdbc:mysql://127.0.0.1:3306/test?user=root&password=123456&useUnicode=true&c
haracterEncoding=utf8&autoReconnect=true";
String sql = "select * from user where id= ?";
ModelAndView mav = new ModelAndView("test2"); 
try{
Class.forName(mysqldriver);
Connection conn = DriverManager.getConnection(mysqlurl);
PreparedStatement pstt = conn.prepareStatement(sql);
//pstt.setObject(1, id); //一般使用有误的是没有用这一句,编码者以为在上面的sql语句中直
接使用占位符就可以了。
ResultSet rs = pstt.executeQuery();

审计步骤:查看预编译的完整性,关键函数定位 setObject()、setInt()、setString()、setSQLXML()关联上下文搜索 set* 开头的函数。

(3) %和_(oracle 中模糊查询)问题

@RequestMapping("/SqlInjection/{id}")
public ModelAndView SqlInjectTest(@PathVariable String id){
String mysqldriver = "com.mysql.jdbc.Driver";
String mysqlurl = 
"jdbc:mysql://127.0.0.1:3306/test?user=root&password=123456&useUnicode=true&c
haracterEncoding=utf8&autoReconnect=true";
String sql = "select * from user where id= ?";
ModelAndView mav = new ModelAndView("test2"); 
try{
Class.forName(mysqldriver);
Connection conn = DriverManager.getConnection(mysqlurl);
PreparedStatement pstt = conn.prepareStatement(sql);
pstt.setObject(1, id); //使用预编译
ResultSet rs = pstt.executeQuery();

审计步骤:定位相关 sql 语句上下文,查看是否有显式过滤机制。

修复方案:上面的代码片段即使这样依然存在 sql 注入,原因是没有手动过滤%。预编译是不能处理这个符号的,以需要手动过滤,否则会造成慢查询,造成 dos。

(4) order by 问题

String sql = “Select * from news where title =?”+ “order by ‘” + time + 
“’asc”

审计步骤:定位相关 sql 语句上下文,查看是否有显式过滤机制。

修复方案:类似上面的这种 sql 语句 order by 后面是不能用预编译处理的只能通过拼接处理,所以需要手动过滤。

(5) 有关$符号的情况

#{}:相当于jdbc中的preparedstatement,传入的字符串,需要赋值后使用,可以有效防止sql注入

${}:是输出变量的值,传入的变量,直接在sql中执行,无法防止sql注入 简单的说就是#{}传过来的参数带单引号’’,而${}传过来的参数不带单引号。

但是orderby是动态SQL,只能用${},用#{}会多个’ ‘导致sql语句失效。此外还有一个like 语句后也需要用${}。

//需要转义的字符串仍使用$
delete from ${tableName}

修复方案:对于可以使用#{}的情况,直接使用#{}即可解决问题对于不能使用#{}的情况(如orderby),需要增加额外的过滤逻辑,以此判断输入内容是否正常,如字段类型、字段长度等

4. maven不安全模块

漏洞描述

Maven,是一个Java开发比较常用的项目管理工具,可以对Java项目进行构建、依赖管理。当它配置一个不安全的模块时,即存在安全风险。

审计方法

查看配置的版本是否属于安全版本区间。 如果是,则确认:

//3.9版本是存在漏洞的版本。安全版本是3.11以上
<dependency>
    <groupId>com.hazelcast</groupId>
    <artifactId>hazelcast-client</artifactId>
    <version>3.9</version>
</dependency>

修复方案

配置为安全版本即可(同时应注意解决兼容性问题)

5. 服务端请求伪造(SSRF)

漏洞描述

SSRF是攻击者让服务端发起指定的请求,攻击的目标一般是从外网无法访问的内网系统。SSRF形成的原因大都是由于代码中提供了从其他服务器应用获取数据的功能但没有对目标地址做过滤与限制。比如从指定URL链接获取图片、下载等。一般利用http协议来探测端口,利用file协议读取任意文件。

利用场景

SSRF漏洞一般位于远程图片加载与下载、图片或文章收藏功能、URL分享、通过URL在线翻译、转码等功能点处。

关键词/接口/类包

// Java
HttpURLConnection.getInputStream
URLConnection.getInputStream
Request.Get.execute
Request.Post.execute
URL.openStream
ImageIO.read
OkHttpClient.newCall.execute
HttpClients.execute
HttpClient.execute

审计方法

1、内网系统的SSRF直接为误报(内网系统之间互调互传)

2、检查请求的URL是否为外部可控,即由外部传入

3、检查请求的返回,是否对请求的返回数据做了安全处理

漏洞示例

//请求URL为外部可控,返回数据直接展示
String url = request.getParameter("picurl");
StringBuffer response = new StringBuffer();

URL pic = new URL(url);
HttpURLConnection con = (HttpURLConnection) pic.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("User-Agent", "Mozilla/5.0");
//发起请求,触发漏洞
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
	response.append(inputLine);
  }
in.close();
modelMap.put("resp",response.toString());
return "getimg.htm";

再举一例:

//HttpClients函数的SSRF漏洞代码,审计时为确认:
String url = request.getParameter("url");
CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
HttpResponse httpResponse = client.execute(httpGet); //发起请求

修复方案

  1. 使用白名单校验HTTP请求url地址
  2. 避免请求url外部可控
  3. 避免将请求响应及错误信息返回给用户
  4. 禁用不需要的协议及限制请求端口,仅仅允许http和https请求等

6. 路径遍历

目录/路径遍历、任意文件上传/下载

漏洞描述

在下载文件相关的代码中,若不对HTTP请求中的待下载文件名进行检查,则有可能产生任意文件下载漏洞。攻击者可以指定文件名、文件路径等文件操作的参数,越权访问正常情况下无法到达的系统资源。

审计方法

  1. 判断是否存在上传下载行为
  2. 判断路径是否可被外部控制,如果外部可控则向下
  3. 判断是否对文件路径做了安全措施,如果做了安全措施为误报

漏洞示例

没有对路径做任何安全处理,如:

//没有对路径做任何安全处理措施
path = "config/"+path;
File file = new File(path);
System.out.println(path);
response.setHeader("Content-Disposition", "attachment;filename=\\""
        + new String(path.getBytes(), "ISO8859-1") + "\\"");
response.setContentLength((int) file.length());
byte[] buffer = new byte[4096];// 缓冲区
BufferedOutputStream output = null;
BufferedInputStream input = null;

再举一例:

//对下载的文件未做安全处理
public Response getImage(@javax.ws.rs.PathParam("image") String image) {
    File file = new File("resources/images/", image);
    if (!file.exists()) {
        return Response.status(Status.NOT_FOUND).build();
    }
    return Response.ok().entity(new FileInputStream(file)).build();
}

修复方案

  1. 使用getCanonicalPath()、getAbsolutePath()等方法获取规范路径
  2. 对文件操作参数进行输入验证,过滤特殊字符
  3. 通过文件头判断来限制文件类型,而不是通过文件后缀来判断
  4. 服务器安全配置策略文件。将所能读取的文件限定在特定的目录下

7. 命令注入

漏洞描述

命令注入是指通过提交恶意构造的参数破坏命令语句结构,当对用户输入的命令没有进行限制或者过滤不严导致用户可以执行任意命令时,就会造成命令执行漏洞。通常表现为攻击者能够篡改程序执行的命令或命令执行的环境,从而直接或间接的控制了所执行的命令。

常见的命令执行方法

// Java
Runtime.exec
ProcessBuilder.start
GroovyShell.evaluate

审计方法

  1. 检查是否是系统命令,如果不是系统命令而是常规字符串拼接,则为误报
  2. 检查所执行的命令是否为外部可控制,如果外部不可控(例如命令拼接参数不为外部控制)则为误报
  3. 外部可控制的情况下检查命令的拼接参数是否做了安全措施,如果未做安全措施即为确认

漏洞示例

(1) 没有对外部传入的命令拼接参数做任何安全处理措施,如:

//没有对外部传入的命令参数command做任何安全处理
System.out.println("Command: ping"+ command);
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("cmd.exe /C ping "+command);
int res = proc.waitFor();
if(res !=0){
    System.out.println("process error: "+ res);
}
InputStream in = (res == 0)? proc.getInputStream() : proc.getErrorStream();
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
buffer=new StringBuffer();
String line;
while((line = reader.readLine())!=null){
    buffer.append(line+"\\n");
}

再举一例:

//同样没有对外部传入的命令拼接参数input做任何安全处理
Runtime r = Runtime.getRuntime();
r.exec("/bin/sh -c some_tool" + input);

(2) 对外部传入的命令拼接参数做安全限制,如:

//对外部传入的命令拼接参数做了限制

//正则限定为合法IP地址
if (!Pattern.matches("([1-9]|[1-9]\\\\d|1\\\\d{2}|2[0-4]\\\\d|25[0-5])(\\\\.(\\\\d|[1-9]\\\\d|1\\\\d{2}|2[0-4]\\\\d|25[0-5])){3}", command)){
	//如果不是IP则匹配不成功,则为F,!F则为T,T则执行此代码块
    result.put("message", "Error!");
    return DataUtil.toJson(result);
}
System.out.println("Command: ping"+ command);
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("cmd.exe /C ping "+command);
int res = proc.waitFor();
if(res !=0){
    System.out.println("process error: "+ res);
}
InputStream in = (res == 0)? proc.getInputStream() : proc.getErrorStream();
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
buffer=new StringBuffer();
String line;
while((line = reader.readLine())!=null){
    buffer.append(line+"\\n");
}

修复方案

  1. 构建白名单,只允许其中的字符出现在输入中
  2. 应有应用程序来控制命令,并使用绝对路径来执行命令
  3. 严格的权限限制,程序执行外部命令使用最小权限原则
  4. 严格的参数校验

Reference

  1. 《代码审计:企业级Web代码安全架构》
  2. https://websec.readthedocs.io/zh/latest/misc/aduit.html#id13
  3. https://www.t00ls.net/downloads/pdf/44616f331b0cb3bdffbafe3b3399f34e.pdf

漏洞悬赏计划:涂鸦智能安全响应中心(https://src.tuya.com)欢迎白帽子来探索。

招聘内推计划:涵盖安全开发、安全测试、代码审计、安全合规等所有方面的岗位,简历投递sec@tuya.com,请注明来源

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
大佬不来一句? 抢沙发

请登录后发表评论