First Try at Capture the Flag (CTF) Event

Its time to write again after long long hiatus. From now on I will try to write at least one article/story a week. Okay, let's get started!

Last week, I went to Jakarta to attend Hack-a-Fun — my company annual hackaton event. Part of the events was Capture The Flag (CTF). I formed a team consisting of three person including me to join … and that was a fun and wild experience.

There are at least 15 challenges divided in 5 section in CTF, but I will just cover a small fraction of it. Without any further introduction, let's get started, shall we?

Web Sections

The first section is web, there are a lot of challenges in this section but two of them are the most memorable in my mind.

#1 — Jiheon Challenge

This challenge introduce me into SQL injection. Given a login form and we have to crack the credentials.

The hint given was the source code of that login form written in Go. Here is the snippet of the most important part of the code — where the key to catch the flag persist.

func (u *UserService) Login(username string, password string) error {
  query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s' AND password = '%s'", username, password)
  log.Println(query)

  var users []entity.User
  err := database.MySQL.Select(&users, query)
  if err != nil {
    return err
  }

  // Harusnya hanya ada 1 username unik, kan?
  if len(users) != 1 {
    return errors.New("Terjadi kesalahan")
  }
  return nil
}

For this challenge, I was very lucky because when I googling for sql injection username password the result was on point

Basically those injections will bring you list of all users in database. I try all of them but none works and start asking myself what I'm missing here. After some digging, the key actually lies in this lines:

  if len(users) != 1 {
    return errors.New("Terjadi kesalahan")
  }

If the query result more than one row it will return an error. So, what I need to do is just put a limit, something like 1' or 1=1 limit 1 -- - for username and blah or anything for password because it will be commented.

First flag of the day!

#2 — Jiwon Challenge

The next one is quite similar but little bit tricky. Given the same login form but the hint is different.

func (u *UserService) Login(username string, password string) (entity.User, error) {
  query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s' AND password = '%s'", username, password)
  log.Println(query)

  var user entity.User
  err := database.MySQL.Get(&user, query)
  if err != nil {
    return entity.User{}, err
  }
  return user, nil
}
if user, err := service.User.Login(username, password); err == nil {
  if user.Username == "admin" {
    result = os.Getenv("JIWON_FLAG")
  } else {
    result = "You are not admin"
  }
} else {
  result = "Invalid username or password"
}

At a glance, it seems quite easy, just query user with username equal admin right. This injection should do fine admin' limit 1 -- - but turns out there is no user with username equal admin.

The trick here is to ignore all the query and perform a new one with case function to change whatever username into admin. So, we come up with this very long injection ' and true union select id, case when true then 'admin' else 'admin' end as username, password from users limit 1 -- -. When we run it, as expected we got another flag.

From this flag, we know pattern for the flag which is flag{SOME_SECRET_MESSAGE}

Crypto Sections

One of the crypto challenges is called XORIN. The hint given are two files, one containing the encoded flag and the other containing python script.

Here is what inside those file, flag.txt.enc and encrypt.py respectively:

0:71-.9$g8	387==========	;78"7444+
plain = open('flag.txt').read()
cipher = open('flag.txt.enc', 'w')
key = raw_input("Masukkan key (1 karakter): ")
assert(len(key) == 1)

def enkripsi(plain, key):
  enc = ''
  for i in plain:
    enc += chr(ord(i) ^ ord(key))
  return enc

enc = enkripsi(plain, key)
cipher.write(enc)
cipher.close()
print "Encrypted!"

Okay, I know just a little about python but just enough to run that script. After running it, I know that it will read flag.txt, take a kay character from input, and use that character to somehow encrypt text inside flag.txt and finally store it in flag.txt.enc.

After quite long googling, I know whats going on here:

it simply loop over the caracter from flag.txt, turn it inco ASCII code then perform bitwise XOR operator with ASCII code of key character.

It's called XOR Chiper and turns out that to decrypt it, just input the encoded text and it will do the job. The problem here is how do I know the key that used to encode. Because I'm so lazy to think, the only solution that comes up to my mind is brute force, run the script with every possible keys

abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

So, I modified the file a little bit to look like this:

plain = open('flag.txt').read()

def enkripsi(plain, key):
  enc = ''
  for i in plain:
    enc += chr(ord(i) ^ ord(key))
  return enc

# brute force with all possible keys
for k in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ':
  enc = enkripsi(plain, k)
  # if contains 'flag' print it
  if 'flag' in enc: print enc

When I run the script, …. viola! There is the flag!

That's it folks! Thanks for reading.