Solving a CTF using OpenAI models
The Midsummer Corp Hack - Securing CTF June 2023 Writeup
The Plan
Challenge URL: https://tryhackme.com/jr/midsummer
There is something I wanted to try since I've watched videos of people creating games without any programming knowledge whatsoever. After I have created a honeypot using the ChatGPT v3, I'm ready to solve the CTF using AI only: ChatGPT and Bing AI (mostly strict mode) - latter because it has internet access.
For the readability reason, I'm not going to paste whole conversations, but the important bits.
Recon
For me, the competition starts before launching the first challenge. In the introductory part, I can read that
In this room, every task will allow you to gain access to a new Midsummer Corp employee. On every account you can also find a piece of the final puzzle
fernflower_flag[1-6].png
, which you will need to complete the last quest.
It indicates that the final flag or answer (the Crown Jewels) is distributed in six parts and placed somewhere on the file systems (presumably) of six accounts. The number of challenges (Puck, Leshy, Baba Yaga, Boruta, Twardowski and Popiel) is also six - so that is a match - one account, one challenge - one part of the final flag.
It is also worth noting that two years ago I have participated in the CTF organized by the Securing and my write-up won that year's competition. If you haven't read it yet, I strongly recommend it because it was my first experience of Securing team potential - just see what platform did they use to host the event 😉.
The Setting
Legend has it that the fern flower appears on the eve of the summer solstice at the stroke of midnight. It can only be found deep in the forest, where it grows in a secret and hidden spot known only to the bravest and most skilled of seekers. Those who are lucky enough to find it and pick it up at the right moment are granted great powers and blessings and may even have their wishes come true.
Securing chosen Kupala Night as a topic of this CTF.
I like that because the elusive fern flower is a great analogy to the flag that participants are looking for in the competition.
Also, what ChatGPT didn't mention - fern flower does not exist. Ferns do not bloom.
If you are not familiar with that legend of blooming fern, here are some of my recommendations to familiarize with:
absolutely amazing Polish folk rock band - Żywiołak (Elemental); their songs focus on the Kupala Night and Slavic mythology
"Kwiat Paproci" ("Fern Flower") book series by Katarzyna Berenika Miszczuk
Anyway, let's look at the challenges names in the context of that all - are those names chosen arbitrary or do they have some meaning that could help to solve tasks?
And maybe bot can drop us some ideas of what the challenges may be about?
Interesting - but here is crucial to remember one thing - when OpenAI model doesn't know the answer, it comes up with some (here, here and here). This is just how these kinds of model works. For example, I've asked a Bing AI to find some information about one of the challenges suggested by ChatGPT - and it looks like the bot just made that one up.
The Platform
The application is based on the NextCloud server (GitHub - nextcloud/server). The software and the configuration have been intentionally made vulnerable.
According to the GitHub it is a PHP server with JavaScript frontend for the data storage/management.
Tag cloud:
Interestingly, there is a hacktoberfest
tag there. I've dug deeper and found out Nextcloud is participating in Hacktoberfests in the years 2016 and 2017 (post from 2016). So just for future reference, I'm adding a pull requests list applied during that event because of the following reasons:
Securing team could be using an older version of Nextcloud Server,
Securing team could be introducing/reverting some pull requests to include vulnerabilities,
both
Ok, let's start with the actual challenge.
Midsummer Corp (sanity check)
For the sake of consistency - most actions I'll be performing on the Kali Linux, partially becasue I couldn't connect
openvpn
on Windows.
I'm connecting to the VPN sudo openvpn thm-eu1.vpn
and test the connection using curl -IL 10.10.21.207
to roughly see what headers are being exchanged. Some of them:
Server: Apache/2.4.56 (Debian)
X-Powered-By: PHP/8.1.18
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'none';base-uri 'none';manifest-src 'self';script-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data: blob:;font-src 'self' data:;connect-src 'self';media-src 'self';frame-ancestors 'self';form-action 'self'
Bing AI on HTTP Headers
The sanity check question is to find the base URL of the application. When you look closely - on the footer there is some text (which often contains redirection to the landing page). This is how the page looks like when adding a following CSS rule.
* {
color: white;
}
So I hover on the Midsummer Corp link - it leads to https://files.midsummer.corp.local/
.
I'm adding that to the hosts and browsing the page again. Unfortunately, the page seems not to be served over HTTPS.
# THM
10.10.21.207 files.midsummer.corp.local
From now on, I'll be using the Request Blocking feature of Firefox to not load the background image - the page looks much easier to browse and the size of the image is huge!
Source Code
To be honest - that's the longest .htaccess
file I've ever seen - 132 lines.
$ wc -l .htaccess
132 .htaccess
I've then asked Bing AI to explain to me what some of the sections of that file are doing.
When comparing this file with the current version on the GitHub - there are additional lines under the #### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####
comment.
ErrorDocument 403 /index.php/error/403
ErrorDocument 404 /index.php/error/404
<IfModule mod_rewrite.c>
Options -MultiViews
RewriteRule ^core/js/oc.js$ index.php [PT,E=PATH_INFO:$1]
RewriteRule ^core/preview.png$ index.php [PT,E=PATH_INFO:$1]
RewriteCond %{REQUEST_FILENAME} !\.(css|js|svg|gif|png|html|ttf|woff2?|ico|jpg|jpeg|map|webm|mp4|mp3|ogg|wav|wasm|tflite)$
RewriteCond %{REQUEST_FILENAME} !/core/ajax/update\.php
RewriteCond %{REQUEST_FILENAME} !/core/img/(favicon\.ico|manifest\.json)$
RewriteCond %{REQUEST_FILENAME} !/(cron|public|remote|status)\.php
RewriteCond %{REQUEST_FILENAME} !/ocs/v(1|2)\.php
RewriteCond %{REQUEST_FILENAME} !/robots\.txt
RewriteCond %{REQUEST_FILENAME} !/(ocm-provider|ocs-provider|updater)/
RewriteCond %{REQUEST_URI} !^/\.well-known/(acme-challenge|pki-validation)/.*
RewriteCond %{REQUEST_FILENAME} !/richdocumentscode(_arm64)?/proxy.php$
RewriteRule . index.php [PT,E=PATH_INFO:$1]
RewriteBase /
<IfModule mod_env.c>
SetEnv front_controller_active true
<IfModule mod_dir.c>
DirectorySlash off
</IfModule>
</IfModule>
</IfModule>
Version 26.0.0 (/status.php
)
Requesting status.php
Ok so, unfortunately, it's not as outdated as I thought it would be - the current version at the time of writing this article is 26.0.2
. Couple of interesting commits missing in the 26.0.0
🔸 sec(deps): Update guzzlehttp/psr7
Updates guzzlehttp/psr7 from version 2.4.3
to 2.4.5
- here is the diff. Basically HTTP headers validation.
🔸 fix(security): Mark recording_servers key appconfig as private as it contains a secret
Well, that could be worth checking because of some "secret".
🔸 clear encrypted flag when moving away from encrypted storage
🔸 hide shared files located in group folder's trash bin
Maybe some data exfiltration from the shared files by looking in the group folder's trash bin?
🔸 fix(files_sharing): Allow file actions other than download for hide download shares
Maybe the file download action is still available?
🔸 fix(lostpassword): Also rate limit the setPassword endpoint
Paired with feat(security): Allow to opt-out of ratelimit protection, e.g. for testing on CI by the same author. Adding some comments/attributes on core/Controller/LostController->setPassword(..)
and response throttling - most certainly to prevent brute-forcing.
🔸 fix: Always create user directory when transferring files to new users
Another way to unauthorized read?
/cron.php
Requesting cron.php
/ocs/v1.php
, /ocs/v2.php
Ok, I'm deciding to not chase the rabbit and start doing the challenges.
Puck (intern)
Responsible for creating chaos and confusion to distract people from the company's true intentions
It does sound mischievous.
From the task description, we know that the Puck's password is accessible for us, somewhere in the application - and that the brute-force attack is not required. To be honest, I'm surprised that I haven't stumbled upon the plaintext password so far, as it is often "hidden" in the page source, some page or known URL (like robots.txt
).
So I'm going back to the page source of the "Direct log in". I see there is some quite informative JS code in the head
tag
I am pasting it in the VSCode, and I'm seeing something useful.
And success - first credentials are puck:sfLfSNYavTD4PL4Z
.
Getting the flag from Fern_flower_ritual_shard1.txt
. I'm also downloading a part of the final flag (fernflower_flag1.png
).
Now, I noticed that 1 file is hidden (information under the file list). So, I went to Files settings and enabled Show hidden files.
On the sidenote,
research
folder contains*.txt
files with fern AI-generated content (verified on three AI detectors) :). And some screens from social mediaProfile: https://www.facebook.com/fernflowers17/, original content:
In the hidden folder .mail
I found mails coming from different members of the fern team (like Baba Yaga and Boruta). One of the emails is crucial for me
That helped me answer the last question in Puck challenge and get Leshy credentials (leshy:nQRbhRyxuDuU9GNd
).
Leshy (Forest Tracker)
Leshy is a forest spirit who has an intimate knowledge of the forest and its inhabitants. His job is to lead the team to the locations where the fern flower grows and to keep a watchful eye on any unwanted visitors who may threaten their mission
It is worth noting that we are given a link to OWASP resource that could be helpful to bypass MFA on a Leshy account.
I started by browsing the source code, to see the MFA flow - and at first, I thought that I had found that the token is constant (234567
) - but it didn't work.
So, I have to consult my assistants :)
And now I'm asking ChatGPT directly on that topic, knowing that the 234567
is crucial to solving that task.
Ok, now I've asked it to give me the example in Python.
After a few retries I have finished with that:
#!/usr/bin/python
import pyotp
import base64
def generate_totp_code(seed_value):
totp = pyotp.TOTP(seed_value)
return totp.now()
seed_value = base64.b32encode("234567".encode())
totp_code = generate_totp_code(seed_value)
print(totp_code)
Let's try it. Unfortunately, it doesn't work.
After unveiling the hint - of course, I can use a different user to add MFA to it, so I can have the code generator (because seed is the same, OTP also are the same).
Backup codes are not the same.
Because that secret code is the same for each user, we can use the same authentication codes generated for the puck
.
I'm downloading both the flag and final flag PNG.
No additional findings are reported. Because it is a dedicated instance - I'm removing MFA from both users, in case I have to log in as one again.
Baba Yaga (Witchcraft Researcher)
Baba Yaga is an expert in dark magic and is responsible for researching and discovering new ways to harness the power of the fern flower for the company's purposes. Her extensive knowledge of spells and potions makes her an invaluable member of the team.
The task here is to come up with a rate limit bypass and brute-force the password reset token - possible by using HTTP headers modifications.
Remember when we listed some changes that were introduced in
26.0.2
but missing in26.0.0
? This sounds closely related to the scope of the task.
First, let's ask ChatGPT about those comments on LostController.php
.
/Middleware/Security/RateLimitingMiddleware.php
And in the same Middleware:
That should be easy. In routes.php
there is an endpoint for that password reset:
So, I fire up Burpsuite and proxy browser, intercept the request too /lostpassword/reset/form/{token}/babayaga
and then add X-Forwarded-For
header.
I am setting a new password for babayaga
- that request also has to be intercepted and enhanced with X-Forwarded-For
. After the password is changed, I log in as babayaga
and extract the flag and shard.
Endpoint for resetting a password. I left it over for the next day becasue I couldn't find the exact value that is expected to be entered on the submission form. In the
routes.php
there is a/lostpassword/reset/form/{token}/{userId}
but this is not an acceptable answer. The correct one is/lostpassword/reset/form/<TOKEN>/<USER>
which could be found in theLostController->email(..)
.
No additional steps are to be done on that account.
Boruta (Trickster)
Boruta is a mischievous and unpredictable figure, known for his ability to shape-shift and his love of pranks and practical jokes. As the company's Trickster, Boruta uses his skills to distract and confuse their enemies, often leading them astray with illusions and false leads. Despite his playful nature, Boruta is fiercely loyal to the company and will stop at nothing to help them achieve their goals.
Boruta's account can be accessed only with "app password". Maybe it is related to the WebDAV interface I've been seeing on each account.
I can map the leshy
files
But I can't do that obviously with boruta
because I don't know his password - if he has set any.
So perhaps WebAuthn?
That also didn't seem to work. I'm reading the description once again. And looking at the Settings -> Security page.
"Prerequisites**:** Access to any account."
"Can you figure out a way to use an app password?"
"Mass Assignment vulnerability"
And finally, it clicked when I saw there is an input I haven't seen before
Ok, so probably it is possible to cheat the API and add an app password to boruta
instead of the currently logged-in user.
Ok, let's see the source code for that.
It looks like it is a right area - because of the allowed users collection with boruta
name in it.
When I add loginName
parameter
Makes sense because the first check means exactly that - if loginName
is boruta
and it is not empty - throw that firewall error. So to bypass it and simultaneously skip the second check - I just have to add whitespaces to the boruta
.
Now, I'm using the method of mounting files via DAV interface
Remember to use explicitly
-o ro
, without it you won't be able to even read the contents of the file.
I'm copying the files I'm interested in.
sudo cp /mnt/dav-boruta/*.txt /eth/securing-midsummer-corp-2023
sudo cp /mnt/dav-boruta/fernflower_flag5.png /eth/securing-midsummer-corp-2023
One of the files contains a flag.
On Boruta's account, there are additional files related to CTF:
Fern_flower_residual_shard4.txt
- looks like a more AI-generated storyfernflower_flag5.png
- I don't know if that's an error, but so far, we have 1-3 and 5 final flag parts (Boruta is 4th account)twardowski_sso_config.msg.txt
- message from Twardowski to Boruta about SSO configurationtwardowski_sso_mess_msg.txt
- just information to Boruta about the mentioned SSO config
I have tried to use that endpoint, asked ChatGPT to generate me some sample of SAML Response - but to no avail. Let's go to the next task.
Twardowski (Alchemist) - unsolved
Twardowski is a master of alchemy and uses his knowledge to create potions and elixirs that aid in the search for the fern flower. He is constantly experimenting with different ingredients and formulas to unlock the secrets of the flower's power. Twardowski's work is crucial to the company's mission, as his potions can provide the team with enhanced strength, speed, and intelligence.
Getting some help.
I've dug deeper in the application, looking for some SAML references. Because when looking at how the SAML Assertion is made, it felt like some metadata for that response is needed or at least useful.
I have updated the ChatGPT with the metadata XML, then proceeded to look for the other APIs in custom_apps\user_saml\appinfo\routes.php
. I noticed apps/user_saml/saml/login
endpoint and I viewed it via browser. I got the following redirection:
http://idp.midsummer.corp:5000/saml?SAMLRequest=nZJPbxoxEMXvlfIdor2z613CQixAoqF%2FkCggIDn0Eg32bGLJa7seb5t%2B%2B5hl29JIySFznJn383sjjwlq7fisCY9miz8apHDx4TLWU60N8XY6SRpvuAVSxA3USDwIvpt9W%2FIiZdx5G6ywOnmpe1sGROiDsqbTLeaTZL36tFx%2FWazupRQjkcMBqxxQ9IUEHJajCisYlgPByqsCrkaHPnTaO%2FQUSZMkgmOrAxI1uDAUwIQ4YUW%2Fx8oeu97nJWcjXhTfO%2FU8ZlYGQkt4DMHxLFPSpbWS1NQ1%2BlRY7%2FiAMZYdc3WyTZf7ozJSmYe30x5OS8S%2F7veb3ma923eU2Z8z3FgTX0O%2FQ%2F9TCbzdLv%2BaqZRGemEn1VaAzsA5ypqIuD86a%2B1lICiZnujjY4O3l%2FDT99FqDCAhwDg7Z53hHV%2FFuIv5xmolfp8Gx%2FpsfQ3h9bPkad52lOxV7SpvDDkUqlIok3%2Bcmdb2141HCDhJgm8wuczi%2Byc%2F%2F%2F%2Fc6TM%3D&RelayState=http%3A%2F%2Ffiles.midsummer.corp.local%2Fapps%2Fuser_saml%2Fsaml%2Flogin
I've asked my assistant if I can generate a SAML Assertion having the SAMLRequest
.
I must admit that I'm following blindly the AI here, becasue I've never worked with SAML Auth.
A couple of messages later, I had a plan - and I was even given a Python lines to decode the SAMLRequest.
import base64
import zlib
saml_request = "nZJPbxoxEMXvlfIdor2z613CQixAoqF/kCggIDn0Eg32bGLJa7seb5t++5hl29JIySFznJn383sjjwlq7fisCY9miz8apHDx4TLWU60N8XY6SRpvuAVSxA3USDwIvpt9W/IiZdx5G6ywOnmpe1sGROiDsqbTLeaTZL36tFx/WazupRQjkcMBqxxQ9IUEHJajCisYlgPByqsCrkaHPnTaO/QUSZMkgmOrAxI1uDAUwIQ4YUW/x8oeu97nJWcjXhTfO/U8ZlYGQkt4DMHxLFPSpbWS1NQ1+lRY7/iAMZYdc3WyTZf7ozJSmYe30x5OS8S/7veb3ma923eU2Z8z3FgTX0O/Q/9TCbzdLv+aqZRGemEn1VaAzsA5ypqIuD86a+1lICiZnujjY4O3l/DT99FqDCAhwDg7Z53hHV/FuIv5xmolfp8Gx/psfQ3h9bPkad52lOxV7SpvDDkUqlIok3+cmdb2141HCDhJgm8wuczi+yc////c6TM="
decoded_data = base64.b64decode(saml_request)
decompressed_data = zlib.decompress(decoded_data, -zlib.MAX_WBITS)
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="ONELOGIN_ddc8c1abef1aec3cdae768fefa765c0642a48b3a" Version="2.0"
IssueInstant="2023-06-09T16:08:22Z" Destination="http://idp.midsummer.corp:5000/saml"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
AssertionConsumerServiceURL="http://files.midsummer.corp.local/apps/user_saml/saml/acs">
<saml:Issuer>http://files.midsummer.corp.local/apps/user_saml/saml/metadata</saml:Issuer>
<samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
AllowCreate="true" />
</samlp:AuthnRequest>
Later I found out that all I need is the SAMLTool site. As for the SAML response validations goes, I found that
SAMLResponse has to be sent via POST parameter (
application/x-www-form-urlencoded
) -Auth->processResponse()
- application is usingHTTP_POST Binding
; parameter has to be base64 encoded (Response
constructor)cookie
saml-data
has to be set and its value is provided in the initial response to GEThttp://files.midsummer.corp.local/apps/user_saml/saml/login
-SAMLController->assertionConsumerService()
validation is done in
Response->isValid()
- this method has ~300 lines with some portions commented out
Some of the requirements come from the settings - which I cannot see, so it's a bit hard to guess what went wrong.
This is my final ResponseXML that still is not valid. I have also tried both the assertion and message signing with a certificate generated on the SAMLTool.
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0"
IssueInstant="2023-06-21T17:26:53Z">
<saml:Issuer>http://idp.midsummer.corp</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
</samlp:Status>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75"
Version="2.0" IssueInstant="2023-06-21T17:26:53Z">
<saml:Issuer>http://idp.midsummer.corp</saml:Issuer>
<saml:Subject>
<saml:NameID SPNameQualifier="http://sp.example.com/demo1/metadata.php"
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">
_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z"
Recipient="http://files.midsummer.corp.local/apps/user_saml/saml/acs" />
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2023-06-21T17:26:53Z" NotOnOrAfter="2023-06-26T17:26:53Z">
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2023-06-21T17:26:53Z"
SessionNotOnOrAfter="2023-06-26T17:26:53Z"
SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="alias"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">twardowski</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="role"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xsi:type="xs:string">sso</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
Above XML is not complete - but I've seen that some of the properties like `InResponseTo` are not validated if not present - I have omitted them here.
Last Resort
Because I couldn't progress further without twardowski
account, my journey ends here. Or is it?
I have 4/6 pieces and that is enough to get the part of the phrase used to create a flag.
The fern flower reveals ...
Because of how THM presents the answer text boxes, I also know the exact length of the flag.
**************{***********************************}
Midsummer_Corp{Th3_f3r**f!0w3r_r3**@1*************}
Creative Bing AI wasn't much of a help.
ChatGPT responses make me wonder why AIs have issues counting the length of the word.
Brute forcing answers is against the rules.. So I can only guess it has something to do with the saying "The fern flower reveals itself only to the bravest".
Conclusions
Would it be possible to solve the whole CTF using only an AI processor? Despite the fact, it was a bit of pain to use in this event - I really think it shines when it comes to pure binary and mathematical operations. I've seen reverse challenges solved after one prompt, but it should be understandable that it won't happen for tasks that require pentesting and live analysis of the application flow.
It takes longer to prompt (guide) the AI through the challenge, but when you are in total black - I think it is easier and more efficient to do. This is especially true with Bing AI which links you more resources to read on the topic which can give you additional insight.
Overall, I had fun solving the challenges, and definitely I want to see the solution for that SAML task - to see what did I did wrong.