]> git.somenet.org - pub/jan/ctf-seminar.git/blob - writeups/b3rn1/hxp_36C3_CTF2019.md
Add writeup for hxp 36C3 CTF
[pub/jan/ctf-seminar.git] / writeups / b3rn1 / hxp_36C3_CTF2019.md
1 # Retrospective
2 I found the challenges rather hard and the easier ones were already solved when I had a look at the challenges. However, I felt that these challenges (at least those I had a look at) where less about guessing as it was often the case on the Advent Bonanza, which is good in my opinion. 
3
4 # Challenges
5 ## Bacon [solved]
6 ### Analysis
7 We are provided with a python script that uses two functions Speck and H:
8
9 ```python
10 #!/usr/bin/env python3
11 import os, signal
12
13 def Speck(key, blk):
14     assert tuple(map(len, (key, blk))) == (9,6)
15     S = lambda j,v: (v << j | (v&0xffffff) >> 24-j)
16     ws = blk[:3],blk[3:], key[:3],key[3:6],key[6:]
17     x,y, l1,l0,k0 = (int.from_bytes(w,'big') for w in ws)
18     l, k = [l0,l1], [k0]
19     for i in range(21):
20         l.append(S(16,l[i]) + k[i] ^ i)
21         k.append(S( 3,k[i])        ^ l[-1])
22     for i in range(22):
23         x = S(16,x) + y ^ k[i]
24         y = S( 3,y)     ^ x
25     x,y = (z&0xffffff for z in (x,y))
26     return b''.join(z.to_bytes(3,'big') for z in (x,y))
27
28 # did I implement this correctly?
29 assert Speck(*map(bytes.fromhex, ('1211100a0908020100', '20796c6c6172'))) == b'\xc0\x49\xa5\x38\x5a\xdc'
30
31 def H(m):
32     s = bytes(6)
33     v = m + bytes(-len(m) % 9) + len(m).to_bytes(9,'big')
34     for i in range(0,len(v),9):
35         s = Speck(v[i:i+9], s)
36     return s
37
38 signal.alarm(100)
39
40 h = os.urandom(6)
41 print(h.hex())
42
43 s = bytes.fromhex(input())
44 if H(s) == h:
45     print('The flag is: {}'.format(open('flag.txt').read().strip()))
46 else:
47     print('Nope.')
48 ```
49
50 First a timeout of 100 seconds is set, then a 6 byte random string is generated and its hex value is printed to standard out. Afterwards the user needs to input 6 bytes in hex (meaning 12 hexadecimal digits). Then the script uses the function H with the provided bytes as an argument and checks if it equal to the previously random number. So we need to undo what H does to our value prior to inputting it in vuln.py or the service. 
51
52 First I thought about bruteforcing the value because it doesn't seem that long but I tried it and it already takes way too long to try only the values up to 0xffffff.
53
54 ```python
55 value = sys.argv[1]
56 for i in range(0xffffff):
57     h = hex(i)[2:]
58     if (len(h) % 2):
59         h = "0" + h
60     a = bytes.fromhex(h)
61     if (H(a) == value):
62         print(hex(i))
63 ```
64 Therefore I tried to understand the script and what each line does. So I tried each line separately in a python shell and debugged the code. But after struggling around to find out how to undo each line (e.g. how to undo the shortening to 3 bytes in the line `x,y = (z&0xffffff for z in (x,y))`) I gave up and decided to look at other challenges instead. I spent around 4 h with the challenge (without wirteup).
65 Things I realized/learned: 
66  * to undo xor you only need to xor again
67  * in Speck 
68     * the key is always 9 bytes and the blk always 6 bytes long
69     * S(l, v) = 2^l * v
70     * x is the first 3 bytes of blk
71     * y the last 3 bytes of blk
72     * l1 is the first 3 bytes of the key
73     * l0 is the second 3 bytes of the key
74     * k is the last 3 bytes of the key
75     * b''.join(z.to_bytes(3,'big') for z in (x,y)) calculates the byte representation of x and y and concatenates them
76     * in the first loop the values that get appended to l and k get higher and higher because the use of S
77     * therefore also in the second loop x and y grow and because of the use of k[i]
78     * x,y = (z&0xffffff for z in (x,y)) is used to take the rightest 3 bytes of x and y
79     * x and y are at the first call to Speck in the beginning 0 since blk is then 6 bytes of 0 bytes
80  * in H
81     * len(v) is always divisible by 9 since the second part pads up to make it divisible by 9 with 0 bytes and then there are 9 bytes added with the length of m at the end
82     * in the loop Speck is called multiple times to change s each time
83
84 ### Solution
85 georg found out that it is the speck cipher, with key size 72 and block size 48 and later was able to revert the functions and gather the flag: hxp{7h3Y_f1n4Lly_m4d3_a_t0Y_c1ph3R_f0r_CTF_Ta5kz}
86
87 ## Writeup Bin [unsolved]
88 ### Analysis
89 On opening the webpage we are provided with a minimalistic web page where we are welcomed with an seemingly random number as user name. The form below allows to publish a write up with at least 140 characters. After submitting the form we are provided with the newly published writeup where we can hit the like button to like our own post or show it to the admin, who will like the post on his own. 
90
91 I first started to analyse the files provided.
92 In index.php we can see that general.php, header.php and home.php get included (they used include_once for general.php since the database connection and the nonce are managed there, so there are no complications with multiple database connections). In general.php we can see that the cookies are http only and cookie_samesite is also set to 'Strict' and no errors should be shown. Then there is a function id() that returns a random hex value with 16 digits (or 8 bytes). The nonce is generated once and used then everywhere. There are various security http headers set to the strictest. Then there is a function redirect that redirects to a certain location. When no \$\_SESSION['id'] has been set it is set and also \$\_SESSION['c'] is set both to new random numbers using the function id(). Afterwards a database connection to the database 'writeupbin' with user 'writeupbin' and password '\_\_DB_PASSWORD\_\_' (gets replaced in Dockerfile). Then there is a check if the request method is "POST" or the \$\_POST array is non empty and \$\_SESSION['c'] is non equals to \$\_POST['c'], if thats the case the server dies with message "csrf failed". So if one sends the correct 'c' value he is allowed to send POST requests. Afterwards the writeups are fetched from the database and stored in $writeups. 
93
94 header.php gives a possible hint (or red hering) by having the meta description "WriteupBin - Leak flags with style!". So maybe we should leak the flag through something with style. Although the styles are disabled in general.php. Below that "hint" there are the writeup ids listed to open the separate writeups. It also loads jQuery and parsley. 
95
96 In home.php the already described form for publishing writeups is generated. It also sends the right \$\_SESSION['c'] to be allowed to POST. The frontend checks on the textarea are done with parsley. It is required, has a minimum length of 140 and the pattern allows everything but < and >. The script tag to enable the parsley check has the nonce set so it gets executed. The comment "prevent hacking" sounds quite fishy. 
97
98 If we look at add.php we see that there are no checks if the content holds to the checks that are made in the frontend, so we are able to insert html tags. If the \$\_SESSION['id'] is "admin" the application dies with message "bad admin". 
99
100 admin.php opens a socket to port 1024 where supposedly the file admin.py is running that navigates to the url http://admin:\_\_ADMIN_TOKEN\_\_@127.0.0.1/login_admin.php?id=writeup_id thus logging in the admin and then navigating to the show page of the given writeup id and admin.py then searches for the like button and clicks it. (ADMIN_TOKEN gets replaced in Dockerfile)
101
102 like.php just marks the writeup as liked by the current user in the db and navigates back to the show page. show.php in html loads the things necessary to show the details (content and likes) of a writeup and then includes the header and the other show.php.
103
104 The file db.sql sets up the database and inserts a writeup containing the flag (content of /root/flag.txt) owned by the admin with the writeup_id being the content of /root/writeup-id.txt. So the goal has to be, to either login as admin, get the right writeup id to his first (and only) writeup or access one of the files directly in the root file directly, though I think the first two options are more probable.
105
106 The Dockerfile sets up the application and makes some replacements that were already mentioned. 
107
108 I tried to connect to the server via postman with a get request to the home page to get the cookie set, afterwards I tried a post request with the c value that was provided but always got a "csfr failed". Therefore I set up the system locally, setup lamp for Ubuntu and configured mysql server and used the db.sql to set up the db, to check why the check fails when I use postman. 
109
110 So I split up the check: 
111
112 ```php
113 if( (strtoupper($_SERVER['REQUEST_METHOD']) == 'POST' || count($_POST))) {
114     echo $_SESSION['c'];
115     echo '<br>';
116     echo $_POST['c'];
117     echo '<br>';
118     print_r($_POST);
119     if (! hash_equals($_SESSION['c'], $_POST['c'])) {
120         die('csrf failed');
121     }
122 }
123 ```
124 The result was:
125 ```
126 585439f7f17cdc94<br><br>Array
127 (
128 )
129 csrf failed
130 ```
131 I realized that I input the form params into the wrong POSTMAN Tab, always sending them as query params and not as form-data. After face palming myself several times I tried it with the form-data and was now able to inject html tags into the `<code>` tag that was intended for the content of the write up. `<code><h1>qwer</h1></code>`. However, adding a style to the h1 tag, it was only shown in the postman response; when looking with the browser to the generated writeup the style tag was emptied. 
132
133 Since every id was generated randomly and missing ids were never output on error pages XSS was not an option.
134
135 Unfortunately, I did not know how to continue and stopped there. I spent around 3 hours on the challenge (without writeup).
136
137 While doing the writeup I realized we could have possibly loaded a script file from an external source when the integrity sha value would be set correctly since this is also done with jQuery and parsley. I didn't think about it at first because I thought we would need the right nonce here.
138 Unfortunately, the server was already down one day after the challenge, so I could only try this on my local setup. I wrote an file alert.js that simply contains `alert("Hi")` and calculated the SHA384 value for it and inserted 
139 ```html
140 <script src="/alert.js" integrity="sha384-8061b422d2cf77814b240b282e1c6b69df855f72054a36f23b9d4bd0a1f4cd66" crossorigin="anonymous"></script>
141 ```
142  in to the database. When navigating to the new writeup in the browser nothing happens. So i tried it while all security headers being disabled; still did not work. Then I tried removing the crossorigin=anonymous, then directly alerting in the script tags; still not working. Then I tried removing the integrity-Property (because of course that was not the right value for the direct alert). Now it did work. On this website https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity that the integrity not only uses the sha384 value but the base64 value of it. So i tried the command there: shasum -b -a 384 alert.js | awk '{ print $1 }' | xxd -r -p | base64 and now 
143 ```html
144 <script src="/alert.js" integrity="sha384-2xLpScUwSE9GYj1cbvDJSH9gOhVITydcmwd6+eb9WcrmbwQb2RdYe9RY88pTTU2c" crossorigin="anonymous"></script>
145 ``` 
146 also worked. However, if we uncomment the security headers, it does not work so we obviously indeed need the right nonce to execute code. I invested 2 more hours to find out, that was also a dead end street. 
147
148 ### Solution
149 In the writeup https://ctftime.org/writeup/17891 the authors used parsley to get to the admins writeup. They first constructed a form that gets validated right after page load with an input field that fails validation when left empty and set the `data-parsley-error-container` to `a:contains('Writeup - {payload}'):eq(0)` where payload is the guessed part of the writeup id and `data-parsley-error-message` to `<input id=like type=button>`. If the guessed part of the writeup id is right, the button with id `like` will be placed as a child of that link. Since the admin script only clicks at the first button with the id `like` and the link to his writeup is above the real like button, if the writeup id part was correct, the script will now not trigger a like on the writeup. Thus the writeup id can be constructed step by step. The flag found in the writeup with the id `1800a15d252d318a` was `hxp{Petersilie_lol__I_couldn_t_come_up_with_funny_flag_sorry:/}`
150
151 ## Others
152 I also had a look at nacht, where the parameters of crypto_onetimeauth_poly1305_tweet are provided in the wrong order (key and then message) and we have to calculate the encrypted message with a new key but do not know the message but 32 keys and 32 encrypted messages. However, I didn't really try anything because I was already struggling to understand the challenge bacon.
153
154 I also had a look at includer, where when navigating to the website we only were provided with something like: "Hello your sandbox: 929ea6081435a6ca06987b0dfed624d3" where one could include files via a POST request when the file does not contain "<?". However since I had no clue where to acutally start and was already struggling with the Writeup Bin I had no deeper look into the challenge. 
155