Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29c4ec29ca
|
@@ -10,53 +10,25 @@ defaults:
|
||||
working-directory: ./
|
||||
|
||||
jobs:
|
||||
build:
|
||||
cd:
|
||||
timeout-minutes: 6
|
||||
name: Build
|
||||
name: CD
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup hugo
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: "latest"
|
||||
extended: true
|
||||
|
||||
- name: Build
|
||||
run: hugo --minify
|
||||
|
||||
- name: Upload deployment
|
||||
uses: actions/upload-artifact@v4
|
||||
run: hugo -D -F
|
||||
- name: Create GitHub deployment
|
||||
uses: chrnorm/deployment-action@releases/v1
|
||||
id: deployment
|
||||
with:
|
||||
name: public-html
|
||||
path: ./public
|
||||
|
||||
deploy:
|
||||
timeout-minutes: 6
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
environment:
|
||||
name: production
|
||||
url: https://maik.dev/
|
||||
steps:
|
||||
- name: Download deployment
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: public-html
|
||||
path: ./public-html
|
||||
|
||||
- name: Setup SSH config for remote server
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||
echo "${{ secrets.SSH_HOST }} ssh-ed25519 ${{ secrets.SSH_HOST_KEY }}" > ~/.ssh/known_hosts
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
eval "$(ssh-agent -s)"
|
||||
ssh-add ~/.ssh/id_ed25519
|
||||
|
||||
- name: Transfer deployment to remote server
|
||||
run: |
|
||||
rsync -avh --delete-after ./public-html/ ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:~/maik.dev
|
||||
token: "${{ github.token }}"
|
||||
target_url: https://maik.dev/
|
||||
environment: production
|
||||
initial_status: "success"
|
||||
|
||||
@@ -22,13 +22,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup hugo
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
uses: peaceiris/actions-hugo@v2
|
||||
with:
|
||||
hugo-version: "latest"
|
||||
extended: true
|
||||
|
||||
- name: Build
|
||||
run: hugo -D -F
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "{{ replace .Name "-" " " | title }}"
|
||||
date = {{ .Date }}
|
||||
description = ""
|
||||
images = []
|
||||
tags = [
|
||||
"untagged",
|
||||
]
|
||||
categories = [
|
||||
"uncategorized",
|
||||
]
|
||||
+++
|
||||
@@ -0,0 +1,13 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "{{ replace .Name "-" " " | title }}"
|
||||
date = {{ .Date }}
|
||||
description = ""
|
||||
images = []
|
||||
tags = [
|
||||
"untagged",
|
||||
]
|
||||
categories = [
|
||||
"uncategorized",
|
||||
]
|
||||
+++
|
||||
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 5.2 MiB |
|
Before Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 630 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 610 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 969 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 366 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 363 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,194 @@
|
||||
baseURL = "https://maik.dev/"
|
||||
# baseURL = "http://localhost:1313/"
|
||||
title = "Maik de Kruif"
|
||||
languageCode = "en-us"
|
||||
theme = "maik-blog"
|
||||
|
||||
paginate = 12
|
||||
rssLimit = 10
|
||||
|
||||
copyright = "© Maik de Kruif (maik_dev). All rights reserved."
|
||||
|
||||
PygmentsCodeFences = true
|
||||
pygmentsUseClasses = true
|
||||
PygmentsStyle = "monokai"
|
||||
|
||||
archetypeDir = "archetypes"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
layoutDir = "layouts"
|
||||
publishDir = "public"
|
||||
|
||||
buildDrafts = false
|
||||
buildFuture = false
|
||||
buildExpired = false
|
||||
canonifyURLs = true
|
||||
|
||||
enableRobotsTXT = true
|
||||
enableGitInfo = true
|
||||
enableEmoji = true
|
||||
enableMissingTranslationPlaceholders = false
|
||||
disableRSS = false
|
||||
disableSitemap = false
|
||||
disable404 = false
|
||||
disableHugoGeneratorInject = false
|
||||
|
||||
googleAnalytics = "UA-136337666-1"
|
||||
|
||||
[permalinks]
|
||||
posts = "/posts/:title/"
|
||||
writeups = "/:sections/:title/"
|
||||
|
||||
[author]
|
||||
name = "Maik de Kruif"
|
||||
email = "maik@maik.dev"
|
||||
twitter = "@maik_dev"
|
||||
image = "/img/home/portrait.png"
|
||||
|
||||
[social]
|
||||
twitter = "@maik_dev"
|
||||
|
||||
[outputs]
|
||||
home = ["HTML", "RSS"]
|
||||
page = ["HTML", "RSS"]
|
||||
|
||||
[params]
|
||||
dateform = "Jan 2, 2006"
|
||||
dateformShort = "Jan 2"
|
||||
dateformNum = "2006-01-02"
|
||||
dateformNumTime = "2006-01-02 15:04 -0700"
|
||||
|
||||
# Metadata mostly used in document's head
|
||||
description = "Hey there, my name is Maik de Kruif, I create websites, bots and other kinds of software. At the moment, I'm still studying computer science at Avans University of Applied Sciences. I am also a big fan of open-source software, which is why you'll find many of my projects on Github. And last but not least, I love playing around with cyber-security related stuff."
|
||||
keywords = "Devloper, Software Engineer, Software Developer, Maik, Computer Science, Portfolio, Blog, Maik de Kruif"
|
||||
images = [""]
|
||||
|
||||
genre = "Blog"
|
||||
|
||||
showReadingTimeInList = false
|
||||
|
||||
homeSubtitle = "Software Engineering, Linux and Cyber Security"
|
||||
|
||||
# Prefix of link to the git commit detail page. GitInfo must be enabled.
|
||||
gitUrl = "https://github.com/maikka39/Personal-Website/commit/"
|
||||
|
||||
# Set disableReadOtherPosts to true in order to hide the links to other posts.
|
||||
disableReadOtherPosts = false
|
||||
|
||||
# Sharing buttons
|
||||
# There are a lot of buttons preconfigured. If you want to change them,
|
||||
# generate the buttons here: https://sharingbuttons.io
|
||||
# and add them into your own `layouts/partials/sharing-buttons.html`
|
||||
# Preconfigured: facebook, twitter, tumblr, email, pinterest, linkedin, reddit, xing, whatsapp, hackernews, telegram
|
||||
enableSharingButtons = true
|
||||
sharingButtons = ["twitter", "email", "linkedin", "reddit", "whatsapp", "telegram"]
|
||||
|
||||
# Integrate Javascript files or stylesheets by adding the url to the external assets or by
|
||||
# linking local files with their path relative to the static folder, e.g. "css/styles.css"
|
||||
customCSS = []
|
||||
customJS = []
|
||||
|
||||
# Toggle this option need to rebuild SCSS, requires extended version of Hugo
|
||||
justifyContent = false # Set "text-align: justify" to .post-content.
|
||||
|
||||
# Default theme "light" or "dark"
|
||||
defaultTheme = "light"
|
||||
themeColor = "#252627"
|
||||
|
||||
# Colors for favicons
|
||||
[params.favicon.color]
|
||||
mask = "#252627"
|
||||
msapplication = "#252627"
|
||||
theme = "#252627"
|
||||
|
||||
# Uncomment this if you want a portrait on your start page
|
||||
[params.portrait]
|
||||
path = "/img/home/portrait.png"
|
||||
alt = "Portrait"
|
||||
size = "175px"
|
||||
|
||||
# Wallets
|
||||
[[params.wallets]]
|
||||
name = "XMR"
|
||||
address = "83cfsXGHPaW81hKJtAPYZEgbfFqANMTXXRwghsBpjf5oTDRV3BnCC4jFno8tLTnF4C6gJzGjPEmpvjntXaiGBB7kVitkDKi"
|
||||
|
||||
# Social icons
|
||||
[[params.social]]
|
||||
name = "email"
|
||||
url = "mailto:maik@maik.dev"
|
||||
|
||||
[[params.social]]
|
||||
name = "github"
|
||||
url = "https://github.com/maikka39"
|
||||
|
||||
[[params.social]]
|
||||
name = "linkedin"
|
||||
url = "https://www.linkedin.com/in/maikdekruif/"
|
||||
|
||||
[[params.social]]
|
||||
name = "twitter"
|
||||
url = "https://twitter.com/maik_dev"
|
||||
|
||||
[[params.social]]
|
||||
name = "discord"
|
||||
url = "https://discord.gg/wXCMdzCDcH"
|
||||
|
||||
[[params.social]]
|
||||
name = "instagram"
|
||||
url = "https://www.instagram.com/maik.dev/"
|
||||
|
||||
[[params.social]]
|
||||
name = "stackoverflow"
|
||||
url = "https://stackoverflow.com/users/9146982/maik-de-kruif"
|
||||
|
||||
# [[params.social]]
|
||||
# name = "rss"
|
||||
# url = "posts/index.xml"
|
||||
|
||||
# [[params.social]]
|
||||
# name = "hackerrank"
|
||||
# url = "https://www.hackerrank.com/maik_dev"
|
||||
|
||||
# [[params.social]]
|
||||
# name = "medium"
|
||||
# url = "https://medium.com/@maikka39"
|
||||
|
||||
[[params.social]]
|
||||
name = "steam"
|
||||
url = "https://steamcommunity.com/id/maikka39"
|
||||
|
||||
[[params.social]]
|
||||
name = "reddit"
|
||||
url = "https://www.reddit.com/user/maikka39"
|
||||
|
||||
# [[params.social]]
|
||||
# name = "lbry"
|
||||
# url = "https://lbry.tv/@Maik:c"
|
||||
|
||||
[[params.social]]
|
||||
name = "youtube"
|
||||
url = "https://www.youtube.com/channel/UCepNO6s7q0Kcyn8cc3rZNFA"
|
||||
|
||||
[languages]
|
||||
[languages.en]
|
||||
subtitle = "maik.dev"
|
||||
weight = 1
|
||||
copyright = "© Maik de Kruif (maik_dev). All rights reserved."
|
||||
|
||||
[menu]
|
||||
[[menu.main]]
|
||||
identifier = "about"
|
||||
name = "About"
|
||||
url = "about/"
|
||||
[[menu.main]]
|
||||
identifier = "portfolio"
|
||||
name = "Portfolio"
|
||||
url = "portfolio/"
|
||||
[[menu.main]]
|
||||
identifier = "posts"
|
||||
name = "Posts"
|
||||
url = "posts/"
|
||||
[[menu.main]]
|
||||
identifier = "writeups"
|
||||
name = "Writeups"
|
||||
url = "writeups/"
|
||||
@@ -1,210 +0,0 @@
|
||||
baseURL = "http://localhost:1313/"
|
||||
title = "Maik de Kruif"
|
||||
languageCode = "en-us"
|
||||
theme = "maik-blog"
|
||||
|
||||
copyright = "© Maik de Kruif (maik_dev). All rights reserved."
|
||||
|
||||
PygmentsCodeFences = true
|
||||
pygmentsUseClasses = true
|
||||
PygmentsStyle = "monokai"
|
||||
|
||||
archetypeDir = "archetypes"
|
||||
contentDir = "content"
|
||||
dataDir = "data"
|
||||
layoutDir = "layouts"
|
||||
publishDir = "public"
|
||||
|
||||
buildDrafts = false
|
||||
buildFuture = false
|
||||
buildExpired = false
|
||||
canonifyURLs = true
|
||||
|
||||
enableRobotsTXT = true
|
||||
enableGitInfo = true
|
||||
enableEmoji = true
|
||||
|
||||
ignoreLogs = ["warning-goldmark-raw-html"]
|
||||
|
||||
[permalinks]
|
||||
[permalinks.page]
|
||||
posts = "/posts/:title/"
|
||||
writeups = "/:sections/:title/"
|
||||
# writeups = "/writeups/:title/"
|
||||
portfolio = "/portfolio/:title/"
|
||||
|
||||
[outputs]
|
||||
home = ["HTML", "RSS"]
|
||||
page = ["HTML", "RSS"]
|
||||
|
||||
[pagination]
|
||||
pagerSize = 12
|
||||
|
||||
[params]
|
||||
dateform = "Jan 2, 2006"
|
||||
dateformShort = "Jan 2"
|
||||
dateformNum = "2006-01-02"
|
||||
dateformNumTime = "2006-01-02 15:04 -0700"
|
||||
dateformMonth = "Jan, 2006"
|
||||
|
||||
# Metadata mostly used in document's head
|
||||
description = "Hey there, my name is Maik de Kruif, I create websites, bots and other kinds of software. At the moment, I'm still studying computer science at Avans University of Applied Sciences. I am also a big fan of open-source software, which is why you'll find many of my projects on Github. And last but not least, I love playing around with cyber-security related stuff."
|
||||
keywords = "Devloper, Software Engineer, Software Developer, Maik, Computer Science, Portfolio, Blog, Maik de Kruif"
|
||||
images = [""]
|
||||
|
||||
genre = "Blog"
|
||||
|
||||
rssLimit = 15
|
||||
|
||||
showReadingTimeInList = false
|
||||
|
||||
homeSubtitle = "Software Engineering, Linux and Cyber Security"
|
||||
|
||||
# Prefix of link to the git commit detail page. GitInfo must be enabled.
|
||||
gitUrl = "https://github.com/maikka39/Personal-Website/commit/"
|
||||
|
||||
# Add domain for GoatCounter to enable
|
||||
goatCounter = "webstats.maik.dev"
|
||||
|
||||
# Show pageviews on posts, writeups, etc.
|
||||
showPageViews = true
|
||||
|
||||
# Set disableReadOtherPosts to true in order to hide the links to other posts.
|
||||
disableReadOtherPosts = false
|
||||
|
||||
# Sharing buttons
|
||||
# There are a lot of buttons preconfigured. If you want to change them,
|
||||
# generate the buttons here: https://sharingbuttons.io
|
||||
# and add them into your own `layouts/partials/sharing-buttons.html`
|
||||
# Preconfigured: facebook, twitter, tumblr, email, pinterest, linkedin, reddit, xing, whatsapp, hackernews, telegram
|
||||
enableSharingButtons = true
|
||||
sharingButtons = [
|
||||
"twitter",
|
||||
"email",
|
||||
"linkedin",
|
||||
"reddit",
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
]
|
||||
|
||||
# Integrate Javascript files or stylesheets by adding the url to the external assets or by
|
||||
# linking local files with their path relative to the static folder, e.g. "css/styles.css"
|
||||
customCSS = []
|
||||
customJS = []
|
||||
|
||||
# Toggle this option need to rebuild SCSS, requires extended version of Hugo
|
||||
justifyContent = false # Set "text-align: justify" to .post-content.
|
||||
|
||||
# Default theme "light" or "dark"
|
||||
defaultTheme = "light"
|
||||
themeColor = "#252627"
|
||||
|
||||
# Colors for favicons
|
||||
[params.favicon.color]
|
||||
mask = "#252627"
|
||||
msapplication = "#252627"
|
||||
theme = "#252627"
|
||||
|
||||
[params.logo]
|
||||
logoText = "$ cd /home/"
|
||||
logoHomeLink = "/"
|
||||
|
||||
# Uncomment this if you want a portrait on your start page
|
||||
[params.portrait]
|
||||
path = "/img/home/portrait.png"
|
||||
alt = "Portrait"
|
||||
size = "175px"
|
||||
|
||||
[params.author]
|
||||
name = "Maik de Kruif"
|
||||
email = "maik@maik.dev"
|
||||
twitter = "@maik_dev"
|
||||
image = "/img/home/portrait.png"
|
||||
|
||||
[params.general]
|
||||
twitter = "@maik_dev"
|
||||
|
||||
# Wallets
|
||||
[[params.wallets]]
|
||||
name = "XMR"
|
||||
address = "83cfsXGHPaW81hKJtAPYZEgbfFqANMTXXRwghsBpjf5oTDRV3BnCC4jFno8tLTnF4C6gJzGjPEmpvjntXaiGBB7kVitkDKi"
|
||||
|
||||
# Social icons
|
||||
[[params.social]]
|
||||
name = "email"
|
||||
url = "mailto:maik@maik.dev"
|
||||
|
||||
[[params.social]]
|
||||
name = "github"
|
||||
url = "https://github.com/maikka39"
|
||||
|
||||
[[params.social]]
|
||||
name = "linkedin"
|
||||
url = "https://www.linkedin.com/in/maikdekruif/"
|
||||
|
||||
[[params.social]]
|
||||
name = "twitter"
|
||||
url = "https://twitter.com/maik_dev"
|
||||
|
||||
[[params.social]]
|
||||
name = "discord"
|
||||
url = "https://discord.gg/wXCMdzCDcH"
|
||||
|
||||
[[params.social]]
|
||||
name = "instagram"
|
||||
url = "https://www.instagram.com/maik.dev/"
|
||||
|
||||
[[params.social]]
|
||||
name = "stackoverflow"
|
||||
url = "https://stackoverflow.com/users/9146982/maik-de-kruif"
|
||||
|
||||
# [[params.social]]
|
||||
# name = "rss"
|
||||
# url = "posts/index.xml"
|
||||
|
||||
# [[params.social]]
|
||||
# name = "hackerrank"
|
||||
# url = "https://www.hackerrank.com/maik_dev"
|
||||
|
||||
# [[params.social]]
|
||||
# name = "medium"
|
||||
# url = "https://medium.com/@maikka39"
|
||||
|
||||
[[params.social]]
|
||||
name = "steam"
|
||||
url = "https://steamcommunity.com/id/maikka39"
|
||||
|
||||
[[params.social]]
|
||||
name = "reddit"
|
||||
url = "https://www.reddit.com/user/maikka39"
|
||||
|
||||
# [[params.social]]
|
||||
# name = "lbry"
|
||||
# url = "https://lbry.tv/@Maik:c"
|
||||
|
||||
[[params.social]]
|
||||
name = "youtube"
|
||||
url = "https://www.youtube.com/channel/UCepNO6s7q0Kcyn8cc3rZNFA"
|
||||
|
||||
[languages]
|
||||
[languages.en]
|
||||
weight = 1
|
||||
copyright = "© Maik de Kruif (maik_dev). All rights reserved."
|
||||
|
||||
[menu]
|
||||
[[menu.main]]
|
||||
identifier = "about"
|
||||
name = "About"
|
||||
url = "about/"
|
||||
[[menu.main]]
|
||||
identifier = "portfolio"
|
||||
name = "Portfolio"
|
||||
url = "portfolio/"
|
||||
[[menu.main]]
|
||||
identifier = "posts"
|
||||
name = "Posts"
|
||||
url = "posts/"
|
||||
[[menu.main]]
|
||||
identifier = "writeups"
|
||||
name = "Writeups"
|
||||
url = "writeups/"
|
||||
@@ -1,5 +0,0 @@
|
||||
baseURL = "https://maik.dev/"
|
||||
|
||||
[services]
|
||||
[services.googleAnalytics]
|
||||
id = "UA-136337666-1"
|
||||
@@ -1,23 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Advent of Code"
|
||||
start_date = 2020-10-15
|
||||
end_date = 2099-05-04
|
||||
company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
cover = "img/portfolio/adventofcode/screenshot.png"
|
||||
description = "Advent of Code is an annual programming event that presents daily coding puzzles. I actively participate in the events, solving the challenges and improving my coding skills."
|
||||
source_code = "https://github.com/maikka39/Advent-of-Code"
|
||||
skills = [
|
||||
"Problem-solving",
|
||||
"Programming efficiency",
|
||||
"Time management",
|
||||
]
|
||||
+++
|
||||
|
||||
|
||||
[Advent of Code](https://adventofcode.com/) is a popular annual programming event that presents a series of coding puzzles, one for each day of December until Christmas. Each puzzle involves solving a problem using a programming language of your choice, with (generally) increasing difficulty as the event progresses.
|
||||
|
||||
I actively participate in Advent of Code, relishing the daily challenges and the opportunity to sharpen my coding skills. The puzzles covered a wide range of topics, from string manipulation and data structures to algorithms and game theory.
|
||||
|
||||
By consistently solving the puzzles, I hone my problem-solving abilities, improve my understanding of different programming paradigms, and discover new and innovative approaches to coding challenges.
|
||||
@@ -1,26 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Google Foobar"
|
||||
start_date = 2020-10-15
|
||||
end_date = 2021-05-04
|
||||
company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
cover = "img/portfolio/google-foobar/screenshot.png"
|
||||
description = "Google Foobar is a mysterious coding challenge platform launched by Google. I successfully solved all the challenges presented by the platform, showcasing my coding abilities and problem-solving skills."
|
||||
source_code = "https://github.com/maikka39/Google-Foobar"
|
||||
skills = [
|
||||
"Problem-solving",
|
||||
"Algorithm design",
|
||||
"Time management",
|
||||
]
|
||||
+++
|
||||
|
||||
Google Foobar is a mysterious coding challenge platform launched by Google. It's designed to identify exceptional engineering talent through a series of progressively difficult coding problems. The platform's unique approach involves inviting users to solve challenges directly via the Google Search page, adding an element of intrigue and surprise.
|
||||
|
||||
I was fortunate enough to receive an invitation to Google Foobar. The initial challenge was a simple warm-up, but subsequent levels escalated in complexity, requiring me to delve into various algorithms and data structures.
|
||||
|
||||
Throughout my journey, I faced a diverse range of problems, from graph traversal and dynamic programming to string manipulation and number theory. Each challenge presented a unique opportunity to test my problem-solving skills and explore new coding techniques.
|
||||
|
||||
With dedication and perseverance, I managed to successfully solve all the challenges presented by Google Foobar. It was a rewarding experience that allowed me to showcase my coding abilities and learn new maths concepts.
|
||||
|
||||
~~If you're a passionate coder looking for a stimulating challenge, Google Foobar is definitely worth exploring. It's a great way to test your skills, learn new concepts, and potentially open doors to exciting opportunities.~~ Unfortunately, Google shut down the platform, and the challenges are no longer available. You can still try them by taking a look at the challenge texts in my GitHub repo, by clicking the link at the top of this post.
|
||||
@@ -7,12 +7,11 @@ company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
cover = "img/portfolio/learn-to-program/screenshot.png"
|
||||
description = "This website was created for a school assignment. It was created with HTML and CSS only and no frameworks were used."
|
||||
source_code = "https://github.com/maikka39/Learn-to-Program"
|
||||
live_demo = "https://maikka39.github.io/Learn-to-Program/"
|
||||
skills = [
|
||||
"Pure HTML and CSS",
|
||||
"Web design",
|
||||
]
|
||||
+++
|
||||
|
||||
This website was created for a school assignment. It was created with HTML and CSS only and no frameworks were used. Not even JS was allowed (so I had to get creative with animations).
|
||||
This website was created for a school assignment. It was created with HTML and CSS only and no frameworks were used.
|
||||
|
||||
[Github](https://github.com/maikka39/Learn-to-Program)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "MailServerSetup"
|
||||
start_date = 2021-04-22
|
||||
end_date = 2021-06-24
|
||||
company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
description = "A script that automates the grueling process of installing and setting up an email server. It perfectly reproduces my successful steps to ensure the same setup time and time again, now with many improvements."
|
||||
source_code = "https://github.com/maikka39/MailServerSetup"
|
||||
skills = [
|
||||
"Mailing protocol",
|
||||
"SPF, DKIM, DMARC",
|
||||
"Bash scripting",
|
||||
]
|
||||
+++
|
||||
|
||||
A script that automates the grueling process of installing and setting up an email server. It perfectly reproduces my successful steps to ensure the same setup time and time again, now with many improvements.
|
||||
|
||||
MailServerSetup ensures a consistent and efficient deployment. It leverages Postfix, Dovecot, and SpamAssassin, providing an all-round email solution.
|
||||
|
||||
Simply specify your desired domain, and the script will handle the setup, including generating the necessary SPF, DMARC, and DKIM records and providing them to you.
|
||||
@@ -1,32 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "MinecraftClient"
|
||||
start_date = 2022-08-30
|
||||
end_date = 2022-12-29
|
||||
company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
cover = "img/portfolio/minecraft-client/cover.png"
|
||||
description = "A Minecraft client that was designed to overcome a series of challenges presented by a YouTuber named LiveOverflow. The client was built using Kotlin and Fabric, and it featured creative solutions to bypass the server's defenses."
|
||||
source_code = "https://github.com/maikka39/MinecraftClient"
|
||||
skills = [
|
||||
"Java code injection",
|
||||
"Reverse engineering",
|
||||
"Java & Gradle",
|
||||
]
|
||||
+++
|
||||
|
||||
A Minecraft client that was designed to overcome a series of challenges presented by a YouTuber named LiveOverflow. The client was built using Kotlin and Fabric, and it featured creative solutions to bypass the server's defenses.
|
||||
|
||||
LiveOverflow set up a secret Minecraft server with a twist: finding it was the first challenge. After some clever detective work, including exploiting a slip-up in one of LiveOverflow's videos, I gained access.
|
||||
|
||||
The real fun began with the server's challenges. One required the client to move the player in a way that mimicked a bot, not a human. This meant overcoming the limitations of how computers represent numbers to ensure the server wouldn't detect "human-like" rounding errors.
|
||||
|
||||
Another challenge involved reaching a protected chest. The initial solution involved some trickery, but the server was patched. To adapt, the client needed to exploit a loophole in how the server tracked player movement. By manipulating the game's internal state and sending carefully crafted packets, the client achieved imperceptible movements, bypassing the server's defenses.
|
||||
|
||||
Reverse engineering the server's code was crucial in finding these exploits. By understanding the server's logic and implementation, I could identify vulnerabilities and devise strategies to exploit them. This process required a deep dive into the Minecraft server's source code and a keen eye for potential weaknesses.
|
||||
|
||||
The code for this client was written in Kotlin and injected into the Minecraft client using Fabric. This approach allowed for flexibility and customization, perfectly suited for tackling the server's unique challenges.
|
||||
|
||||
This project was a fascinating exploration of Minecraft's inner workings and the creativity required to overcome seemingly impossible challenges. As new challenges emerge, this client was ready to evolve and conquer them!
|
||||
|
||||
See also [my post about the experience]({{% ref "posts/liveoverflow-minecraft-server.md" %}}) for a more in-depth explanation.
|
||||
@@ -6,7 +6,6 @@ end_date = 2021-04-24
|
||||
company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
description = "A Discord bot I wrote for my own server so that I could manage the server better and listen to music in voice channels."
|
||||
source_code = "https://github.com/maikka39/SuperBot"
|
||||
skills = [
|
||||
"Python",
|
||||
"Asynchronous programming",
|
||||
@@ -16,6 +15,8 @@ skills = [
|
||||
|
||||
An amazing Discord bot with lots of functionalities!
|
||||
|
||||
SuperBot is a Discord bot I wrote for my own server so that I could manage the server better and listen to music together in voice channels.
|
||||
A Discord bot I wrote for my own server so that I could manage the server better and listen to music in voice channels.
|
||||
|
||||
I developed SuperBot using Python. Through this project, I gained experience in Discord bot development, including API interactions, event handling, and user interface design. Additionally, I learned valuable lessons in error handling, optimization, and scalability.
|
||||
SuperBot is the solution to your crowded and chaotic Discord server!
|
||||
|
||||
[Github](https://github.com/maikka39/SuperBot)
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
author = "Maik de Kruif"
|
||||
title = "Toy-Neural-Network-Py"
|
||||
start_date = 2019-03-02
|
||||
end_date = 2021-06-07
|
||||
end_date = 2019-04-16
|
||||
company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
description = "Neural Network Python library for experimenting with Machine Learning."
|
||||
source_code = "https://github.com/maikka39/Toy-Neural-Network-Py"
|
||||
description = "Neural Network Python library for experimenting with Machine Learning"
|
||||
skills = [
|
||||
"Machine Learning",
|
||||
"Neural Networks",
|
||||
@@ -15,8 +14,6 @@ skills = [
|
||||
]
|
||||
+++
|
||||
|
||||
Neural Network Python library for experimenting with Machine Learning.
|
||||
Neural Network Python library for experimenting with Machine Learning
|
||||
|
||||
Based on the amazing work done by [Daniel Shiffman](https://github.com/shiffman) at [TheCodingTrain](https://thecodingtrain.com/), I built my own Neural Network library in Python. I used [Chapter 10: Neural Networks](https://natureofcode.com/neural-networks/) of his Nature of Code book as a reference.
|
||||
|
||||
Building this library was a very valuable learning experience. It’s helped me grasp the underlying principles of these models, from basic neuron structures to more complex algorithms like backpropagation.
|
||||
[Github](https://github.com/maikka39/Toy-Neural-Network-Py)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "UnEngine"
|
||||
start_date = 2022-11-09
|
||||
end_date = 2022-12-22
|
||||
company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
cover = "img/portfolio/unengine/screenshot.png"
|
||||
description = "Together with three other students, I built a game engine from the ground up using C++. To put our engine to the test, we also developed a fun racing game."
|
||||
skills = [
|
||||
"Low level programming",
|
||||
"Game engine architecture",
|
||||
"Event-driven programming",
|
||||
"Networking",
|
||||
"Teamwork",
|
||||
]
|
||||
|
||||
[source_code]
|
||||
engine = "https://github.com/Systems-Programming-in-C-Minor/engine"
|
||||
game = "https://github.com/Systems-Programming-in-C-Minor/game"
|
||||
server = "https://github.com/Systems-Programming-in-C-Minor/server"
|
||||
+++
|
||||
|
||||
Together with three other students, I built a game engine from the ground up using C++. To put our engine to the test, we also developed a fun racing game.
|
||||
|
||||
The racing game offered a variety of features: AI-controlled opponents that challenged players, controller support for a more immersive experience, local multiplayer for split-screen fun, and online multiplayer capabilities using a game server.
|
||||
|
||||
To add variety and replayability, we included multiple maps and game modes. Players could race on different tracks, each with its own unique challenges, obstacles and sound effects.
|
||||
|
||||
Our game engine was crafted using C++ and SDL2, and provided the foundation for our racing game. We adopted an event-driven architecture, allowing our engine to efficiently handle various inputs and events. To create a realistic experience, we implemented physics calculations for collisions, speed, and traction.
|
||||
|
||||
To enhance the audio experience, we developed a sound engine that allowed us to incorporate a variety of sound effects into our game. To optimize performance and memory usage, we implemented texture pooling, which efficiently manages and reuses textures. Finally, to enable multiplayer functionality, we integrated networking capabilities into our engine, allowing players to connect and compete with each other online.
|
||||
@@ -2,7 +2,7 @@
|
||||
author = "Maik de Kruif"
|
||||
title = "WatchTogether"
|
||||
start_date = 2018-08-17
|
||||
end_date = 2021-01-25
|
||||
end_date = 9999-01-01
|
||||
company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
cover = "img/portfolio/watch-together/screenshot.png"
|
||||
@@ -15,16 +15,10 @@ skills = [
|
||||
"Real time synchronization",
|
||||
"REST API",
|
||||
]
|
||||
|
||||
[source_code]
|
||||
frontend = "https://github.com/maikka39/WatchTogether"
|
||||
backend = "https://github.com/maikka39/WatchTogether-Server"
|
||||
+++
|
||||
|
||||
WatchTogether is an online service that lets you watch a video together with your friends without being in the same room!
|
||||
|
||||
WatchTogether is an online service that synchronizes video playback across users' web browsers, allowing friends to enjoy videos together virtually. This project challenged me to build both the front-end and back-end. I utilized React to create a user-friendly interface and Node.js with Socket.IO in the back-end to handle the real-time communication and video synchronization.
|
||||
[Github](https://github.com/maikka39/WatchTogether)
|
||||
|
||||
However, I quickly realized that synchronizing anything between clients on terrible network connections is a real challenge. This project pushed me to find solutions to ensure a smooth viewing experience, even on potato internet connections.
|
||||
|
||||
WatchTogether not only provided a fun solution for remote movie nights, but also deepened my knowledge of front-end frameworks like React and back-end technologies like Node.js and Socket.IO.
|
||||
[Github (backend)](https://github.com/maikka39/WatchTogether-Server)
|
||||
|
||||
@@ -7,7 +7,6 @@ company_name = "Personal"
|
||||
company_url = "https://maik.dev"
|
||||
cover = "img/portfolio/youtube-music-desktop-player/screenshot.png"
|
||||
description = "YouTube Music Desktop Player converts YouTube Music to a standalone desktop app. Because of this, you will never have to go through all your tabs to control your music."
|
||||
source_code = "https://github.com/maikka39/Youtube-Music-Desktop-Player"
|
||||
skills = [
|
||||
"Desktop Application Development",
|
||||
"Electron",
|
||||
@@ -18,8 +17,6 @@ skills = [
|
||||
|
||||
YouTube Music Desktop Player converts YouTube Music to a standalone desktop app. Because of this, you will never have to go through all your tabs to control your music.
|
||||
|
||||
Before Google released their official YouTube Music desktop app, I created my own using Electron. I used script injection to add custom features like buttons, media key control, Discord rich presence, and custom styling. Additionally, I implemented [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/) support for integration with desktop environments.
|
||||
_This software has no affiliation this Google, YouTube Music is a trademark of Google LCC._
|
||||
|
||||
This project was a valuable learning experience, as it allowed me to explore the capabilities of Electron and deepen my understanding of web technologies and desktop app development. It also gave me the opportunity to solve real-world problems and create a tool that enhanced the YouTube Music experience.
|
||||
|
||||
_This software has no affiliation this Google, YouTube Music is a trademark of Google LLC._
|
||||
[Github](https://github.com/maikka39/Youtube-Music-Desktop-Player)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "FIRST LEGO League"
|
||||
date = 2020-01-01T08:00:00+02:00
|
||||
description = "Together with the other members of Brainbuddies, I participated twice in the FIRST LEGO League competition. We reached the Benelux finals and received prizes for both our Robot and Core Values presentations."
|
||||
cover = "img/posts/first-lego-league/cover.jpg"
|
||||
tags = [
|
||||
"personal",
|
||||
]
|
||||
categories = [
|
||||
"personal",
|
||||
"misc",
|
||||
]
|
||||
+++
|
||||
|
||||
|
||||
Together with the other members of Brainbuddies, I participated twice in the FIRST LEGO League competition. We reached the Benelux finals and received prizes for both our Robot and Core Values presentations.
|
||||
|
||||
[FIRST LEGO League (FLL)](https://en.wikipedia.org/wiki/FIRST_Lego_League_Challenge) is an international competition for elementary and middle school students. Each year in August FIRST LEGO League introduces a scientific and real-world challenge for teams to focus and research on. The robotics part of the competition involves designing and programming LEGO Mindstorms robots to complete tasks. The students also work out a solution to a problem related to the theme (changes every year) and then meet for regional, national and international tournaments to compete, share their knowledge, compare ideas, and display their robots.
|
||||
|
||||
## The beginning
|
||||
|
||||
The story starts in 2014. Being 12 years old at the time, the amount of things you need to manage as a team can be overwhelming.
|
||||
|
||||
The FLL challenge consists of three parts; research a real-world problem, design a solution, and create a LEGO robot to complete missions related to the theme. At the finals, the teams also present their research project and robot design to judges, demonstrating their creativity, problem-solving skills, and teamwork.
|
||||
|
||||
Our team consisted of six people, one of which being [Raymon](https://raymon.dev/). Since we each had different strenths, we each were in charge of own subject. My primary focus was on developing the robot.
|
||||
|
||||
## Robot design and programming
|
||||
|
||||
When I started, I had hardly any knowledge of LEGO Mindstorms, let alone how to program one. I had played around a bit with [Scratch](https://en.wikipedia.org/wiki/Scratch_(programming_language)) at the time, which is what got me interested in programming, but never developed something tangible.
|
||||
|
||||
As a team we went to work on an old track from a previous edition first. This way, we could increase our knowledge of LEGO Mindstorms, and how its programming works. In the beginning it was quite a challenge. As it turns out, physics is a real thing, and telling the robot to go straight doesn't mean it actually will:smiling_face_with_tear:. In the end we were able to solve most of the challenges from this previous edition.
|
||||
|
||||
Since we were young and had plenty of other things to distract us, we took our time, and worked on the robot on and off for a few years. As we were perfecting the previous track, the new theme was announced: Hydrodynamics. The track that came with it was on a completely different level. It took us quite some time to adapt.
|
||||
|
||||
{{< figure class="small" src="/img/posts/first-lego-league/robot.jpg" title="Our robot around 2016" >}}
|
||||
|
||||
## Regionals
|
||||
|
||||
It's now November 24th 2017, regional finals time!
|
||||
|
||||
There were many teams present. Each and every one of them prepared themselves and now the competition can truly begin. All the teams were putting the finishing touches on their robot, and it was fun to see the creative solutions the other teams came up with.
|
||||
|
||||
### The Robot Competition
|
||||
|
||||
In the first round we got 80 points, which gave us first place for the first round. After that we remained high, but the Facimus team overtook us in the second round. In the third round we were unfortunately again overtaken, this time by the Robodirck team. We eventually came third in the robot section.
|
||||
|
||||
### The Project Presentation
|
||||
|
||||
When we walked into the jury room, and put the project on the table, you could already see that the jury was impressed. The presentation itself went well, and they thought it was a nice project.
|
||||
|
||||
### The Core Values presentation
|
||||
|
||||
The core values presentation went fantastically, we were able to tell a lot about our team, how we worked and how we experienced it. The jury gave us full points, they really liked our team and presentation.
|
||||
|
||||
### The Robot presentation
|
||||
|
||||
Finally we showed our robot. We explained how we came up with the robot, how we first we had an internal competition within the team to experiment, and later had brought back the best aspects of all robots in one new robot. This presentation was also well received.
|
||||
|
||||
### The award ceremony
|
||||
|
||||
Finally the most exciting moment came, the award ceremony. We were not called forward for the 'smaller' prizes for the separate components, which we thought was a bit strange having received very good feedback from the juries. But in the end we turned out to have won the main prize!!! This meant that we could go straight to the Benelux finals. A moment never to forget.
|
||||
|
||||
{{< figure class="small" src="/img/posts/first-lego-league/nationals-invitation.jpg" title="Invitation to the Benelux finals" >}}
|
||||
|
||||
_Note: Benelux is an intergovernmental union of Belgium, the Netherlands, and Luxembourg._
|
||||
|
||||
## The Benelux finals
|
||||
|
||||
The moment we won the regional finals, we could breathe again. The pressure from the regionals came of, but was partly replaced by the upcoming finals. Luckily there was quite a bit of time between the events, and we needed it. We decided to mainly work on improving our robot. We gained a lot of experience from the regional finals, and our most important lesson was that changing the robot for the different challenges took far too long. The runs on the table are time limited, and we were losing half our time changing the extensions. Extensions were the LEGO constructions that we were adding to the robot to perform certain tasks. We had to think of a different design and create it quickly.
|
||||
|
||||
Unfortunately, we couldn't win this one:cry:. The competition was simply too fierce.
|
||||
|
||||
## Looking back
|
||||
|
||||
Looking back at this all, I learned a lot. Working with a team on a complex project at that age, was an experience that helped me throughout my early career. To all kids between age 9 and 15 I would say to try and participate in an event like this if they have the chance. I certainly would do it again.
|
||||
@@ -1,425 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "LiveOverflow's Minecraft Server"
|
||||
date = 2022-09-14T02:46:01+02:00
|
||||
description = "Around five months ago LiveOverflow started a series called Minecraft:HACKED. He invited everyone to find the server and join, but that was easier said than done."
|
||||
cover = "img/posts/liveoverflow-minecraft-server/cover.png"
|
||||
tags = [
|
||||
"minecraft",
|
||||
"challenge",
|
||||
"hacking",
|
||||
]
|
||||
categories = [
|
||||
"games",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
Around five months ago [LiveOverflow](https://www.youtube.com/c/LiveOverflow) started a series called [Minecraft:HACKED](https://www.youtube.com/playlist?list=PLhixgUqwRTjwvBI-hmbZ2rpkAl4lutnJG). He invited everyone to find the server and join, but that was easier said than done.
|
||||
|
||||
## Concept
|
||||
|
||||
During one of his earlier videos, LiveOverflow challenged his viewers to find the Minecraft server he was playing on and come join it. At first, this was the only challenge, but as time went on and the server existed for longer, more challenge were added. At first, players had to bypass an algorithm that would kick players who moved like humans, but later on, we had to bypass a popular server plugin. All in all, I had/have a great time playing on the server and learned a lot about how Minecraft works, and how to write mods for it.
|
||||
|
||||
## Finding the server
|
||||
|
||||
In LiveOverflow's eighth video, [he told his viewers that the server has been opened to the public](https://youtu.be/QradKmQ27JY?t=1246). I immediately started looking for ways to find it. After researching for a while, I found multiple ways to find it, but I thought they would all take a fair bit of time (I'll get back to this later). So, instead of spending time actually learning stuff, I rewatched his videos to see if there would be a clue in there. Luckily it worked out. In [one video](https://youtu.be/Hmmr1oLt-V8?t=28) he blurred the IP inside the terminal, but forgot about the terminal title bar :smile:. I notified him of this, as it was obviously unintended, and the IP was changed shortly after, as many people apparently found it.
|
||||
|
||||
|
||||
{{< figure src="/img/posts/liveoverflow-minecraft-server/first_ip_screenshot.png" title="Screenshot of video containing the IP address" >}}
|
||||
|
||||
### IP change
|
||||
|
||||
The IP change happened of the 26th of August. Finding the new IP was going to be a harder challenge, as the change meant there was no way to find the server using the videos. At the time I was looking into it, no new video had been released, after all.
|
||||
|
||||
During my research, I came across masscan. This is a piece of software that lets you scan the entire internet, and even does it pretty quickly. This may come as a surprise, but It doesn't support Minecraft packets… I could add support for this, but I would have to get familiar with the code base, and I thought it would over all just be easier to write a python script that interfaces or uses the output from masscan.
|
||||
|
||||
I got to work and wrote a script that scans the IP range in which the old IP was located. It starts by using masscan to scan the entire range of IP addresses, and saves it to a file to use later. I then read this file, and request basic Minecraft server information like its MOTD, players and favicon. This information was saved in another file, in which, I could easily search for LiveOverflow. In the end, the new IP only had one digit changed.
|
||||
|
||||
{{< code language="python" title="scan.py" isCollapsed="true" >}}
|
||||
|
||||
```py
|
||||
import masscan
|
||||
import json
|
||||
import os
|
||||
from mcstatus import JavaServer
|
||||
from queue import Queue, Empty
|
||||
from threading import Thread
|
||||
import base64
|
||||
from time import sleep
|
||||
from contextlib import suppress
|
||||
from os import remove
|
||||
|
||||
ip_ranges = [
|
||||
"116.202.0.0/16",
|
||||
"116.203.0.0/16",
|
||||
"128.140.0.0/17",
|
||||
"135.181.0.0/16",
|
||||
"136.243.0.0/16",
|
||||
"138.201.0.0/16",
|
||||
"142.132.128.0/17",
|
||||
"144.76.0.0/16",
|
||||
"148.251.0.0/16",
|
||||
"157.90.0.0/16",
|
||||
"159.69.0.0/16",
|
||||
"162.55.0.0/16",
|
||||
"167.233.0.0/16",
|
||||
"167.235.0.0/16",
|
||||
"168.119.0.0/16",
|
||||
"171.25.225.0/24",
|
||||
"176.9.0.0/16",
|
||||
"178.212.75.0/24",
|
||||
"178.63.0.0/16",
|
||||
"185.107.52.0/22",
|
||||
"185.110.95.0/24",
|
||||
"185.112.180.0/24",
|
||||
"185.126.28.0/22",
|
||||
"185.12.65.0/24",
|
||||
"185.136.140.0/23",
|
||||
"185.157.176.0/23",
|
||||
"185.157.178.0/23",
|
||||
"185.157.83.0/24",
|
||||
"185.171.224.0/22",
|
||||
"185.189.228.0/24",
|
||||
"185.189.229.0/24",
|
||||
"185.189.230.0/24",
|
||||
"185.189.231.0/24",
|
||||
"185.209.124.0/22",
|
||||
"185.213.45.0/24",
|
||||
"185.216.237.0/24",
|
||||
"185.226.99.0/24",
|
||||
"185.228.8.0/23",
|
||||
"185.242.76.0/24",
|
||||
"185.36.144.0/22",
|
||||
"185.50.120.0/23",
|
||||
"188.34.128.0/17",
|
||||
"188.40.0.0/16",
|
||||
"193.110.6.0/23",
|
||||
"193.163.198.0/24",
|
||||
"193.25.170.0/23",
|
||||
"194.35.12.0/23",
|
||||
"194.42.180.0/22",
|
||||
"194.42.184.0/22",
|
||||
"194.62.106.0/24",
|
||||
"195.201.0.0/16",
|
||||
"195.248.224.0/24",
|
||||
"195.60.226.0/24",
|
||||
"195.96.156.0/24",
|
||||
"197.242.84.0/22",
|
||||
"201.131.3.0/24",
|
||||
"213.133.96.0/19",
|
||||
"213.232.193.0/24",
|
||||
"213.239.192.0/18",
|
||||
"23.88.0.0/17",
|
||||
"45.136.70.0/23",
|
||||
"45.148.28.0/22",
|
||||
"45.15.120.0/22",
|
||||
"46.4.0.0/16",
|
||||
"49.12.0.0/16",
|
||||
"49.13.0.0/16",
|
||||
"5.75.128.0/17",
|
||||
"5.9.0.0/16",
|
||||
"65.108.0.0/16",
|
||||
"65.109.0.0/16",
|
||||
"65.21.0.0/16",
|
||||
"78.46.0.0/15",
|
||||
"85.10.192.0/18",
|
||||
"88.198.0.0/16",
|
||||
"88.99.0.0/16",
|
||||
"91.107.128.0/17",
|
||||
"91.190.240.0/21",
|
||||
"91.233.8.0/22",
|
||||
"94.130.0.0/16",
|
||||
"94.154.121.0/24",
|
||||
"95.216.0.0/16",
|
||||
"95.217.0.0/16",
|
||||
]
|
||||
|
||||
thread_count = 100
|
||||
# selected_range = "176.9.0.0/16"
|
||||
selected_range = None
|
||||
|
||||
mas = masscan.PortScanner()
|
||||
|
||||
ip_ranges_to_scan = [selected_range] if selected_range else ip_ranges
|
||||
|
||||
for i, ip_range in enumerate(ip_ranges_to_scan):
|
||||
print(f"Starting on range: {ip_range} [{i+1}/{len(ip_ranges_to_scan)}]")
|
||||
mas.scan(ip_range, ports="25565", arguments="--max-rate 8000")
|
||||
with open("hosts.json", "r+") as file:
|
||||
hosts_json = json.load(file)
|
||||
previous_hosts = hosts_json.get(ip_range, [])
|
||||
hosts_json[ip_range] = list(set(previous_hosts) | set(mas.all_hosts))
|
||||
file.seek(0)
|
||||
json.dump(hosts_json, file)
|
||||
|
||||
with open("hosts.json", "r") as file:
|
||||
hosts_json = json.load(file)
|
||||
|
||||
if selected_range:
|
||||
hosts = hosts_json.get(selected_range, [])
|
||||
else:
|
||||
hosts = [host for ip_range in hosts_json.values() for host in ip_range]
|
||||
|
||||
hosts = list(set(hosts))
|
||||
hosts[:5], len(hosts)
|
||||
|
||||
with open("masscans/hosts.json", "r") as file:
|
||||
json_data = json.load(file)
|
||||
|
||||
hosts = [entry['ip'] for entry in json_data]
|
||||
|
||||
hosts = list(set(hosts))
|
||||
hosts[:5], len(hosts)
|
||||
|
||||
with open("valid_hosts.json", "r") as file:
|
||||
valid_hosts = json.load(file)
|
||||
|
||||
def handle_ip(ip):
|
||||
try:
|
||||
server = JavaServer.lookup(ip)
|
||||
status = server.status()
|
||||
|
||||
server_info = {
|
||||
"version": status.version.name,
|
||||
"motd": status.description,
|
||||
"favicon": status.favicon is not None,
|
||||
"player_count": status.players.online,
|
||||
"players": [player.name for player in status.players.sample or []],
|
||||
}
|
||||
|
||||
valid_hosts[ip] = server_info
|
||||
|
||||
if status.favicon:
|
||||
with open(f"servers/{ip}.png", 'wb') as file:
|
||||
file.write(base64.b64decode(status.favicon[len("data:image/png;base64,"):]))
|
||||
except Exception as e:
|
||||
print(f"Failed for IP: {ip} ({type(e)})")
|
||||
|
||||
|
||||
def do_stuff(queue):
|
||||
with suppress(Empty):
|
||||
while True:
|
||||
handle_ip(queue.get(block=False))
|
||||
queue.task_done()
|
||||
|
||||
|
||||
def print_progress(queue):
|
||||
initial_size = queue.qsize()
|
||||
def p(current_size):
|
||||
print(f"Progress: {(((initial_size-current_size)/initial_size)*100):.2f}% [{initial_size-current_size}/{initial_size}]")
|
||||
|
||||
while (current_size := queue.qsize()) > 0:
|
||||
p(current_size)
|
||||
sleep(1)
|
||||
p(current_size)
|
||||
|
||||
|
||||
queue = Queue(maxsize=0)
|
||||
for host in hosts:
|
||||
queue.put(host)
|
||||
|
||||
threads = []
|
||||
|
||||
progress_printer = Thread(target=print_progress, args=(queue,))
|
||||
progress_printer.daemon = True
|
||||
progress_printer.start()
|
||||
threads.append(progress_printer)
|
||||
|
||||
for _ in range(thread_count):
|
||||
worker = Thread(target=do_stuff, args=(queue,))
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
threads.append(worker)
|
||||
|
||||
queue.join()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
with open("valid_hosts.json", "w") as file:
|
||||
json.dump(valid_hosts, file)
|
||||
|
||||
|
||||
with open("valid_hosts.json", "r") as file:
|
||||
valid_hosts_data = json.load(file)
|
||||
|
||||
|
||||
# list({data.get("version") for ip, data in valid_hosts_data.items()})
|
||||
current_servers = [filename[:-4] for filename in os.listdir("possible_hits")]
|
||||
|
||||
def is_1_19(ip) -> bool:
|
||||
return "1.19" in valid_hosts_data.get(ip).get("version")
|
||||
|
||||
|
||||
valid_servers = list(filter(is_1_19, current_servers))
|
||||
|
||||
for filename in os.listdir("possible_hits"):
|
||||
if filename[:-4] not in valid_servers:
|
||||
remove(f"possible_hits/{filename}")
|
||||
|
||||
print(len(current_servers))
|
||||
print(len(valid_servers))
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
_This code is copied from a Jupyter Notebook, so it's not the most efficient and may not even work, the [here for the Github Gist of the Notebook](https://gist.github.com/maikka39/8019e2f1a45e1021fff05bd1e1688e14)_
|
||||
|
||||
Later, I found out the physical server was actually the same, and you could also use `traceroute` to find the new server as well.
|
||||
|
||||
```txt
|
||||
traceroute to 176.9.20.201 (176.9.20.201), 30 hops max, 60 byte packets
|
||||
1 _gateway (10.0.0.1) 1.435 ms 1.407 ms 1.381 ms
|
||||
2 192.168.2.254 (192.168.2.254) 16.733 ms 16.722 ms 16.712 ms
|
||||
3 static.kpn.net (195.190.228.90) 17.893 ms 17.882 ms 17.882 ms
|
||||
4 139.156.98.101 (139.156.98.101) 128.740 ms 128.730 ms 128.720 ms
|
||||
5 * * *
|
||||
6 core24.fsn1.hetzner.com (213.239.224.253) 32.245 ms core23.fsn1.hetzner.com (213.239.224.249) 30.209 ms core24.fsn1.hetzner.com (213.239.224.253) 43.727 ms
|
||||
7 ex9k1.dc6.fsn1.hetzner.com (213.239.229.94) 30.182 ms 19.697 ms 22.575 ms
|
||||
8 static.205.20.9.176.clients.your-server.de (176.9.20.205) 19.665 ms 22.329 ms 22.306 ms
|
||||
9 * * *
|
||||
10 * * *
|
||||
```
|
||||
|
||||
### Third IP change
|
||||
|
||||
The “new” IP quickly got leaked, and so a new server had been created. This time, `traceroute` did not yield any useful results, so I used the above script again. The only change I made, was adding all Hetzner ranges to the list of ranges.
|
||||
|
||||
### Abuse emails
|
||||
|
||||
Not every ISP likes people scanning the internet. I ran the script on both a VPS, and on my home network. Especially the latter was a mistake, though, as I quickly received an email telling me to stop, or I would have my internet cut off. So, If you want to scan the whole internet, don't do it from home :smile:.
|
||||
|
||||
{{< figure src="/img/posts/liveoverflow-minecraft-server/kpn_abuse.png" title="Screenshot of abuse email (in Dutch)" >}}
|
||||
|
||||
## Challenges
|
||||
|
||||
I had a great time playing on the server. The other players were mostly nice, having to have some technical skills to find the server probably kept most griefers out. What I found most fun about the server, were the challenges, though.
|
||||
|
||||
### Bot movement challenge
|
||||
|
||||
After some time playing on the server, the first challenge was released. As LiveOverflow explained in [this video](https://www.youtube.com/watch?v=WEMOCFe4EFE), if you move on the server, without rounding your player position to 1/100th, you get kicked with the following message:
|
||||
|
||||
{{< figure src="/img/posts/liveoverflow-minecraft-server/human_movement_kick_message.png" title="Human movement kick message" >}}
|
||||
|
||||
#### Writing a Fabric mod
|
||||
|
||||
Where do you even get started with solving this challenge? I didn't have any Minecraft modding experience, but had heard about [Fabric](https://fabricmc.net/). I downloaded [an example mod](https://github.com/FabricMC/fabric-example-mod) from their GitHub, and got it to intercept `PlayerMoveC2SPacket`. These are the packets your Minecraft client sends to the server, so it knows where you are.
|
||||
|
||||
Unfortunately, I don't have my original code anymore, so I can't give any code snippets for this part. I _can_ explain the business logic, though.
|
||||
|
||||
#### Moving like a bot
|
||||
|
||||
You might say this is a straightforward challenge, but you might be surprised. This is because the naive approach of simply multiplying a coordinate by one hundred, rounding it, and then dividing it by one hundred again will only work some of the time. The reason for this is [how floating-point numbers work](https://en.wikipedia.org/wiki/Floating-point_arithmetic). You might have heard about the classic meme that `0.1` + `0.2` equals `0.30000000000000004`. We humans work in base 10, but computers work in base 2, and not all base 10 numbers can efficiently be represented in base 2. Our mod will not work when we're sending a number that's rounded down to, for instance, x.9999999, as casting that to an int would result into a non-rounded number. The easiest way to fix this is by adding the following step:
|
||||
|
||||
```kt
|
||||
fun round(n: Double): Double {
|
||||
val rounded = Math.round(n * 100) / 100;
|
||||
return Math.nextAfter(rounded, rounded + Math.signum(rounded)); // <--
|
||||
}
|
||||
```
|
||||
|
||||
This will get the first exact floating-point number after the given number. We can then pass the x and/or z value of the current coordinate to this method, and replace the value with the result.
|
||||
|
||||
### Club Mate challenge
|
||||
|
||||
#### Original version
|
||||
|
||||
Some time after the bot movement challenge, a new challenge was released. This time, we had to open a chest in a protected area. Part of the challenge was actually finding it, I stumbled across it by accident, but a hint was otherwise also given [at the end of the bot movement video](https://www.youtube.com/watch?v=WEMOCFe4EFE).
|
||||
|
||||
Upon entering the area, you're teleported to a floating island and greeted with a message;
|
||||
|
||||
{{< figure src="/img/posts/liveoverflow-minecraft-server/club_mate_original.png" title="Club Mate message" >}}
|
||||
|
||||
This challenge had some unintended solutions. I'll get to the intended one later, but let's first solve it the easy way :smile:.
|
||||
|
||||
When looking at the given pseudocode, we can see we're only teleported back on a `PlayerMoveEvent`. As LiveOverflow [later showed in a video](https://www.youtube.com/watch?v=RDkWagIW6gw), many people found bypasses to it. My solution was to throw an Ender Pearl into the region, and then immediately block all outgoing movement packets. This way the server teleports us into the region, which does not trigger a `PlayerMoveEvent`, after which, we can open a chest. A message is also sent in chat after opening the chest;
|
||||
|
||||
{{< figure src="/img/posts/liveoverflow-minecraft-server/acquired_club_mate.png" title="Club Mate message" >}}
|
||||
|
||||
#### Patched version
|
||||
|
||||
After LiveOverflow noticed the unintended solutions, he used the plugin WorldGuard instead of his own. This made the challenge quite a bit harder, as silly things like throwing an Ender Pearl don't work.
|
||||
|
||||
After reading lots of [Paper source code](https://github.com/PaperMC/Paper), I came across the following code:
|
||||
|
||||
```java
|
||||
// Prevent 40 event-calls for less than a single pixel of movement >.>
|
||||
double delta = Math.pow(this.lastPosX - to.getX(), 2) + Math.pow(this.lastPosY - to.getY(), 2) + Math.pow(this.lastPosZ - to.getZ(), 2);
|
||||
float deltaAngle = Math.abs(this.lastYaw - to.getYaw()) + Math.abs(this.lastPitch - to.getPitch());
|
||||
|
||||
if ((delta > 1f / 256 || deltaAngle > 10f) && !this.player.isImmobile()) {
|
||||
// ...
|
||||
PlayerMoveEvent event = new PlayerMoveEvent(player, from, to);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In short, this code means we can move around a tiny bit before a `PlayerMoveEvent` is sent. This caught my interest, as I thought there might be a way to change the `lastPos`. After reading more code, I found out there was!
|
||||
|
||||
If we send a position that is far away, like x+100, y+100, the player will be teleported back before the `PlayerMoveEvent` gets sent. During this teleport action, `lastPos` get set to the position the player had before the “far away packet” was sent.
|
||||
|
||||
```java
|
||||
public void handleMovePlayer(ServerboundMovePlayerPacket packet) {
|
||||
// ...
|
||||
// d11 and d10 contain a difference in player position
|
||||
if (d11 - d10 > Math.max(f2, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2)) && !this.isSingleplayerOwner()) {
|
||||
// ...
|
||||
this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot());
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
public boolean teleport(double d0, double d1, double d2, float f, float f1, Set<ClientboundPlayerPositionPacket.RelativeArgument> set, boolean flag, PlayerTeleportEvent.TeleportCause cause) {
|
||||
// ...
|
||||
this.internalTeleport(d0, d1, d2, f, f1, set, flag);
|
||||
// ...
|
||||
}
|
||||
|
||||
public void internalTeleport(double d0, double d1, double d2, float f, float f1, Set<ClientboundPlayerPositionPacket.RelativeArgument> set, boolean flag) {
|
||||
// ...
|
||||
this.awaitingPositionFromClient = new Vec3(d0, d1, d2);
|
||||
// ...
|
||||
|
||||
this.lastPosX = this.awaitingPositionFromClient.x;
|
||||
this.lastPosY = this.awaitingPositionFromClient.y;
|
||||
this.lastPosZ = this.awaitingPositionFromClient.z;
|
||||
this.lastYaw = f;
|
||||
this.lastPitch = f1;
|
||||
|
||||
// ...
|
||||
this.player.moveTo(d0, d1, d2, f, f1);
|
||||
this.player.connection.send(new ClientboundPlayerPositionPacket(d0 - d3, d1 - d4, d2 - d5, f - f2, f1 - f3, set, this.awaitingTeleport, flag));
|
||||
}
|
||||
```
|
||||
|
||||
This meant that, as long as we sent a "far away packet" every `(1 / 256)**0.5` (`0.0625`) blocks, we can travel without triggering any events.
|
||||
|
||||
After implementing this, I could enter the region and jump in the fountain, after which, I got the HACKER rank and a bottle of Club Mate.
|
||||
|
||||
> maik_dev drank from the Club Mate fountain and became a real Minecraft HACKER
|
||||
|
||||
{{< figure src="/img/posts/liveoverflow-minecraft-server/club_mate_bottle_patched.png" title="Club Mate bottle" >}}
|
||||
|
||||
##### Speeding things up
|
||||
|
||||
This method was very slow, however. After taking another look at the [WorldGuard source code](https://github.com/EngineHub/WorldGuard), I noticed that it checks whether the player actually moved a block;
|
||||
|
||||
```java
|
||||
public void onPlayerMove(PlayerMoveEvent event) {
|
||||
Location from = event.getFrom();
|
||||
Location to = event.getTo();
|
||||
if (from.getBlockX() == to.getBlockX()
|
||||
&& from.getBlockY() == to.getBlockY()
|
||||
&& from.getBlockZ() == to.getBlockZ()) {
|
||||
return;
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
This check means we only have to actually prevent a `PlayerMoveEvent` when we are changing from one block to another. The final code for which can be found in [my GitHub repo](https://github.com/maikka39/MinecraftClient).
|
||||
|
||||
## Conclusion
|
||||
|
||||
As of writing this, these are all the challenges I'm aware of. I'll update this post when more challenges are added.
|
||||
@@ -1,8 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "SANS Holiday Hack Challenge 2024"
|
||||
breadcrumb = "2024"
|
||||
description = "A collection of my writeups for the 2024 edition of the SANS Holiday Hack Challenge (HHC). HHC is a free series of fun, quality, hands-on cybersecurity challenges."
|
||||
+++
|
||||
|
||||
A collection of my writeups for the 2024 edition of the SANS Holiday Hack Challenge (HHC). HHC is a free series of fun, quality, hands-on cybersecurity challenges.
|
||||
@@ -1,8 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Act 1 - SANS HHC 2024"
|
||||
breadcrumb = "Act 1"
|
||||
description = "A collection of my writeups for the 2024 edition of the SANS Holiday Hack Challenge (HHC). HHC is a free series of fun, quality, hands-on cybersecurity challenges."
|
||||
+++
|
||||
|
||||
A collection of my writeups for the 2024 edition of the SANS Holiday Hack Challenge (HHC). HHC is a free series of fun, quality, hands-on cybersecurity challenges.
|
||||
@@ -1,322 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Curling"
|
||||
subtitle = "Act 1 - SANS Holiday Hack Challenge 2024"
|
||||
date = 2024-11-23T11:44:53+01:00
|
||||
description = "In the fourth challenge of the Holiday Hack Challenge 2024, we'll explore how to use curl using the Linux manpages."
|
||||
cover = "img/writeups/holiday-hack-challenge/2024/act1/curling/cover.png"
|
||||
tags = [
|
||||
"Holiday Hack Challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
## Link
|
||||
|
||||
If you want to play the challenge yourself, you can find it here:
|
||||
|
||||
<https://2024.holidayhackchallenge.com/>
|
||||
|
||||
## Story line
|
||||
|
||||
Let's start off by talking to Bow Ninecandle:
|
||||
|
||||
```txt
|
||||
Well hello there! I'm Bow Ninecandle, bright as a twinkling star! Everyone's busy unpacking, but I've grown quite bored of that. Care to join me for a lovely game?
|
||||
|
||||
Oh Joy! Today, We're diving into something delightful: the curling challenge—without any ice, but plenty of sparkle!
|
||||
|
||||
No icy brooms here though! We're all about Curl, sending web requests from the command line like magic messages.
|
||||
|
||||
So, have you ever wielded Curl before? If not, no worries at all, my friend!
|
||||
|
||||
It's this clever little tool that lets you whisper directly to web servers. Pretty neat, right?
|
||||
|
||||
Think of it like sending secret scrolls through the interwebs, awaiting a wise reply!
|
||||
|
||||
To begin, you can type something like curl https://example.com. Voilà! The HTML of the page appears, like conjuring a spell!
|
||||
|
||||
Simple enough, huh? But oh, there's a whole world of magic you can cast with Curl!
|
||||
|
||||
We're just brushing the surface here, but trust me—it’s a hoot and a half!
|
||||
|
||||
If you get tangled up or need help, just give me a shout! I’m here to help you ace this curling spectacle.
|
||||
|
||||
So, are you ready to curl those web requests like a pro? Let’s see your magic unfold!
|
||||
```
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge, we're greeted with a terminal. Looks like we'll have to use the `curl` command to solve the challenge.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/start.png" title="Welcome screen" >}}
|
||||
|
||||
Let's enter "y", press enter, and start the challenge.
|
||||
|
||||
## Silver
|
||||
|
||||
### Task 1
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/http.png" title="Question 1" >}}
|
||||
|
||||
So, for our first task, we'll have to send a simple http request to `http://curlingfun:8080/`. Those of use who have used curl before will have no issues here, but since this writeup isn't just for them, I'll pretend to have never used curl before.
|
||||
|
||||
To get more info on how curl works, we can open its manpage. A manpage is the documentation of a command on linux systems. We can open it by running `man curl` in the terminal.
|
||||
|
||||
{{< figure class="small" src="/img/writeups/holiday-hack-challenge/2024/act1/curling/manpage.png" title="manpage of curl" >}}
|
||||
|
||||
We can scroll through the page using the arrow keys (or vim shortcuts), and search for things by typing `/[SEARCH TERM HERE]`.
|
||||
|
||||
For the first task, we don't need any of the special options, and we can just append the url after the curl command.
|
||||
|
||||
```sh
|
||||
curl http://curlingfun:8080/
|
||||
```
|
||||
|
||||
### Task 2
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/insecure-https.png" title="Question 2" >}}
|
||||
|
||||
For tasks 2, we'll have to send an https request. Sounds easy enough, and normally that would be the case, but here it also specifies that the destination is using a self-signed certificate.
|
||||
|
||||
By default curl will verify the remote certificate, and block the request if the certificate is not issued by a trusted party. Since the certificate here is self-signed, meaning there is no trusted party involved, curl will block the request.
|
||||
|
||||
To circumvent this protection mechanism, we'll have to tell curl to allow insecure connections. If we look at the manpage, we can find the following option:
|
||||
|
||||
```txt
|
||||
-k, --insecure
|
||||
(TLS) By default, every SSL connection curl makes is verified to be secure.
|
||||
This option allows curl to proceed and operate even for server connections
|
||||
otherwise considered insecure.
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
So we can add the `-k` option, and run the command like this:
|
||||
|
||||
```sh
|
||||
curl -k https://curlingfun:9090/
|
||||
```
|
||||
|
||||
### Task 3
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/post.png" title="Question 3" >}}
|
||||
|
||||
On to task three, where we'll have to send a POST request with some data.
|
||||
|
||||
We can again look at the manpage. If type in `/POST`, press enter, and then press `n` to go to the next occurrence a few times, we'll find the following entry:
|
||||
|
||||
```txt
|
||||
-d, --data <data>
|
||||
(HTTP) Sends the specified data in a POST request to the HTTP server, in the
|
||||
same way that a browser does when a user has filled in an HTML form and
|
||||
presses the submit button. This will cause curl to pass the data to the server
|
||||
using the content-type application/x-www-form-urlencoded. Compare to -F,
|
||||
--form.
|
||||
|
||||
...
|
||||
|
||||
If any of these options is used more than once on the same command line, the
|
||||
data pieces specified will be merged together with a separating &-symbol.
|
||||
Thus, using '-d name=daniel -d skill=lousy' would generate a post chunk that
|
||||
looks like 'name=daniel&skill=lousy'.
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Using this option, we can send the requested data. The page also shows an example of how to specify the data, so we can use that in our case like so:
|
||||
|
||||
```sh
|
||||
curl -k https://curlingfun:9090/ --data "skip=alabaster"
|
||||
```
|
||||
|
||||
In the screenshot below, I also added the `-X POST` option. The `-X` option as you may gave guessed sets the request method. I did this out of habit, but it is not needed at all. The `--data` option automatically converts it to a POST request for you.
|
||||
|
||||
### Task 4
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/cookie.png" title="Question 4" >}}
|
||||
|
||||
Next up is sending a cookie along. A quick search in the manpage will yield us the following result:
|
||||
|
||||
```txt
|
||||
-b, --cookie <data|filename>
|
||||
(HTTP) Pass the data to the HTTP server in the Cookie header. It is supposedly
|
||||
the data previously received from the server in a "Set-Cookie:" line. The
|
||||
data should be in the format "NAME1=VALUE1; NAME2=VALUE2".
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
The page also conveniently shows us the format in which the data is expected. Let's fill it in and send it.
|
||||
|
||||
```sh
|
||||
curl -k https://curlingfun:9090/ --cookie "end=3"
|
||||
```
|
||||
|
||||
### Task 5
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/headers.png" title="Question 5" >}}
|
||||
|
||||
This time, we'll have to get curl to show us the headers it received as well.
|
||||
|
||||
You know the drill by now, this is what the manpage shows:
|
||||
|
||||
```txt
|
||||
-i, --include
|
||||
Include the HTTP response headers in the output. The HTTP response headers can
|
||||
include things like server name, cookies, date of the document, HTTP version
|
||||
and more...
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
We won't have to send any data, so we can leave all other things out.
|
||||
|
||||
```sh
|
||||
curl -k -i https://curlingfun:9090/
|
||||
```
|
||||
|
||||
### Task 6
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/header.png" title="Question 6" >}}
|
||||
|
||||
For the sixth task, we need to send a header along with the request.
|
||||
|
||||
Once again, the manpage helps us out:
|
||||
|
||||
```txt
|
||||
-H, --header <header/@file>
|
||||
(HTTP) Extra header to include in the request when sending HTTP to a server.
|
||||
You may specify any number of extra headers.
|
||||
|
||||
...
|
||||
|
||||
Example:
|
||||
|
||||
curl -H "X-First-Name: Joe" http://example.com/
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
```sh
|
||||
curl -k https://curlingfun:9090/ -H "Stone: Granite"
|
||||
```
|
||||
|
||||
### Task 7
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/path-as-is.png" title="Question 7" >}}
|
||||
|
||||
We have made it to the last task; making a request with `./` in the url. Because actually wanting to send `./` or `../` is very rare and often unintended (only hackers would want to), curl resolves them locally before sending the request.
|
||||
|
||||
Luckily, we can also disable this functionality:
|
||||
|
||||
```txt
|
||||
--path-as-is
|
||||
Tell curl to not handle sequences of /../ or /./ in the given URL path. Nor‐
|
||||
mally curl will squash or merge them according to standards but with this op‐
|
||||
tion set you tell it not to do that.
|
||||
```
|
||||
|
||||
We'll just add that option to our final command:
|
||||
|
||||
```sh
|
||||
curl -k --path-as-is https://curlingfun:9090/../../etc/hacks
|
||||
```
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/silver.png" title="Silver" >}}
|
||||
|
||||
And... bam! We got the medal!
|
||||
|
||||
### Tl;dr
|
||||
|
||||
These are the commands in order:
|
||||
|
||||
```sh
|
||||
curl http://curlingfun:8080/
|
||||
curl -k https://curlingfun:9090/
|
||||
curl -k https://curlingfun:9090/ --data "skip=alabaster"
|
||||
curl -k https://curlingfun:9090/ --cookie "end=3"
|
||||
curl -k -i https://curlingfun:9090/
|
||||
curl -k https://curlingfun:9090/ -H "Stone: Granite"
|
||||
curl -k --path-as-is https://curlingfun:9090/../../etc/hacks
|
||||
```
|
||||
|
||||
## Gold
|
||||
|
||||
### Continued story line
|
||||
|
||||
Let's first talk to the elf again, he'll tell us what we'll have to do for gold.
|
||||
|
||||
```txt
|
||||
Bravo! Look at you, curling through that like a true web wizard!
|
||||
|
||||
You zipped through that challenge faster than a curling stone on enchanted ice!
|
||||
|
||||
You know... rumor has it you can breeze through this with just three commands. Why don’t you give it a whirl?
|
||||
```
|
||||
|
||||
### Task 1
|
||||
|
||||
At first, I though we could just combine the previous commands in one, and solve it, but, alas, this was not the case...
|
||||
|
||||
This is the input I tried:
|
||||
|
||||
```sh
|
||||
curl http://curlingfun:8080/
|
||||
curl -k -X POST https://curlingfun:9090/ --data "skip=alabaster" --cookie "end=3" -H "Stone: Granite"
|
||||
curl -k --path-as-is https://curlingfun:9090/../../etc/hacks
|
||||
```
|
||||
|
||||
Curious as to what we needs to be done, we should explore further. Let's start by listing the files in the current directory. This turns out to be a good idea, as we find a file there; `HARD-MODE.txt`.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/hard-mode.png" title="HARD-MODE.txt" >}}
|
||||
|
||||
Turns out we get some more steps to follow. We have already practiced all the things needed here with the silver one, so we can apply the knowledge gained there with this task.
|
||||
|
||||
```sh
|
||||
curl -k https://curlingfun:9090/ --data "skip=bow" --cookie "end=10" -H "Hack: 12ft"
|
||||
```
|
||||
|
||||
### Task 2
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/gold2.png" title="Question 2" >}}
|
||||
|
||||
The second one also brings nothing new, the `--path-as-is` option will help us again here:
|
||||
|
||||
```sh
|
||||
curl -k --path-as-is https://curlingfun:9090/../../etc/button
|
||||
```
|
||||
|
||||
### Task 3
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/gold3.png" title="Question 3" >}}
|
||||
|
||||
For the third task, we got something new. The message shows us that the url will redirect us.
|
||||
|
||||
By default, curl will not follow redirects. As you may have guessed by now though, there is way to enable this functionality, and it's written in the manpage:
|
||||
|
||||
```txt
|
||||
-L, --location
|
||||
(HTTP) If the server reports that the requested page has moved to a different
|
||||
location (indicated with a Location: header and a 3XX response code), this op‐
|
||||
tion will make curl redo the request on the new place.
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
We can then add this option to the command as follows:
|
||||
|
||||
```sh
|
||||
curl -k -L https://curlingfun:9090/GoodSportsmanship
|
||||
```
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/act1/curling/gold.png" title="Gold" >}}
|
||||
|
||||
And that's it, we got the gold medal!
|
||||
@@ -1,8 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Prologue - SANS HHC 2024"
|
||||
breadcrumb = "Prologue"
|
||||
description = "A collection of my writeups for the 2024 edition of the SANS Holiday Hack Challenge (HHC). HHC is a free series of fun, quality, hands-on cybersecurity challenges."
|
||||
+++
|
||||
|
||||
A collection of my writeups for the 2024 edition of the SANS Holiday Hack Challenge (HHC). HHC is a free series of fun, quality, hands-on cybersecurity challenges.
|
||||
@@ -1,171 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Elf Connect"
|
||||
subtitle = "Prologue - SANS Holiday Hack Challenge 2024"
|
||||
date = 2024-11-21T14:23:34+01:00
|
||||
description = "Let's play our first game of the Holiday Hack Challenge. To win, we'll have to use the DevTools to read some code, and figure out how the scoring mechanism works."
|
||||
cover = "img/writeups/holiday-hack-challenge/2024/prologue/elf-connect/cover.png"
|
||||
tags = [
|
||||
"Holiday Hack Challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
## Link
|
||||
|
||||
If you want to play the challenge yourself, you can find it here:
|
||||
|
||||
<https://2024.holidayhackchallenge.com/>
|
||||
|
||||
## Story line
|
||||
|
||||
Let's start off by talking to Angel Candysalt:
|
||||
|
||||
```txt
|
||||
Welcome back, island adventurer! I'm Angel Candysalt — so happy to finally meet you!
|
||||
|
||||
I'm thrilled you're here because I could really use a hand with something.
|
||||
|
||||
Have you ever heard of a game called Connections?
|
||||
|
||||
It’s simple! All you need to do is find groups of four related words.
|
||||
|
||||
I've been stuck on it all day, and I'm sure someone as sharp as you will breeze through it.
|
||||
|
||||
Oh, and while you're at it, check out randomElf's score — they hit fifty thousand points, which seems… oddly suspicious.
|
||||
|
||||
Think they might have tampered with the game? Just a hunch!
|
||||
```
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge, we're greeted with an explanation of the game:
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-connect/game-welcome.png" title="Game explanation" >}}
|
||||
|
||||
## Silver
|
||||
|
||||
The game seems simple enough, click four words that are connected and go through the rounds. Depending on your knowledge of Christmas terms, you may fly through this no problem. My knowledge about it is not so good, but I decided to play the game normally at first anyway. With some Googling I got through the challenge and got the silver medal.
|
||||
|
||||
There are, however, multiple ways to solve the game. And we'll need to exploit this to get the gold medal.
|
||||
|
||||
## Gold
|
||||
|
||||
For gold, we'll need to inspect the code behind the game.
|
||||
|
||||
We can open the DevTools, and under "Sources" we can find the iframe in which the game is running. Here we can see all the files that are being used.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-connect/sources.png" title="DevTools Sources" >}}
|
||||
|
||||
Looking through this code, we can actually find the correct word sets.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
```js
|
||||
const wordSets = {
|
||||
1: ["Tinsel", "Sleigh", "Belafonte", "Bag", "Comet", "Garland", "Jingle Bells", "Mittens", "Vixen", "Gifts", "Star", "Crosby", "White Christmas", "Prancer", "Lights", "Blitzen"],
|
||||
2: ["Nmap", "burp", "Frida", "OWASP Zap", "Metasploit", "netcat", "Cycript", "Nikto", "Cobalt Strike", "wfuzz", "Wireshark", "AppMon", "apktool", "HAVOC", "Nessus", "Empire"],
|
||||
3: ["AES", "WEP", "Symmetric", "WPA2", "Caesar", "RSA", "Asymmetric", "TKIP", "One-time Pad", "LEAP", "Blowfish", "hash", "hybrid", "Ottendorf", "3DES", "Scytale"],
|
||||
4: ["IGMP", "TLS", "Ethernet", "SSL", "HTTP", "IPX", "PPP", "IPSec", "FTP", "SSH", "IP", "IEEE 802.11", "ARP", "SMTP", "ICMP", "DNS"]
|
||||
};
|
||||
// ...
|
||||
let correctSets = [
|
||||
[0, 5, 10, 14], // Set 1
|
||||
[1, 3, 7, 9], // Set 2
|
||||
[2, 6, 11, 12], // Set 3
|
||||
[4, 8, 13, 15] // Set 4
|
||||
];
|
||||
```
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
This means two things. Firstly, if your knowledge is as bad as mine, you can just write some code to get the correct combinations. But, secondly, and more importantly, if the correct set is here, the checks are also likely done client-side (meaning in your browser, and not on the server).
|
||||
|
||||
If you're wondering how to get the correct combinations, you can do it like this:
|
||||
|
||||
```js
|
||||
Object.keys(wordSets).map((round) =>
|
||||
correctSets.map((correctSet) =>
|
||||
correctSet.map((index) => wordSets[round][index])
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
This might look a little complicated, so let me explain it for you. We start by looping over `wordSets`, this contains all the words for a specific round. We then look at the correct sets, and map the four indices to the actual word in the list. If we execute this code, we get the following output:
|
||||
|
||||
{{< code language="json" title="Results" isCollapsed="true" >}}
|
||||
|
||||
```json
|
||||
[
|
||||
[
|
||||
["Tinsel", "Garland", "Star", "Lights"],
|
||||
["Sleigh", "Bag", "Mittens", "Gifts"],
|
||||
["Belafonte", "Jingle Bells", "Crosby", "White Christmas"],
|
||||
["Comet", "Vixen", "Prancer", "Blitzen"]
|
||||
],
|
||||
[
|
||||
["Nmap", "netcat", "Wireshark", "Nessus"],
|
||||
["burp", "OWASP Zap", "Nikto", "wfuzz"],
|
||||
["Frida", "Cycript", "AppMon", "apktool"],
|
||||
["Metasploit", "Cobalt Strike", "HAVOC", "Empire"]
|
||||
],
|
||||
[
|
||||
["AES", "RSA", "Blowfish", "3DES"],
|
||||
["WEP", "WPA2", "TKIP", "LEAP"],
|
||||
["Symmetric", "Asymmetric", "hash", "hybrid"],
|
||||
["Caesar", "One-time Pad", "Ottendorf", "Scytale"]
|
||||
],
|
||||
[
|
||||
["IGMP", "IPX", "IP", "ICMP"],
|
||||
["TLS", "SSL", "IPSec", "SSH"],
|
||||
["Ethernet", "PPP", "IEEE 802.11", "ARP"],
|
||||
["HTTP", "FTP", "SMTP", "DNS"]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
This just get us the correct answer though, and we'll need more for gold.
|
||||
|
||||
Scanning further through the code, we find the `checkSelectedSet` function with some logic in it:
|
||||
|
||||
```js
|
||||
function checkSelectedSet(scene) {
|
||||
// ...
|
||||
if (isCorrectSet) {
|
||||
// ...
|
||||
// Update score by 100 points
|
||||
score += 100;
|
||||
scoreText.setText("Score: " + score);
|
||||
|
||||
// Add high-score board
|
||||
if (score > 50000) {
|
||||
highScoreText.setText("High Score: " + score);
|
||||
emitter.explode(20);
|
||||
submitAction(2);
|
||||
displaySuccessMessage(
|
||||
"Great Job Hacker! Elf Connect Complete and Hacked!",
|
||||
function () {}
|
||||
);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
In the code we can see that once a score of over 50000 has been achieved, it calls `submitAction(2)`. This looks suspicious. The only other place where the function is being called is on a normal win, in that case it passes `1` as the argument instead of `2`.
|
||||
|
||||
Let's execute this function on its own. To do this, we'll first need to attach our console to the iframe the game is running in. We can do so by clicking "top" in the top left corner of the DevTools, and selecting the iframe.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-connect/iframe-console.png" title="Attach console to iframe" >}}
|
||||
|
||||
We can then enter the code in the console, and..., we got the gold medal!
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-connect/submitaction.png" title="Running the code" >}}
|
||||
@@ -1,264 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Elf Minder"
|
||||
subtitle = "Prologue - SANS Holiday Hack Challenge 2024"
|
||||
date = 2024-11-21T15:18:53+01:00
|
||||
description = "On to our second game of the Holiday Hack Challenge. This time we'll have to find some routes."
|
||||
cover = "img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/cover.png"
|
||||
tags = [
|
||||
"Holiday Hack Challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
## Link
|
||||
|
||||
If you want to play the challenge yourself, you can find it here:
|
||||
|
||||
<https://2024.holidayhackchallenge.com/>
|
||||
|
||||
## Story line
|
||||
|
||||
Let's start off by talking to Poinsettia McMittens:
|
||||
|
||||
```txt
|
||||
Center your mind, and become one with the island!
|
||||
|
||||
Relax...
|
||||
|
||||
This isn't working! I'm trying to play this game but the whole "moving back to the North Pole" thing completely threw me off.
|
||||
|
||||
Say, how about you give it a try. It's really simple. All you need to do is help the elf get to the exit.
|
||||
|
||||
The faster you get there, the better your score!
|
||||
|
||||
I've run into some weirdness with the springs though. If I had created this game it would've been a lot more stable, but I won't comment on that any further.
|
||||
```
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge, we're greeted with yet another game. This time with twelve levels.
|
||||
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/levels.png" title="Welcome screen" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/game-help.png" title="Game help" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/game.png" title="Game screen" >}}
|
||||
|
||||
## Silver
|
||||
|
||||
Just like with the previous challenge, we can likely get silver here by just playing the game as described. They seem progressively more difficult, and near the end we'll have to get creative with some.
|
||||
|
||||
Here are some screenshots of how I solved them. Definitely didn't get on the leaderboard with them, but they're enough for silver.
|
||||
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level1.png" title="Sandy Start" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level2.png" title="Waves and Crates" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level3.png" title="Tidal Treasures" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level4.png" title="Dune Dash" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level5.png" title="Coral Cove" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level6.png" title="Shell Seekers" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level7.png" title="Palm Grove Shuffle" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level8.png" title="Tropical Tangle" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level9.png" title="Crate Caper" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level10.png" title="Shoreline Shuffle" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level11.png" title="Beachy Bounty" >}}
|
||||
{{< figure class="small inline" src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level12.png" title="Driftwood Dunes" >}}
|
||||
|
||||
Once we finish them all, we get a silver medal and a popup message.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/silver.png" title="Silver Popup" >}}
|
||||
|
||||
Looking at the message, we'll probably have to dive into the game's code again.
|
||||
|
||||
## Gold
|
||||
|
||||
Looking at the last level, it seems impossible. It doesn't seem like there is any way to reach the flag.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level13-default.png" title="A Real Pickle" >}}
|
||||
|
||||
This is where we'll have to delve into the code, and see what's going on behind the scenes. In this process, I found there were multiple ways to reach the end flag, let's go over them one by one.
|
||||
|
||||
### Method 1 - Admin Tools
|
||||
|
||||
After opening the DevTools, we can immediately find something suspicious, a div with a class called `admin-controls`.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/admin-controls-devtool.png" title="Admin controls in DevTools" >}}
|
||||
|
||||
Let's double click on `hidden`, and remove it.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/admin-controls.png" title="Admin controls" >}}
|
||||
|
||||
Wooah, a nice toolbar shows up! In it, there are several options we can use. The one that can really help us is the "Clear Entities" button; it removes all the entities like rocks from the map. Without these entities, we can place tunnels on places which are normally already occupied.
|
||||
|
||||
Let's draw a route which goes by all the boxes, and end with a tunnel. Since the end flag is no longer there, we can put a tunnel there to end. Don't forget to also put a path here, so we can walk to the finish.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level13-clear.png" title="Cleared level" >}}
|
||||
|
||||
Once the map has been drawn, we can click "Go Home" on the top left, and then reopen the level again. Now, all the removed entities have been put back, and our route is still there.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level13.png" title="Finished level" >}}
|
||||
|
||||
Now we can click start, and let the elf walk to the finish. Don't forget to rotate the angled path towards the tunnel when the elf is on its way up.
|
||||
|
||||
We should now have received gold!
|
||||
|
||||
### Method 2 - Springs
|
||||
|
||||
From the hint given in the conversation with the elf Poinsettia, we got the following line: "I've run into some weirdness with the springs though." This hints at the possibility to use springs to get to the end as well.
|
||||
|
||||
A good starting point here would be to first figure out how the springs work.
|
||||
|
||||
{{< code language="js" title="guide.js (getSpringTarget)" isCollapsed="true" >}}
|
||||
|
||||
```js
|
||||
getSpringTarget(springCell) {
|
||||
const journey = this.hero.journey;
|
||||
const dy = journey[1][1] - journey[0][1];
|
||||
const dx = journey[1][0] - journey[0][0];
|
||||
|
||||
let nextPoint = [springCell[0], springCell[1]];
|
||||
let entityHere;
|
||||
let searchLimit = 15;
|
||||
let searchIndex = 0;
|
||||
let validTarget;
|
||||
|
||||
do {
|
||||
searchIndex += 1;
|
||||
nextPoint = [nextPoint[0] + dx, nextPoint[1] + dy];
|
||||
|
||||
entityHere = this.entities.find(
|
||||
(entity) =>
|
||||
~[EntityTypes.PORTAL, EntityTypes.SPRING].indexOf(
|
||||
entity[2]
|
||||
) &&
|
||||
searchIndex &&
|
||||
entity[0] === nextPoint[0] &&
|
||||
entity[1] === nextPoint[1]
|
||||
);
|
||||
|
||||
if (searchIndex >= searchLimit) {
|
||||
break;
|
||||
}
|
||||
|
||||
validTarget = this.isPointInAnySegment(nextPoint) || entityHere;
|
||||
} while (!validTarget);
|
||||
|
||||
if (this.isPointInAnySegment(nextPoint) || entityHere) {
|
||||
if (entityHere) return this.segments[0][0]; // fix this
|
||||
return nextPoint;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
In one of the source files, `guide.js`, there is a function called `getSpringTarget`. This function, as the name suggests, returns the location where the elf should go after encountering a spring.
|
||||
|
||||
Let's go over the code. I have attached the original version above and below a version with comments added by me.
|
||||
|
||||
```js
|
||||
getSpringTarget(springCell) {
|
||||
const journey = this.hero.journey; // This contains the last two positions of the elf
|
||||
const dy = journey[1][1] - journey[0][1]; // The vertical direction of the elf
|
||||
const dx = journey[1][0] - journey[0][0]; // The horizontal direction of the elf
|
||||
|
||||
let nextPoint = [springCell[0], springCell[1]]; // Initialize the destination to the spring location
|
||||
let entityHere; // Initialize the variable, will later contain whether an entity has been found on the path
|
||||
let searchLimit = 15; // Set a limit after which to stop searching for a destination
|
||||
let searchIndex = 0; // Current offset from the spring
|
||||
let validTarget; // Initialize the variable, will contain whether the destination which is currently being checked is valid
|
||||
|
||||
do {
|
||||
searchIndex += 1; // Up the offset from the spring
|
||||
nextPoint = [nextPoint[0] + dx, nextPoint[1] + dy]; // Up the destination by one in the direction of travel
|
||||
|
||||
entityHere = this.entities.find( // Loop over the entities until the condition below is true
|
||||
(entity) =>
|
||||
~[EntityTypes.PORTAL, EntityTypes.SPRING].indexOf( // Check if the entity is a tunnel of spring
|
||||
entity[2] // entity[2] is the type of the entity
|
||||
) &&
|
||||
searchIndex && // searchIndex should be bigger than 0, always true in this scenario
|
||||
entity[0] === nextPoint[0] && // Check if the entity is on the destination (horizontally)
|
||||
entity[1] === nextPoint[1] // Check if the entity is on the destination (vertically)
|
||||
);
|
||||
|
||||
if (searchIndex >= searchLimit) { // If we gave gone too far from the spring
|
||||
break; // Stop the search
|
||||
}
|
||||
|
||||
validTarget = this.isPointInAnySegment(nextPoint) || entityHere; // Check if there is a path at the current position, or that an entity has been found
|
||||
} while (!validTarget); // Keep going as long as there is no valid destination
|
||||
|
||||
if (this.isPointInAnySegment(nextPoint) || entityHere) { // Same check as above
|
||||
if (entityHere) return this.segments[0][0]; // If an entity has been found, return the location of the very first path
|
||||
return nextPoint; // Return the destination we found
|
||||
} else {
|
||||
return; // If we ever end up here, something has gone very wrong
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The key line in the code is the following:
|
||||
|
||||
```js
|
||||
if (entityHere) return this.segments[0][0]; // fix this
|
||||
```
|
||||
|
||||
_Note: The comment here if from the original file._
|
||||
|
||||
What this line does, is that, if an entity has been found in the path of the spring, it directs the elf to the very first path we put down.
|
||||
|
||||
So, if we draw the very **first path** on the finish line, and make sure we end our path with two springs or a spring and a tunnel in a row, we can travel there.
|
||||
|
||||
To test this hypothesis, we can draw a route that follows these rules.
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/elf-minder/level13-spring-tp.png" title="Finished level" >}}
|
||||
|
||||
Afterwards, we hit start, and the elf reached the flag!
|
||||
|
||||
## Bonus
|
||||
|
||||
Here's some nice bonus content. In the level's grid, we can also place paths and entities on corners. Also, the amount of springs if not validated. Of course, we can't do this by hand, we'll have to do it in code.
|
||||
|
||||
Using this method we can create more optimized (or the opposite for evil boss mode) paths.
|
||||
|
||||
Try the following code for example to see how it works.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
```js
|
||||
game.segments = [
|
||||
[[10, 9], [11, 9]],
|
||||
[[1, 1], [2, 1]],
|
||||
[[9, 1], [9, 2]],
|
||||
[[9, 2], [9, 3]],
|
||||
[[9, 3], [8, 3]],
|
||||
[[6, 3], [5, 3]],
|
||||
[[5, 3], [5, 4]],
|
||||
[[5, 4], [5, 5]],
|
||||
[[5, 5], [6, 5]],
|
||||
[[6, 5], [7, 5]],
|
||||
[[7, 5], [7, 6]],
|
||||
[[7, 6], [7, 7]],
|
||||
[[7, 7], [6, 7]],
|
||||
[[4, 7], [3, 7]],
|
||||
[[3, 7], [2, 7]],
|
||||
[[2, 7], [2, 6]],
|
||||
[[2, 6], [2, 5]],
|
||||
[[2, 5], [2, 4]],
|
||||
];
|
||||
|
||||
game.entities.push([2, 1, EntityTypes.SPRING]);
|
||||
game.entities.push([8, 3, EntityTypes.SPRING]);
|
||||
game.entities.push([6, 7, EntityTypes.SPRING]);
|
||||
game.entities.push([2, 4, EntityTypes.SPRING]);
|
||||
|
||||
startBtn.classList.add("ready") // Makes sure the start button is clickable
|
||||
```
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -1,57 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Holiday Hack Orientation"
|
||||
subtitle = "Prologue - SANS Holiday Hack Challenge 2024"
|
||||
date = 2024-11-21T13:46:55+01:00
|
||||
description = "An easy start of the Holiday Hack Challenge 2024."
|
||||
cover = "img/writeups/holiday-hack-challenge/2024/prologue/orientation/cover.png"
|
||||
tags = [
|
||||
"Holiday Hack Challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
## Link
|
||||
|
||||
If you want to play the challenge yourself, you can find it here:
|
||||
|
||||
<https://2024.holidayhackchallenge.com/>
|
||||
|
||||
## Story line
|
||||
|
||||
Let's start off by talking to Jingle Ringford:
|
||||
|
||||
```txt
|
||||
Welcome to the Geese Islands and the 2023 SANS Holiday Hack Challenge!
|
||||
|
||||
I'm Jingle Ringford, one of Santa's many elves.
|
||||
|
||||
...
|
||||
|
||||
Just kidding! It's actually the 2024 SANS Holiday Hack Challenge!
|
||||
|
||||
And although we're on Frosty's Beach on Christmas Island, we'll soon be on our way back to the North Pole.
|
||||
|
||||
I thought it best to wait here for people that heard we're on the Geese Islands but may not know we're leaving.
|
||||
|
||||
I haven't seen Santa since we started packing up, but he always asks me to give a quick orientation to newcomers, so I'm continuing the tradition.
|
||||
|
||||
Before you head out any further onto the island, you need to accomplish two simple tasks.
|
||||
```
|
||||
|
||||
## Solving
|
||||
|
||||
Okay, I doubt we'll need a long writeup here, the HHC starts off easy.
|
||||
|
||||
Next the elf there is a "First Terminal", let's open it. At the top we can see a simple instruction; "Enter the answer here".
|
||||
|
||||
{{< figure src="/img/writeups/holiday-hack-challenge/2024/prologue/orientation/terminal.png" title="First Terminal" >}}
|
||||
|
||||
Let's follow the instruction and enter the text in the top console. And that's it! We got the gold medal!
|
||||
@@ -1,7 +0,0 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "SANS Holiday Hack Challenge"
|
||||
description = "A collection of my writeups for the SANS Holiday Hack Challenge (HHC). HHC is a free series of fun, quality, hands-on cybersecurity challenges."
|
||||
+++
|
||||
|
||||
A collection of my writeups for the SANS Holiday Hack Challenge (HHC). HHC is a free series of fun, quality, hands-on cybersecurity challenges.
|
||||
@@ -1,16 +1,13 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "{{ replace .Name "-" " " | title }}"
|
||||
subtitle = ""
|
||||
# showPageViews = false
|
||||
date = {{ .Date }}
|
||||
description = ""
|
||||
cover = "img/writeups/google-ctf/2021/beginners-quest/10/cover.png"
|
||||
images = []
|
||||
tags = [
|
||||
"untagged",
|
||||
]
|
||||
categories = [
|
||||
"uncategorized",
|
||||
]
|
||||
# aliases = []
|
||||
+++
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "{{ replace .Name "-" " " | title }}"
|
||||
date = {{ .Date }}
|
||||
description = ""
|
||||
cover = ""
|
||||
tags = [
|
||||
"untagged",
|
||||
]
|
||||
categories = [
|
||||
"uncategorized",
|
||||
]
|
||||
+++
|
||||
@@ -0,0 +1,26 @@
|
||||
function resizeItem(masonry, item){
|
||||
let rowHeight = parseInt(window.getComputedStyle(masonry).getPropertyValue('grid-auto-rows'));
|
||||
let rowGap = parseInt(window.getComputedStyle(masonry).getPropertyValue('grid-row-gap'));
|
||||
let rowSpan = Math.ceil((item.querySelector('.masonry-item-content').getBoundingClientRect().height+rowGap)/(rowHeight+rowGap));
|
||||
item.style.gridRowEnd = "span "+rowSpan;
|
||||
}
|
||||
|
||||
function resizeAll(){
|
||||
let masonry = document.getElementsByClassName("masonry")[0];
|
||||
|
||||
if (!masonry) return;
|
||||
|
||||
masonry.style.gridAutoRows = "20px"
|
||||
masonry.style.marginBottom = "80px"
|
||||
let allItems = masonry.getElementsByClassName("masonry-item");
|
||||
|
||||
for (const item of allItems) {
|
||||
resizeItem(masonry, item)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
resizeAll()
|
||||
|
||||
window.addEventListener("resize", resizeAll);
|
||||
})
|
||||
@@ -1,42 +1 @@
|
||||
window.addEventListener("load", () => {
|
||||
// Enlarged floating figures
|
||||
for (let figure of document.getElementsByTagName("figure")) {
|
||||
if (
|
||||
figure.classList.contains("post-cover") ||
|
||||
figure.classList.contains("portfolio-cover")
|
||||
)
|
||||
continue;
|
||||
|
||||
figure.addEventListener("click", () => {
|
||||
figure.classList.toggle("floated-focus");
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.keyCode !== 27) return;
|
||||
|
||||
for (let figure of document.getElementsByClassName("floated-focus")) {
|
||||
figure.classList.remove("floated-focus");
|
||||
}
|
||||
});
|
||||
|
||||
// Add anchors to article headings
|
||||
for (let article of document.getElementsByTagName("article")) {
|
||||
for (let heading of article.querySelectorAll(
|
||||
"h1, h2, h3, h4, h5, h6"
|
||||
)) {
|
||||
if (!heading.id) continue;
|
||||
|
||||
let anchor = document.createElement("a");
|
||||
anchor.innerHTML = heading.innerHTML;
|
||||
anchor.href = "#" + heading.id;
|
||||
heading.innerHTML = "";
|
||||
heading.appendChild(anchor);
|
||||
heading.classList.add("article-heading");
|
||||
|
||||
anchor.addEventListener("click", (e) => {
|
||||
navigator.clipboard.writeText(anchor.href);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// Some code could be here ...
|
||||
|
||||
@@ -1,40 +1,80 @@
|
||||
// Path navigation on top left
|
||||
window.addEventListener("load", () => {
|
||||
const terminal = document.getElementById("nav-terminal");
|
||||
const terminalCommand = document.getElementById("terminal-command")
|
||||
const terminalInput = document.getElementById("terminal-input")
|
||||
|
||||
const keyDown = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
if (document.activeElement !== document.body) return;
|
||||
if (terminal.textContent.length === 0) return;
|
||||
const onSubmit = () => {
|
||||
if (document.activeElement !== document.body) return
|
||||
if (terminalInput.textContent.length === 0) return
|
||||
|
||||
if (terminal.textContent.includes("~")) terminal.textContent = "";
|
||||
|
||||
window.location.href =
|
||||
window.location.origin + "/" + terminal.textContent;
|
||||
return;
|
||||
terminalCommand.click()
|
||||
}
|
||||
|
||||
const handleKey = (e) => {
|
||||
if (e.keyCode === 13) return onSubmit()
|
||||
|
||||
if (e.keyCode === 8) {
|
||||
terminal.textContent = terminal.textContent.slice(0, -1);
|
||||
return;
|
||||
terminalInput.textContent = terminalInput.textContent.slice(0, -1)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode === 192) {
|
||||
if (terminal.textContent.length !== 0) return;
|
||||
if (terminalInput.textContent.length !== 0) return
|
||||
|
||||
terminal.textContent += "~";
|
||||
return;
|
||||
terminalInput.textContent += "~"
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode === 189) {
|
||||
terminalInput.textContent += "-"
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode === 191) {
|
||||
terminal.textContent += "/";
|
||||
return;
|
||||
let lastChar = terminalInput.textContent.slice(-1)
|
||||
if (lastChar === "/") return
|
||||
|
||||
terminalInput.textContent += "/"
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode < 65 || e.keyCode > 90) return;
|
||||
if (e.keyCode === 190) {
|
||||
let lastChar = terminalInput.textContent.slice(-1)
|
||||
if (terminalInput.textContent.length > 0 && lastChar !== "/" && lastChar !== ".") return
|
||||
|
||||
terminal.textContent += e.key.toLowerCase();
|
||||
};
|
||||
if (terminalInput.textContent.length >= 2 && lastChar === "." && terminalInput.textContent.slice(-2)[0] === ".") return
|
||||
|
||||
document.addEventListener("keydown", keyDown);
|
||||
});
|
||||
terminalInput.textContent += "."
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode >= 65 && e.keyCode <= 90) {
|
||||
let lastChar = terminalInput.textContent.slice(-1)
|
||||
if (lastChar === "." || lastChar === "~") return
|
||||
|
||||
terminalInput.textContent += e.key.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
const updateCommandHref = () => {
|
||||
let href = terminalInput.textContent
|
||||
|
||||
if (href.includes("~")) href = "/" + href.substring(1)
|
||||
|
||||
if (href.length === 0)
|
||||
href = window.location.origin
|
||||
else if (href[0] === "/")
|
||||
href = window.location.origin + href
|
||||
else
|
||||
href = window.location.href + href
|
||||
|
||||
terminalCommand.href = href
|
||||
}
|
||||
|
||||
const keyDown = (e) => {
|
||||
handleKey(e)
|
||||
updateCommandHref()
|
||||
}
|
||||
|
||||
// terminalCommand.addEventListener("click", onSubmit)
|
||||
document.addEventListener("keydown", keyDown)
|
||||
})
|
||||
|
||||
@@ -48,12 +48,10 @@ a.button {
|
||||
}
|
||||
|
||||
&.primary {
|
||||
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11),
|
||||
0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 6px rgba(50, 50, 93, .11), 0 1px 3px rgba(0, 0, 0, .08);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 6px rgba(50, 50, 93, 0.21),
|
||||
0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 2px 6px rgba(50, 50, 93, .21), 0 1px 3px rgba(0, 0, 0, .08);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +61,7 @@ a.button {
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: 0.8rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
&.wide {
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
~ .highlight,
|
||||
~ .code-toolbar .highlight {
|
||||
|
||||
~.highlight,
|
||||
~.code-toolbar .highlight {
|
||||
max-height: 0;
|
||||
|
||||
padding: 0;
|
||||
@@ -29,7 +30,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
~ .code-toolbar {
|
||||
~.code-toolbar {
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
|
||||
@@ -38,11 +39,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
~ label {
|
||||
~label {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
~ label .collapsable-code__toggle:after {
|
||||
~label .collapsable-code__toggle:after {
|
||||
content: attr(data-label-expand);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@font-face {
|
||||
font-family: "Inter UI";
|
||||
font-family: 'Inter UI';
|
||||
font-style: normal;
|
||||
font-display: fallback;
|
||||
font-weight: 400;
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter UI";
|
||||
font-family: 'Inter UI';
|
||||
font-style: italic;
|
||||
font-display: fallback;
|
||||
font-weight: 400;
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter UI";
|
||||
font-family: 'Inter UI';
|
||||
font-style: normal;
|
||||
font-display: auto;
|
||||
font-weight: 600;
|
||||
@@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter UI";
|
||||
font-family: 'Inter UI';
|
||||
font-style: italic;
|
||||
font-display: fallback;
|
||||
font-weight: 600;
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter UI";
|
||||
font-family: 'Inter UI';
|
||||
font-style: normal;
|
||||
font-display: fallback;
|
||||
font-weight: 800;
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter UI";
|
||||
font-family: 'Inter UI';
|
||||
font-style: italic;
|
||||
font-display: fallback;
|
||||
font-weight: 800;
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
@media #{$media-size-tablet} {
|
||||
flex-direction: column;
|
||||
}
|
||||
& > *:not(:last-child)::after {
|
||||
&>*:not(:last-child)::after {
|
||||
content: "•";
|
||||
padding: 0 5px;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
& > *:last-child {
|
||||
&>*:last-child {
|
||||
padding: 0 5px;
|
||||
|
||||
@media #{$media-size-tablet} {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
font-weight: bold;
|
||||
font-display: auto;
|
||||
font-family: monospace, monospace;
|
||||
white-space: pre;
|
||||
|
||||
img {
|
||||
height: 44px;
|
||||
@@ -12,6 +13,7 @@
|
||||
|
||||
&__mark {
|
||||
margin-right: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__text {
|
||||
|
||||
@@ -38,8 +38,7 @@ body {
|
||||
color: $light-color;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-feature-settings: "liga", "tnum", "case", "calt", "zero", "ss01",
|
||||
"locl";
|
||||
font-feature-settings: "liga", "tnum", "case", "calt", "zero", "ss01", "locl";
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
@@ -185,7 +184,7 @@ figure {
|
||||
figcaption {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.3;
|
||||
margin-top: 0.4em;
|
||||
margin: 1.33em 0;
|
||||
font-weight: bold;
|
||||
opacity: 0.8;
|
||||
|
||||
@@ -205,43 +204,11 @@ figure {
|
||||
&.small {
|
||||
max-width: 300px;
|
||||
width: 90%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
&.inline {
|
||||
display: inline-table;
|
||||
}
|
||||
|
||||
&.floated-focus {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
max-width: unset;
|
||||
height: 100vh;
|
||||
padding: 3rem;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background-color: #212020ec;
|
||||
z-index: 10000;
|
||||
|
||||
figcaption {
|
||||
color: #ddd3c6;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "Close";
|
||||
margin-top: 1rem;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
color: #999288;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
@@ -454,73 +421,3 @@ table {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feather {
|
||||
display: inline-block;
|
||||
vertical-align: -0.125em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.nostyle {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: inline;
|
||||
|
||||
ol {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
display: inline;
|
||||
|
||||
li {
|
||||
display: inline;
|
||||
|
||||
&::after {
|
||||
content: " > ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article-heading {
|
||||
a {
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 100%;
|
||||
|
||||
background-image: url("data:image/svg+xml, %3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2224%22%20height=%2224%22%20viewBox=%220%200%2024%2024%22%20fill=%22none%22%20stroke=%22currentColor%22%20stroke-width=%222%22%20stroke-linecap=%22round%22%20stroke-linejoin=%22round%22%20class=%22feather%20feather-link%22%3E%3Cpath%20d=%22M10%2013a5%205%200%200%200%207.54.54l3-3a5%205%200%200%200-7.07-7.07l-1.72%201.71%22%3E%3C/path%3E%3Cpath%20d=%22M14%2011a5%205%200%200%200-7.54-.54l-3%203a5%205%200%200%200%207.07%207.07l1.71-1.71%22%3E%3C/path%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: auto 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$media-size-tablet-min} {
|
||||
&:hover {
|
||||
&::after {
|
||||
right: 100%;
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$media-size-tablet} {
|
||||
&:hover {
|
||||
&::after {
|
||||
left: 100%;
|
||||
width: 1.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
.masonry {
|
||||
display: grid;
|
||||
margin-top: 20px;
|
||||
grid-gap: 20px 90px;
|
||||
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
||||
|
||||
> * {
|
||||
margin: 0;
|
||||
}
|
||||
// grid-auto-rows: 20px; // Set by JavaScript
|
||||
// margin-bottom: calc(4 * 20px); // Set by JavaScript
|
||||
|
||||
@media #{$media-size-medium} {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@mixin dimmed {
|
||||
opacity: 0.6;
|
||||
opacity: .6;
|
||||
}
|
||||