July 19, 2026
HOW SMALL MISCONFIGURATIONS CHAINED TO ROOT ON EMPIRE: LUPINONE
An educational walkthrough of the reasoning, discoveries, and privilege escalation decisions behind the complete attack chain.

By Eth3r34l
21 min read
OBJECTIVE
The objective of this machine was to enumerate the target, discover the available services, find a valid path to initial access, and then escalate privileges until root access was achieved.
The main thing I wanted to focus on in this article is not just the commands that worked, but the reasoning behind each step.
This article documents an authorized assessment of an intentionally vulnerable VulnHub machine, in an isolated lab environment.
LAB ENVIRONMENT
The machine was downloaded from vulnhub, and imported into oracle virtualbox, where it was configured to be on the same subnet as another parrot operating system (attacker machine).
PHASE 1: CHECKING FOR DEFAULT CREDENTIALS
When we boot up the machine, we are presented with a login page requesting for a login username and password. Let's test for default credentials. the following are just some of some commonly used default credentials written in the format
USERNAME:PASSWORD:
admin:admin root:root admin:password guest:guest user:user
PHASE 2: HOST DISCOVERY
As none of the default credentials we tried worked, let's move on to something else.
Now, we need to know the IP address of the target machine.
We can already see the IP address on the login page that comes on, but let's assume we can't
In the subsequent commands, remember to replace
[Your_IP_Address]/[CIDR]with your own machine's ip address and CIDR
And replace
[target_IP_Address]with the discovered IP address of the Empire: LupinOne machine
We could use:
sudo netdiscover -psudo netdiscover -pto listen to traffic on the same subnet as ours, with the purpose of discovering other devices on the same network.
The "-p" option in the above command means passive mode. It listens for ARP traffic, but in some cases, this method may not discover much if there's little or no traffic
An alternative to try is:
sudo netdiscover -r [Your_IP_Address]/[CIDR]sudo netdiscover -r [Your_IP_Address]/[CIDR]This does active discovery and sends ARP requests across the subnet.
Alternatively too, we can use:
sudo nmap -sn [Your_IP_Address]/[CIDR]sudo nmap -sn [Your_IP_Address]/[CIDR]to do host discovery.
The "-sn" option tells Nmap to do host discovery only, meaning no port scanning or service scanning is performed.
PHASE 3: SERVICE ENUMERATION
Now that we have our target's IP address, we can begin the reconnaissance process.
Nmap is the perfect tool for this.
You can read through the manual page for Nmap to see its capabilities, using:
man nmapman nmapIn this case, I'll be using:
sudo nmap -A [target_IP_Address]sudo nmap -A [target_IP_Address]to get more details about our target.
The "-A" option enables aggressive scanning. It attempts to perform service detection, OS detection, script scanning, and traceroute.
From our scan results, we got two useful pieces of information and a few extra details we'll come back to later.
We now know that our target has two services running:
SSH — Port 22
HTTP — Port 80
This tells us that the target may likely be running a web server.
Security note: Every open service gives us a possible direction to investigate. SSH gives us an authentication path, while HTTP gives us a content discovery and web enumeration path.
PHASE 4: WEB ENUMERATION
Checking the Webpage:
Since we don't have a username, password, private key or any form of credential, trying SSH might be a dead end. So, let's move on to the possible webserver
The first step is to try loading up a webpage.
Let's go to our web browser and type:
http://[target_IP_Address]http://[target_IP_Address]Since the service is running on port 80, this is HTTP without the "s".
When looking at webpages, areas of interest are usually input fields such as forms, file uploads, login panels, search bars, and anything that accepts user input.
But there's something interesting about this particular webpage.
Except for the visible image, it's just… empty.
No visible input fields whatsoever.
Running Nikto:
In the meantime, I started a Nikto scan in the background:
nikto -h http://[target_IP_Address]nikto -h http://[target_IP_Address]Nikto is a web scanner for vulnerabilities. It allows us to scan web servers for misconfigurations, outdated software, and security risks.
From the results, we can see that the web server has some directories.
/manual/
/image/
/manual/ is the web server's manual, we could look into it to get extra potentially useful information about the server, and /image/ only directs us back to the image on the main page.
At this stage, nothing directly exploitable has appeared yet.
robots.txt Discovery:
Remember how I said we would come back to the additional results that Nmap returned?
If we pay attention, Nmap gives us a useful hint:
|http-robots.txt: 1 disallowed entry
|_~myfiles
A robots.txt file is a public text file on a website that tells automated bots which pages or folders they are allowed or forbidden to scan and list in search results.
Ps: The results of the Nikto scan also gave us this information.
Now, let's manually view the file in the browser:
http://[target_IP_Address]/robots.txthttp://[target_IP_Address]/robots.txt
The webpage that loads gives us additional information. The line:
User-agent: *
means the following rule should apply to all automated web crawlers and bots. So, we now know that there is another directory on the web server:
/~myfiles
Let's try navigating to it:
http://[target_IP_Address]/~myfileshttp://[target_IP_Address]/~myfilesSecurity note: robots.txt should never be used to hide sensitive locations. It is public by design. If a path is sensitive, it should be protected with proper access control, not just listed as "disallowed" for bots.
Investigating ~myfiles:
This page takes us to what seemingly looks like an Error 404 page, but it looks… different.
If you've seen an actual Error 404 page, you'd know it looks nothing like this.
This page was intentionally designed to look like this. We can take a look at the source code to confirm.
Just a little background info: the convention ~[username] has been used historically on Linux machines to denote a user's home directory. So, if we have a user called John on the machine, we could have something like:
~John
for John's home directory.
This does not always confirm that a real system user exists, but it is enough of a clue to investigate further.
Security note: A page that looks like an error page is still worth checking properly. Custom error pages, strange paths, and naming conventions can all reveal useful clues during enumeration.
Content Discovery with Gobuster:
Now, since we know there's a ~myfiles directory, we should also check if there are other hidden directories under the parent directory. I'll be making use of Gobuster for this.
Gobuster is one of the many tools used for content discovery, also known as directory brute-forcing. It works by taking a wordlist and appending each word in the list to the target URL to find hidden directories and files.
First, let's run Gobuster against the webpage's root directory:
gobuster dir -u http://[target_IP_Address]/ -w /usr/share/dirb/wordlists/common.txt --no-error -b 404gobuster dir -u http://[target_IP_Address]/ -w /usr/share/dirb/wordlists/common.txt --no-error -b 404
And since "/~myfiles/" could potentially be a user's home directory, we should also check if there are other subdirectories within it
gobuster dir -u http://[target_IP_Address]/~myfiles/ -w /usr/share/dirb/wordlists/common.txt --no-error -b 404gobuster dir -u http://[target_IP_Address]/~myfiles/ -w /usr/share/dirb/wordlists/common.txt --no-error -b 404
Command breakdown:
- "gobuster dir " specifies which mode of gobuster we want to use, in this case dir for directory/file enumeration
- "-u [TARGET_URL/IP ADDRESS] "specifies which target we're trying to enumerate
- "-w [WORDLIST] " specifies which wordlist we'd be utilizing for enumerating our target.
- "-b 404 " tells gobuster to exclude results that return with the status code 404, since 404 would mean directories that don't exist
There are many other wordlists available. For starters, you can play around in:
/usr/share/wordlists//usr/share/wordlists/to find out.
As we saw, both commands return a few results. Some are results we already knew about, and others returned a 403 Forbidden response.
So, nothing really helpful yet.
Fuzzing for Other User-Style Directories:
Like I noted earlier, the presence of ~myfiles implies the possible existence of a user-style directory.
So, what if, instead of looking only for subdirectories, we try to look for the presence of other user-style directories?
We can do this through something called fuzzing. There are other tools useful for fuzzing too, but Gobuster also has a fuzzing mode, and we'll be using it.
gobuster fuzz -u http://[target_IP_Address]/~FUZZ -w /usr/share/dirb/wordlists/common.txt --no-error -b 404gobuster fuzz -u http://[target_IP_Address]/~FUZZ -w /usr/share/dirb/wordlists/common.txt --no-error -b 404Command breakdown:
- "gobuster fuzz" specifies which mode of Gobuster we want to use. In this case, fuzz is for fuzzing mode.
Unlike when we do directory/file enumeration, we have to enter the FUZZ keyword so Gobuster knows what part of the target URL to test.
- "-u http://[target_IP_Address]/~FUZZ" means FUZZ is the part of the target we want Gobuster to replace with words from the wordlist.
As we can see, our suspicions were correct.
There is another existing user-style directory:
~secret
If we open up the directory in our web browser, we are directed to another webpage.
http://[target_IP_Address]/~secrethttp://[target_IP_Address]/~secret
Security note: Content discovery is not only about finding common paths like /admin or /backup. Sometimes the naming pattern itself becomes the clue. Since "~myfiles" looked user-related, testing for other similar paths became a logical next step.
PHASE 5: DISCOVERY OF ENCODED SSH PRIVATE KEY
We get a few hints from the ~secret webpage:
"ssh private key…hided somewhere here"
"crack my passphrase with fasttrack."
"Your best friend icex64"
These tell us a few things:
- There is a hidden file in the directory containing an SSH private key.
- The SSH key is encrypted, and we would likely have to crack the passphrase.
- The word "fasttrack" is likely pointing us to the fasttrack wordlist.
- The username is likely icex64, which may also be useful later.
Since we know there is still a hidden file in the ~secret directory, we should fuzz once again. This time, we'll look for hidden files (In Linux, hidden files usually start with a dot ".", so we'll use that).
gobuster fuzz -u http://[target_IP_Address]/~secret/.FUZZ.txt -w /usr/share/dirb/wordlists/common.txt --no-error -b 404,403gobuster fuzz -u http://[target_IP_Address]/~secret/.FUZZ.txt -w /usr/share/dirb/wordlists/common.txt --no-error -b 404,403
Ps: I added the .txt extension because I figured the file might also have an extension. Of course, it only dawned on me to try this after I had already run Gobuster fuzz multiple times with different wordlists and no extensions, and did not get any result.
As we can see from the image above, we discover an hidden existing file:
.mysecret.txt
Now, let's navigate to the file in our web browser.
http://[target_IP_Address]/~secret/.mysecret.txthttp://[target_IP_Address]/~secret/.mysecret.txt
Just as the message from the ~secret directory hinted, we can see a bunch of what seems like encoded text. Let's download the file to our machine for further analysis.
We can use the wget command for this:
wget http://[target_IP_Address]/~secret/.mysecret.txtwget http://[target_IP_Address]/~secret/.mysecret.txt
Confirm the file already exists in your local directory by using:
ls -als -aSecurity note: Hidden files are not a form of access control. A filename starting with a dot may hide it from normal directory listings, but if the web server can serve the file, an attacker can still request it directly.
Decoding the Hidden File:
Because our hint already told us that the piece of text we had was encoded, I proceeded to try using the base64 and base32 commands to decode it, but all I got was more gibberish.
After this, I did a quick google search for online cipher/encoding identifying tools, and I stumbled upon a tool, the name was:
DCODE CIPHER IDENTIFIER
I entered the content of ".mysecret.txt", and the tool tried to find out what encoding was used for the file.
The top suggestion seemed to be Base58.
With this new knowledge that the encoding used for the file ".mysecret.txt" might be Base58, let's proceed to
CyberChefCyberChefCyberChef is a well-known online encoding/decoding tool, let's look it up with google, and try using it to convert the content of the file from Base58.
To use this tool, simply: select the encoding you want to convert from by the left side (in our case, Base58), enter the encoded text in the input field by the right, and click on the "BAKE" button below.
We can see that the output field gives us the SSH private key. Let's save that output to a file and call it:
privateKey (You can name it whatever you want).
Security note:
- Encoding is not encryption. Base58 only changes how the data is represented. It does not protect the content from someone who can identify and decode it.
- Since this is an intentionally vulnerable lab, using online decoding tools is acceptable here, but during a real assessment, sensitive material such as credentials or private keys should not be uploaded to third-party websites. Use a trusted local or offline tool instead.
PHASE 6: CRACKING THE SSH KEY PASSPHRASE
Do you remember how I mentioned that trying SSH without having a valid form of credential might be a dead end?
Do you also remember how from our previous enumeration, we deduced that the best friend's name, icex64, might be the username?
Guess what?
This means we now have:
- a private key, and
- a possible username
Now, we can try connecting to the remote server using SSH
ssh -i [private key] [username]@[remote server IP address]ssh -i [private key] [username]@[remote server IP address]From our earlier scan, we already know SSH is running on the default port 22, so there's no need to specify the -p option.
Command breakdown:
- "-i [private key]" is how we pass the private key file for authentication
- "[username]@[remote server IP address]" is the user and target we're trying to authenticate as/to.
But then, a warning occurs: "Permissions 0644 for 'privateKey' are too open"
And it says private Key will be ignored, as a result, it is trying to fall back to another method of authentication: icex64's password.
Since we don't have the password, we should try to fix the cause of the warning.
If you remember your Linux fundamentals, you'd recognize the 644 permissions.
A quick Google search about this error, and I discovered that the private key file needs to have read permissions set only to the user.
We can correct the permissions by running:
chmod 600 [private key]chmod 600 [private key]
We try the command again, but this time, we are asked to enter the private key's passphrase.
A passphrase is like an extra password used to encrypt a private key. It provides an extra layer of protection in case of unauthorized access or theft of the user's private key
Since we don't know icex64's passphrase, we have to find another way to get it.
One tool we can use for cracking the passphrase is John the Ripper.
But before we can do that, we first have to extract the private key into a John-compatible hash format, so it is crackable with John the Ripper.
We can do this using ssh2john:
ssh2john [private key] > [john format].txtssh2john [private key] > [john format].txtNote: While I was working on this, ssh2john didn't come as a package with my Linux distribution, and I discovered that the Python script at:
/usr/share/john//usr/share/john/was an older version that was incompatible with my current version of Python — Python 3.9.2.
Updating my distro packages didn't fix it, but what ultimately worked was installing directly from GitHub:
a) I cloned with:
git clone -b bleeding-jumbo https://github.com/openwall/john.gitgit clone -b bleeding-jumbo https://github.com/openwall/john.git
b) Then I went into the cloned directory at:
john/run/john/run/and ran:
python3 ssh2john.py [privateKey] > [john format].txtpython3 ssh2john.py [privateKey] > [john format].txt
The leading ../../ I used in my own command was to escape back into my home directory.
Now that we have the John-compatible format, we can use John the Ripper to crack the passphrase.
Still remember the hints we got when we found the ~secret folder?
The one where icex64 mentioned that they hid their private key so hackers don't crack it with fasttrack?
This gives us an idea that we can use the fasttrack wordlist to crack the John-compatible hash that we extracted from the private key.
john --wordlist=/usr/share/wordlists/fasttrack.txt [john format].txtjohn --wordlist=/usr/share/wordlists/fasttrack.txt [john format].txt
Voila!
From the output, we can see that the passphrase is:
P@55w0rd!
Security note: A passphrase protects the private key file if it is stolen, but its strength matters. Once an attacker has the encrypted key, they can attempt offline cracking without interacting with the SSH server.
As we just witnessed, the passphrase slowed us down, but because it was guessable with a common wordlist, it did not provide strong protection.
PHASE 7: INITIAL ACCESS AS ICEX64
Now that we know the passphrase, let's try logging in with the private key again:
ssh -i [private key] [username]@[remote server IP address]ssh -i [private key] [username]@[remote server IP address]Then enter the correct passphrase when it requests for it.
And as we can see, we are successfully logged in. We now have remote access to the SSH server as icex64
Security note: As a security researcher, Information gathering is one of the most important phases of an assessment. The pieces of exposed information and clues we gathered was what led us up to this point of Initial access.
Local Enumeration as icex64:
But we're not done yet.
Now that we have access to the server, we need to see if we can escalate our privileges.
First, let's run the
lslscommand to list files in icex64's home directory.
All we can see is a user.txt file, and running "cat user.txt" presents us with a message that we found the first flag
Now, let's run:
sudo -lsudo -lto see what commands the user we are logged in as, icex64, can run with sudo privileges.
From the output, we can tell that:
a. icex64 can run the following command as (arsene)
b. NOPASSWD means icex64 doesn't need a password to run it.
c. The allowed command is: /usr/bin/python3.9 /home/arsene/heist.py
This also tells us that there's another directory:
/home/arsene/
likely belonging to another user.
Let's navigate to /home/arsene/ to check out the file heist.py:
cd /home/arsene/cd /home/arsene/Examining note.txt:
A quick
lslsin the directory /home/arsene/ shows two files:
heist.py note.txt
Let's view the content of note.txt
cat note.txtcat note.txt
Arsene is asking their friend Icex64 to check if their code is secure to run.
They mentioned that Icex64 is the only one that can access the program, as it can compromise their account.
We already discovered this from the output of sudo -l, that we ran earlier, which showed that Icex64 can run:
/usr/bin/python3.9 /home/arsene/heist.py
as the user arsene without needing a password.
Arsene also mentioned that they are entrusting their program to Icex64 because they know that Icex64's account is secure.
Big mistake!.
They never considered the possibility that icex64's account could be compromised.
Examining heist.py:
Now let's examine the content of "heist.py" using the cat command
cat /home/arsene/heist.pycat /home/arsene/heist.pyOr, in my case, using the nano text editor:
nano /home/arsene/heist.pynano /home/arsene/heist.py
The file doesn't really do a lot. It is just a simple script to open a webpage, but it doesn't always have to be sophisticated to be exploited.
The script looks like this:
import webbrowser
print("Its not yet ready to get in action")
webbrowser.open("https://empirecybersecurity.co.mz")import webbrowser
print("Its not yet ready to get in action")
webbrowser.open("https://empirecybersecurity.co.mz")The first line of the script imports another Python module, which is pretty much another Python file somewhere else on the server.
When we used the nano text editor to open the file, we could already see that the file was unwritable to us.
We can also confirm this by viewing the file's permissions:
ls -l /home/arsene/heist.pyls -l /home/arsene/heist.py
Not having permissions to write to the file means we can't edit the file directly to do something else, so our thought process should be along the lines of:
Can we influence the location that Python is pulling the module from?
Can we replace the original module with our own malicious file?
Can we edit the original module to do something different instead?
Let's try to find the location of the module: webbrowser, on the server.
There are multiple ways to do this, but I used a simple Python command:
python3 -c "import webbrowser; print(webbrowser.__file__)"python3 -c "import webbrowser; print(webbrowser.__file__)"
Command breakdown:
- "-c" tells Python to execute the following string as a Python script directly from the terminal.
- "import webbrowser" loads the webbrowser module.
- "print(webbrowser.file)" prints the specific location where the module is stored.
PHASE 8: PRIVILEGE ESCALATION TO ARSENE
Checking the webbrowser.py Module:
Now that we know the module's location, remember the thought process I said we should be having?
Let's see if we can pull off any of them.
We already know the webbrowser module is in: /usr/lib/python3.9/
Let's see what permissions the directory has:
ls -ld /usr/lib/python3.9ls -ld /usr/lib/python3.9The "-d" option lists the directory itself instead of the files in it.
As we can see, our current account does not have permission to write into this directory. This means we won't be able to create or delete files in the directory.
In other words, we've now discovered that we will not be able to replace the original webbroswer.py module with our own file in that directory.
This leaves us with one more option to try: editing the original file.
Like we did with the directory, let's see what permissions the actual file has:
ls -l /usr/lib/python3.9/webbrowser.pyls -l /usr/lib/python3.9/webbrowser.py
There!
We spot another error in system configuration.
The webbrowser.py module is configured to allow full permissions for everyone.
Therefore, we can definitely edit this file as we wish.
Abusing the Writable Python Module:
Let's open the file with our text editor of choice:
nano /usr/lib/python3.9/webbrowser.pynano /usr/lib/python3.9/webbrowser.pyWe'll need the os module to be imported first, for our line of code to run.
Since the script already originally imports os, we can insert our line of code after the block of import statements:
os.system('/bin/bash')os.system('/bin/bash')
Command breakdown
- os.system([command]) is used to execute the string [command].
So inserting: "/bin/bash" is simply telling Python to run the system command: "/bin/bash", which launches an interactive shell.
Since we'll be executing heist.py as arsene, we'll automatically get a shell as arsene.
Now let's run heist.py so it can call webbrowser.py and execute our code:
sudo -u arsene /usr/bin/python3.9 /home/arsene/heist.pysudo -u arsene /usr/bin/python3.9 /home/arsene/heist.py
The option: "-u arsene" allows us to execute the following command as another user, arsene.
After running the command, we can see that our current user changes from icex64 to arsene.
We have successfully obtained a shell as arsene.
Security notes:
- This emphasizes the importance of a cybersecurity concept: Zero-Trust Security. It's unsafe to assume that just because the user icex64 is making the request, they are actually the one behind the account. An attacker could have taken over that account and be making the request on their behalf. An additional form of authentication, or access control mechanism would have made it harder for us to execute heist.py.
- A privileged script is only as safe as the files and modules it loads. Even if the main script is not writable, imported libraries can still become part of the attack path.
- System libraries should not be writable by normal users. If a low-privileged user can modify code that a privileged process imports, they can potentially execute commands as that privileged user.
PHASE 9: PRIVILEGE ESCALATION TO ROOT
Local Enumeration as arsene:
Just like we did with icex64, let's see what commands arsene can run with higher privileges:
sudo -lsudo -l
We can see something interesting from the output.
Arsene can run the command: /usr/bin/pip as (root) without needing a password.
This is also another system misconfiguration.
Pip happens to be a Python package installer, meaning we can take advantage of this to install and execute code of our choice.
To exploit this, let's go to GTFOBins; a list of legitimate functions of Unix-like executables that can be abused to break out of restricted shells, escalate privileges, maintain elevated privileges, and more.
https://gtfobins.org/https://gtfobins.org/Since the binary we are trying to abuse is: /usr/bin/pip, we look for: pip on GTFOBins site.
From there, we click on: Inherit. We're choosing the Inherit function because the binary we're trying to exploit can execute another program or shell while inheriting the elevated privileges of the allowed command.
For pip, the abuse works because package installation can execute Python code from a setup.py file.
Security note: GTFOBins does not always mean the binary itself is vulnerable. It means a legitimate binary can become dangerous when it is granted unsafe privileges.
Abusing pip to Get Root:
The GTFOBins documentation tells us to run the following commands:
echo 'import os; os.system("exec /bin/sh < /dev/tty > /dev/tty 2> /dev/tty")' > setup.pyecho 'import os; os.system("exec /bin/sh < /dev/tty > /dev/tty 2> /dev/tty")' > setup.pyIf we scroll a little lower and click on the sudo section, since this particular command relies on us executing it with sudo privileges, we can see the rest of the command we're supposed to run.
The documentation tells us to run: pip install --break-system-packages .
But earlier, just before we scrolled down to the sudo section, it also told us that the --break-system-packages flag can be omitted in older systems.
So now we know the second command we are running is:
sudo /usr/bin/pip install .sudo /usr/bin/pip install .Let's break down what each line of command does.
Line 1:
echo 'import os; os.system("exec /bin/sh < /dev/tty > /dev/tty 2> /dev/tty")' > setup.pyecho 'import os; os.system("exec /bin/sh < /dev/tty > /dev/tty 2> /dev/tty")' > setup.py- The
echocommand outputs whatever is given to it, and in this case, the output is redirected to a file called:setup.py - In Linux shells, single quotes '' usually mean "take everything in this quote literally," meaning there is usually no need to escape special characters inside them
import osimports theosmodule so we can use thesystem()function.os.system(...)is a Python function used to execute system commands. Every string in the parentheses is taken as a command and run in a subshellexec /bin/shuses the built in commandexecto replace the current process with the specified command,/bin/sh/bin/shstarts a new shell./bin/shis the choice of shell because it comes by default with most Unix systems, so there's a high chance the target machine has it/dev/ttyis a special file in Linux that represents the controlling terminal of the current process< /dev/ttyredirects standard input from/dev/tty. This ensures the new shell reads commands from our terminal input> /dev/ttyredirects standard output to/dev/tty. This ensures output is returned to our terminal screen2> /dev/ttyredirects standard error to/dev/tty. This ensures error messages are also printed back to our terminal screen.
Line 2:
sudo /usr/bin/pip install .sudo /usr/bin/pip install .- The dot:"." refers to the current directory.
So we're telling pip to read and install from the Python package located in the current directory, which contains the setup.py file we created.
Because arsene is running /usr/bin/pip as root, the shell spawned from executing: exec /bin/sh automatically starts as root, therefore granting us root privileges on the machine.
The remaining redirections make sure the new shell reads input from us and returns its output and errors back to us.
Security note: Package managers and interpreters are dangerous in sudo rules because they are designed to execute code. Allowing pip to run as root without a password can effectively allow arbitrary root code execution.
-Extra: Executing the above commands should already grant you root privileges, you can confirm it using:
ididor
whoamiwhoamiThe expected output should show:
uid=0(root)uid=0(root)or
rootrootAt this point, we have successfully escalated from:
icex64 → arsene → rooticex64 → arsene → rootbut there's a few extra lines I added for cleaner execution
Cleaner Execution with a Temporary Directory:
Executing the above commands should already grant root privileges, but there are a few extra lines I added for cleaner execution:
TEMP=$(mktemp -d)
echo 'import os; os.system("exec /bin/sh < /dev/tty > /dev/tty 2> /dev/tty")' > $TEMP/setup.py
sudo /usr/bin/pip install $TEMPTEMP=$(mktemp -d)
echo 'import os; os.system("exec /bin/sh < /dev/tty > /dev/tty 2> /dev/tty")' > $TEMP/setup.py
sudo /usr/bin/pip install $TEMP
What does this little addition do?
mktemp -dcreates a temporary directory in/tmp.TEMPis just a variable name for storing the result.- The second line is pretty much the same as the first command from the previous method, except instead of creating
setup.pyin the current directory, it creates it in the temporary directory:$TEMP/setup.py - The third line is also the same as the second command from the previous method, except instead of the current directory, pip reads from the temporary directory:
$TEMP
Why this little addition?
- Creating a temporary directory makes it easier to have a cleaner and isolated directory where the payload can be executed from without the risk of interfering with other system files.
- It also makes it easier to clean up after you're done. For example, if pip or whatever program you're executing generates other files, you can simply clean up by deleting the directory instead of trying to remember what new files were generated.
- It reduces the risk of experiencing permission conflicts. Since it's a directory you created, you can read and write to it as you wish.
Final Attack Chain
The full path looked like this:
Host discovery
↓
Nmap service enumeration
↓
HTTP service discovered
↓
robots.txt revealed /~myfiles
↓
~myfiles suggested user-style directories
↓
Gobuster fuzzing discovered /~secret
↓
~secret hinted at an encoded SSH private key and username
↓
.mysecret.txt was discovered
↓
Base58 decoding revealed an encrypted SSH private key
↓
ssh2john extracted a John-compatible hash
↓
John cracked the weak private key passphrase
↓
SSH login as icex64
↓
sudo -l revealed icex64 could run heist.py as arsene
↓
heist.py imported webbrowser
↓
webbrowser.py was world-writable
↓
Python module modification gave a shell as arsene
↓
sudo -l revealed arsene could run pip as root
↓
pip setup.py execution spawned a root shellHost discovery
↓
Nmap service enumeration
↓
HTTP service discovered
↓
robots.txt revealed /~myfiles
↓
~myfiles suggested user-style directories
↓
Gobuster fuzzing discovered /~secret
↓
~secret hinted at an encoded SSH private key and username
↓
.mysecret.txt was discovered
↓
Base58 decoding revealed an encrypted SSH private key
↓
ssh2john extracted a John-compatible hash
↓
John cracked the weak private key passphrase
↓
SSH login as icex64
↓
sudo -l revealed icex64 could run heist.py as arsene
↓
heist.py imported webbrowser
↓
webbrowser.py was world-writable
↓
Python module modification gave a shell as arsene
↓
sudo -l revealed arsene could run pip as root
↓
pip setup.py execution spawned a root shellKEY LESSONS LEARNED
The main lesson learned from this machine is that root access did not happen because of one single issue.
It happened because of a chain of smaller misconfigurations:
- Exposed web paths
- Public robots.txt hints
- Hidden but accessible sensitive files
- Encoding mistaken for protection
- Weak private key passphrase
- Passwordless sudo delegation
- Writable Python standard library file
- Dangerous pip sudo permission
Each one created the next step.
That is why good security is not just about fixing one thing. It is about breaking the attack chain at as many points as possible.