Capturing the Flag with GPT-4

Posted April 23, 2023 in ai ctf

This weekend I went to BSides SF 2023 and had a blast. I went to some really interesting talks (including an excellent one about adversarial machine learning), but mostly I spent my time solving CTF hacking challenges. And this time, I did it with the help of GPT-4, the latest generation of OpenAI's ChatGPT generative language model. GPT-4 straight up solved some challenges for me, which blew my mind. There were definitely several flags I got that I wouldn't have gotten without the help of GPT-4. For challenges that GPT-4 didn't solve on its own, it provided incredibly helpful tips, or quickly wrote scripts that would have been tedious or time consuming for me to write myself. Good thing there's (almost) no such thing as cheating in CTF!

I also found several situations where ChatGPT simply errored out and refused to give me answers. I think this was the case when it was overtly clear that I was trying to get help with hacking. For example, when I asked it how to write some JavaScript code that would bypass a specific XSS filter, and used language that made it clear that I was trying to bypass an XSS filter, it just failed with an error. It seems that ChatGPT has some nominal safeguards to prevent people from using it for malicious hacking, but if you simply ask detailed technical questions (which could be used for offense or defense), it tends to answer them.

All that said, I wanted to share my experience with a few challenges. This post includes write-ups for:

  • Shamir Secret Sharing
  • perckel
  • Shell Hurdles

I used GPT-4 for help on several other challenges, but these three especially impressed me. I also solved several challenges without its help--partially because it's not always easy to give it all the context it needs to answer a question. You can't, for example, send it a 30MB APK file and then ask for help reverse engineering it, or copy all of the source code for a web app into GPT-4 and ask it to find the vulnerabilities. You can get help with smaller chunks of these problems, but I could see this technology getting way more powerful in the future.

Shamir Secret Sharing

In this challenge, I had to walk around the BSides SF venue looking for QR codes to scan. In Shamir's Secret Sharing scheme, a secret is divided into a certain number of parts, and if one person has a threshold of those parts they can decrypt the secret. In this case, there were 7 parts with a threshold of 5, and I wandered around and found 6 of them on QR codes. I then simply asked GPT-4 to solve the problem for me:

Here are parts for Shamir Secret Sharing:

Share 1 of 7 (min 5): (x1, y1) = (1, 12214173319090360239218007) Field prime = 2^89 - 1 Flag = CTF{secretasletters} Secret -> base 27 a = 1, b = 2, ... z = 26 (base 27)

Share 3 of 7 (min 5): (x3, y3) = (3, 272214528378786743506941922) Field prime = 2^89 - 1 Flag = CTF{secretasletters} Secret -> base 27 a = 1, b = 2, ... z = 26 (base 27)

Share 4 of 7 (min 5): (x4, y4) = (4, 404905998942651879217397287) Field prime = 2^89 - 1 Flag = CTF{secretasletters} Secret -> base 27 a = 1, b = 2, ... z = 26 (base 27)

Share 5 of 7 (min 5): (x5, y5) = (5, 589183787842889173793388269) Field prime = 2^89 - 1 Flag = CTF{secretasletters} Secret -> base 27 a = 1, b = 2, ... z = 26 (base 27)

Share 6 of 7 (min 5): (x6, y6) = (6, 338337360147368973687481536) Field prime = 2^89 - 1 Flag = CTF{secretasletters} Secret -> base 27 a = 1, b = 2, ... z = 26 (base 27)

Share 7 of 7 (min 5): (x7, y7) = (7, 479528534189573769684386994) Field prime = 2^89 - 1 Flag = CTF{secretasletters} Secret -> base 27 a = 1, b = 2, ... z = 26 (base 27)

How do I decrypt the secret?

And GPT-4 delivered.

Wow. Does it work? I copied the Python script into decrypt.py and tried running it.

$ python3 decrypt.py
CTF{perasperaadastra}

Holy shit. Mind blown. Especially because this challenge actually includes a very tricky part related to base-27, but GPT-4 just solved it all on the first try, without additional prompting.

I submitted the flag and got the points.

perckel

I downloaded the file flag.bin, and then tried to figure out what this file is by running file on it.

$ file flag.bin
flag.bin: perl Storable (v0.7) data (network-ordered) (major 2) (minor 11)

I had no idea what this is and it's been well over a decade since I wrote any code in Perl, so I asked GPT-4.

What is this file?

flag.bin: perl Storable (v0.7) data (network-ordered) (major 2) (minor 11)

Huh, cool. Maybe GPT-4 can write a Perl script for me so I can see what's in that file.

Write a Perl script that will display what's in that file

Nice, I don't have to re-teach myself Perl. I saved the script as script.pl and tried running it:

$ perl script.pl
Can't eval, please set $Storable::Eval to a true value at /System/Library/Perl/5.30/darwin-thread-multi-2level/Storable.pm line 421, at script.pl line 9.

Hmm, it wants to be able to execute arbitrary code. I'll have to be careful when running this on my computer, but in the meantime let's see if GPT-4 can help fix this issue.

Can't eval, please set $Storable::Eval to a true value at /System/Library/Perl/5.30/darwin-thread-multi-2level/Storable.pm line 421, at script.pl line 9.

Nice, it fixed the script. I updated script.pl, but I didn't want to run it directly on my computer though. (It was nice of GPT-4 to warn me three separate times about the security risks.) So I decided to run it in a Docker container. I created a new Ubuntu container, installed the correct Perl dependencies, and ran it:

root@22b72f43fc3d:/src# perl ./script.pl 
Name "Storable::Eval" used only once: possible typo at ./script.pl line 9.
$VAR1 = {
          'shqa' => 'iryaeuxfc',
          'jwjepylpxecq' => 'yfebe',
--snip--
          'iruvqhgdnfstz' => 'smkohnqw',
          'qcotpliscszql' => 'efzhu',
          'decode_flag' => sub { "DUMMY" },
          'htxj' => 'pmifolpyh',
          'lftq' => 'pxqqgeqy',
          'adbsjrhouqhgg' => 'srndfdboqqmfc',
          'avarqfgzrufc' => 'thgzdhdstum',
          'pdeqoerfqxbro' => 'grugrmrocnekt',
          'ujlw' => 'ooxnyiyu',
          'flag' => '��;���@��(�P��@HTX�Т�@��ڂ���@(!k�8�@��Q�
��@�,��}F�@0Z��W�@��R�J�@���JM��@�#*,�@��E~���@�A����@���Y]��@�v���@@�N

b�@���co6�@������@��
                    �9�D�|�
                           �@�\'Mi�e�@i��^9�@',
          'lquykkphh' => 'bjyrsce',
          'ruybrycyo' => 'othebxvqngu',
--snip--
          'zxyybaaawc' => 'kbzr',
          'pojhbzugch' => 'dqxiybapwepfh'
        };
root@22b72f43fc3d:/src#

The script spewed out over 1,000 lines of output, displaying the value of this Perl object. Most of the keys-value pairs seemed to be garbage, but two of the keys were interesting: flag was a block of binary data, and decode_flag was apparently a Perl function. It sounds like maybe the flag is encoded, and I can call decode_flag to decode it.

The $data object has two interesting keys, flag which is binary data, and decode_flag which is a function. Update the script to call the decode_flag function and pass in the value of flag

I updated my script and ran it again:

root@22b72f43fc3d:/src# perl script.pl
Name "Storable::Eval" used only once: possible typo at script.pl line 9.
Decoded flag: CTF{ooohhh_purrrrl_<3}

Shell Hurdles

The challenge provided a link to a web terminal, or a host that I can netcat into to get a shell. Here's what the web terminal looked like:

Here's the output of a few commands (the hostname is edited for clarity):

ctf@shurdles:~$ ls -l
total 2220
-r-x--x--x 1 root ctf 2270141 Apr 16  2020 shurdles

My home folder has an executable file called shurdles in it. I tried running it.

ctf@shurdles:~$ ./shurdles 
shurdle 0 failed: expected at least 1 argument

It failed with an error message saying it needs at least 1 argument. This was the first shurdle (shell hurdle), but it's an easy fix--I'll just pass in an argument.

ctf@shurdles:~$ ./shurdles ASDF
shurdle 2 failed: I expected to be called /shurdles, not ./shurdles

This worked, and I moved onto the next shurdle. This one was pretty hard. This program wants me to run /shurdles, not ./shurdles. How can I do that? Let's ask GPT-4. I asked this:

I have a bash shell. There is an executable file called /home/ctf/shurdles. I can run it by running:

/home/ctf/shurdles

Or by changing to the folder /home/ctf, and running:

./shurdles

Is it possible for me to run it with this command though, without creating a symlink?

/shurdles

It responded with a decent idea: add /home/ctf to my PATH so I can just run shurdles, though it wasn't perfect.

I tried it anyway, and it didn't work:

ctf@shurdles:~$ export PATH=$PATH:/home/ctf
ctf@shurdles:~$ shurdles ASDF
shurdle 2 failed: I expected to be called /shurdles, not shurdles

So I went back to GPT-4 and pleaded for additional help.

That works to run:

shurdles

But how can I run it with:

/shurdles

It told me in that case to create a symlink, though that requires root.

I don't have root, so I asked for other ideas.

Is there any way to do this without using symlinks, and without using root?

It then came up with a great idea that I might not have thought up on my own: use a shell function.

Unfortunately, this didn't work:

ctf@shurdles:~$ function /shurdles() { /home/ctf/shurdles "$@"; }
ctf@shurdles:~$ /shurdles ASDF
shurdle 2 failed: I expected to be called /shurdles, not /home/ctf/shurdles

It seems that even though I'm running /shurdles, that's a shell function that's running /home/ctf/shurdles, so that's the first argument that the process sees. I asked for more help:

That was a great idea, but I actually need the first value of argv to be /shurdles, and that makes the first value /home/ctf/shurdles

It responded with an idea that I totally wouldn't have thought of, because I had never even heard of exec -a. Fascinating.

I decided to give it a try, but I realized the box I was on didn't have vim or nano. So, out of laziness, I just asked for the commands to save shurdles_wrapper.sh using echo instead.

How can I create that shurdles_wrapper.sh script using echo, instead of a text editor?

It gave me the exact command to run.

I changed it a bit myself--I stored the script in /tmp/wrapper.sh instead of /home/ctf/shurdles_wrapper.sh, because I didn't have write access to /home/ctf, but it worked:

ctf@shurdles:~$ echo -e '#!/bin/bash\nexec -a /shurdles /home/ctf/shurdles "$@"' > /tmp/wrapper.sh
ctf@shurdles:~$ chmod +x /tmp/wrapper.sh
ctf@shurdles:~$ /tmp/wrapper.sh ASDF    
shurdle 3 failed: I expected the environment variable "HACKERS" to look like hack the planet on separate lines

Got past that shurdle! And onto the next. I could totally figure this one out, but why not save time and get GPT-4 to do it for me?

How can I set the environment variable "HACKERS" to be "hack the planet", but with each word on a separate line?

And it worked:

ctf@shurdles:~$ export HACKERS=$'hack\nthe\nplanet'
ctf@shurdles:~$ /tmp/wrapper.sh ASDF               
shurdle 5 failed: expected workdir "/run/. -- !!"

Onto the next shurdle. This one confused me a bit. I asked GPT-4 how I could make my current working directory look like: /run/. -- !!, without actually creating that folder and changing to it. It had some good ideas, including running export PWD='/run/. -- !!' before running the command, but it didn't work. I asked it the different ways a command can learn its working directory, and it told me it can use the PWD environment variable, but also the getcwd function. When I asked how getcwd learned the working directory without the PWD environment variable, it gave me a detailed answer.

All of this is nitty gritty Linux details that I didn't know beforehand. And sure, generative language models confidently say things that are false sometimes, so it might not be exactly right... but honestly, it seems about as likely to be right as if I had asked a human Linux nerd. They get things wrong sometimes too.

In the end though, I was barking up the wrong tree. I realized the ctf user had write permission to /run, so I could just create the folder and change to it. So I got past this shurdle on my own.

ctf@shurdles:~$ mkdir '/run/. -- !!'
ctf@shurdles:~$ cd /run/.\ --\ \!\!/
ctf@shurdles:/run/. -- !!$ /tmp/wrapper.sh ASDF
shurdle 6 failed: fd 3 isn't open

I didn't quite know what this one meant. Did I have to open a file or something? I asked GPT-4.

What does "fd 3 isn't open" mean?

Interesting. So the program I'm running needs to open a file, and that will be file descriptor 3... I still had no idea how to make the program open a new file. I tried passing in a valid filename like /home/ctf/.bashrc instead of ASDF as the first argument, but no luck. So I asked GPT-4 for more help:

Is there any way I could run the program that's giving this error in a way that will make it open an additional resource, so that file descriptor 3 will be open?

I really ought to learn how to use the exec command... I had no idea about any of this, but I decided to try it.

ctf@shurdles:/run/. -- !!$ exec 3>/tmp/fd3_output.txt
ctf@shurdles:/run/. -- !!$ /tmp/wrapper.sh ASDF
shurdle 6 failed: expected fd 3 to be a file of 1337 bytes in length

Progress! So I need /tmp/fd3_output.txt to be 1337 bytes long. I knew how to do this, but thought it would be faster to ask GPT-4 than to look up the specific dd arguments, or write Python code, or however I wanted to do it.

How can I make /tmp/fd3_output.txt be 1337 bytes long?

When I ran the dd command it made /tmp/fd3_output.txt 1337 bytes long, however when I ran exec 3>/tmp/fd3_output.txt it truncated the file so it was zero bytes. Hmm, how could I solve that?

How can I modify the command:

exec 3>/tmp/fd3_output.txt

So that when it creates the file /tmp/fd3_output.txt, it makes it 1337 bytes long?

I tried it... but it didn't work.

ctf@shurdles:/run/. -- !!$ dd if=/dev/zero of=/tmp/fd3_output.txt bs=1 count=1337 && exec 3>/tmp/fd3_output.txt
1337+0 records in
1337+0 records out
1337 bytes (1.3 kB, 1.3 KiB) copied, 0.00219604 s, 609 kB/s
ctf@shurdles:/run/. -- !!$ /tmp/wrapper.sh ASDF
shurdle 6 failed: expected fd 3 to be a file of 1337 bytes in length

I wonder what's going on.

When I run:

dd if=/dev/zero of=/tmp/fd3_output.txt bs=1 count=1337 && exec 3>/tmp/fd3_output.txt

It seems that /tmp/fd3_output.txt is still 0 bytes.

$ ls -l /tmp/fd3_output.txt -rw-r--r-- 1 ctf ctf 0 Apr 23 06:17 /tmp/fd3_output.txt

Let's see...

ctf@shurdles:/run/. -- !!$ dd if=/dev/zero of=/tmp/fd3_output.txt bs=1 count=1337 && exec 3<>/tmp/fd3_output.txt
1337+0 records in
1337+0 records out
1337 bytes (1.3 kB, 1.3 KiB) copied, 0.0021625 s, 618 kB/s
ctf@shurdles:/run/. -- !!$ /tmp/wrapper.sh ASDF
shurdle 7 failed: tz Local != America/Los_Angeles

One more shurdle down! Okay, so I need to update the time zone next.

How do I set the time zone to "America/Los_Angeles" so that the next command I run recognizes that as the time zone?

ctf@shurdles:/run/. -- !!$ exec 3<>/tmp/fd3_output.txt && TZ="America/Los_Angeles" /tmp/wrapper.sh ASDF
shurdle 8 failed: could not find shurdles-helper

Another shurdle down! Now it needs to find shurdles-helper. This one I can solve on my own, by creating a new folder, putting an executable file called shurdles-helper into it, and adding it to the PATH.

ctf@shurdles:/run/. -- !!$ mkdir /tmp/bin
ctf@shurdles:/run/. -- !!$ echo -e '#!/bin/bash\necho helper' > /tmp/bin/shurdles-helper
ctf@shurdles:/run/. -- !!$ chmod +x /tmp/bin/shurdles-helper
ctf@shurdles:/run/. -- !!$ export PATH=/tmp/bin:$PATH
ctf@shurdles:/run/. -- !!$ exec 3<>/tmp/fd3_output.txt && TZ="America/Los_Angeles" /tmp/wrapper.sh ASDF
shurdle 9 failed: expected /home/ctf/.cache/shurdles, does it exist?

It worked. Onto the next shurdle. In this case, it needs a file called /home/ctf/.cache/shurdles to exist. I think I can solve this one on my own too by creating that file.

ctf@shurdles:/run/. -- !!$ mkdir -p /home/ctf/.cache
ctf@shurdles:/run/. -- !!$ touch /home/ctf/.cache/shurdles
ctf@shurdles:/run/. -- !!$ exec 3<>/tmp/fd3_output.txt && TZ="America/Los_Angeles" /tmp/wrapper.sh ASDF
shurdle 9 failed: /home/ctf/.cache/shurdles was modified in the last day, sorry

It worked too. Now, it needs to timezone to be set to America/Los_Angeles. How you do update the modified timestamp on a file? GPT-4 will know.

How you do update the modified timestamp on a file?

Okay, simple enough. I'll set it to a month ago.

ctf@shurdles:/run/. -- !!$ touch -t 202303231430 /home/ctf/.cache/shurdles
ctf@shurdles:/run/. -- !!$ exec 3<>/tmp/fd3_output.txt && TZ="America/Los_Angeles" /tmp/wrapper.sh ASDF
Congratulations!!!
CTF{you_made_it_past_the_hurdles}

I only made it past the hurdles thanks to you, GPT-4.