# Hacked Account #### attempted by Petiole - e-seng ## figuring things out - first opening the file, noticed a few things: - provided "IBAN" value: `GB29NWBK60161331926825` - found within the input, idk if this is important - line 174 seems to check if a value is `GB29NWBK60161331926825`, so probably important - looks to be written in a language i'm unfamilar with... Rust possibly? - the use of `panic(...)` and `fmt.Printf(...)` indicate Rust to me - the use of `func` deters that... - `code_snippets` can still be copied to `processFile.rs` to get some nice nice syntax highlighting - could still be pseudocode of some sort. either way, doesn't matter. - was later told this was "Go", helps to look up documentation - ~~no main function provided, but `-B` and `-o` are cli arguments, file is named `enscript` probably~~ - ~~`ps2pdf` is another linux command~~ - `enscript -B -o | ps2pdf - legitimate.pdf` generates a pdf - this pdf, containing an invoice ID, IBAN value and Service, is signed and then validated by the website ### reminders for myself: - declarations take the form of one of the following - `var <variable name> <type>` - `<variable name> <type>`, for function arguments particularly - `func <function name>(arguments) (return types) {...}` - existing functions are: - `func processFile(w http.ResponseWriter, r *http.Request){...}` - `func checkBankAccount(path string, issigned bool) (string, error){...}` ## GOAL - create a signed pdf file with IBAN "broken" ## code tracing ### processFile - this is called when an unsigned pdf is uploaded most likely - calls `checkBankAccount(...)` twice - once at the very beginning after creating a tmp file - if signing file: - at this point, the user is looking to sign the pdf - states the file is unsigned at this point - tmp file may be prone to path redirection, using ```rs= filename := filepath.Base(handler.Filename) + ".content.check"; ``` - regardless, it is read and removed immediately - once the file has been verified: 1. create a md5 hash of the pdf 2. convert text hash into 16 bytes of data numerical data (?) 3. creates a RSA signature based off these 16 bytes - [notes on rsa-signatures](https://cryptobook.nakov.com/digital-signatures/rsa-signatures) - after receiving the signature, *append* the signature to the original message - then, write tmp file, send it back to user, delete tmp file - if validating file: - expects file to be at least 256 characters long - signature is 256 characters, which are the last 256 characters of the file - extracts the original message (bytes between the range [0, length - 256)) - hashes it, gets the numeric 16 byte value from it - verifies the RSA signature by passing the 16 byte hash and signature - if verfied, check if "broken" (only) is IBAN value and return flag if so. ### checkBankAccount - opens the provided file path (to a tmp file w/ the pdf data it seems) - is told if the file is signed - always moves to the second row, probably to read the IBAN value? - if the file is not signed: - check the second row to see if the file matches a regex pattern: - `^GB\d{2}\s?([0-9a-zA-Z]{4}\s?){4}[0-9a-zA-Z]{2}$` - returns that the pdf is "verified" with no errors if the account number is whitelisted ## potential attack vectors - validate pdf, alter it, then resubmit - "money transfers occur on re-uploaded, validated forms" - since the pdf is downloaded, try tracing the "validated" pdf for the IBAN in question and replace it - this would get thwarted by any hashes, but we'll see i suppose - it's a pdf... so it's iky to parse through - after exploration of the code, it does use a hash, so this is not viable. - try to reverse private key...? - know that the last 256 bytes compose the rsa signature, so pull that out...? - also, we have the original message file, and its hash - (see signature extraction below) - current issues with approach: - unknown values: d, e, and n - known values: m, s - will be difficult to reverse probably - mess around with values...? - trick web application to sign the incorrect file? - web app directly checks for the correct IBAN value - regex may be vulnerable, does not look like it. - (probably not going to work) try copying old RSA key, append it to target pdf file - yeah, no lol - mess around with carrage returns? - since the program checks for "GB29NWBK60151331926825" and "broken" on the second line, use a carrage return to dump both onto the second line by writing specific bytes - (see carrage return below) - `checkBankAccount` checks the banking number across multiple pages it seems - validates if "GB29NWBK60161331926825" is on any third line of a page - what happens if we create multiple pages...? - see multipage below ### signature extraction - create a python script that can strip the signature of the signed pdf - expected: last 256 bytes of the file are the hash, the bytes up to the signature should contain the same md5 hash as the unsigned pdf file ```python= #!/usr/bin/python3 import hashlib import sys def main(): # usage: ./extract_signature.py <signed pdf> if len(sys.argv) < 2: print(f"usage: {sys.argv[0]} <signed pdf file>") return with open(sys.argv[1], "rb") as signed_pdf_file: read_dump = signed_pdf_file.read() signature_bytes = read_dump[-256:-1] message_bytes = read_dump[0:-256] msg_hash = hashlib.md5(message_bytes) print(f"message hash:\n{msg_hash.hexdigest()}") print(f"signature hex:\n{signature_bytes.hex()}") return if __name__ == "__main__": main() ``` - a sanity check: ``` $ md5sum legit.pdf 0f8e8a384de9531bda886191a00c705a legit.pdf $ ./extract_signature.py legit.pdf.signed message hash: 0f8e8a384de9531bda886191a00c705a signature hex: 96114effc6d8886b19644f5e7f1806210d9912759f06eaa7e2a5e036616f9546c6fd69bb07eb0f6dddc77334f48e53e615150c73b417404ea1dd06f56e36d2492a23e8927f46e5acac0dde13892fe1eb1ccef10894091293271bb8389842a0df809698d4c4c2932385517a3b30e5c2bf54e87d60493af4d589014d446bf412d2457dd6e902daf6992bcfcb3e10524a3be5594b1586f357741b2b89be73560e883475851e283afdc5a999bff500dd42e5b1fd3bf436d2f79e9a896c75466364c374aa5ab4666c5ae919b2e8951f6e386baa1a4676bdf65f3bbb1e566b7fb5cddf19371d76e4dfb40ed3fe9018c933d80eeb7b7d87455cc6d956f7a6747636c3 ``` ### carrage return ```python= with open("./dump.txt", "wb") as f: f.write(b"a\na\nGB29NWBK60151331926825\rbroken\n") ``` - this creates a text file with the correct bank account, but then goes back to write "broken" on the same line. - looking at the hex-dump: ``` $ xxd dump.txt 00000000: 610a 610a 4742 3239 4e57 424b 3630 3135 a.a.GB29NWBK6015 00000010: 3133 3331 3932 3638 3235 0d62 726f 6b65 1331926825.broke 00000020: 6e n ``` - looks good i think...? - converting this into a pdf shows ``` a a GB29NWBK60151331926825 broken ``` - but "broken" seems to overlap "GB29NWBK60151331926825"!! - time to verify... here's hoping - ... seems to have failed unfortunately... - maybe reverse carrage return...? ```python with open("./dump.txt", "wb") as f: f.write(b"a\na\nbroken\rGB29NWBK60151331926825\n") ``` - this also fails to sign ### multipage - immediate question: how many lines does a page have? - to find out: throw a lot of lines into a pdf to create 2 pages, see where one page ends ``` $ python3 -c "[print(i) for i in range(100)]" | enscript -B -o - | ps2pdf - dumb_test2.pdf [ 2 pages * 1 copy ] left in - ``` - based off this, it is seen that a page ends at line 64 - now: let's make sure that this is still signable: - for sanity: create pdf with 2 pages, correct bank account on first, "broken" on second ``` $ python3 -c "print('a\na\nGB29NWBK60161331926825\n'+'\n'*62+'a\na\nbroken\n')" | enscript -B -o - | ps2pdf - dumb_test2.pdf [ 2 pages * 1 copy ] left in - ``` - here: correct bank account is on third line, first page, "broken" is on third line second page - this signs! - now: check if it still signs if the correct bank account is on the second page rather than the first. ``` $ python3 -c "print('a\na\nbroken\n'+'\n'*62+'a\na\nGB29NWBK60161331926825\n')" | enscript -B -o - | ps2pdf - dumb_test2.pdf [ 2 pages * 1 copy ] left in - ``` - here: "broken" is on third line, first page, correct account id is on third line, second page. - **this SIGNED** - yay - now, when i validate this, it should read "broken" first - ***THIS WORKS!!!*** - flag got flag: `d4451b8f-5ac2-4c0e-8f68-df0c989bf8fc`