漏洞介绍

Apache APISIX Dashboard是一个用于管理Apache APISIX的前端面板。CVE-2021-45232是一个身份验证绕过漏洞,攻击者可以在未授权的情况下访问某些API接口,从而导出或导入配置,甚至执行远程代码。

漏洞范围

2.7.0 < APISIX Dashboard< 2.10.1

漏洞靶场

使用vulhub的靶场:

vulhub-master/apisix/CVE-2021-45232

1
docker-compose up -d

漏洞原理

Apache APISIX Dashboard github 地址

https://github.com/apache/apisix-dashboard

找到commits 进行查看

https://github.com/apache/apisix-dashboard/commits/master

img

https://github.com/apache/apisix-dashboard/commit/b565f7cd090e9ee2043fbb726fbaae01737f83cd#diff-a16bc2c469646367bf6d9f635ee85a8e13109732bdb0caba8cec71f015bc0c1c

更新了如下代码

具体参考:

https://githistory.xyz/apache/apisix-dashboard/blob/b565f7cd090e9ee2043fbb726fbaae01737f83cd/api/internal/filter/authentication.go

img

api/internal/filter/authentication.go#L48-L54

/apisix开头的URL,除了/apisix/admin/tool/version/apisix/admin/user/login以外均需要认证,通过判断HTTP Header中的Authorization来完成鉴权处理

打开路由表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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,
}

api/internal/route.go#L52-L97

注册了如上的这些路由

授权中间件是在droplet中注册的 而导入导出的路由没有用wgin.Wraps()函数转换为droplet的路由函数

img

未授权的没用wgin.Wraps()进行二次包装转换

通过全局搜索r.GET

img

发现两个未授权的接口

1
2
r.GET("/apisix/admin/migrate/export", h.ExportConfig)
r.POST("/apisix/admin/migrate/import", h.ImportConfig)

首先访问一下。这两个接口

img

首先看看代码导出api/internal/handler/migrate/migrate.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
}

最终返回的是所有信息的结构体

1
2
3
4
5
6
7
8
9
10
11
12
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),
}
}

再看看导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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的模式。这里是分三种模式的

1
2
3
4
5
var modeMap = map[string]migrate.ConflictMode{
"return": migrate.ModeReturn, // 返回??
"overwrite": migrate.ModeOverwrite, //覆盖
"skip": migrate.ModeSkip, //跳过
}

api/internal/handler/migrate/migrate.go#L52-L133

然后是取最后的4位看看是和前面的计算出来的值是否相等。如果相等 就根据模式的不同的对 上面的那个全局的一个配置结构体进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
}

api/internal/core/migrate/migrate.go#L63-L99

漏洞复现

访问http://127.0.0.1:9080/进入靶场页面

image-20250308184421235

exp使用

https://github.com/wuppp/apisix_dashboard_rce下载exp

1
python apisix_dashboard_rce.py http://127.0.0.1:9000

image-20250308184646010

burp抓取页面的数据包,并修改路径为工具返回出来的

image-20250308185237380

image-20250308185329613

成功执行命令

参考

https://blog.csdn.net/2301_78721909/article/details/145891066

https://blog.csdn.net/weixin_44411509/article/details/122292459