Challenge Description
name: funny lfr
category: web exploitation
points: 183
solves: 36 solves
You can access the challenge via SSH:
ncat -nlvp 2222 -c "ncat --ssl funny-lfr.chals.sekai.team 1337" & ssh -p2222 user@localhost
SSH access is only for convenience and is not related to the challenge.
Analysis
We are given the following source files:
├── app.py
└── Dockerfile
Which represent a simple Starlette application:
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import FileResponse
async def download(request):
return FileResponse(request.query_params.get("file"))
app = Starlette(routes=[Route("/", endpoint=download)])
And a Dockerfile
FROM python:3.9-slim
RUN pip install --no-cache-dir starlette uvicorn
WORKDIR /app
COPY app.py .
ENV FLAG="SEKAI{test_flag}"
CMD ["uvicorn", "app:app", "--host", "0", "--port", "1337"]
At first glance the challenge seems very simple. You make a request to /?file=<path>
and get the file contents displayed to you.
Inside SSH
The flag as highlighted by the Dockerfile is stored inside an environment variable
called FLAG
. Doing a quick google search, we can see that environment variables
in linux systems are stored in the /proc/pid/environ
file.
With that knowledge in hand, we should get the flag just by getting the results of
/proc/self/environ
which stores the environment variables of the current running process.
Right?
We got nothing… That’s weird.
We know the application should return the contents of the files we ask for. However,
asking for /proc/self/environ
doesn’t return anything. Why?
Well, the python application is a Starlette application that adheres to ASGI specs, the FileResponse class is responsible for returning files to the client. Before serving a file, FileResponse checks the file’s size using the os.stat syscall.
from Starlette source code
The os.stat
function retrieves various attributes about a file, such as its size, modification time, and permissions. When os.stat
is called on a file, it checks the filesystem for this information.
However, in the case of /proc/self/environ
, which resides within the procfs
virtual filesystem (VFS), there’s a unique situation. The procfs
VFS provides access to kernel and process information, and many files within it are not regular files but rather interfaces to the kernel’s data structures. These files often have special behaviors, and their contents may be dynamically generated when accessed.
When os.stat
checks /proc/self/environ
, it reports the file size as zero because, in many cases, the file doesn’t have a traditional size; it’s an interface to process-specific information that’s only generated on demand. Consequently, when FileResponse
sees a size of zero, it might interpret this as an empty or non-existent file, even though reading from /proc/self/environ
would normally return environment variables for the process.
This behavior can lead to the application not returning any content when asked for /proc/self/environ
, despite the file being non-empty in a traditional sense. The mismatch between how os.stat
reports the file’s size and the file’s actual contents in procfs
is the root cause of this issue.
So, what can we do then?
It turns out, we can trigger a race condition to do the following:
- request a file whose size is greater than 0.
- right after
os.stat
and before the actual read, we swap the latter file, with the desired file which is/proc/pid/environ
.
To achieve such a thing, we can make use of symlinks, create a symlink that points
to a bigger file, bypass the os.stat
step, then right before the file read, we change
the link to /proc/pid/environ
and successfully get the flag.
Exploitation
To trigger the race condition, let’s first create a bash script that creates a large file,
create a symlink to it, and then create an infinite loop which swaps the links between this file
and /proc/pid/environ
.
Note that you should replace pid with the actuall process id of the running python application. You can figure that using
ps aux
. pid=7 in my case.
solve.sh
#!/usr/bin/env bash
cat /etc/passwd > /home/user/big-file.txt
ln -s big-file.txt /home/user/the-link
while true; do
ln -sf /proc/7/environ /home/user/the-link
ln -sf /home/user/big-file.txt /home/user/the-link
done
Running this on remote, and doing few curls of the the-link
file should give us the flag.
Flag is: SEKAI{b04aef298ec8d45f6c62e6b6179e2e66de10c542}
Things we learned from this challenge:
Here are the key lessons from the challenge:
os.stat
andprocfs
: Learned howos.stat
retrieves file attributes and why it reports/proc/self/environ
as size zero inprocfs
.Race Condition Exploit: Discovered how to exploit race conditions using symlinks to bypass
os.stat
and read sensitive files.Symlink Usage: Learned to manipulate file paths using symlinks for exploitation purposes.