CSAW CTF Writeups 2018

Just like previous years, OSIRIS Lab from New York University (NYU) managed to put awesome challenges for CSAW Quals 2018. I will keep adding/updating tasks time to time. So, consider this write-up(s) under construction.

 

Twitch Plays Test Flag (MISC) – 1 Point

A mandatory attendance check for every Capture the Flag event.

flag{typ3_y3s_to_c0nt1nue}

 

LDAB (WEB) – 50 Points

We were given a website http://web.chal.csaw.io:8080/

The first page, has one search bar and list of Users with their associated group information.

http://web.chal.csaw.io:8080/index.php?search=*

Lists the same content on the web-page. Interesting as the search query understands * . Numerous possibilities can be explored like Elastic Search etc. They have been covered previously in CTF ( For instance: SECCON 2017)

But, in this case we can try assuming LDAP due to the structure of columns name. A basic injection payload goes as

http://web.chal.csaw.io:8080/index.php/index.php?search=*)(uid=*))(|(uid=*

That provides us the flag at bottom

flag{ld4p_inj3ction_i5_a_th1ng}

 

Algebra (MISC) – 100 Points

Challenge description provides us

nc misc.chal.csaw.io 9002

Basically, it looks like simple algebra where we have to find missing term ‘X’. sympy module makes thing lot faster and easier when performing calculations as such.

from pwn import *
from sympy import *
import time

'''
It's just a hacky solution as I didn't wanted to spend a lot of time on it
'''

#context.log_level='DEBUG'

r = remote('misc.chal.csaw.io', 9002)
r.recvline()
r.recvline()
r.recvline()
r.recvline()
r.recvline()
r.recvline()
r.recvline()

for i in xrange(0,400):
    print "[+] We are at "+str(i)+"\n"
    feq = r.recvline()
    a = sympify(feq.split('=')[0])
    b = sympify(feq.split('=')[1])
    try:
        solution = map(float, solve(Eq(a,b)))
    except TypeError:
        solution=0.0
    x = r.recvuntil("al?: ")
    r.sendline(str(solution[0]))
    print r.recvline()

print r.recvall()

Run the solver, sit back & relax. The complexity of the problems get difficult but it should be no problem for our solver.

flag{y0u_s0_60od_aT_tH3_qU1cK_M4tH5}

WhyOS (MISC) – 300 Points

Most painful challenge which tests your grepping skills. A debian and log file was provided. I used binwalk to extract the content of the debian package. We have to look for some entry-point, so I navigated to Library/PreferenceBundles/whyOSsettings.bundle and opened the file Root.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>items</key>
    <array>
        <dict>
            <key>cell</key>
            <string>PSGroupCell</string>
            <key>footerText</key>
            <string>Put the flag{} content here</string>
        </dict>
        <dict>
            <key>AutocapitalizationType</key>
            <string>None</string>
            <key>AutocorrectionType</key>
            <string>No</string>
            <key>cell</key>
            <string>PSEditTextCell</string>
            <key>defaults</key>
            <string>com.yourcompany.whyos</string>
            <key>key</key>
            <string>flag</string>
            <key>label</key>
            <string>flag content</string>
        </dict>
        <dict>
            <key>action</key>
            <string>setflag</string>
            <key>cell</key>
            <string>PSButtonCell</string>
            <key>label</key>
            <string>Set flag</string>
        </dict>
    </array>
    <key>title</key>
    <string>whyOS Settings</string>
</dict>
</plist>

No flag hardcoded in the plist file. Seems we have to reverse the dylib and macho files. setflag is what we need to reverse in order to see what is placed as the contents of flag. We reverse the binary & convert into a pseudo-code

// CSAWRootListController - (void)setflag
void __cdecl -[CSAWRootListController setflag](struct CSAWRootListController *self, SEL a2)
{
  void *v2; // r0@1
  __CFString *v3; // [sp+4h] [bp-34h]@2
  void *v4; // [sp+20h] [bp-18h]@1

  v2 = objc_msgSend(&OBJC_CLASS___NSMutableDictionary, "alloc");
  v4 = objc_msgSend(v2, "initWithContentsOfFile:", CFSTR("/var/mobile/Library/Preferences/com.yourcompany.whyos.plist"));
  if ( objc_msgSend(v4, "objectForKey:", CFSTR("flag")) )
    v3 = (__CFString *)objc_msgSend(v4, "objectForKey:", CFSTR("flag"));
  else
    v3 = &stru_8044;
  NSLog((int)CFSTR("%@"), (int)v3);
}

Looks, like flag will be logged under CFString. I started grepping, bummer it failed. A struggle followed thereafter, I made the list of most used services in the log. Tried to grep from the lowest used service one by one. Then I thought, the flag isn’t in flag format, but it may have alphanum_alphanum. Tried that

cat console.log | grep -v 'legacy' | grep -v 'set' | grep -v 'block' | grep -v 'state' | grep -v 'types' | grep -v 'extra' | grep  '[0-9a-z@]\{3\}_[0-9a-z@]\{6\}_[0-9a-z@]\{3\}'

Tried word brute

awk '$4~/^Bet/' console.log 
item:<CFBF7AE2-60B2-4F85-A028-81AB59A3B7DD/LOST TAPE! Ill Bet Youve NEVER Seen This Character Before! | JEFF DUNHAM/PlaybackRate: 0.000000>
 item:<CFBF7AE2-60B2-4F85-A028-81AB59A3B7DD/LOST TAPE! Ill Bet Youve NEVER Seen This Character Before! | JEFF DUNHAM/PlaybackRate: 0.000000>

cat console.log | grep 'whyOS'
default 19:10:53.647765 -0400   amfid   We got called! /Library/PreferenceBundles/whyOSsettings.bundle/whyOSsettings with {
default 19:10:53.659202 -0400   amfid   We got called! AFTER ACTUAL /Library/PreferenceBundles/whyOSsettings.bundle/whyOSsettings with {

Apparently, it fails. Very time consuming process considering I have to work on numerous challenges.

I am basically guessing 6 as mid-part. I guessed a lot of range, no success. Then, Hint came that flag is a hex string and that too does not contain 0x

Immediately, I constructed this

grep -vE 'begin_match|timeStamp|kernel|backboardd|locationd|zip|cpio|Task|securityd|timed|assertiond|CommCentr|Resume|symptomsd|cloudd|sharingd|ADVERTISING|mediaserverd|DETERMINE|powerd|identityservicesd|BK|bulletin|rapportd|accessoryd' console.log | grep -E '[0-9a-fA-F]{10,}'

Basically, I filtered out useless services which made log. It failed as well. That’s not over, I tried to also convert all hex strings to plain-text using xxd -r -p but well all was blobs of data pushing me far away from flag. I tried grepping ‘5f’ (hex of _) considering flag is preserved in hex and assuming it must have _ somewhere. It failed.

So either there are two options:

1.) Flag is not printable in ASCII, must be other encoding done before Hex conversion

2.) It is byte-by-byte flag

2nd option seems less likely. First is , yeah well. I tried, but failed. So, it’s hex string, what if it is a hash like SHA-1 or MD5. I saw other hint from the organizers on IRC by @pa_ssion (I believe) saying it is more reverse than forensics. Looking closely into app bundle, it seems the log must be under Preferences. I grepped (again) with varying length of hash either 32 or 40 with -E switch, but this time to solve. Just 1 and half hour before end time.

cat console.log | grep 'Preferences' | grep -E '[0-9a-fA-F]{32,}'

stdout:

default 19:12:18.884704 -0400   Preferences ca3412b55940568c5b10a616fa7b855e

Flag is ca3412b55940568c5b10a616fa7b855e 

 

Flatcrypt (Crypto) – 100 Points

We were given out handout code which uses AES CTR Stream cipher to encrypt data. A peculiarity of AES-CTR is that it encrypts every byte separately unlike encrypting the whole block like AES-ECB or CBC Mode. In AES-CTR mode no padding is required, hence the length of ciphertext is always equal to the length of input. There is a RFC which explains CTR in great detail.

The hand-out code looks as follow:

def encrypt(data, ctr):
    return AES.new(ENCRYPT_KEY, AES.MODE_CTR, counter=ctr).encrypt(zlib.compress(data))

while True:
    f = input("Encrypting service\n")
    if len(f) < 20:
        continue
    enc = encrypt(
      bytes(
        (PROBLEM_KEY + f).encode('utf-8')
      ),
      Counter.new(64, prefix=os.urandom(8))
    )
    print("%s%s" %(enc, chr(len(enc))))

The encryption key is not provided to us and the counter is instantiated with 8 random bytes. Hence, we cannot break the AES-CTR implementation itself but we have to look for other vulnerabilities. The part which stands out in the code is that the data is being compressed by zlib library and then it is feed into the encrypt routine. Zlib works on back referencing, so if the text which is to be compressed has multiple repeats then zlib will return a lower value then say if the text has no multiple repeats.

Hence, we can compare the length of the cipher-text and look at what character it returns a lowest length which indicates compression is successful and we will keep on prepending other characters to known_flag and brute again until we receive the flag. But, there is a twist. The server requires us to enter at least 20 bytes before it performs the encryption process. We are provided in the distributed file that the character set is lowercase letters and underscores. So, if our first 20 bytes contains none of those, we can utilize our logic properly. We can use

ABCDEFGHIJKLMNOPQRST

Note that, the last character is the length of the ciphertext.

from pwn import *
import string

#context.log_level='DEBUG'

charset_flag = string.ascii_lowercase + '_'
padding = 'ABCDEFGHIJKLMNOPQRST'

r = remote('127.0.0.1', 8040)
known_flag = ''

while True:
    bestchar = None
    lowestlen = 9999
    worstlen = -1

    for c in charset_flag:
        send_this =  padding + c + known_flag + padding
        r.recvuntil('ervice')
        r.sendline(send_this)
        r.recvline().strip()
        x = r.readline().strip()
        reslen = ord(x[-1])
        if reslen < lowestlen:
            lowestlen = reslen
            bestchar = c
        worstlen = max(reslen, worstlen)

    if worstlen == lowestlen:
        break

    known_flag = bestchar + known_flag
    print known_flag

Just like that, we will retrieve the flag, although we have to guess the first character. Now, to come to the main point — This type of exploit targeting compression is known as CRIME.

Flag: flag{crime_doesnt_have_logo}

Lowe (Crypto) – 200 Points

Typical RSA crypto problem.

import gmpy2
import gmpy
import codecs
from Crypto.PublicKey import RSA
import base64
from itertools import cycle, izip


target_file = "kStoynmN5LSniue0nDxli9csSrBgexZ/YOo5e+MUkfJKwvht8hHsYyMGVYzMlOp9sAFBrPCbm4UA4n7oMr2zlg=="
target_dec = base64.b64decode(target_file)

enc_key = 219135993109607778001201845084150602227376141082195657844762662508084481089986056048532133767792600470123444605795683268047281347474499409679660783370627652563144258284648474807381611694138314352087429271128942786445607462311052442015618558352506502586843660097471748372196048269942588597722623967402749279662913442303983480435926749879440167236197705613657631022920490906911790425443191781646744542562221829319509319404420795146532861393334310385517838840775182

with codecs.open('pubkey.pem') as fr:
    pub = fr.read()
    pub = RSA.importKey(pub)

print "[+] n = "+str(pub.n)
print "[+] e = "+str(pub.e)

gs = gmpy.mpz(enc_key)
gm = gmpy.mpz(pub.n)
g3 = gmpy.mpz(pub.e)
meh = gs+gm

# we go on like c+n , c+2*n etc until we hit _ = True
 
root, _ = meh.root(g3)
#print _
kek = hex(int(root))[2:-1].decode('hex')

print len(hex(int(root))[2:-1].decode('hex'))

assert len(target_dec) == len(hex(int(root))[2:-1].decode('hex'))
flaglol= ''.join(chr(ord(c)^ord(k)) for c,k in izip(target_dec, cycle(kek)))
print flaglol

 

Flag: flag{saltstacksaltcomit5dd304276ba5745ec21fc1e6686a0b28da29e6fc}

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s