前言
这篇文章主要是总结一下在安全工作中常见漏洞的代码审计方法,以及修复方案,希望能对初学代码审计小伙伴们有所帮助。
这是大白给粉丝盆友们整理的代码审计阶段第3篇。
喜欢的朋友们,记得给大白点赞支持和收藏一下,关注我,学习黑客技术。
代码审计的思路
1. 懂得漏洞类型产生原理
2. 懂得危险函数的参数不当使用可造成的漏洞威胁。例如:
涉及到命令执行代码执行的eval,assert,array\_map,usort等
本身函数的脆弱性,is\_numeric,md5等。
3. 晓得php函数的脆弱性。例如:
==与===
===并不是强大无比不可绕过的,也要结合代码设计逻辑。
4. php的淫技技巧。
5. php版本及配置不当结合函数不当利用造成的漏洞威胁。
常见漏洞的代码审计
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注入
KaTeX parse error: Expected ‘EOF’, got ‘#’ at position 44: …法防止sql注入 简单的说就是#̲{}传过来的参数带单引号’',…{}传过来的参数不带单引号。
但是orderby是动态SQL,只能用KaTeX parse error: Expected ‘EOF’, got ‘#’ at position 5: {},用#̲{}会多个’ '导致sql语句…{}。
//需要转义的字符串仍使用$
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); //发起请求
修复方案
-
使用白名单校验HTTP请求url地址
-
避免请求url外部可控
-
避免将请求响应及错误信息返回给用户
-
禁用不需要的协议及限制请求端口,仅仅允许http和https请求等
6. 路径遍历
目录/路径遍历、任意文件上传/下载
漏洞描述
在下载文件相关的代码中,若不对HTTP请求中的待下载文件名进行检查,则有可能产生任意文件下载漏洞。攻击者可以指定文件名、文件路径等文件操作的参数,越权访问正常情况下无法到达的系统资源。
审计方法
-
判断是否存在上传下载行为
-
判断路径是否可被外部控制,如果外部可控则向下
-
判断是否对文件路径做了安全措施,如果做了安全措施为误报
漏洞示例
没有对路径做任何安全处理,如:
//没有对路径做任何安全处理措施
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();
}
修复方案
-
使用getCanonicalPath()、getAbsolutePath()等方法获取规范路径
-
对文件操作参数进行输入验证,过滤特殊字符
-
通过文件头判断来限制文件类型,而不是通过文件后缀来判断
-
服务器安全配置策略文件。将所能读取的文件限定在特定的目录下
7. 命令注入
漏洞描述
命令注入是指通过提交恶意构造的参数破坏命令语句结构,当对用户输入的命令没有进行限制或者过滤不严导致用户可以执行任意命令时,就会造成命令执行漏洞。通常表现为攻击者能够篡改程序执行的命令或命令执行的环境,从而直接或间接的控制了所执行的命令。
常见的命令执行方法
// Java
Runtime.exec
ProcessBuilder.start
GroovyShell.evaluate
审计方法
-
检查是否是系统命令,如果不是系统命令而是常规字符串拼接,则为误报
-
检查所执行的命令是否为外部可控制,如果外部不可控(例如命令拼接参数不为外部控制)则为误报
-
外部可控制的情况下检查命令的拼接参数是否做了安全措施,如果未做安全措施即为确认
漏洞示例
(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");
}
修复方案
-
构建白名单,只允许其中的字符出现在输入中
-
应有应用程序来控制命令,并使用绝对路径来执行命令
-
严格的权限限制,程序执行外部命令使用最小权限原则
-
严格的参数校验
常见代码审计工具,代码审计为什么不能只用工具?
代码审计是一种发现程序漏洞,安全分析为目标的程序源码分析方式。今天主要分享的是几款常用的代码审计工具,以及代码审计工具有哪些优缺点?
代码审计工具
seay代码审计工具,是一款开源的利用C#开发的一款代码审计工具。主要有SQL注入、xss跨站、命令执行、文件包含、文件上传、正则匹配、数据库执行监控等程序漏洞的监测。
frotify sca是惠普开发的一款商业性质的代码审计工具,主要包含了数据流、控制流、语义、配置、结构五大分析引擎。
rips是一款php开发,监测php程序漏洞的代码分析工具。该工具现目前的版本是0.5,并很早之前就已经停止更新。该工具能够发现SQL注入、xss跨站,文件包含,文件上传、代码执行、文件读取等漏洞。
findbugs是一款静态分析工具,属于eclipse的插件工具。
burp suite属于一款工具集成平台,主要包含了proxy(拦截http/s的代理服务器)、Spider(只能感应的网络爬虫)、Scanner(web应用漏洞)、Intruder(高度可配置工具)、Repeater(应用响应工具)、Sequencer(预测一些不可预知的漏洞,补发单独的http请求)、Decoder(程序的解码)、Comparer(反映请求和响应的差异)。该工具需要安装java环境。
Gerrit是一个基于 Web 的代码审查系统,适于采用 Git 版本控制系统开发的项目进行在线代码审查。
Phabricator是一个完整的开源软件应用程序,以Web基础,代码审查、计划、测试、浏览与审计评分、发现Bug等功能。
Review Assistant是Visual Studio的代码审查插件。可以创建审阅请求并在不离开Visual Studio的情况下对其进行响应。支持TFS,Subversion,Git,Mercurial和Perforce。
cobra:支持PHP、Jav等主要开发语言及其它数十种文件类型,支持检测多种漏洞类型,支持命令行模式和API模式。
VCG:它是一个基于字典的自动化源代码扫描工具,支持 C++, C#, VB, PHP, Java, PL/SQL and COBOL等多种语言,可以由用户自定义需要扫描的数据,可以对源代码中所有可能存在风险的函数和文本做一个快速定位和检索。
RIPS:PHP代码审计工具,支持跨平台部署,小巧强大。
SonarQube:SonarQube 是一款用于代码质量管理的开源工具,它主要用于管理源代码的质量。通过插件形式,可以支持众多计算机语言,比如 java, C#, go,C/C++, PL/SQL, Cobol, JavaScrip, Groovy 等。sonar可以通过PMD,CheckStyle,Findbugs等代码规则检测工具来检测代码,帮助发现代码漏洞。
代码审计工具优缺点
优点
1.降低人工成本
对于应用程序,扫描模式是通用的,计算机比人类更擅长这类扫描。在这种情况下,扫描器在扫描大型代码库漏洞时扮演着重要角色
2.发现漏洞的所有实例
扫描器可以非常有效地识别特定漏洞的所有实例及具体位置。跟踪大型代码库中所有文件的缺陷很难,扫描器对于这种情况很有帮助
3.源库分析
一些分析器通过源库分析的方式跟踪代码并识别漏洞。分析器识别可能的应用输入,并在所有源代码中彻底追踪输入,直到发现所有不符合安全代码模式的应用输入。源库分析能够帮助开发人员更好地理解代码缺陷和获得代码缺陷的根本原因
4.详细的报告格式
扫描器能够提供详细的分析报告,包括具体代码段的漏洞、漏洞的风险等级和详细描述。
缺点
1.未涉及业务逻辑缺陷
扫描器不能识别应用程序的业务逻辑、事务处理和敏感数据。通常,扫描器不能发现应用程序中需要实现的特定于某些功能和设计的安全控制。这些被认为是静态代码分析器的最大限制
2.范围有限
通常,静态代码分析器被设计用于特定的框架或语言,并且能够在一定范围内搜索特定漏洞模式的集合。在此范围之外,静态代码分析器并不能解决在搜索模式库中未涵盖的问题
3.不能发现设计缺陷
代码框架的设计缺陷没有固定模式,静态代码扫描器专注于代码层面。需要人工查看代码才能识别设计问题
4.误报
被扫描器标记的问题并非完全真实,需要有经验、了解安全编码的技术人员理解问题缺陷并对其进行分类。
总结
在源代码审计实际实施过程中,通常采用工具+人工的方式,首先使用源代码审计的扫描工具对源代码进行扫描,完成初步的信息收集,然后由人工的方式对源代码扫描结果进行人工的分析和确认。
根据收集的各类信息对客户要求的重要功能点进行人工源代码审计。
结合自动化源代码扫描和人工源代码审计两方的结果,源代码审计服务人员需整理源代码审计服务的输出结果并编制源代码审计报告,最终提交客户和对报告内容进行沟通。
为了帮助大家更好的学习网络安全,我给大家准备了一份网络安全入门/进阶学习资料,里面的内容都是适合零基础小白的笔记和资料,不懂编程也能听懂、看懂这些资料!
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
[2024最新CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享]
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
[2024最新CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享]
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取