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 - 点击领取
















