Challenge Context

I rarely do capture the flag write-ups but I solved this in a fairly unique way so here it is. This challenge was part of DragonSec DCTF 2021 by DragonSec SI. It was was solved by around 35 teams out of nearly 1100 teams. It took me around 2 hours to solve this and I did so on the second day of the CTF. My team was the Gungan Grand Army.

Here are the event details on CTFTime.

The Challenge

We were given a binary which, on being executed said:

“Last message in #general is: Welcome to our secret server! Only trusted bots and users have access to the secret channels. Our secret plans have been moved from here to a secret channel. Bot is now running. Press CTRL-C to exit."

Running strings and file on it told me it was written in Go.

discordbot: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked,
Go BuildID=_ZEAdEO-wvSQGBQYhkGM/xFUNbqa5yDNs4HrN4WQF/IfIgpWbUNMnt11lPcn7W/sNa0sC3hsaXC9dymZEeD, not stripped

Extracting The Token

It was a web challenge so I decided reverse engineering was not the answer. Packet capture wouldn’t reveal anything else either since requests would be SSL encrypted. I thought I might be able to solve it if I could see all its http traffic.

So I setup mitmproxy and added its root certificates to /etc/pki/ca-trust/source/anchors and then ran sudo update-ca-trust to enable them. (works this way for Fedora/RHEL family distributions) This needs to be done because otherwise applications using the proxy would throw untrusted certificate error. Instead of mitmproxy, you can also use any other http proxy like Owasp Zap or Burpsuite.

Now the problem was forcing the go binary to use my proxy. I thought of putting it in a container and then proxying all its traffic. I knew podman had a --http-proxy option but upon reading the documentation further, I realized all it did was create an environment variable and any program running inside the container may choose to ignore it. There is probably a way to pass all container traffic through a proxy, but it involved fighting with iptables so I decided against it.

Next thing I thought of was proxychains. Its forces any TCP connection made by any given application to follow through a proxy even if the application does not have inbuilt support for proxies. It works by hooking network-related libc functions in dynamically linked programs via a preloaded DLL and redirects the connections through SOCKS4a/5 or HTTP proxies. Unfortunately this approach does not work in programs written in Go. Go does not use libc and programs written in Go are statically linked not dynamically linked.

In the issue tracker of proxychains, I came across this (relatively) obscure program called graftcp. graftcp works in a different way than proxychains and is able to work against statically linked programs as well. This is what I needed.

There were no publicly available compiled builds of graftcp, so I had to compile it myself. Then:

# in one terminal
# mitmproxy is running on 127.0.0.1:8080
./graftcp-local/graftcp-local -http_proxy 127.0.0.1:8080 -select_proxy_mode only_http_proxy

# in another terminal (discordbot is the name of the provided binary)
./graftcp -n  discordbot

# now discordbot will use that proxy even if its dev never included proxy support

Now all discordbot traffic was going through my mitmproxy.

I found the authorization header in mitmproxy logs.

Authorization: Bot ODM4ODY3MDA4MTI3ODkzNTQ1.YJBVyA.zTQk10ZIQpMfT_Evi00sAkt5KIA

Getting The Flag

This was simple.

import discord

class MyClient(discord.Client):
    async def on_ready(self):
        print('Logged on as', self.user)

        for guild in client.guilds:
            for channel in guild.text_channels:
                # print all messages in all channels
                async for message in channel.history(limit=200):
                    print(message.content)

                # since other teams had sent their greeting messages in the channel, I decided to send mine as well

                # try:
                #     await channel.send('Hi from the Gungan Grand Army !')
                # except:
                #     pass

    async def on_message(self, message):
        # don't respond to ourselves
        if message.author == self.user:
            return

        print(message.content)


client = MyClient()
client.run('ODM4ODY3MDA4MTI3ODkzNTQ1.YJBVyA.zTQk10ZIQpMfT_Evi00sAkt5KIA')

Got the flag dctf{7h3_R0b0t_Upr151ng_15_NEAR!}