# Unis Love Code [nullcon HackIM CTF : web] > All students at universities love to code. What could possibly go wrong? There is a site where the source code is displayed. ![](https://i.imgur.com/L022v13.png) ## Recon Let's go read it. > Title "Unis Love Code" Unis Love Code... Love Unis Code... Love Unicode... OK... Me too... > `username=<username> via POST` Input is provided via POST. > here you go: ... The python code is given in the compressed form. Let's start by formatting the python code to make it easier to read. ## Code Reading Since line breaks, etc., have been removed, restoring line breaks and spaces is the first step. ```python= #!/usr/bin/python import http.server import socketserver import re import os import cgi import string from io import StringIO from flag import FLAG import urllib.parse class UnisLoveCode(http.server.SimpleHTTPRequestHandler): server_version="UnisLoveCode" username='ADMIN' check_funcs=["strip","lower"] def do_GET(self): self.send_response(-1337) self.send_header('Content-Length',-1337) self.send_header('Content-Type','text/plain') s=StringIO() s.write("""Wait, what is HTML?! I should have listened more carefully to the professor...\nAnyhow, passwordless is the new hot topic, so just provide me the correct username=<username> via POST and I might show you my homework.\nOh, in case you need the source, here you go:\n""") s.write("---------------------------------\n") s.write(re.sub(r"\s+",'',open(os.path.realpath(__file__),"r").read())) s.write("\n") s.write("---------------------------------\n") s.write("\nChallenge created with <3 by @gehaxelt\n") self.end_headers() self.wfile.write(s.getvalue().encode()) def _check_access(self,u): for cf in UnisLoveCode.check_funcs: if getattr(str,cf)(UnisLoveCode.username)==u: return False for c in u: if c in string.ascii_uppercase: return False return UnisLoveCode.username.upper()==u.upper() def do_POST(self): self.send_response(-1337) self.send_header('Content-Length',-1337) self.send_header('Content-Type','text/plain') s=StringIO() try: length=min(int(self.headers['content-length']),64) field_data=self.rfile.read(length) fields=urllib.parse.parse_qs(field_data.decode("utf8")) if not 'username' in fields: s.write("I asked youfor a user name!\n") raise Exception("Wrong param.") username=fields['username'][0] if not self._check_access(username): s.write("No.\n") raise Exception("No.") s.write(f"OK, here is your flag:{FLAG}\n") except Exception as e: s.write("Tryharder;-)!\n") print(e) self.end_headers() self.wfile.write(s.getvalue().encode()) if __name__=="__main__": PORT=8000 HANDLER=UnisLoveCode with socketserver.ThreadingTCPServer(("0.0.0.0",PORT),HANDLER) as httpd: print("servingatport",PORT) httpd.serve_forever() ``` I remember the words of my teacher. <font size="5">🐱‍👤</font> "Do a static analysis first because running it will give you a preconceived idea." Before running the application, let's read the code. This is a web application with HTTPServer. The UnisLoveCode class is the primary part, so we will focus on this class. Two endpoints are provided. - GET / - Show the first page. No input is accepted. So no particular attack can be made. - POST / - Output the flag according to given input. - Let's follow the code of the do_POST method. ## do_POST ```python= def do_POST(self): self.send_response(-1337) self.send_header('Content-Length',-1337) self.send_header('Content-Type','text/plain') s=StringIO() try: ``` At first, preparation of headers, etc. The part of the try is the important part. ```python=7 length=min(int(self.headers['content-length']),64) field_data=self.rfile.read(length) ``` The maximum size of the body is 64. ```python=9 fields=urllib.parse.parse_qs(field_data.decode("utf8")) if not 'username' in fields: s.write("I asked youfor a user name!\n") raise Exception("Wrong param.") username=fields['username'][0] ``` Input is read as **UTF-8**. It is a common conversion, but since the problem's name is associated with Unicode, we can imagine that we can use special characters for an attack. The input is processed as a query string, and if there is no key named username, it is assumed to be an error. Therefore, `username=<username>` seems to be the input format, as we saw on the first site. ```python=14 if not self._check_access(username): s.write("No.\n") raise Exception("No.") s.write(f"OK, here is your flag:{FLAG}\n") ``` Finally, the input value is validated with the _check_access method. If the return value is True, we can get the flag. Overall, do_POST is just processing the input and passing it to the validation function, so understanding the _check_access method is the critical part. ```python=18 except Exception as e: s.write("Tryharder;-)!\n") print(e) self.end_headers() self.wfile.write(s.getvalue().encode()) ``` The rest is exception handling and output processing. Nothing special. ## _check_access ```python= def _check_access(self,u): for cf in UnisLoveCode.check_funcs: if getattr(str,cf)(UnisLoveCode.username)==u: return False for c in u: if c in string.ascii_uppercase: return False return UnisLoveCode.username.upper()==u.upper() ``` Constants defined at the beginning of the class are used. For clarity, we can rewrite it by embedding constants and replacing the results of string processing in advance. ```python= def _check_access(self,u): if 'ADMIN' == u: return False if 'admin' == u: return False for c in u: if c in string.ascii_uppercase: return False return 'ADMIN'==u.upper() ``` It is much easier to understand. Therefore, the following conditions must be met if this method returns True. - u != 'ADMIN' - u != 'admin' - u must not contain capital letters - 'ADMIN' == u.upper() It contains contradictory conditions, and there appears to be no input u that satisfies all of them. **Is it possible to give input with a return value of True by entering special characters?** A normal comparison would distinguish Unicode. By looking for where the input changes, you can find one place where it is converted. The `u.upper()` part of the last condition. ## str.upper() I searched for "python unicode upper ctf" and got a hit of writeups about Japanese CTF. [SECCON2021_online_CTF/misc/case-insensitive at main · SECCON/SECCON2021_online_CTF](https://github.com/SECCON/SECCON2021_online_CTF/tree/main/misc/case-insensitive) The assumed solution to this problem uses the fact that `'ffi'.upper() == 'FFI'` is True to solve the problem. This is an attractive solution. If any of the ADMIN characters could be created this way, the problem would be solved. Search Unicode to find characters that could be used. Search Unicode... Search... <font size="5">🏌</font> <font size="30">ı</font> I found a mysterious letter. I have no idea where or who is using it. But it is the magic character that satisfies `'ı'.upper() == 'I'`. Therefore, if we pass `admın` with the letter i changed to this letter, we should get the flag. ## Capture The Flag ![](https://i.imgur.com/wDtbYK6.png) After converting `admın` using CyberChef, you can get the flag by sending a request with `username=adm%C4%B1n` as input. ![](https://i.imgur.com/k3as3uZ.png) ENO{PYTH0Ns_Un1C0d3_C0nv3rs1on_0r_C0nfUs1on} # Bonus ## Are There Similar Letters? To find this magic letter, I wrote the following python code to find the letter. ```python= from Crypto.Util.number import * import tqdm for b in tqdm.tqdm(range(256 * 256 * 256)): try: raw = long_to_bytes(b).decode('utf-8') if any([raw[i] in "abcdefghijklmnopqrstuvwxyzADCBEFGHIJLKMNOPQRSTUVWYXZ" for i in range(len(raw))]): continue if len(raw) != 1: continue enc = raw.upper() if all([enc[i] in "ADCBEFGHIJLKMNOPQRSTUVWYXZ" for i in range(len(enc))]): print(f"found! {raw} -> {enc}") except: pass ``` found! ß -> SS found! ı -> I found! ſ -> S found! ff -> FF found! fi -> FI found! fl -> FL found! ffi -> FFI found! ffl -> FFL found! ſt -> ST found! st -> ST In Unicode, the above characters are converted to uppercase in Ascii when converted with the upper method. That seems to indicate that only ı can be used for attacks. ## How about str.lower()? Since I made the source code, I also tested str.lower(). ```python= from Crypto.Util.number import * import tqdm for b in tqdm.tqdm(range(256 * 256 * 256)): try: raw = long_to_bytes(b).decode('utf-8') if any([raw[i] in "abcdefghijklmnopqrstuvwxyzADCBEFGHIJLKMNOPQRSTUVWYXZ" for i in range(len(raw))]): continue if len(raw) != 1: continue enc = raw.lower() if all([enc[i] in "abcdefghijklmnopqrstuvwxyz" for i in range(len(enc))]): print(f"found! {raw} -> {enc}") except: pass ``` found! K -> k There is one letter. Although the lower method may be used to check for case-insensitive duplicate usernames, etc., we must be careful about this Unicode character. ## About me (hamayanhamayan) I usually writes CTF writeups in Japanese. I have also written writeups of other problems of nullcon HackIM CTF. (but sorry, in Japanese) If you are interested, please have a look. [nullcon HackIM CTF 2022 Writeups - はまやんはまやんはまやん](https://blog.hamayanhamayan.com/entry/2022/08/14/160421) Thanks for the nice CTF. [Twitter](https://twitter.com/hamayanhamayan)