CVE-2021-45232 Apache APISIX Dashboard RCE 分析
一、环境搭建
https://github.com/apache/apisix-docker
git clone https://github.com/apache/apisix-docker 修改为2.7 后 docker-compose up -d
二、代码分析
Apache APISIX Dashboard github 地址
https://github.com/apache/apisix-dashboard
找到commits 进行查看
https://github.com/apache/apisix-dashboard/commits/master
https://github.com/apache/apisix-dashboard/commit/b565f7cd090e9ee2043fbb726fbaae01737f83cd#diff-a16bc2c469646367bf6d9f635ee85a8e13109732bdb0caba8cec71f015bc0c1c
更新了如下代码
具体参考:
https://githistory.xyz/apache/apisix-dashboard/blob/b565f7cd090e9ee2043fbb726fbaae01737f83cd/api/internal/filter/authentication.go
以`/apisix`开头的URL,除了`/apisix/admin/tool/version`和`/apisix/admin/user/login`以外均需要认证,通过判断HTTP Header中的`Authorization`来完成鉴权处理
打开路由表:
func SetUpRouter() *gin.Engine { if conf.ENV == conf.EnvLOCAL || conf.ENV == conf.EnvDEV { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } r := gin.New() logger := log.GetLogger(log.AccessLog) r.Use(filter.CORS(), filter.RequestId(), filter.IPFilter(), filter.RequestLogHandler(logger), filter.SchemaCheck(), filter.RecoverHandler()) r.Use(gzip.Gzip(gzip.DefaultCompression)) r.Use(static.Serve("/", static.LocalFile(filepath.Join(conf.WorkDir, conf.WebDir), false))) r.NoRoute(func(c *gin.Context) { c.File(fmt.Sprintf("%s/index.html", filepath.Join(conf.WorkDir, conf.WebDir))) }) factories := []handler.RegisterFactory{ route.NewHandler, ssl.NewHandler, consumer.NewHandler, upstream.NewHandler, service.NewHandler, schema.NewHandler, schema.NewSchemaHandler, healthz.NewHandler, authentication.NewHandler, global_rule.NewHandler, server_info.NewHandler, label.NewHandler, data_loader.NewHandler, data_loader.NewImportHandler, tool.NewHandler, plugin_config.NewHandler, migrate.NewHandler, proto.NewHandler, stream_route.NewHandler, }
注册了如上的这些路由
授权中间件是在droplet中注册的 而导入导出的路由没有用wgin.Wraps()函数转换为droplet的路由函数
未授权的没用wgin.Wraps()进行二次包装转换
通过全局搜索r.GET
发现两个未授权的接口
r.GET("/apisix/admin/migrate/export", h.ExportConfig) r.POST("/apisix/admin/migrate/import", h.ImportConfig)
首先访问一下。这两个接口
首先看看代码导出
func (h *Handler) ExportConfig(c *gin.Context) { data, err := migrate.Export(c) if err != nil { log.Errorf("Export: %s", err) c.JSON(http.StatusInternalServerError, err) return } // To check file integrity // Add 4 byte(uint32) checksum at the end of file. checksumUint32 := crc32.ChecksumIEEE(data) checksum := make([]byte, checksumLength) binary.BigEndian.PutUint32(checksum, checksumUint32) fileBytes := append(data, checksum...) c.Writer.WriteHeader(http.StatusOK) c.Header("Content-Disposition", "attachment; filename="+exportFileName) c.Header("Content-Type", "application/octet-stream") c.Header("Content-Transfer-Encoding", "binary") _, err = c.Writer.Write([]byte(fileBytes)) if err != nil { log.Errorf("Write: %s", err) } }
func Export(ctx context.Context) ([]byte, error) { exportData := newDataSet() store.RangeStore(func(key store.HubKey, s *store.GenericStore) bool { s.Range(ctx, func(_ string, obj interface{}) bool { err := exportData.Add(obj) if err != nil { log.Errorf("Add obj to export list failed:%s", err) return true } return true }) return true }) data, err := json.Marshal(exportData) if err != nil { return nil, err } return data, nil }
最终返回的是所有信息的结构体
func newDataSet() *DataSet { return &DataSet{ Consumers: make([]*entity.Consumer, 0), Routes: make([]*entity.Route, 0), Services: make([]*entity.Service, 0), SSLs: make([]*entity.SSL, 0), Upstreams: make([]*entity.Upstream, 0), Scripts: make([]*entity.Script, 0), GlobalPlugins: make([]*entity.GlobalPlugins, 0), PluginConfigs: make([]*entity.PluginConfig, 0), } }
再看看导入:
func (h *Handler) ImportConfig(c *gin.Context) { paraMode := c.PostForm("mode") mode := migrate.ModeReturn if m, ok := modeMap[paraMode]; ok { mode = m } file, _, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusInternalServerError, err) return } content, err := ioutil.ReadAll(file) if err != nil { c.JSON(http.StatusInternalServerError, err) return } // checksum uint32,4 bytes importData := content[:len(content)-4] checksum := binary.BigEndian.Uint32(content[len(content)-4:]) if checksum != crc32.ChecksumIEEE(importData) { c.JSON(http.StatusOK, &data.BaseError{ Code: consts.ErrBadRequest, Message: "Checksum check failure,maybe file broken", }) return } conflictData, err := migrate.Import(c, importData, mode) if err != nil { if err == migrate.ErrConflict { c.JSON(http.StatusOK, &data.BaseError{ Code: consts.ErrBadRequest, Message: "Config conflict", Data: ImportOutput{ConflictItems: conflictData}, }) } else { log.Errorf("Import failed: %s", err) c.JSON(http.StatusOK, &data.BaseError{ Code: consts.ErrBadRequest, Message: err.Error(), Data: ImportOutput{ConflictItems: conflictData}, }) } return } c.JSON(http.StatusOK, &data.Response{ Data: ImportOutput{ConflictItems: conflictData}, }) }
先确定一下mode的模式。这里是分三种模式的
var modeMap = map[string]migrate.ConflictMode{ "return": migrate.ModeReturn, // 返回?? "overwrite": migrate.ModeOverwrite, //覆盖 "skip": migrate.ModeSkip, //跳过 }
后是取最后的4位看看是和前面的计算出来的值是否相等。如果相等 就根据模式的不同的对 上面的那个全局的一个配置结构体进行修改
func Import(ctx context.Context, data []byte, mode ConflictMode) (*DataSet, error) { importData := newDataSet() err := json.Unmarshal(data, &importData) if err != nil { return nil, err } conflict, conflictData := isConflicted(ctx, importData) if conflict && mode == ModeReturn { return conflictData, ErrConflict } store.RangeStore(func(key store.HubKey, s *store.GenericStore) bool { importData.rangeData(key, func(i int, obj interface{}) bool { _, e := s.CreateCheck(obj) if e != nil { switch mode { case ModeSkip: return true case ModeOverwrite: _, e := s.Update(ctx, obj, true) if e != nil { err = e return false } } } else { _, e := s.Create(ctx, obj) if err != nil { err = e return false } } return true }) return true }) return nil, err }
官方的文档比较少。
https://apisix.apache.org/docs/apisix/architecture-design/script
官网有一个执行script 的lua代码的介绍。但是,没有然后了。
后台随便建立一个路由。然后进行修改
然后进行访问。这里注意的是。不是访问当前的9000端口的。而是访问他管理的apache/apisix 的端口 9080
curl http://192.168.1.72:9080/
然后查看容器中是否执行了代码
三、漏洞利用
那么结果就是。只要获取到export 中的信息。然后只要在路由中添加一个script 进行代码执行。后访问API 即可触发。如下:
首先需要把export 返回的信息。进行checksum 即可
进行导入
成功执行
第二种RCE的方式
https://www.bookstack.cn/read/apache-apisix-1.4-zh/33860207d6bb4917.md
例如:
“filter_func”: “function(vars) return vars[“arg_name”] == “json” end”,
也是可以执行任意代码的。
非常感谢CoolCat师傅的指导
参考文章:https://mp.weixin.qq.com/s/WEfuVQkhvM6k-xQH0uyNXg
参考:https://twitter.com/sirifu4k1/status/1476010802968555522
- Pingback: CVE-2021-45232 Apache APISIX 从未授权访问到RCE - 点击领取