From e1805d1b56ca768cd50482a44807b5c19148d109 Mon Sep 17 00:00:00 2001 From: Bernhard Neumann Date: Wed, 1 Jan 2020 23:08:01 +0100 Subject: [PATCH] Add writeup for hxp 36C3 CTF --- writeups/b3rn1/hxp_36C3_CTF2019.md | 155 +++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 writeups/b3rn1/hxp_36C3_CTF2019.md diff --git a/writeups/b3rn1/hxp_36C3_CTF2019.md b/writeups/b3rn1/hxp_36C3_CTF2019.md new file mode 100644 index 0000000..dcc8554 --- /dev/null +++ b/writeups/b3rn1/hxp_36C3_CTF2019.md @@ -0,0 +1,155 @@ +# Retrospective +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. + +# Challenges +## Bacon [solved] +### Analysis +We are provided with a python script that uses two functions Speck and H: + +```python +#!/usr/bin/env python3 +import os, signal + +def Speck(key, blk): + assert tuple(map(len, (key, blk))) == (9,6) + S = lambda j,v: (v << j | (v&0xffffff) >> 24-j) + ws = blk[:3],blk[3:], key[:3],key[3:6],key[6:] + x,y, l1,l0,k0 = (int.from_bytes(w,'big') for w in ws) + l, k = [l0,l1], [k0] + for i in range(21): + l.append(S(16,l[i]) + k[i] ^ i) + k.append(S( 3,k[i]) ^ l[-1]) + for i in range(22): + x = S(16,x) + y ^ k[i] + y = S( 3,y) ^ x + x,y = (z&0xffffff for z in (x,y)) + return b''.join(z.to_bytes(3,'big') for z in (x,y)) + +# did I implement this correctly? +assert Speck(*map(bytes.fromhex, ('1211100a0908020100', '20796c6c6172'))) == b'\xc0\x49\xa5\x38\x5a\xdc' + +def H(m): + s = bytes(6) + v = m + bytes(-len(m) % 9) + len(m).to_bytes(9,'big') + for i in range(0,len(v),9): + s = Speck(v[i:i+9], s) + return s + +signal.alarm(100) + +h = os.urandom(6) +print(h.hex()) + +s = bytes.fromhex(input()) +if H(s) == h: + print('The flag is: {}'.format(open('flag.txt').read().strip())) +else: + print('Nope.') +``` + +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. + +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. + +```python +value = sys.argv[1] +for i in range(0xffffff): + h = hex(i)[2:] + if (len(h) % 2): + h = "0" + h + a = bytes.fromhex(h) + if (H(a) == value): + print(hex(i)) +``` +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). +Things I realized/learned: + * to undo xor you only need to xor again + * in Speck + * the key is always 9 bytes and the blk always 6 bytes long + * S(l, v) = 2^l * v + * x is the first 3 bytes of blk + * y the last 3 bytes of blk + * l1 is the first 3 bytes of the key + * l0 is the second 3 bytes of the key + * k is the last 3 bytes of the key + * b''.join(z.to_bytes(3,'big') for z in (x,y)) calculates the byte representation of x and y and concatenates them + * in the first loop the values that get appended to l and k get higher and higher because the use of S + * therefore also in the second loop x and y grow and because of the use of k[i] + * x,y = (z&0xffffff for z in (x,y)) is used to take the rightest 3 bytes of x and y + * x and y are at the first call to Speck in the beginning 0 since blk is then 6 bytes of 0 bytes + * in H + * 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 + * in the loop Speck is called multiple times to change s each time + +### Solution +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} + +## Writeup Bin [unsolved] +### Analysis +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. + +I first started to analyse the files provided. +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. + +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. + +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. + +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". + +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) + +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. + +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. + +The Dockerfile sets up the application and makes some replacements that were already mentioned. + +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. + +So I split up the check: + +```php +if( (strtoupper($_SERVER['REQUEST_METHOD']) == 'POST' || count($_POST))) { + echo $_SESSION['c']; + echo '
'; + echo $_POST['c']; + echo '
'; + print_r($_POST); + if (! hash_equals($_SESSION['c'], $_POST['c'])) { + die('csrf failed'); + } +} +``` +The result was: +``` +585439f7f17cdc94

Array +( +) +csrf failed +``` +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 `` tag that was intended for the content of the write up. `

qwer

`. 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. + +Since every id was generated randomly and missing ids were never output on error pages XSS was not an option. + +Unfortunately, I did not know how to continue and stopped there. I spent around 3 hours on the challenge (without writeup). + +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. +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 +```html + +``` + 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 +```html + +``` +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. + +### Solution +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 ``. 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:/}` + +## Others +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. + +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 "