信息搜集

甲方的道友们在做白盒安全测试时,一般都是让业务线提供系统代码,之后直接开始审计,业务线给什么代码,就审什么代码。当遇到一个架构较为复杂的系统时,后台可能会存在多个web服务(如微服务架构),而业务线往往只提供他认为最主要的一个web服务的代码,这时就出现了代码遗漏的问题。所以为了确保我们审计的代码是系统的全量代码,就需要进行信息搜集的工作。

信息搜集的内容:搜集系统对外暴漏的所有web服务的端口,然后定位每个端口对应的服务代码。(使用k8s集群部署时则需要搜集所有对外提供服务的Service,然后定位每个Service中的Pod中的服务代码)

直接在测试环境所在服务器上进行信息搜集是最快捷高效的方式。Nginx由于它内存占用少,启动速度快,高并发能力强,因此广泛应用于互联网项目中。所以本章节描述了如何通过查看Nginx服务开放的端口进行信息搜集,当系统未使用Nginx时,可参考该思路自行寻找方案。

1.1 Windows系统

  1. 如下命令定位nginx可执行文件所在位置

wmic process where "Name like '%nginx.exe%'" get Name, CommandLine, ExecutablePath

2. 定位nginx配置文件所在位置,配置文件所在路径分为如下两种:

(1)查看第一步中nginx的启动命令行,其-c参数即为所采用的配置文件。(如上图中的conf/nginx_console.conf文件)

(2)当未通过-c参数指定配置文件时,则使用的默认路径,即nginx.exe所在的同级目录中的conf/nginx.conf文件。

3. 分析nginx配置文件:

(1)nginx配置文件会存在引用的情况,因此需关注include关键字。

(2)搜索proxy_pass关键字,找到所有反向代理地址对应的服务。    

4. 最后确认上一步找到的所有服务的代码是否齐全即可。 

1.2 Linux系统

  1. 如下命令定位nginx可执行文件所在位置:

ps -ef | grep nginx | grep master

可以看到上图中一共启动了2个nginx服务。

2. 查看启动nginx时,执行的具体命令,来确定其使用的配置文件:

cat /proc/[pid]/cmdline

以140922进程为例:

其中未使用-c参数指定配置文件,因此可以判断使用的是默认路径的配置文件,使用如下命令查找默认配置文件路径:

nginx -t

可以看到上图中共用到了两个配置文件。

3. 分析nginx配置文件:

(1)nginx配置文件会存在引用的情况,因此需关注include关键字。    

(3)搜索proxy_pass关键字,找到所有反向代理地址对应的服务。

4. 最后确认上一步找到的所有服务的代码是否齐全即可。   

1.3 小结

在分析nginx配置文件,除了确认代码是否完整外,还需要仔细分析配置文件,确认其中是否有直接配置的未授权接口。

梳理应用权限校验逻辑

这一节给道友们介绍几种Java和Go应用常见的鉴权方式

2.1 Java权限校验示例

1. 过滤器Filter

如下以Spring-Cloud-Gateway框架举例说明:

GlobalFilter接口:重写filter方法,在其中进行权限校验。

Order接口:过滤器执行顺序,数值越小,优先级越高。多个Filter会串起来挨个执行。

从如下代码片段中可以看出,shouldFilter方法用来判断路由是否需要进行权限校验,因此通过阅读shouldFilter方法,即可找到在其中配置的未授权接口;还可以看到具体权限校验逻辑是在run方法中,因此通过分析run方法,可以发现其在权限校验过程中存在的逻辑漏洞。

另外需要注意的是,走特定校验逻辑的接口(如供三方系统访问的接口)大概率会存在硬编码key的问题,因此需重点关注。

public class MyAccessFilter implements GlobalFilter, Ordered {    @Value("${gateway-path}")    
    private String gatewayPath;    @Override    public int getOrder() {        return -1;//优先级    }    @Override    public Monofilter(ServerWebExchange exchange, GatewayFilterChain chain) {
            if (shouldFilter(exchange)) {                run(exchange.getAttributes());            }        return chain.filter(exchange);    }          
    public boolean shouldFilter(ServerWebExchange exchange) {        ServerHttpRequest request = exchange.getRequest();            //……    }    public void run(Mapattributes) {
       //….    } }

2. 拦截器Interceptor

如下以SpringBoot框架举例说明:   

WebMvcConfigurer 接口:重写addInterceptors方法来添加拦截器:

@Configurationpublic class AuthConfig implements WebMvcConfigurer {    @Override    public void addInterceptors(InterceptorRegistry registry) {        //注册认证拦截器        InterceptorRegistration registration = registry.addInterceptor(new AuthInterceptor());        //该认证拦截器会拦截所有请求        registration.addPathPatterns("/**");        //认证拦截器的白名单        registration.excludePathPatterns(                //….                "/base/login/**"       );        //注册权限校验拦截器        registry.addInterceptor(new AccessInterceptor());        //注册数据处理拦截器        InterceptorRegistration urlInterceptorRegistration = registry.addInterceptor(new UrlInterceptor());        urlInterceptorRegistration.excludePathPatterns(                "/base/login/**",                "/common/config/**"    
        );    }}

上述用于认证的拦截器 AuthInterceptor,通过addPathPatterns("/**")的方式来拦截所有请求,后又对部分接口开启了白名单

AuthInterceptor实现如下:通过重写preHandle方法来进行校验:

public class AuthInterceptor implements HandlerInterceptor {    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {        if(authCheck()){            return true;        } else {            response.setStatus(401);        }    }

可以根据拦截器对路由的过滤规则,快速排查出代码中未授权接口,对自定义权限校验方法也可以重点审计下函数的校验逻辑。

3. Shiro框架鉴权

Shiro可以用来进行身份验证 、权限控制、会话管理等工作,因为我们比较关注未授权接口的梳理,因此这里仅介绍一下如何使用Shiro进行身份验证。

一般在web应用中使用Shiro进行身份验证时,需要实现如下三个向量:

(1)Realms的Authentication ,用于身份认证。如下示例代码中的MainRealm。   

(2)SecurityManager:用于管理所有的Subject。如下示例代码中的DefaultWebSecurityManager。

(3)ShiroFilterFactoryBean:用于配置路由校验规则。

@Configuration          public class ShiroConfig {              @Bean              public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {                  ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();                  shiroFilterFactoryBean.setSecurityManager(securityManager);                  // filterChainDefinitionMap接口过滤规则                  MapfilterChainDefinitionMap = new HashMap();
        //登录页面,无权限时跳转的路径                  shiroFilterFactoryBean.setLoginUrl("/hello/login");                 shiroFilterFactoryBean.setUnauthorizedUrl("/api/pong");                  shiroFilterFactoryBean.setUnauthorizedUrl("/api/backup/save");                  shiroFilterFactoryBean.setSuccessUrl("/common/success");        // anon默认放行接口,Shiro内置的,它对应的过滤器里面是空的                  filterChainDefinitionMap.put("/hello", "anon");        // authc需要通过校验的接口,配置的url都必须认证通过才可以访问,它是Shiro内置的一个过滤器对应的实现类                  filterChainDefinitionMap.put("/api/*", "authc");                  shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);                  return shiroFilterFactoryBean;              }              @Bean              public MainRealm shiroRealm() {                  return new MainRealm();              }              @Bean              public DefaultWebSecurityManager securityManager() {                  DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();                  securityManager.setRealm(shiroRealm());                  return securityManager;              }          }    
          
public class MainRealm extends AuthorizingRealm {    protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principalCollection) {        return null;    }    
    protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) throws AuthenticationException {        final String username = (String) authenticationToken.getPrincipal();        final String password = new String((char[]) authenticationToken.getCredentials());        if (auth(username, password)) {            return new SimpleAuthenticationInfo(username, password, this.getName());        }        throw new IncorrectCredentialsException("Username or password is incorrect.");    }}

通过搜索anon、setUnauthorizedUrl等关键字,即可定位到应用中的未授权接口。另外也可以根据Shiro框架版本和配置方式查看是否会存在框架层面的漏洞。

2.2 go权限校验示例  

1. gRPC框架的拦截器

如下在gRPC的Server端注册拦截器:

opts = append(opts, grpc.UnaryInterceptor(interceptor))          s := grpc.NewServer(opts...)

interceptor函数各参数说明如下:

type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)    
ctx context.Context:请求上下文req interface{}:RPC 方法的请求参数info *UnaryServerInfo:RPC 方法的所有信息handler UnaryHandler:RPC 方法具体实现

interceptor函数具体示例如下,如下通过在clientWhiteList函数中设置白名单的方式来配置可以未授权访问的方法:

func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {             if !clientWhiteList(info.FullMethod) {                err := authAppToken(ctx)                if err != nil {                   return nil, err                }             }   return handler(ctx, req)          }func clientWhiteList(fullMethod string) bool {             var whiteList = []string{"Upload", "UploadAccessLogs", "Download"}             for _, v := range whiteList {                if strings.Contains(fullMethod, v) {                   return true                }             }             return false          }    

认证方法authAppToken具体实现如下,通过从ctx中获取客户端请求中携带的认证字段进行校验:

func authAppToken(ctx context.Context) error {             logger := log.WithContext(ctx).WithField("Auth", "Token")             var meta = "metadata is nil , error"             var id, signature, nonce string             header, ok := metadata.FromIncomingContext(ctx)             if !ok {                logger.Error(meta)                return status.Errorf(codes.Unauthenticated, meta)             }             if val, ok := header["id"]; ok {                id = val[0]             }             if val, ok := header["signature"]; ok {                signature = val[0]             }             if val, ok := header["nonce"]; ok {                nonce = val[0]             }             appSecret := config.String("app.secret")             if config.String("app.id") != id || utils.Signature(id, appSecret, nonce) != signature {                return status.Errorf(codes.Unauthenticated, "auth err")             }             return nil          }    

通过如下方法可以快速查找gRPC应用中的未授权接口:

一般在protoc生成的go代码(**.pb.go后缀的文件)中会定义接口名字FullMethod,如下:

func _ProdService_GetProdStock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {             in := new(ProdRequest)             if err := dec(in); err != nil {                return nil, err             }             if interceptor == nil {                return srv.(ProdServiceServer).GetProdStock(ctx, in)             }             info := &grpc.UnaryServerInfo{                Server:     srv,                FullMethod: "/services.ProdService/GetProdStock",             }             handler := func(ctx context.Context, req interface{}) (interface{}, error) {                return srv.(ProdServiceServer).GetProdStock(ctx, req.(*ProdRequest))             }             return interceptor(ctx, in, info, handler)          }  

通过编写脚本爬取所有pb.go文件中所有的FullMethod值,然后根据拦截器的具体逻辑来判断是否为未授权接口。

2. Gin框架的MiddleWare

Gin框架允许在处理请求过程中,加入用户自己的钩子函数,这个钩子函数就叫 MiddleWare,即中间件。

Gin中间件常用于处理一些公共业务逻辑,比如登录校验,耗时统计,日志打印等工作。

如下是APISIX中间件代码,可以看出存在如下未授权接口:

(1)/apisix/admin/user/login

(2)/apisix/admin/tool/version

(3)所有不以/apisix为前缀的路由

func Authentication() gin.HandlerFunc {             return func(c *gin.Context) {                if c.Request.URL.Path == "/apisix/admin/user/login" ||                   c.Request.URL.Path == "/apisix/admin/tool/version" ||                   !strings.HasPrefix(c.Request.URL.Path, "/apisix") {                   c.Next()                   return                }                         tokenStr := c.GetHeader("Authorization")                // verify token                token, err := jwt.ParseWithClaims(tokenStr, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {                   return []byte(conf.AuthConf.Secret), nil                })    
……

在Gin服务初始化时,将Authentication添加进Gin服务的中间件中:

r := gin.Default()r.Use(filter.CORS(), filter.RequestId(), filter.IPFilter(), filter.RequestLogHandler(logger), filter.SchemaCheck(), filter.RecoverHandler(), filter.Authentication())

启动服务后访问任意接口均会进入中间件流程:   

3. Beego框架的拦截器

在Beego框架中可以使用InsertFilter方法来自定义拦截器:

其各个参数说明如下:

func InsertFilter(pattern string, pos int, filter FilterFunc, params ...bool) *App {             BeeApp.Handlers.InsertFilter(pattern, pos, filter, params...)             return BeeApp          }
patter:需要拦截的路由,可使用通配符pos:过滤器执行的时刻,有5种情况:BeforeStatic  静态地址之前BeforeRouter  寻找路由之前BeforeExec  找到路由之后,开始执行相应的Controller之前AfterExec  执行完Controller逻辑之后执FinishRouter  执行完整个逻辑之后filter:实现拦截器的函数params:可选参数    

具体使用示例如下:

beego.InsertFilter("/*",beego.BeforeRouter, func(context *context.Context) {             context.ResponseWriter.ResponseWriter.Header().Set("url",context.Request.RequestURI)                      if ! strings.Contains( context.Request.RequestURI,"/login")  {                authtoken(context)                      }          })

未授权接口审计

这一步就没啥好讲的了,道友们从前面第一步的信息搜集和第二步的权限校验逻辑中梳理出未授权接口后,就可以对这些接口进行代码审计了,这个时候道友们可不能偷懒,不要只是进行简单的污点函数回溯就完事了,因为当未授权接口存在高危漏洞的时候,其造成的危害不言而喻,所以我们需要保证未授权接口的绝对安全,那为了实现这个目标,就需要道友们通读未授权接口的所有代码,全面挖掘其中存在的安全风险。

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。