Write-up CTFZone Quals 2019: Chicken
Despite the postponement of the OFFZONE 2020 conference, there will be a CTFZone final! This year it will be held online for the first time and will be actively broadcast on social networks.
We will announce the details later, but for now we suggest reviewing the web-task write-up from the qualifying stage. The analysis of the solution was sent to us by Devand MacLean from Canada. We invite you to find out, which vulnerability chain the participants have encountered and what the chicken has to do with.
Author: Pavel Sorokin
# of teams solved: 2
At first glance, the web app contained:
· a homepage (/Home/Index)
· a page containing information about their hens (/Home/Hens)
· a contact page (/Home/Contact)
· a login page (/Auth/Login)
Later, through enumeration and investigation, the web app was discovered to also contain a page for changing a user’s password, as well as a second interface detailing the API used for logging in, recovering a password, and changing a password.
Upon browsing to /Home/Hens, I noticed there was a link to download each of the hens’ “Passports”:
The link for this particular hen’s passport was:
The value for the filename parameter, when decoded using Base64, was “1.txt”. Using CyberChef, I tried to encode “/etc/passwd” into Base64 and browsed to the following URL:
This returned the contents of the “/etc/passwd” file on the server to my browser, which meant that I now had Arbitrary File Read on the system.
After trying to hunt for some other files on the system, I noticed a particular error appeared whenever I requested a file that didn’t exist (or that the privileges granted to the web application user were insufficient to read):
Making note of the error’s reference to the “ASPNETCORE_ENVIRONMENT” environment variable, I began to research ASP.NET Core web application layouts. I found the following descriptions of how they are laid out:
At this point, I decided to write a small python script (fetch.py) to assist with conversion of the paths / filenames I was looking to exfiltrate to Base64:
Using the knowledge I had gained about the layout of an MVC web app built with ASP.NET Core, I deduced that I should be able to use fetch.py to exfiltrate the source code of the following files:
After doing so, and inspecting the source code of each page, I discovered something interesting in the following source code of the Hens.cshtml view:
Line 1 of that View is an include statement, “@using StackHenneryMVCAppProject”, which in ASP.NET references a .dll file. I exfiltrated “../StackHenneryMVCAppProject.dll” using my web browser on my Windows host machine instead of with my python utility in Kali Linux, as I was going to be decompiling the dll on Windows. I used the following URL to do so:
Upon opening this .dll file in dnSpy (a .NET decompiler), one of the first things I noticed was that within the Initialize() method of the Config.Configuration class, which is executed upon starting the web app, it reads in the contents of a file called “chicken_domains_internal.txt”. Using my python script I was able to exfiltrate the contents of that file as follows:
The Initialize() method continued on to connect to “web-chicken-flag” over port 4321 and receive a number of parameters, such as “secret_token”, “RSA_key”, and “flag”.
Unfortunately, I was unable to initiate any sort of connection to this “web-chicken-flag”, however when I set a local DNS entry in my hosts file pointing “web-chicken-auth” to the external IP address of the server (184.108.40.206), I was able to browse to http://web-chicken-auth/ and was presented with an instance of “Swagger UI”, an interface designed to detail and execute commands using a REST API.
Upon further investigation of the decompiled .dll file, I discovered that the AuthController controller not only had a Login() method, but it also had a method called Change_Password_Test():
This indicated to me that I should be able to browse to “/Auth/Change_Password_Test”, and was presented with the following form upon doing so:
Investigating the Change_Password_Test() method, I was able to determine that it handled 4 parameters, all of which it was expecting to be of type String, via an HTTP POST request:
Slightly further down in the Change_Password_Test() method, I noticed that if it does not receive a value for “base_url”, it assigns it the value of “conf.auth_server”, which evaluates to “web-chicken-auth” and then appends “/auth/login” to the end of the “base_url” value, assigning the combined string to the variable “requestUri”:
The next portion of code I’d like to draw attention to takes the string values of the “username” and “old_password” parameters sent in the HTTP POST request, as well as the “secret_token” from the config, and inserts them into a JSON string. It then creates a JSON “StringContent” object using that data, and sends it as HTTP POST data to the “requestURI” created in the previous step.
From here, we can see that the server waits to see if the HTTP POST request sent to the “requestUri” returns a JSON object with the property “code” set to 0. If it does not, we enter the code branch starting on line 6, which returns the “/Views/Auth/Login” view with the error “Invalid login/password”.
If however, that HTTP POST request does return a JSON object whose “code” property is equal to 0, we enter the code branch beginning on line 11 instead. At this point, it assigns the value “http://web-chicken-auth” to the variable “requestUri2”, then creates another JSON “StringContent” object using the same parameters in the initial HTTP POST request, similar to the previous section, and submits that data to the “requestUri2” address via an HTTP PUT request. Finally, it returns “Password changed” to the client.
So, just to quickly recap here, this entire process kicks off with an HTTP POST request to “/Auth/Change_Password_Test”, which is expecting a number of parameters. It takes those values, and creates a JSON object to submit to “<base_url>/auth/login” via HTTP POST, and if it receives a JSON response with the “code” property equal to 0, it forms another JSON object to submit to “http://web-chicken-auth/auth/change_password” via HTTP PUT.
From here, I began to examine the SwaggerUI REST API more closely, and noticed that it contained not only the “/auth/login” and “/auth/change_password” routes seen in the .dll file I had decompiled, but also a route for “/auth/password_recovery”.
This API route expected a JSON object containing 2 parameters, “email” and “secret_token”, and if it executed successfully, it returned a JSON object containing a “code” with a value of 0, along with a “message” and a “token”.
This meant that if I were somehow able to get the Check_Password_Test() method to make its initial request to the “/auth/password_recovery” route instead of the “/auth/login” route, the only information required to return the expected response necessary to enter the password changing code branch would be a valid e-mail address.
Looking back at the portion of code where Check_Password_Test() assigns the “requestUri” value which is used for that initial HTTP POST request, I realized that if I submitted the value:
as the “base_url” parameter, the “requestUri” would ultimately be assigned the value:
The reason that the # is important here, is because in an HTTP URL, that symbol indicates that the URL portion of the request has finished. Thus, the “/auth/login” portion would be completely ignored by the server receiving the request.
This was all and well, but I still needed to find a way to submit the “email” value to the REST endpoint I was now redirecting this request to, otherwise the request would return an error and not continue through the rest of the code.
Looking closer at the way the Check_Password_Test() method creates the JSON payloads, I noticed that there was a distinct lack of any sort of input sanitization, which meant that it was vulnerable to parameter injection. Submitting the following parameters in my HTTP POST request data:
username = admin
old_password = pwned”,”email”:”email@example.com”,”lol”:”
new_password = p0tat0
base_url = http://web-chicken-auth/auth/password_recovery#
Would result in the following JSON object being created, and sent to “/auth/password_recovery”:
With this JSON payload, the requirements for “/auth/password_recovery” to return the necessary response which allows the server to continue into the password changing code branch are satisfied (JSON object contains an “email” property). Because of this, the value for the “old_password” property is completely irrelevant. Once it continues into that code branch, the required values to change the password are supplied (“username” property is “admin”, and “password” property is “p0tat0”), so the request is carried out, and we receive “Password changed” in response to the original request.
All that’s left to do now is log in using the new username and password on the login page!
And we are presented with the flag: