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! I’ll Bet You’ve NEVER Seen This Character Before! | JEFF DUNHAM/PlaybackRate: 0.000000> item:<CFBF7AE2-60B2-4F85-A028-81AB59A3B7DD/LOST TAPE! I’ll Bet You’ve 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}