甲方视角下的代码审计

甲方视角下的代码审计

前言

前段时间看了平安SRC线上沙龙系列主题分享活动,其中有一个议题是hldf师傅讲的甲方视角下的代码审计,收货颇丰。

正好最近在学习代码审计的相关知识,结合之前学习的SDL流程整理一下笔记。

不管是SDL还是DevSecOps,都强调安全左移,在软件开发的生命周期中更早的嵌入安全动作,更快的收敛安全漏洞问题,其中的关键点之一就是代码审计。

代码审计流程

image-20221112152139994

目前主流的代码审计大体上可以分为自动化代码扫描方案和人工代码审计两种。

自动化代码扫描方案:主要是将静态代码扫描嵌入到CI/CD中,将发现漏洞和修复漏洞的工作提前到开发阶段,降低修复漏洞的成本。这一部分需要投入较多的资源去运营规则,秉持“宁可漏报,不要误报”的原则。这种方案常常只能检测出一些常规的基础漏洞,对工具没有覆盖的场景就无能为力了,所以自动化代码扫描方案比较适用于编码红线的检查。

人工代码审计:将自动化代码扫描工具和人工代码审计进行结合,最大程度的挖掘到系统可能存在的漏洞,这种方案更适用于重点系统的漏洞挖掘。

本文主要讲解代码审计,所以来看看人工代码审计的流程和注意要点。

人工代码审计的流程:

  1. 未授权接口审计:未授权接口的高危漏洞造成的危害是很大的,对于这部分内容需要保证审计的代码是系统的全部代码;收集系统对外暴露的所有Web服务的端口,定位每个端口对应的服务代码。
  2. 后台接口审计:这一部分就不能审全部代码了,可以结合静态代码分析工具和人工代码审计两种方法来审计后台接口。这里的人工代码审计的方向主要是敏感函数和敏感功能点。

接下来看Java项目中如何梳理未授权接口。Java中的权限校验主要分为1、自定义的过滤器、拦截器、AOP等方法,去拦截Http请求或者方法调用,进行权限/授权校验。2、使用第三方的安全框架进行认证/授权校验,如Shiro、SpringSecurity。

针对这两部分也有不同的侧重点:

未授权接口审计

首先来看Filter,基础的JavaWeb提供的功能,在web.xml中注册,下图为SpringCloud gateway框架下通过注解实现的过滤器。只需要实现全局过滤器GlobalFilter中的filter方法就可以实现一个过滤器。

可以看到这个代码通过shouldFilter方法判断哪些路由需要拦截,所以我们在审计的时候可以从shouldFilter方法中找到系统的未授权接口。这个代码还有一个需要注意的点是这个Order参数,这个参数代表Filter执行的顺序,数值越小代表执行的优先级越高。有时候有很多过滤器,这个时候就需要根据优先级来进一步分析。

image-20230224005432147

接下来看Spring内置的WebMvcConfigurer类注册的拦截器,实战这个配置类只需要重写addInterceptors来添加一个拦截器即可。

这个拦截器是进行认证校验的,通过拦截路径可以看到它是拦截所有请求,下面通过excludePathPatterns添加了一些白名单,这里就找到了系统的未授权接口。

这里还有一个preHandle方法(预处理),这里是通过authCheck方法来进行校验是否执行下一个拦截器,我们在审计的时候可以重点的审计一下这个方法,分析它的校验逻辑是否能被绕过。image-20230224010811132

接下来看Shiro框架的认证鉴权,一般Web应用在使用Shrio进行身份验证的时候需要三个向量,第一个是ShiroFilterFactoryBean,这个是用来配置路由校验规则,可以看到里面的一系列规则,我们要找未授权的接口就要去看配置的setUnauthorizeUrl(未授权)和anno(默认放行)字段;

这里还有authc字段是需要通过校验的接口,它在第二个向量MainRealm中配置,这个方法需要继承一个AuthorizingRealm类,通过实现里面的doGetAuthorizationInfo这个方法来配置。可以看到下面是通过auth方法来判断用户名和密码是否合法。实际的项目中我们可以进入这个方法来审计一下它的逻辑。

另外就是可以根据运行的Shiro版本以及配置方式来看是否有框架层面的漏洞。

image-20230224012307256

以上部分定位了未授权的接口,我们可以通过寻找中间件中配置的未授权接口来进一步确保审计的代码是系统的全量代码。

以Nginx为例,看如何查找系统对外开放的服务。

  1. 定位Nginx可执行文件所在位置

ps -ef|grep nginx|grep master

  1. 查看启动Nginx时,执行的具体命令

cat /proc/[pid]/cmdline

  1. 获取到具体命令时,定位Nginx配置文件所在位置
  • 查看是否使用nginx -c参数指定配置文件路径

  • 若为配置文件路径,则使用默认路径: nginx -t

  1. 分析Nginx配置文件
  • nginx配置文件会存在引用的情况,需关注include关键字

  • 搜索proxy_pass关键字,找到所有反向代理对应的服务。

  1. 确认上一部找到的所有反向代理服务的代码是否齐全

后台接口审计

静态代码分析工具:可以使用CodeQL、FindSecBugs、GoSec等等工具。

也存在工具无法识别的漏洞场景,例如

  • 存在风险的函数未加入到污点追踪的汇点sink中的情况
    • Mybatis的PageHelper插件,在调用 startPage 函数时,order 参数存在注入
    • 通用 Mapper的 selectByIds函数存在SQL注 入
  • web框架接收参数的函数未加入到污点追踪的源点 source中的情况
    • 由于上传文件的内容无法标记为污点追踪的source, 因此需关注SQL语句中拼接了上传文件中的数据的情况
  • 程序调用外部脚本(sh、bat、py等)时,将用户 可控参数传入到脚本中的情况
  • 静态代码分析工具追踪数据流中断的情况
    • 如java异步方法调用(Thread类),java、go反射调用等

这种情况下就需要配合人工代码审计,有如下几点技巧

  • IDEA工具可以使用全局搜索功能配合正则表达式搜索,比如搜索原生SQL语句;也可以使用Find Usages功能查看整个函数的调用栈,这点我们在之前的源码分析中经常用到。
  • XSS漏洞审计注意点:当遇到程序对输入数据进行全局的 HTML编码时,需关注以下场景存在的风险
    • 如果在输出时对数据进行了url 解码,这时可通过双重url编码进行绕过
    • 如果将数据输出到了
  • SQL注入漏洞审计注意点:当遇到程序对输入数据进行全局的单双引号转义时,需关注以下场景存在的风险
    • 如果在进行SQL拼接时,未在拼接的参数两端添加引号,则仍然存在注入,该场景容易出现在拼接整数类型参数的sql语句中
  • 路径穿越漏洞审计注意点
    • 在审计路径穿越漏洞时,除了关注常见的文件操作方法外,还 需要关注通过命令行进行文件操作(如mv、cp等)时造成的路径穿越问题
    • 当程序使用自定义函数对压缩 包进行解压时,需关注压缩包穿越问题

常见高危漏洞代码审计Checklist

以Java为例,讨论对应漏洞涉及到的函数

SQL注入:

1
2
3
4
5
6
7
Map sqlFunc = Map.of("java.sql.Statement", new String[]{"executeQuery", "executeUpdate", "execute",
"addBatch", "executeLargeUpdate"},
"org.hibernate.Session", new String[]{"createQuery", "createSQLQuery", "createNativeQuery",
"createCriteria"},
"org.hibernate.criterion.Restrictions", new String[]{"sqlRestriction"},
"MyBatisMapper", new String[]{"${}"}
);

命令注入:

1
2
3
4
Map commandFunc = Map.of("java.lang.Runtime", new String[]{"exec"}, 
"java.lang.ProcessBuilder", new String[]{"start"},
"javax.script.ScriptEngine", new String[]{"eval"} // 解析JavaScript代码
);

使用上述函数执行系统命令时,需满足以下条件才会存在命令注入风险(解析JS代码的函数除外): 1、指定了执行程序为sh、bash、cmd.exe等系统shell; 2、使用系统shell程序的-c或/c参数,来指定执行的命令。

文件上传:

1
2
3
4
Map uploadFunc = Map.of("javax.servlet.ServletRequest", new String[]{"getInputStream", "getReader"}, "org.springframework.web.multipart.MultipartFile", new String[]{"transferTo", "getInputStream"}, "org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload", new String[]{"parseRequest"}, 
"java.io.FileOutputStream", new String[]{"write"},
"java.util.zip.ZipEntry", new String[]{"getName"} //不安全的解压缩
)

Java反序列化漏洞:根据传入数据类型分为以下几类

1、二进制数据流反序列化

1
2
Map deserializeByteFunc = Map.of( "java.io.ObjectInputStream", new String[]{"readObject", "readUnshared"}
);

2、xml数据反序列化漏洞

1
2
Map deserializeXmlFunc = Map.of( "java.beans.XMLDecoder", new String[]{"readObject"}, "com.thoughtworks.xstream.XStream", new String[]{"fromXML"}
);

3、Json数据反序列化漏洞

1
2
Map deserializeJsonFunc = Map.of( "com.alibaba.fastjson.JSON", new String[]{"parseObject"}, "com.fasterxml.jackson.databind.ObjectMapper", new String[]{"readValue"}
);

4、Yaml数据

1
2
Map deserializeYamlFunc = Map.of( "org.yaml.snakeyaml.Yaml", new String[]{"load", "loadAs", "loadAll"}, "org.ho.yaml.Yaml", new String[]{"load", "loadType", "loadStream", "loadStreamOfTyp"}, "com.esotericsoftware.yamlbeans.YamlReader", new String[]{"read", "readAll", "readValue"}
);

Json和Yaml两种数据类型的反序列化漏洞原理较为类似,其在反序列化过程中,均会调用javabean对象的setter方法,最典型的例子就是fastjson1.2.47版本的漏洞,因此在审计时,需关注业务代码中的setter方法是否存在高危操作。

权限绕过:

1、路径归一化问题:程序没有对用户请求的路径进行标准的统一处理,而是将路径原模原样的取出来,然后去进行权限校验。

​ Java应用中,使用request.getRequestURL()方法,会获取用户请求的URL地址,如判断用户请求的路径是不是以/js/开头,如果以/JS/开头直接放走,不进行验证。只有以/admin/开头才进行验证。这时候就可以以/JS开头进行绕过;又或者路径以.js结尾放行,这时可以请求如:/admin/addUser;.js进行绕过。

2、身份令牌处理不当

  • 硬编码Token
  • 生成的Token时的签名密钥硬编码于代码或者可预测
  • 通过构造特定数据包未授权创建Token

3、免密登录的逻辑缺陷:当系统需要给第三方系统提供认证接口,通常存在此类问题。

  • HTTP头绕过(如XFF等):开发人员为了方便,直接判断请求的IP地址
  • 硬编码Token或未授权获取Token

总结

本节议题很大程度的丰富了代码审计的思路,期间又重新学习了一下开发生命周期的安全,也去巩固了之前所学的组件漏洞。任重而道远,之后的学习一定还要再回来在丰富总结一下。

参考连接:

[1] 干货与奖品齐飞,平安SRC线上沙龙第四期圆满落幕!

[2] 从SDL到DevSecOps:始终贯穿开发生命周期的安全