CVE-2021-45232 Apache APISIX Dashboard RCE 分析

作者: print("") 分类: 信息安全 发布时间: 2021-12-29 20:59

一、环境搭建

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

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

一条评论

发表评论

您的电子邮箱地址不会被公开。