From 1b7c4039e360cb22fda6a279931771546d9fe3b4 Mon Sep 17 00:00:00 2001
From: andwati
Date: Sun, 30 Aug 2026 19:10:53 +0300
Subject: [PATCH] Migrate legacy Zola posts into content/writings/ (additive)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
scripts/migrate-posts-to-writings.mjs converts all 20 posts under
content/posts/ into the new writings schema — normalizes dates to
bare TOML local-dates, converts the one unambiguous Zola shortcode in
use (youtube embeds) to a plain
` tag of our website’s main index page. This script allows the user to redirect back to the `/admin/` path after completing the login with the Netlify Identity Widget:
+
+```html
+
+```
+
+**Step 5**
+
+Now we’re all set. Since we set our registration preferences to **“Invite Only”**, we need to invite ourselves as site users. To do this, select the Identity tab from the Netlify site dashboard and select the Invite Users button. Add your email address and send an invitation. You will receive an email. In your email, click **“accept the invitation”**. You will be directed to your website admin sign-up panel. Choose a password. Now you are ready to log in to Netlify CMS.
+
+# Setting up a custom domain
+
+I bought a custom domain from [Namecheap](https://namecheap.com), they provide some pretty good deals. After pointing the nameservers for the domain to the Netlify nameservers, voila! everything worked smoothly. For HTTPS support, Netlify enables automatic TLS certificates with Let’s Encrypt.
diff --git a/content/writings/javascript-exporting-modules/index.md b/content/writings/javascript-exporting-modules/index.md
new file mode 100644
index 0000000..1de00dc
--- /dev/null
+++ b/content/writings/javascript-exporting-modules/index.md
@@ -0,0 +1,199 @@
++++
+title = "Export default and module.exports in Javascript"
+description = "Modules are the building blocks of modern JavaScript applications, allowing for the encapsulation of code into reusable components. Two primary systems govern the exportation of these modules: export default in ES6 and module.exports in CommonJS. "
+author = "andwati"
+date = 2024-01-17
+draft = false
+
+[taxonomies]
+tags = [ "javascript", "node" ]
+
++++
+
+Modules are the building blocks of modern JavaScript applications, allowing for the encapsulation of code into reusable components. Two primary systems govern the exportation of these modules: export default in ES6 and module.exports in CommonJS.
+
+Javascript is a popular programming language that can run both in the browser and on the server. However, different environments may have different ways of organizing and importing Javascript modules. In this article, we will compare two common methods of exporting and importing modules: **export default** and **module.exports**.
+
+## What are modules?
+
+Modules are reusable pieces of code that can be separated into different files. Modules can export values, functions, classes, or objects that can be imported by other modules. This way, modules can avoid polluting the global scope and enable code reuse and modularity.
+
+## How to export modules in Node.js?
+
+Node.js is a runtime environment that allows Javascript to run on the server. Node.js uses a module system called CommonJS, which is based on the require and module.exports syntax.
+
+To export a module in Node.js, you can assign the value that you want to export to the module.exports object. For example:
+
+```javascript
+// my-module.js
+// export a function
+module.exports = function add(a, b) {
+ return a + b;
+};
+
+// export an object
+module.exports = {
+ name: 'Alice',
+ age: 25,
+};
+```
+
+To import a module in Node.js, you can use the require function and pass the path of the module file as an argument. The require function will return the value of module.exports from the module file. For example:
+
+```javascript
+// main.js
+// import a function
+const add = require('./my-module.js');
+console.log(add(2, 3)); // 5
+
+// import an object
+const person = require('./my-module.js');
+console.log(person.name); // Alice
+console.log(person.age); // 25
+```
+
+Note that Node.js will cache the modules that are imported with require, so if you import the same module multiple times, you will get the same instance of the module.
+
+## How to export modules in ES6?
+
+ES6 is the version of Javascript that introduced many new features, including a native module system. ES6 modules use the `import` and `export` syntax.
+
+To export a module in ES6, you can use the `export` keyword followed by the declaration of the value that you want to export. You can also use the export keyword with curly braces to export a list of named values. For example:
+
+```javascript
+// my-module.js
+// export a function
+export function add(a, b) {
+ return a + b;
+}
+
+// export a class
+export class Person {
+ constructor(name, age) {
+ this.name = name;
+ this.age = age;
+ }
+}
+
+// export an object
+export const person = {
+ name: 'Alice',
+ age: 25,
+};
+```
+
+To import a module in ES6, you can use the `import` keyword followed by the name of the value that you want to import and the path of the module file. You can also use the import keyword with curly braces to import a list of named values. For example:
+
+```javascript
+// main.js
+// import a function
+import { add } from './my-module.js';
+console.log(add(2, 3)); // 5
+
+// import a class
+import { Person } from './my-module.js';
+const person = new Person('Bob', 30);
+console.log(person.name); // Bob
+console.log(person.age); // 30
+
+// import an object
+import { person as Alice } from './my-module.js';
+console.log(Alice.name); // Alice
+console.log(Alice.age); // 25
+```
+
+Note that ES6 modules are imported and executed only once, so if you import the same module multiple times, you will get the same instance of the module.
+
+## What is the difference between export default and module.exports?
+
+One of the main differences between export default and module.exports is that export default is an ES6 feature, while module.exports is a CommonJS feature. Therefore, they are not compatible with each other by default. However, some tools like Babel or Webpack can transpile ES6 modules to CommonJS modules, or vice versa, to make them work together.
+
+Another difference is that export default can only export one value per module, while module.exports can export any value, including multiple values. For example:
+
+```javascript
+// my-module.js
+// export default a function
+export default function add(a, b) {
+ return a + b;
+}
+
+// export default an object
+export default {
+ name: "Alice",
+ age: 25,
+};
+
+// export default a class
+export default class Person {
+ constructor(name, age) {
+ this.name = name;
+ this.age = age;
+ }
+}
+```
+
+```javascript
+// my-module.js
+// export a function
+module.exports = function add(a, b) {
+ return a + b;
+};
+
+// export an object
+module.exports = {
+ name: 'Alice',
+ age: 25,
+};
+
+// export a class
+module.exports = class Person {
+ constructor(name, age) {
+ this.name = name;
+ this.age = age;
+ }
+};
+```
+
+Note that you can only have one export default statement per module, but you can have multiple module.exports statements. However, each module.exports statement will overwrite the previous one, so only the last one will be exported.
+
+To import a default export, you can use the import keyword without curly braces and assign any name to the imported value. For example:
+
+```javascript
+// main.js
+// import a default export
+import add from './my-module.js';
+console.log(add(2, 3)); // 5
+
+// import another default export
+import person from './my-module.js';
+console.log(person.name); // Alice
+console.log(person.age); // 25
+
+// import another default export
+import Person from './my-module.js';
+const person = new Person('Bob', 30);
+console.log(person.name); // Bob
+console.log(person.age); // 30
+```
+
+To import a module.exports value, you can use the require function and assign any name to the imported value. For example:
+
+```javascript
+// main.js
+// import a module.exports value
+const add = require('./my-module.js');
+console.log(add(2, 3)); // 5
+
+// import another module.exports value
+const person = require('./my-module.js');
+console.log(person.name); // Alice
+console.log(person.age); // 25
+
+// import another module.exports value
+const Person = require('./my-module.js');
+const person = new Person('Bob', 30);
+console.log(person.name); // Bob
+console.log(person.age); // 30
+```
+
+Node’s JavaScript has diverged from that of the browser. Understanding both CommonJS modules and ES6 modules is your first step in becoming a 10X developer. Happy coding!
diff --git a/content/writings/mmuctf2025/index.md b/content/writings/mmuctf2025/index.md
new file mode 100644
index 0000000..cac7919
--- /dev/null
+++ b/content/writings/mmuctf2025/index.md
@@ -0,0 +1,411 @@
++++
+title = "mmuctf 2025 writeup"
+description = "mmuctf 2025 challenges writeup"
+date = 2025-08-03
+draft = false
+
+[taxonomies]
+tags = [ "ctf", "rev", "web", "forensics" ]
+
++++
+
+Over the weekend, I had the opportunity to participate in the mmuctf 2025. It was an individual jeopardy style CTF targeted towards beginners and intermediate people in cybersecurity. This is a write-up to explore some challenges I did, or at least almost did.
+
+## Welcome
+
+> Through the shadows of the cave, at the door I had a zeal to get a welcome flag, but I couldn't see it though
+> I could hear it calling for me to get it. Happy hacking
+> {: .prompt-info }
+
+This was a sanity check flag. I believe competitions pose such challenges that you can't get zero points. It's the same as flags in Discord servers, a flag after completing the feedback form, or just other free points.
+
+The challenge description talked about the `door`, my first instinct was to view the source of the front page. I got this html comment with what looked liked a base64 encoded string ``
+
+```zsh
+┌──(mockingspectre㉿blackout)-[~/ctf/mmuctf2025]
+└─$ echo -n "bW11Y3Rme3dlbGNvbWVfZmxhZ19hZGlvc19NdWNoQGNob30=" | base64 -d
+mmuctf{welcome_flag_adios_Much@cho}
+```
+
+## My First App
+
+> Our intern dev swears this app’s secure...
+
+This was a web challenge with an account creation form. Let's fire up Burp. The account creation request didn't seem suspicious for now, so I sent it to the repeater to abuse it some more. The response seemed interesting, the user id was in the query parameter: `GET /profile.php?id=5 HTTP/1.1`. This was a nice candidate for an Insecure Direct Object Reference(IDOR). I tried repeating the request with values from 0-4. The flag was at 1, the admin user's account.
+
+```txt
+mmuctf{1d0r_4dm1n_4cc3ss_1s_c0mpr0m1s3d}
+```
+
+## Ledilect
+
+> Sometimes the path you take is just a jump away.
+
+This was a simple one, I knew what the challenge wanted, but I couldn't get myself to exploit it, definitely a skill issue. The webapp had a landing page that had a button that made this request:
+
+```txt
+GET /portal/jump.php?url=https://example.com HTTP/1.1
+Host: redacted
+User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
+Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+Accept-Language: en-US, en;q=0.5
+Accept-Encoding: gzip, deflate, br
+Connection: keep-alive
+Referer: redacted
+Cookie: PHPSESSID=ce7743f6cd447d7a1ee94e230c3d9cdb
+Upgrade-Insecure-Requests: 1
+Priority: u=0, i
+```
+
+Looking at that, I suspected an open redirect vulnerability, but I don't know anything about this bug class. I let it rest.
+
+## Meet my X
+
+> In the�days оf cοmfort i met this girl at the Jobless соrner. Аt the�doоrs and windows of iddleness i approached, oops she smiled and thats how i knew she was the one. I guess thatsss whyyy i learned on getting Flags because be warned she later left me at first it feels like a green flag i guess no life was there on blood(heart_break)
+
+This challenge involved homoglyphs, a character or a sequence of characters that looks very similar or identical to another character or sequence of characters, but has a different meaning, encoding, or origin.
+
+A sentence might look normal, but if you copy it into a text editor that shows Unicode details or a specialized homoglyph detector, you'll see different underlying character codes.
+
+I used these online decoders, and to get the flag:
+
+```txt
+mmuctf{oops_twitter_steg_1s_fun}
+```
+
+## Shadow
+
+> Look beyond the appearance, perhaps their numerical essence will guide you to the flag.
+> Can you reveal what lurks in the shadow?
+
+This was another stego challenge; there is an image with what looks like a color palette. The description hints at 'numerical essence', so it might be something to deal with the hex values of the colors. I used this site to extract the hex values.
+
+```txt
+#6d6d75
+#637466
+#7b346c
+#773479
+#355f68
+#316464
+#336e5f
+#316e35
+#64335f
+#6d337d
+```
+
+Decoded becomes
+
+```txt
+mmuctf{4lw4y5_h1dd3n_1n5d3_m3}
+```
+
+## Revealme
+
+> We found this mysterious executable on an old USB stick... Can you figure out how to get it to reveal the flag?
+
+The provided file is an ELF executable binary.
+
+```zsh
+┌──(mockingspectre㉿blackout)-[~/ctf/mmuctf2025]
+└─$ file revealme
+revealme: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d8546bb2c10c0226384fb2771684af9bc150b55e, for GNU/Linux 3.2.0, stripped
+```
+
+```zsh
+┌──(mockingspectre㉿blackout)-[~/ctf/mmuctf2025]
+└─$ checksec --file=revealme
+RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
+Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols No 0 2 revealme
+```
+
+Opening it with Binary Ninja gives us a quick win:
+
+```asm
+00401179 int32_t main(int32_t argc, char** argv, char** envp)
+
+
+00401179 55 push rbp {__saved_rbp}
+0040117a 4889e5 mov rbp, rsp {__saved_rbp}
+0040117d 4883ec70 sub rsp, 0x70
+00401181 488d05800e0000 lea rax, [rel data_402008]
+00401188 4889c7 mov rdi, rax {data_402008, "Enter the secret passphrase: "}
+0040118b b800000000 mov eax, 0x0
+00401190 e8abfeffff call printf
+00401195 488b15a42e0000 mov rdx, qword [rel stdin]
+0040119c 488d4590 lea rax, [rbp-0x70 {buf}]
+004011a0 be64000000 mov esi, 0x64
+004011a5 4889c7 mov rdi, rax {buf}
+004011a8 e8b3feffff call fgets
+004011ad 488d4590 lea rax, [rbp-0x70 {buf}]
+004011b1 488d156e0e0000 lea rdx, [rel data_402026]
+004011b8 4889d6 mov rsi, rdx {data_402026}
+004011bb 4889c7 mov rdi, rax {buf}
+004011be e88dfeffff call strcspn
+004011c3 c644059000 mov byte [rbp+rax-0x70 {buf}], 0x0
+004011c8 488d4590 lea rax, [rbp-0x70 {buf}]
+004011cc 488d15550e0000 lea rdx, [rel data_402028]
+004011d3 4889d6 mov rsi, rdx {data_402028, "OpenSesame123!"}
+004011d6 4889c7 mov rdi, rax {buf}
+004011d9 e892feffff call strcmp
+004011de 85c0 test eax, eax
+004011e0 7511 jne 0x4011f3
+
+
+004011e2 488d054f0e0000 lea rax, [rel data_402038]
+004011e9 4889c7 mov rdi, rax {data_402038, "Correct! The flag is: mmuctf{r3v3rs1ng_st4rt3r}"}
+004011ec e83ffeffff call puts
+004011f1 eb0f jmp 0x401202
+
+
+004011f3 488d056e0e0000 lea rax, [rel data_402068]
+004011fa 4889c7 mov rdi, rax {data_402068, "Nope, try again."}
+004011fd e82efeffff call puts
+
+
+00401202 b800000000 mov eax, 0x0
+00401207 c9 leave {__saved_rbp}
+00401208 c3 retn {__return_addr}
+```
+
+Flag:
+
+```txt
+mmuctf{r3v3rs1ng_st4rt3r}
+```
+
+## Flicker
+
+> An ordinary looking Android app... or is it?
+
+The provided app is an Android APK. Let's fire up jadx-gui. The package name is `com.example.blink`. There is an interesting class `r2d2`. In the onCreate method, there is an image string variable that seems to contain base64 image data.
+
+```zsh
+┌──(mockingspectre㉿blackout)-[~/ctf/mmuctf2025]
+└─$ cat output.txt | base64 -d > decoded_image.png
+```
+
+flag:
+
+```txt
+CTF{PUCKMAN}
+```
+
+## Open_Secrets
+
+> Someone trusted the wrong site and network didn’t forget. Can you piece together what was left behind?
+> Some people still think locks are optional on digital doors.\*
+
+The provided file was a network capture pcap file. For people who don't know, pcap (a packet capture) consists of an application programming interface (API) for capturing network traffic.The challenge title and filename hint at unencrypted traffic. Open up Wireshark and apply an HTTP filter. There is an HTTP stream that leaks the flag:
+
+```txt
+mmuctf{plaintext_login_leak}
+```
+
+## C-x C-s
+
+> Ugh, I keep typing ^x ^s in my shell instead of saving, should’ve stuck with Emacs
+
+This one made me mad. I thought I had the flag, but I couldn't get it. The provided file is a PCAP file. Another network forensics challenge, or so I thought.
+
+opened the file with Wireshark packet and noticed a new kind of communication, to be honest, I didn't know this was possible until I understood what was happening...
+
+I found some articles dealing with this
+
+-
+-
+-
+
+The source and destination use object IDs for communication, something I gathered when reading about the SNMP
+protocol, USB(Universal Serial Bus)
+it's apparent I'm not dealing with any 802.x traffic
+
+Interrupts happen whenever you press a key or click a button, anything that "interrupts" the CPU, after which it has to process your input. Each URB_INTERRUPT in the file corresponds to a key pressed, and the Leftover Capture Data field shows the hex value of the key in 8-byte format.
+
+Creating a filter to list all interrupt communication with non-empty 8 bytes
+
+```txt
+usb.transfer_type == 0x01
+```
+
+```txt
+usb.transfer_type == 0x01 && usb.dst == "host" && !(usb.capdata == 00:00:00:00:00:00:00:00)
+```
+
+Crafting a tshark command to get the leftover capture data for the above filters and redirect it into a file
+
+```zsh
+tshark -r log.pcap -Tfields -Eseparator=, -e thekey.pcapng -Y 'usb.transfer_type == 0x01 && usb.dst == "host" && !(usb.capdata == 00:00:00:00:00:00:00:00)' | sed 's/://g' > usb.capdata
+```
+
+```zsh
+┌──(mockingspectre㉿blackout)-[~/ctf/mmuctf2025]
+└─$ tshark -r thekey.pcapng -Tfields -Eseparator=, -e usb.capdata -Y 'usb.transfer_type == 0x01 && usb.dst == "host" && !(usb.capdata == 00:00:00:00:00:00:00:00)' | sed 's/://g' > usb.capdata
+
+┌──(mockingspectre㉿blackout)-[~/ctf/mmuctf2025]
+└─$ cat usb.capdata
+0000190000000000
+00000c0000000000
+00000c1000000000
+0000102c00000000
+00002c0000000000
+0000090000000000
+00000f0000000000
+0000040000000000
+0000040a00000000
+................
+```
+
+Python script to decode the hex values to strings:
+
+```python
+keyboard = {
+ 2: "PostFail", 4: "a", 5: "b", 6: "c", 7: "d", 8: "e", 9: "f", 10: "g", 11: "h", 12: "i", 13: "j", 14: "k", 15: "l", 16: "m", 17: "n", 18: "o", 19: "p", 20: "q", 21: "r", 22: "s", 23: "t", 24: "u", 25: "v", 26: "w", 27: "x", 28: "y", 29: "z", 30: "1", 31: "2", 32: "3", 33: "4", 34: "5", 35: "6", 36: "7", 37: "8", 38: "9", 39: "0", 40: "Enter", 41: "esc", 2: "del", 43: "tab", 44: "space", 45: "-", 47: "[", 48: "]", 51: "DownArrow", 54: "1", 55: "*", 56: "/", 57: "CapsLock", 79: "RightArrow", 80: "LeftArrow"
+}
+
+
+with open("usb.capdata") as file:
+ i = 1
+ for line in file:
+ bytesArray = bytearray.fromhex(line.strip())
+ for byte in bytesArray:
+ if byte != 0:
+ keyVal = int(byte)
+
+
+ if keyVal in keyboard:
+ print(keyboard[keyVal])
+ else:
+ print("no map found for this value: " + str(keyVal))
+
+
+ i += 1
+```
+
+Running the code outputs what seems to be Vim motions. I wasn't able to decipher this to a correct flag:
+
+```txt
+v
+i
+m
+space
+space
+f
+l
+a
+g
+g
+*
+t
+t
+x
+t
+Enter
+i
+3
+t
+t
+h
+e
+space
+f
+l
+a
+g
+g
+space
+i
+s
+space
+c
+t
+f
+esc
+v
+b
+3
+u
+u
+3
+a
+3
+[
+[
+m
+y
+3
+-
+3
+f
+a
+v
+o
+r
+i
+t
+e
+e
+3
+-
+e
+d
+i
+t
+o
+r
+3
+-
+3
+i
+s
+3
+-
+v
+i
+m
+3
+]
+esc
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+h
+a
+u
+esc
+v
+i
+3
+[
+3
+3
+u
+3
+esc
+3
+DownArrow
+DownArrow
+w
+q
+Enter
+a
+tab
+a
+e
+```
+
+These are the challenges I had the time to tackle. if you have any insights or suggestions. Share them in the comments
diff --git a/content/writings/obsidian-sync/index.md b/content/writings/obsidian-sync/index.md
new file mode 100644
index 0000000..008ac3c
--- /dev/null
+++ b/content/writings/obsidian-sync/index.md
@@ -0,0 +1,121 @@
++++
+title = "Setting up Obsidian sync in Windows"
+description = "Syncing Obsidian in Windows with Windows Task Scheduler with git"
+author = "andwati"
+date = 2023-10-01
+draft = false
+
+[taxonomies]
+tags = [ "obsidian", "git", "sync", "windows", "cron", "task scheduler", "powershell" ]
+
++++
+
+Syncing Obsidian in Windows with Windows Task Scheduler and git
+
+[Obsidian](https://obsidian.md/) is a powerful note-taking and knowledge management tool that has gained a dedicated following among researchers, writers, and knowledge workers. To make the most of Obsidian, it's essential to keep your notes synchronized across devices and ensure your data is regularly backed up.Obsidian is the private and flexible writing app that adapts to the way you think.
+
+Obsidian is free for personal use.It does not charge based on features or usage.You only pay if you use Obsidian commercially.
+
+In this article, we'll guide you through the process of setting up automated synchronization for your Obsidian vault on Windows using Windows Task Scheduler and a PowerShell script.[^note]
+
+## Prerequisites
+
+Before we dive into setting up the synchronization task, ensure you have the following prerequisites in place:
+
+1. **Obsidian Installed:** Make sure Obsidian is [installed](https://obsidian.md/download) on your Windows computer, and you have an existing Obsidian vault that you want to sync.
+
+1. **Git Installed:** You'll need Git installed and configured with your Obsidian vault's repository. If you haven't already done this, you can download Git from the [official website](https://git-scm.com/downloads) and follow the installation instructions.
+
+1. **PowerShell Script:** The provided PowerShell script, sync.ps1, should be located in your Obsidian vault folder.
+
+## Understanding the PowerShell Script
+
+The provided PowerShell script, sync.ps1, is the heart of our synchronization task. It automates the process of committing changes in your Obsidian vault's Git repository and pushing them to your remote repository (e.g., GitHub or GitLab). Here's a brief overview of the script:
+
+```ps1
+# Define the folder path
+$FolderPath = "C:\path\to\obsidian-vault"
+
+# Change directory to the repository folder
+Set-Location -Path $FolderPath
+
+# Check if the working tree is clean
+$IsClean = (git diff-index --quiet HEAD --) -or (git diff-files --quiet)
+
+if ($IsClean) {
+ Write-Host "Working tree is clean. Skipping commit and pushing changes."
+ git push
+} else {
+ # Get the current date in the desired format (Day - Date)
+ $CommitMessage = "$(Get-Date -Format 'dddd - yyyy-MM-dd HH:mm:ss')"
+
+ # Commit the changes
+ git add .
+ git commit -m $CommitMessage
+
+ # Push the changes
+ git push
+}
+```
+
+This script does the following:
+
+- Sets the folder path to your Obsidian vault.
+- Checks if there are any changes to commit. If the vault is clean (no changes), it skips the commit and only pushes changes.
+- If there are changes, it commits them with a timestamped message and then pushes them to the remote repository.
+
+## Setting up Windows Task Scheduler
+
+Now that we have our PowerShell script ready, let's set up a scheduled task using Windows Task Scheduler to automate the synchronization process. Follow these steps:
+
+1. **Open an Elevated PowerShell Prompt:** To run the necessary commands, open PowerShell as an administrator.
+
+1. **Define Task Variables:** Execute the following commands to define variables for your task:
+
+ ```ps1
+ $ScriptPath = "C:\path\to\obsidian-vault\sync.ps1"
+ $TaskName = "obsidian-sync"
+ $UserAccount = "$env:USERDOMAIN\$env:USERNAME"
+ ```
+
+ Replace $ScriptPath with the actual path to your sync.ps1 script, $TaskName with a name for your task.
+
+1. **Configure Task Trigger:** Set up a trigger for your task. In this example, we'll schedule the task to run daily at 9:00 PM:
+
+ ```ps1
+ $Trigger = New-ScheduledTaskTrigger -Daily -At '9:00 PM'
+ ```
+
+ You can adjust the schedule according to your preference.
+
+1. Define Task Principal and Settings: Configure the task's principal (the user account under which the task will run) and settings:
+
+ ```ps1
+ $Principal = New-ScheduledTaskPrincipal -UserId $UserAccount -RunLevel Highest
+ $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -RunOnlyIfNetworkAvailable -WakeToRun
+ ```
+
+ These settings ensure that the task runs with the necessary privileges and conditions.
+
+1. **Define Task Action:** Specify the action that the task will perform, which is running the PowerShell script:
+
+ ```ps1
+ $Action = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""
+ ```
+
+ This action runs PowerShell with the provided script.
+
+1. **Create the Scheduled Task:** Finally, create the scheduled task and register it with Windows Task Scheduler:
+
+ ```ps1
+ $Task = New-ScheduledTask -Action $Action -Trigger $Trigger -Principal $Principal -Settings $Settings
+ Register-ScheduledTask -TaskName $TaskName -InputObject $Task
+ ```
+
+The Obsidian synchronization task is now set up and ready to run automatically based on the schedule you defined.
+
+# Inspiration
+
+When looking up on how to implement this I found some breakthrough from this [stack overflow](https://stackoverflow.com/questions/7195503/setting-up-a-cron-job-in-windows) post. There are suggestions on how to do this using cron jobs in wsl. They are worth a look at. This article on [powershell](https://learn.microsoft.com/en-us/powershell/module/scheduledtasks/new-scheduledtask?view=windowsserver2022-ps) with windows server was also helpful.
+
+[^note]: This method is only optimal for desktop sync. To sync across devices obsidian has a paid plan that has some extra features Such as Sync notes across devices, End-to-end encryption, Version history, Priority email support. Consider checking it out at [https://obsidian.md/pricing](https://obsidian.md/pricing)
diff --git a/content/writings/scriptctf2025/index.md b/content/writings/scriptctf2025/index.md
new file mode 100644
index 0000000..c09bd57
--- /dev/null
+++ b/content/writings/scriptctf2025/index.md
@@ -0,0 +1,791 @@
++++
+title = "ScriptCTF 2025 Writeup"
+description = "ScriptCTF 2025 Writeup"
+author = "andwati"
+date = 2025-08-18
+draft = false
+
+[taxonomies]
+tags = [ "ctf", "rev", "web", "forensics" ]
+
++++
+
+![ScriptCTF mascot illustration of a wizard coding at a laptop, with the tagline "scriptCTF makes CTFs fun and approachable for all skill levels!"][image1]
+Over the weekend, I participated in ScriptCTF, a capture-the-flag competition packed with diverse challenges spanning cryptography, forensics, reverse engineering, and web exploitation.
+
+In this write-up, I’ll walk through some of the problems I solved, breaking down my approach, the tools I used, and the reasoning behind each step. Rather than just dropping final answers, my goal is to document the problem-solving process in a way that’s reproducible and useful for others looking to sharpen their cybersecurity skills.
+
+# Challenge: Renderer
+
+![ScriptCTF challenge card for "Renderer" describing the free image-rendering app and its chall.zip attachment][image2]
+
+This is the first web challenge; it launches an instance, and the source code is provided for manual code review. There are three important files: two Jinja templates and a Python file.
+
+upload.html
+
+```html
+
+
+
+ Upload Image
+
+
+
Upload Images
+
This free to use website allows you to render your images!
+
+
+```
+
+`[app.py](http://app.py)`
+
+```python
+from flask import Flask, request, redirect, render_template, make_response, url_for
+app = Flask(__name__)
+from hashlib import sha256
+import os
+def allowed(name):
+ if name.split('.')[1] in ['jpg','jpeg','png','svg']:
+ return True
+ return False
+
+@app.route('/',methods=['GET','POST'])
+def upload():
+ if request.method == 'POST':
+ if 'file' not in request.files:
+ return redirect(request.url)
+ file = request.files['file']
+ if file.filename == '':
+ return redirect(request.url)
+ if file and allowed(file.filename):
+ filename = file.filename
+ hash = sha256(os.urandom(32)).hexdigest()
+ filepath = f'./static/uploads/{hash}.{filename.split(".")[1]}'
+ file.save(filepath)
+ return redirect(f'/render/{hash}.{filename.split(".")[1]}')
+ return render_template('upload.html')
+
+@app.route('/render/')
+def render(filename):
+ return render_template('display.html', filename=filename)
+
+@app.route('/developer')
+def developer():
+ cookie = request.cookies.get("developer_secret_cookie")
+ correct = open('./static/uploads/secrets/secret_cookie.txt').read()
+ if correct == '':
+ c = open('./static/uploads/secrets/secret_cookie.txt','w')
+ c.write(sha256(os.urandom(16)).hexdigest())
+ c.close()
+ correct = open('./static/uploads/secrets/secret_cookie.txt').read()
+ if cookie == correct:
+ c = open('./static/uploads/secrets/secret_cookie.txt','w')
+ c.write(sha256(os.urandom(16)).hexdigest())
+ c.close()
+ return f"Welcome! There is currently 1 unread message: {open('flag.txt').read()}"
+ else:
+ return "You are not a developer!"
+
+if __name__ == '__main__':
+ app.run(host='0.0.0.0', port=1337)
+```
+
+From the files, we can see that this is an app built using Flask. What the app does:
+
+`/` — Upload endpoint:
+
+- Accepts `jpg`, `jpeg`, `png`, `svg` files.
+
+- Saves them under `./static/uploads/.`.
+
+- Redirects to `/render/`.
+
+`/render/` — Renders the uploaded image using `display.html`.
+
+`/developer` — Protected route:
+
+- Checks a cookie `developer_secret_cookie` against `secret_cookie.txt`.
+
+- If it matches, it reveals `flag.txt`.
+
+`secret_cookie.txt` is generated if empty and is overwritten after a successful visit.
+
+Key Observations
+
+1. File upload: Only the extension is checked; there is no content verification. So you can upload anything with .jpg, .png, etc.
+
+2. Render template: The render() route calls render_template('display.html', filename=filename).
+
+3. Secrets: The /developer route requires knowing secret_cookie.txt. We need a way to leak it.
+
+Navigating to the developer endpoint without the cookie returns this request.
+
+```
+HTTP/1.1 200 OK
+Server: Werkzeug/3.1.3 Python/3.10.16
+Date: Sun, 17 Aug 2025 11:51:49 GMT
+Content-Type: text/html; charset=utf-8
+Content-Length: 24
+Connection: close
+
+You are not a developer!
+```
+
+Now I tried some path traversal techniques, but I couldn't reach the cookie. I turned my focus on how the uploaded images were being rendered and saw that it was an inline iframe. Hmm… interesting, what if I could get code execution on this? I thought of uploading a malicious SVG.
+Here is an innocent-looking SVG
+
+```svg
+
+```
+
+And…
+![Browser alert dialog reading "You are not a developer!" from the failed cookie-theft attempt][image3]
+I didn't think that through. I can't make that request without the cookie. Another approach I tried was Local File Inclusion(LFI) using the SVG. Here is another not-so-evil one:
+
+```svg
+
+```
+
+The browser shows it as a broken image, but on opening it on another tab, I was able to leak the cookie `5eb7b76fbb08895de572fbe6028a4ee4abe3663641416deb5b889e0127920c6e`. I copied and pasted the direct URL on another browser tab, and I got straight to it. I might have over-engineered something so simple, but anyway, Do Hard Things!!
+Retrieving the flag
+
+```zsh
+$ curl -b "developer_secret_cookie=5eb7b76fbb08895de572fbe6028a4ee4abe3663641416deb5b889e0127920c6e" http://play.scriptsorcerers.xyz:10257/developer
+
+Welcome! There is currently 1 unread message: scriptCTF{my_c00k135_4r3_n0t_s4f3!_601871f7aad5}
+```
+
+Flag: `scriptCTF{my_c00k135_4r3_n0t_s4f3!_601871f7aad5}`
+
+# Challenge: Div
+
+![ScriptCTF challenge card for "Div" with its chall.py attachment and an instance launcher][image4]
+Here are the contents of the [chall.py](http://chall.py)
+
+```python
+import os
+import decimal
+decimal.getcontext().prec = 50
+
+secret = int(os.urandom(16).hex(),16)
+num = input('Enter a number: ')
+
+if 'e' in num.lower():
+ print("Nice try...")
+ exit(0)
+
+if len(num) >= 10:
+ print('Number too long...')
+ exit(0)
+
+fl_num = decimal.Decimal(num)
+div = secret / fl_num
+
+if div == 0:
+ print(open('flag.txt').read().strip())
+else:
+ print('Try again...')
+```
+
+Let’s break it down:
+
+1. The server picks a secret — a random 128-bit integer (`os.urandom(16)` → 32 hex chars → huge number).
+
+2. You input a number `num`.
+
+3. If your input contains `'e'` or `'E'` (scientific notation), you get blocked.
+
+4. If your input is ≥ 10 characters, you get blocked.
+
+5. It converts `num` to a `decimal.Decimal` with precision 50.
+
+6. It computes `div = secret / fl_num`.
+
+7. If `div == 0`, you get the flag.
+
+Key observation
+
+- `secret` is positive and huge (on the order of `2128`).
+
+- Normally, `secret / something` won’t be 0.
+
+- In Python `decimal.Decimal`, `div == 0` only if the result is _exactly_ 0. Decimal doesn’t overflow or underflow like float — it has arbitrary precision (set to 50).
+
+So how do we get exactly zero?
+
+- Decimal division only returns `0` if the numerator is `0` OR if the denominator is so huge that the quotient rounds down to 0 at the given precision.
+
+- Precision is 50 significant digits. That means if the result is smaller than `10^(-50)`, it will round to 0.
+
+We want
+
+`secret / fl_num < 10^-50`.
+
+`fl_num > secret × 10^50`
+
+Since
+
+`secret` ≈ `2^128` ≈ `3.4 × 10^38`,
+
+we need
+
+`fl_num > (3.4 × 10^38) × 10^50 = 3.4 × 10^88`
+
+But there’s a catch
+We can’t use exponent notation (e), and the input length must be < 10 characters.
+
+That means we can’t type a literal 100000... with 89 zeros, it’s too long.
+But in Decimal, you can input a decimal point and make something big by making it a very small number and dividing 1 by it.
+
+This is about NaN / Infinity in `decimal.Decimal`. If `fl_num` is `'Infinity'` (allowed, length 8), `secret / Infinity`
+
+```zsh
+nc play.scriptsorcerers.xyz 10456
+
+Enter a number: Infinity
+scriptCTF{70_1nf1n17y_4nd_b3y0nd_20e422d293fc}
+```
+
+Flag: `scriptCTF{70_1nf1n17y_4nd_b3y0nd_20e422d293fc}`
+
+Some context
+Python’s decimal.Decimal module implements IEEE 754 decimal floating-point arithmetic.
+It accepts strings like:
+
+"Infinity" or "inf" → positive infinity
+
+"-Infinity" → negative infinity
+
+"NaN" → not-a-number
+
+These are special constants, not parsed as normal numbers.
+
+Under IEEE 754 rules:
+
+- finite_number / Infinity = 0 exactly
+
+- finite_number / -Infinity = -0 (also treated as zero)
+
+Infinity represents a number so large that any finite numerator divided by it tends towards 0 exactly, with no rounding error.
+
+# Challenge: Emoji
+
+![ScriptCTF challenge card for "emoji" describing the challenge and its out.txt attachment][image5]
+Out.txt has some symbols.
+
+```txt
+🁳🁣🁲🁩🁰🁴🁃🁔🁆🁻🀳🁭🀰🁪🀱🁟🀳🁮🁣🀰🁤🀱🁮🁧🁟🀱🁳🁟🁷🀳🀱🁲🁤🁟🀴🁮🁤🁟🁦🁵🁮🀡🀱🁥🀴🀶🁤🁽
+```
+
+The symbols are Unicode Domino Tiles (in the U+1F030–U+1F093 range). If you subtract 0x1F000 (the start of the Mahjong/Domino emoji area) from each character’s code point, you get plain ASCII bytes. You can learn more about Unicode here.
+
+- [https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/](https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/)
+- [https://en.wikipedia.org/wiki/Dominoes#Dominoes_in_Unicode:~:text=%5B38%5D-,Dominoes%20in%20Unicode,-%5Bedit%5D](https://en.wikipedia.org/wiki/Dominoes#Dominoes_in_Unicode:~:text=%5B38%5D-,Dominoes%20in%20Unicode,-%5Bedit%5D)
+- https://www.reedbeta.com/blog/programmers-intro-to-unicode/
+
+Here is a Python script for that(old habits die hard)
+
+```python
+s = "🁳🁣🁲🁩🁰🁴🁃🁔🁆🁻🀳🁭🀰🁪🀱🁟🀳🁮🁣🀰🁤🀱🁮🁧🁟🀱🁳🁟🁷🀳🀱🁲🁤🁟🀴🁮🁤🁟🁦🁵🁮🀡🀱🁥🀴🀶🁤🁽"
+print(''.join(chr(ord(ch) - 0x1F000) for ch in s))
+```
+
+```zsh
+$ python3 solve.py
+scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}
+```
+
+Flag : `scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}`
+
+# Challenge: Enchant
+
+![ScriptCTF challenge card for "Enchant" describing the Minecraft enchantment-table glyphs and its enc.txt attachment][image6]
+After some Google search, I found out that’s the Standard Galactic Alphabet, the glyphs used by Minecraft’s enchantment table. The enchantment-table glyphs map one-to-one to letters in the Standard Galactic Alphabet (SGA). I used the SGA ↔ Latin mapping to translate each symbol into its English letter at [https://www.dcode.fr/standard-galactic-alphabet](https://www.dcode.fr/standard-galactic-alphabet). The result is MINECRAFTISFUN.
+
+Flag: `scriptCTF{MINECRAFTISFUN}`
+
+Challenge: diskchal
+![ScriptCTF challenge card for "diskchal" describing the vanished flag and its stick.img attachment][image7]
+This was a forensics challenge; we were given a disk image, probably to recover some files, as the challenge description suggests.
+
+```zsh
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/diskchal$ file stick.img
+stick.img: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkfs.fat", sectors 49152 (volumes <=32 MB), Media descriptor 0xf8, sectors/track 32, heads 4, FAT (32 bit), sectors/FAT 378, reserved 0x1, serial num
+ber 0x8caae860, unlabeled
+```
+
+The file is a FAT32 filesystem image created with `mkfs.fat`.I try to mount the image and see what's inside.
+
+```zsh
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/diskchal$ sudo mount stick.img mnt/
+
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/diskchal$ ls mnt
+notes.txt random_thoughts.txt
+
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/diskchal$ cat * mnt/
+cat: mnt: Is a directory
+�X�mkfs.fat �� z�)`誌NO NAME FAT32 �w|�"�t
+ V^2�����This is not a bootable disk. Please insert a bootable floppy and
+press any key to try again ...
+U�RRaArrAa��U��X�mkfs.fat �� z�)`誌NO NAME FAT32 �w|�"�t
+ V���^��2�����This is not a bootable disk. Please insert a bootable floppy and
+press any key to try again ...
+U�RRaArrAa��U�������������������������������Anotes�.txt������NOTES TXT Rk��Z[k��ZLBts.tx hash.txt
+
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/justsome-avocado$ john --format=PKZIP hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
+
+Using default input encoding: UTF-8
+Loaded 1 password hash (PKZIP [32/64])
+Will run 4 OpenMP threads
+Press 'q' or Ctrl-C to abort, almost any other key for status
+impassive3428 (justsomezip.zip)
+1g 0:00:00:01 DONE (2025-08-17 17:16) 0.7633g/s 5609Kp/s 5609Kc/s 5609KC/s in4nd0keren..imissdesi
+Use the "--show" option to display all of the cracked passwords reliably
+Session completed.
+```
+
+After a few seconds, the password is revealed to be `impassive3428`. Inside the zip were two files with the same name as before
+
+```zsh
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/justsome-avocado/justsomezip$ tree
+.
+├── justsomezip.zip
+└── staticnoise.wav
+
+1 directory, 2 files
+
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/justsome-avocado/justsomezip$ file *
+justsomezip.zip: Zip archive data, made by v3.0 UNIX, extract using at least v1.0, last modified Jun 24 2025 19:12:42, uncompressed size 28, method=store
+staticnoise.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, stereo 22050 Hz
+```
+
+I tried brute-forcing the password again, but there were no positive results, so I shifted my focus onto the WAV file. Running exiftool on it showed a telling comment. The password couldn't be brute-forced
+
+```zsh
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/justsome-avocado/justsomezip$ exiftool staticnoise.wav
+ExifTool Version Number : 13.25
+File Name : staticnoise.wav
+Directory : .
+File Size : 438 kB
+File Modification Date/Time : 2025:06:25 13:38:40+03:00
+File Access Date/Time : 2025:08:17 17:18:23+03:00
+File Inode Change Date/Time : 2025:08:17 17:18:17+03:00
+File Permissions : -rw-r--r--
+File Type : WAV
+File Type Extension : wav
+MIME Type : audio/x-wav
+Encoding : Microsoft PCM
+Num Channels : 2
+Sample Rate : 22050
+Avg Bytes Per Sec : 88200
+Bits Per Sample : 16
+Comment : What if my password isn't on rockyou.txt
+Software : Lavf61.7.100
+Duration : 4.96 s
+
+```
+
+I opened the file in Sonic Visualizer and applied a spectrogram view. Here is what shows
+![Sonic Visualizer spectrogram of staticnoise.wav showing partially clipped hidden text resembling "d41"][image11]
+
+It looks like some embedded text, but it's clipped. I redid the same process in Audacity, and this is what it shows.
+
+![Audacity spectrogram of staticnoise.wav showing more legible hidden text beginning with "d41v3"][image12]
+
+I can make out the first three characters, d41, and the fifth `3`. We can use John's mask mode to generate the possible combinations.
+
+```zsh
+john --mask='d41?1?1?1?1?1' --1='?l?d' --format=PKZIP hash.txt
+```
+
+- d41 = known prefix
+- ?1?1?1?1?1 = 5 unknown characters
+- --1='?l?d' = defines ?1 as “lowercase letters or digits”
+- --format=PKZIP = tells John it’s a ZIP hash
+
+```zsh
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/justsome-avocado/justsomezip$ john --mask='d41?1?1?1?1?1' --1='?l?d' --format=PKZIP hash.txt
+
+Using default input encoding: UTF-8
+Loaded 1 password hash (PKZIP [32/64])
+Will run 4 OpenMP threads
+Press 'q' or Ctrl-C to abort, almost any other key for status
+d41v3ron (justsomezip.zip/flag.txt)
+1g 0:00:00:00 DONE (2025-08-17 21:23) 1.388g/s 9545Kp/s 9545Kc/s 9545KC/s d41v7non..d41oddon
+Use the "--show" option to display all of the cracked passwords reliably
+Session completed.
+
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/justsome-avocado/justsomezip$ unzip justsomezip.zip
+Archive: justsomezip.zip
+[justsomezip.zip] flag.txt password:
+extracting: flag.txt
+
+mockingspectre@kali:~/ctf/scriptctf2025/forensics/justsome-avocado/justsomezip$ cat flag.txt
+scriptCTF{1_l0ve_d41_v3r0n}
+```
+
+Flag: `scriptCTF{1_l0ve_d41_v3r0n}`
+
+# Challenge: RSA-1
+
+![ScriptCTF challenge card for "RSA-1" describing the broadcast attack scenario and its out.txt attachment][image13]
+
+The attachment out.txt has
+
+```txt
+out.txt has: n1 = 156503881374173899106040027210320626006530930815116631795516553916547375688556673985142242828597628615920973708595994675661662789752600109906259326160805121029243681236938272723595463141696217880136400102526509149966767717309801293569923237158596968679754520209177602882862180528522927242280121868961697240587 c1 = 77845730447898247683281609913423107803974192483879771538601656664815266655476695261695401337124553851404038028413156487834500306455909128563474382527072827288203275942719998719612346322196694263967769165807133288612193509523277795556658877046100866328789163922952483990512216199556692553605487824176112568965 n2 = 81176790394812943895417667822424503891538103661290067749746811244149927293880771403600643202454602366489650358459283710738177024118857784526124643798095463427793912529729517724613501628957072457149015941596656959113353794192041220905793823162933257702459236541137457227898063370534472564804125139395000655909 c2 = 40787486105407063933087059717827107329565540104154871338902977389136976706405321232356479461501507502072366720712449240185342528262578445532244098369654742284814175079411915848114327880144883620517336793165329893295685773515696260299308407612535992098605156822281687718904414533480149775329948085800726089284 n3 = 140612513823906625290578950857303904693579488575072876654320011261621692347864140784716666929156719735696270348892475443744858844360080415632704363751274666498790051438616664967359811895773995052063222050631573888071188619609300034534118393135291537302821893141204544943440866238800133993600817014789308510399 c3 = 100744134973371882529524399965586539315832009564780881084353677824875367744381226140488591354751113977457961062275480984708865578896869353244823264759044617432862876208706282555040444253921290103354489356742706959370396360754029015494871561563778937571686573716714202098622688982817598258563381656498389039630 e = 3
+```
+
+this is the classic low-exponent (e = 3) broadcast attack. To crack it:
+
+1. Use the three ciphertexts c1,c2,c3c_1,c_2,c_3c1,c2,c3 and moduli n1,n2,n3n_1,n_2,n_3n1,n2,n3.
+
+2. Combine them with the Chinese Remainder Theorem to reconstruct M=m3 mod NM = m^3 bmod NM=m3modN where N=n1n2n3N = n_1 n_2 n_3N=n1n2n3. Because the original message mmm is smaller than each modulus and m3 $(curl -s -o /dev/null -w '%{http_code}' \
+ -H "Authorization: token ${GITEA_TOKEN}" "${url}/api/v1/version")"
+ for proto in --http2 --http1.1; do
+ code=$(curl -s $proto -o /tmp/pre.json -w '%{http_code}' \
+ -X POST "${url}/api/v1/markdown" \
+ -H "Authorization: token ${GITEA_TOKEN}" \
+ -H "Content-Type: application/json" \
+ -H "Expect:" \
+ --data-binary '{"text":"preflight","mode":"markdown"}')
+ echo "POST /api/v1/markdown ${proto} -> ${code}"
+ [ "$code" = "200" ] || head -c 300 /tmp/pre.json; echo
+ done
+
+ - name: Mirror all repos to Gitea
+ env:
+ GITHUB_TOKEN: ${{ secrets.GH_PAT }}
+ GITEA_URL: ${{ secrets.GITEA_URL }}
+ GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
+ GITEA_OWNER: ${{ secrets.GITEA_OWNER }}
+ DEBUG_HTTP: ${{ inputs.debug_http && '1' || '' }}
+ run: ./mirror-github-to-gitea.sh
+```
+
+A few decisions worth explaining.
+
+**SSH on 2222, not 22.** The host's own sshd already owns port 22, so Gitea's internal SSH got mapped to 2222, with [`SSH_PORT`](https://docs.gitea.com/administration/config-cheat-sheet/) set to match so Gitea generates correct clone URLs.
+
+{% %}
+I later set `GITEA__server__DISABLE_SSH=true` (see [`DISABLE_SSH`](https://docs.gitea.com/administration/config-cheat-sheet/) in Gitea's config cheat sheet) and dropped the port mapping entirely. Cloudflare's proxy only forwards HTTP and HTTPS traffic, not arbitrary TCP, so SSH on 2222 was simply unreachable once `gitea.andwati.com` sat behind the orange cloud. The two ways around that were a DNS-only subdomain, which leaks the origin IP to anyone who runs `dig`, or a Cloudflare Tunnel, which is more moving parts than a personal git server's SSH clone was worth. HTTPS clone already worked fine through the proxy, so disabling SSH was the simpler trade.
+{% %}
+
+**Traefik/Dokploy terminates TLS, Gitea speaks plain HTTP internally.** [Dokploy's domain manager](https://docs.dokploy.com/docs/core/domains) handles the Let's Encrypt cert and routes to the container on port 3000. `REVERSE_PROXY_TRUSTED_PROXIES` is set so Gitea trusts the forwarded headers instead of thinking every request comes from the internal Docker network.
+
+**Mail via Gmail SMTP**, gated behind an App Password since Google dropped plain password auth for SMTP a while back.
+
+**Everything as env vars with sane defaults**, so the only things I actually type into Dokploy per deploy are the domain, the DB password, and the mail credentials.
+
+## Token scopes
+
+Two tokens drive the mirror script, and both need to be scoped correctly or the migrate call fails in ways that look unrelated to permissions.
+
+### **GitHub token (`GH_PAT`).**
+A classic Personal Access Token with the `repo` scope, generated at [github.com/settings/tokens](https://github.com/settings/tokens). This is the scope GitHub's own docs point to for reading private repositories, and it's also what Gitea's own migration guide recommends when the source is GitHub. A fine-grained token works too, provided it has read access to Contents and Metadata across all the repos you want mirrored, but classic `repo` is simpler to reason about for a personal account with dozens of repos rather than picking them one by one.
+
+### **Gitea token (`GITEA_TOKEN`).**
+Needs the `write:repository` scope, generated from your Gitea instance at Settings → Applications → Generate New Token (`https:///user/settings/applications`). `write:repository` covers both the `/repos/migrate` call that creates new mirrors and the `PATCH` call the script uses to sync visibility on existing ones. `read:repository` alone isn't enough since both of those are write operations.
+
+### **Where they get set.**
+Both live as encrypted secrets on the `gitea-mirror-sync` repo, not in the script or the workflow file: Settings → Secrets and variables → Actions → New repository secret, alongside `GITEA_URL` and `GITEA_OWNER`. The workflow references them only as `${{ secrets.GH_PAT }}` and `${{ secrets.GITEA_TOKEN }}`, so neither value is ever visible in a log or checked into the repo itself.
+
+## Bot Fight Mode: the wall you can't route around
+
+The bot-challenge problem looked like a quick fix at first. Add a WAF custom rule that matches the hostname and skips security checks: Bot Fight Mode, managed rules, browser integrity check, all of it. That got a plain `curl` from my own machine through cleanly. It did nothing for the GitHub Actions runner, which kept getting served a JS challenge page on every write request.
+
+The reason turned out to be a real platform limit, not a rule I'd written wrong. Cloudflare's free-tier [Bot Fight Mode](https://developers.cloudflare.com/bots/get-started/bot-fight-mode/) doesn't run on the Ruleset Engine at all. It's a separate evaluation pipeline where `Skip`, `Bypass`, and `Allow` actions have no effect. Only Super Bot Fight Mode, which needs a Pro plan, runs on the Ruleset Engine and actually respects skip rules. On Free there's no way to carve out an exception for a specific host or path. It's off or it's on for the whole zone.
+
+For a git server whose traffic is almost entirely programmatic (API calls, `git clone`, CI runs, registry pushes), Bot Fight Mode was fighting the exact use case the server exists for. I turned it off zone-wide (Security → Settings → Bot traffic) rather than pay for Pro just to get a skip rule that would only ever have applied to this one subdomain anyway.
+
+## The 422 that took a while to track down
+
+With Cloudflare out of the way, the mirror script started reaching Gitea and immediately failed every single repo with:
+
+```
+{"message":"[]: Empty Content-Type","url":".../api/swagger"}
+```
+
+`Content-Type: application/json` was set explicitly on every `curl` call, so the error made no sense at first glance. The actual cause was structural. The migrate request was built with `curl -d @- <\n`, and that bare newline terminates the header block early. Every header written after it, `Content-Type` included, lands in the request body instead of staying a header. GETs survive this because nothing they need comes after `Authorization`. Writes don't. The final version of the script trims all four credentials on load, warns if any of them needed trimming, and falls back to retrying over HTTP/1.1 if a write still comes back with "Empty Content-Type," since some reverse proxies handle HTTP/2 header framing differently than HTTP/1.1.
+
+## What the script does now
+
+It pages through `/user/repos` on GitHub, not the public-only `/users/{user}/repos`, so private repos are included. For each one:
+
+- **Doesn't exist in Gitea yet** → migrates it as a pull mirror, matching GitHub's visibility (`private` flows straight through).
+- **Already mirrored, visibility has drifted** → `PATCH`es the Gitea repo to match GitHub's current visibility. Covers the case where a repo gets made private on GitHub after the mirror was first created.
+- **Already mirrored, visibility matches** → no-op.
+
+It also picked up an `ONLY=` escape hatch for smoke-testing one repo before running the whole account through it, and a `DEBUG_HTTP=1` flag that dumps redacted request and response headers when something's misbehaving.
+
+The [workflow](https://github.com/andwati/gitea-mirror-sync/blob/main/.github/workflows/mirror-to-gitea.yml) runs it daily on a [cron schedule](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule), with a `workflow_dispatch` trigger for manual runs, plus a preflight step that POSTs a harmless request to Gitea's markdown-render endpoint first and reports whether `Content-Type` survived the trip. If Cloudflare or the proxy chain ever regresses again, the workflow fails fast on the preflight instead of quietly mangling every repo migration.
+
+## Takeaways
+
+- **Resource-match the tool to the box.** GitLab is the obvious choice and the wrong one for an 8GB personal server running other things too. Gitea's footprint fits reality.
+- **Cloudflare's proxy has real, non-obvious interactions with anything that isn't a browser.** TLS issuance and bot protection both assume the traffic model is "human on a webpage." A git server's actual traffic model is closer to machine talking to machine, on a schedule, indefinitely.
+- **Free-tier Bot Fight Mode is not configurable, full stop.** If your infrastructure sees more API and CI traffic than browser traffic, know going in that the free bot protection can't be scoped down. It's off or it's Pro.
+- **Heredocs and piped `while read` loops don't mix well when something inside the loop also wants stdin.** Process substitution sidesteps the whole category of bug.
+- **Trim your secrets.** A trailing newline in a pasted token is invisible in every UI that shows it back to you, and it will produce an error message that points nowhere near the actual cause.
diff --git a/content/writings/semantic-version-and-conventional-commits/index.md b/content/writings/semantic-version-and-conventional-commits/index.md
new file mode 100644
index 0000000..6e65de6
--- /dev/null
+++ b/content/writings/semantic-version-and-conventional-commits/index.md
@@ -0,0 +1,86 @@
++++
+title = "Semantic Versioning(SemVer) and Conventional Commits"
+description = "Adopting best practices in your git and build workflows."
+author = "andwati"
+date = 2023-01-24
+draft = false
+
+[taxonomies]
+tags = [ "git", "semver", "workflow", "conventional commits" ]
+
++++
+
+Adopting best practices in your git and build workflows.
+
+# What is Semver?
+
+Semantic versioning or just SemVer is a dependency naming guidline that aims to bring some sanity into naming libraries and packages in software development. It is a standard versioning system created by **Tom Preston Werner** (co-founder of Github) that’s used to communicate what changes are in a release.
+
+An efficient and smooth versioning becomes crucial for efficient software delivery expecially for projects that expose a public API. This saves the developers the chaos that is **dependency hell**.Dependency hell is a colloquial term for the frustration of some software users who have installed software packages which have dependencies on specific versions of other software packages.
+
+The dependency issue arises when several packages have dependencies on the same shared packages or libraries, but they depend on different and incompatible versions of the shared packages. If the shared package or library can only be installed in a single version, the user may need to address the problem by obtaining newer or older versions of the dependent packages. This, in turn, may break other dependencies and push the problem to another set of packages.
+
+## Semantic Versioning Specifications
+
+### Summary
+
+Semantic versioning is based on three numbers that indicate the versions of the software and the compatibilities.They are `X.Y.Z` corresponding to `Major.Minor.Patch`
+
+- **Major Version (X):**Introduces major changes to the code and breaking changes toward backward compatibility
+- **Minor Version (Y):** These code changes don’t introduce breaking changes. This could be adding a new feature that doesn't break changes but are not exactly bug fixes.
+- **Patches (Z):** Mainly correspond to bug fixes
+
+### Examples in common packages
+
+#### React(v18.2.0)
+
+- **Major -> 18**
+- **Minor -> 2**
+- **Patch -> 0**
+
+---
+
+#### Pipx(v1.1.0)
+
+- **Major -> 1**
+- **Minor -> 1**
+- **Patch -> 0**
+
+---
+
+## Start at v0.1.0 or v1.0.0?
+
+Convention recommends that in the initial development phase the starting version should be 0.1.0 which makes sense since it’s not in production yet.
+
+The version should only be bumped to 1.0.0 only if it is being used in production and stable.
+
+If your software is being used in production, it should probably already be 1.0.0. If you have a stable API on which users have come to depend, you should be 1.0.0. If you’re worrying a lot about backwards compatibility, you should probably already be 1.0.0.
+
+# Conventional Commits
+
+Conventional Commits is a convention on top of commit messages that are used to automate version changes. It provides an easy set of rules for creating commit messages and works well with SemVer by describing features, fixes and breaking changes that correspond to SemVer standards.
+
+## Summary
+
+The commit message should be structured as follows:
+
+```
+[optional scope]:
+
+[optional body]
+
+[optional footer(s)]
+```
+
+The commit contains the following structural elements, to communicate intent to the consumers of your library:
+
+1. fix: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
+2. feat: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).
+3. BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
+4. types other than fix: and feat: are allowed, for example @commitlint/config-conventional (based on the Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:, and others.
+5. footers other than BREAKING CHANGE: may be provided and follow a convention similar to git trailer format.
+
+# Resources
+
+
+
diff --git a/content/writings/subresource-integrity/index.md b/content/writings/subresource-integrity/index.md
new file mode 100644
index 0000000..1ee4bb9
--- /dev/null
+++ b/content/writings/subresource-integrity/index.md
@@ -0,0 +1,120 @@
++++
+title = "Subresource Integrity"
+description = "Understanding Subresource Integrity"
+author = "andwati"
+date = 2023-04-29
+draft = false
+
+[taxonomies]
+tags = [ "subresource integrity", "csp", "cors" ]
+
++++
+
+Understanding Subresource Integrity
+
+In my junior year while I was learning HTML and CSS I got to use the Bootstrap css library.The easiest way to obtain the files was from a Content Delivery Network(CDN). I could see links such as:
+
+```html
+
+```
+
+I didn't bother with the `integrity` attribute of the link(guess I wasn't curios enough). The integrity attribute has a cryptographic hash value that is important in CDNs and its called the Subresource Integrity.
+
+Subresource Integrity (SRI) is a security feature in web browsers that helps prevent malicious or compromised scripts from being loaded and executed on a website.a security feature that enables browsers to verify that resources they fetch (for example, from a CDN) are delivered without unexpected manipulation. It works by allowing you to provide a cryptographic hash that a fetched resource must match.
+
+When a resource, such as a script or stylesheet, fails the SRI check, the browser will block the resource from being loaded and displayed on the website.
+
+# How Subresource Integrity Helps
+
+Subresource Integrity (SRI) is a security feature that helps protect against certain types of attacks, such as those that involve malicious modifications to third-party resources that a website loads.
+
+SRI works by allowing a website to specify a cryptographic hash for a particular external resource (such as a JavaScript file or a CSS stylesheet) that it expects to load. When a user's browser fetches that resource, it checks the hash against the actual contents of the resource. If the hash doesn't match, the browser will refuse to load the resource, indicating that it may have been tampered with.
+
+This can help prevent a variety of attacks, such as those involving a compromised content delivery network (CDN) or a man-in-the-middle attacker who intercepts and modifies network traffic. By using SRI, a website can help ensure that it is loading only the resources it expects, and that those resources have not been tampered with in transit.
+
+# Using it
+
+Using Subresource Integrity (SRI) involves a few steps, but it is relatively straightforward.An integrity value begins with at least one string, with each string including a prefix indicating a particular hash algorithm (currently the allowed prefixes are sha256, sha384, and sha512), followed by a dash, and ending with the actual base64-encoded hash.
+
+- Generate a cryptographic hash(a base64-encoded cryptographic hash) of the external resource you want to load on your website. This can be done using a hash function such as SHA-256.
+
+- Add the hash as an attribute to the tag that loads the external resource. For example, for a JavaScript file, you would add the integrity attribute to the `
+```
+
+> An integrity value may contain multiple hashes separated by whitespace. A resource will be loaded if it matches one of those hashes.
+
+Make sure the hash you've generated matches the actual contents of the external resource.
+
+Test your website to ensure that the external resource is loading correctly and that the SRI feature is working as intended.
+
+# Tools for generating SRI hashes
+
+You can use a tool such as the [SRI Hash Generator](https://www.srihash.org/) to generate the hash and check it against the file's contents.
+
+You can generate SRI hashes from the command-line with openssl using a command invocation such as:
+
+```sh
+cat FILENAME.py | openssl dgst -sha384 -binary | openssl base64 -A
+```
+
+or with shasum using a command invocation such as:
+
+```sh
+shasum -b -a 384 FILENAME.js | awk '{ print $1 }' | xxd -r -p | base64
+```
+
+> The pipe-through xxd step takes the hexadecimal output from shasum and converts it to binary.
+> The pipe-through awk step is necessary because shasum will pass the hashed filename in its output to xxd. That can have disastrous consequences if the filename happens to have valid hex characters in it — because xxd will also decode that and pass it to base64.
+
+In a Windows environment, you can create a tool for generating SRI hashes with the following code:
+
+```bat
+@echo off
+set bits=384
+openssl dgst -sha%bits% -binary %1% | openssl base64 -A > tmp
+set /p a= < tmp
+del tmp
+echo sha%bits%-%a%
+pause
+```
+
+To use that code:
+
+1. Save that code in a file named sri-hash.bat in the Windows SendTo folder in your environment (for example, C:\Users\USER\AppData\Roaming\Microsoft\Windows\SendTo).
+1. Right-click a file in the File Explorer, select Send to…, and then select sri-hash. You will see the integrity value in a command box.
+1. Select the integrity value and right-click to copy it to the Clipboard.
+1. Press any key to dismiss the command box
+
+# Cross-Origin Resource Sharing and Subresource Integrity
+
+When verifying the subresource integrity of a resource that originates from a different domain than the document it's embedded in, web browsers will check the resource using Cross-Origin Resource Sharing (CORS) to ensure that the domain serving the resource allows it to be shared with the requesting domain.
+As a result, the resource must be served with an Access-Control-Allow-Origin header that permits the resource to be shared with the requesting domain. For example, the header could include a wildcard (\*) to allow any domain to access the resource.
+
+```
+Access-Control-Allow-Origin: *
+```
+
+# How browsers handle Subresource Integrity
+
+Web browsers employ several mechanisms to handle Subresource Integrity (SRI). When a **script** or **link** element with an integrity attribute is encountered by the browser, it compares the script or stylesheet to the expected hash given in the integrity value before executing the script or applying the stylesheet.
+
+For resources served from a different domain than the document in which it's embedded, the browser also checks the resource using Cross-Origin Resource Sharing (CORS) to ensure that the origin serving the resource allows it to be shared with the requesting domain. This additional check is important for preventing unauthorized access to sensitive resources.
+
+If the script or stylesheet fails to match its associated integrity value, the browser will refuse to execute the script or apply the stylesheet and return a network error, indicating that the fetching of that script or stylesheet failed. This behavior ensures that the resource being loaded is trustworthy and helps to protect users from potential security threats.
+
+# References
+
+- [https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
+- [https://www.srihash.org/](https://www.srihash.org/)
+- [https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)
+- [https://html.spec.whatwg.org/multipage/semantics.html#attr-script-integrity](https://html.spec.whatwg.org/multipage/semantics.html#attr-script-integrity)
+- [https://html.spec.whatwg.org/multipage/semantics.html#attr-link-integrity](https://html.spec.whatwg.org/multipage/semantics.html#attr-link-integrity)
+- [https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute](https://w3c.github.io/webappsec-subresource-integrity/#the-integrity-attribute)
diff --git a/content/writings/the-single-byte-that-kills-your-exploit-understanding-endianness/index.md b/content/writings/the-single-byte-that-kills-your-exploit-understanding-endianness/index.md
new file mode 100644
index 0000000..bad579e
--- /dev/null
+++ b/content/writings/the-single-byte-that-kills-your-exploit-understanding-endianness/index.md
@@ -0,0 +1,81 @@
++++
+title = "The Single Byte That Kills Your Exploit: Understanding Endianness"
+description = "Problem: Computers speak different languages, like people"
+date = 2025-11-09
+draft = false
+
+[taxonomies]
+tags = [ "blog" ]
+
++++
+
+If you’ve ever crafted a perfect shellcode and ROP chain only to have your exploit immediately crash with a SIGSEGV(a signal sent by the operating system to a program when it attempts to access a protected or invalid memory location) or EIP(a 32-bit CPU register in the x86 architecture that holds the memory address of the next machine instruction to be executed) pointing to garbage, you’ve likely met the silent killer of beginners: Endianness.
+
+Endianness is fundamental; it defines the order in which a multi-byte value (like a 4-byte integer or an 8-byte memory address) is stored in a machine’s memory. For us in the pwn game, this translates directly to whether the addresses we pack into our exploit payloads are interpreted correctly by the target program. A single misplaced byte means the difference between a shell and a stack smash(stack buffer overflow**)**.
+
+## Little vs. Big: The Two Orders
+
+There are two primary byte orders
+
+Little-Endian (LE): The Least Significant Byte (LSB) is stored first, at the lowest memory address. This is the byte order used by the vast majority of personal computers, including all modern Intel/AMD (x86/x64) architectures.
+
+Big-Endian (BE): The Most Significant Byte (MSB) is stored first, at the lowest memory address. This is often called Network Byte Order (NBO) and is common in network protocols, as well as some older or embedded architectures like PowerPC, MIPS, and SPARC.
+
+## Cross-architecture exploitation (LE <-> BE)
+
+If you’re attacking an embedded device, router, or IoT target, it might be Big-Endian. Always confirm the target architecture and ABI. Common mistakes when cross-compiling or reusing gadgets:
+
+Using x86/x86_64 gadgets and packers on a MIPS/PowerPC target (different endianness and instruction set) will not work.
+
+pwntools and other frameworks allow you to set `context.arch` and `context.endian` So packers and disassemblers behave correctly.
+
+## How to detect the target’s endianness quickly
+
+Run the small C program above on the target if you have code execution. Use `readelf`, `file`, or `objdump` On binaries from the target filesystem, some formats indicate endianness.
+
+In GDB, examine the memory representation of a known constant or pointer. Put a known value in a register or memory, and inspect the byte order with `x/4xb address`.
+
+```gdb
+(gdb) p/x 0x41424344
+$1 = 0x41424344
+(gdb) set {int}0x601000 = 0x41424344
+(gdb) x/4xb 0x601000
+0x601000: 0x44 0x43 0x42 0x41 # little-endian: ‘DCBA’
+```
+
+## Debugging tips when an exploit “looks right” but crashes
+
+Print the raw payload bytes you are sending, compare with what the target receives (packet capture or logging), and what the target’s memory shows in a debugger.
+
+Verify pointer size. Are you packing 32-bit or 64-bit addresses?
+
+Check alignment and padding. Are there extra bytes inserted by the protocol or `scanf`/`fgets` behavior?
+
+Confirm endianness on the target; don’t assume x86 for embedded devices.
+
+Use tool packing (`struct.pack`, `pwntools`) instead of hand-reversing where possible.
+
+If attacking over the network, confirm whether fields are consumed in network byte order.
+
+## Short checklist for exploit authors
+
+- Confirm target architecture (x86, x86_64, MIPS, ARM, PowerPC).
+- Confirm endianness (LE or BE).
+- Use correct pointer width (`p32` vs `p64`) and pack accordingly.
+- Use library packing (`struct`, `pwntools`) to avoid manual reversal mistakes.
+- For network services, encode multi-byte protocol fields in network (big-endian) byte order unless the protocol documentation says otherwise.
+- Check struct padding and alignment on the target.
+- Inspect the target’s memory with a debugger to verify the actual byte layout.
+
+## Final thoughts
+
+Endianness is simple in concept but vital in practice. For most modern exploit work, you’ll be on x86*64 (Little-Endian), so make it a habit to pack addresses in reverse byte order relative to how you read them in a disassembler. Use `pwntools`, `struct`, or equivalent to do the packing for you, but keep in mind \_why* bytes are reversed; that understanding is what will let you debug the next crash quickly and confidently.
+
+Recommended reading:
+
+-
+-
+-
+-
+
+Disclaimer: This article was written with AI assistance, for a bit of brainstorming and proofreading.
diff --git a/content/writings/utterances-comments/index.md b/content/writings/utterances-comments/index.md
new file mode 100644
index 0000000..d94f09d
--- /dev/null
+++ b/content/writings/utterances-comments/index.md
@@ -0,0 +1,68 @@
++++
+title = "What say you?"
+description = "Adding a comment system by utterances"
+author = "andwati"
+date = 2023-08-24
+draft = false
+
+[taxonomies]
+tags = [ "blog", "utterances" ]
+
++++
+
+Free speech enhanced
+
+There is no point in writing online if your readers cannot provide feedback for your writings. You might as well be shouting at a void. My ideal commenting system has to be free, open source and should not be a hassle to setup. Luckily, [utterances 🔮](https://github.com/utterance/utterances) checks all of these conditions
+
+It is a lightweight comments widget built on GitHub issues. Use GitHub issues for blog comments, wiki pages and more! The notable features are:
+
+- Open source. 🙌
+- No tracking, no ads, always free. 📡🚫
+- No lock-in. All data stored in GitHub issues. 🔓
+- Styled with Primer, the css toolkit that powers GitHub. 💅
+- Dark theme. 🌘
+- Lightweight. Vanilla TypeScript. No font downloads, JavaScript frameworks or polyfills for evergreen browsers. 🐦🌲
+
+## How it works
+
+When Utterances loads, the GitHub [issue search API](https://developer.github.com/v3/search/#search-issues) is used to find the issue associated with the page based on url, pathname or title. If it cannot find an issue that matches the page, [utterances-bot](https://github.com/utterances-bot) will automatically create an issue the first time someone comments.
+
+To comment, users must authorize the utterances app to post on their behalf using the GitHub [OAuth flow](https://developer.github.com/v3/oauth/#web-application-flow). Alternatively, users can comment on the GitHub issue directly.
+
+# Setting up Utterances
+
+### Repository
+
+Choose the repository utterances will connect to.
+
+Make sure the repo is public, otherwise your readers will not be able to view the issues/comments.
+Make sure the utterances app is installed on the repo, otherwise users will not be able to post comments.
+If your repo is a fork, navigate to its settings tab and confirm the issues feature is turned on.
+
+```
+repo:
+ username/repo
+```
+
+A public GitHub repository. This is where the blog post issues and issue-comments will be posted.
+
+### Theme
+
+Choose an Utterances theme that matches your blog.
+
+### Enable Utterances
+
+Add the following script tag to your blog's template. Position it where you want the comments to appear. Customize the layout using the .utterances and .utterances-frame selectors.
+
+```js
+
+```
+
+The complete documentation can be found at [https://utteranc.es](https://utteranc.es/)
diff --git a/content/writings/vuejs-tailwind/index.md b/content/writings/vuejs-tailwind/index.md
new file mode 100644
index 0000000..182d99f
--- /dev/null
+++ b/content/writings/vuejs-tailwind/index.md
@@ -0,0 +1,126 @@
++++
+title = "Configure Vue.js app With Tailwind CSS"
+description = "Tailwindcss in vuejs "
+author = "andwati"
+date = 2021-06-12
+draft = false
+
+[taxonomies]
+tags = [ "javascript", "vuejs", "tailwindcss" ]
+
++++
+
+A guide on how to use [Tailwind CSS](https://tailwindcss.com/) in your Vue project.
+
+> **Note**
+> This article was originaly published on my old medium blog. I moved it here for archival purposes.
+
+Tailwind is a utility-first CSS framework for rapidly building custom user interfaces. It is an un-opinionated framework which means that it doesn't come with custom configurations like Bootstrap or Bulma(Yeah, I know, no one should tell you how to live your life).
+
+Now let's get to the juicy part that brought us all here. I'll assume that we are using Vue 3 and the Vue CLI is already installed.
+
+## Create a new Vue project
+
+Open a terminal window and type the following commands to create a new Vue project in the current directory.
+
+```sh
+vue create vue-app
+```
+
+wait for the dependencies installation to finish then
+
+```sh
+cd vue-app
+```
+
+to change into the project directory.
+
+## Install the Tailwind CSS and its peer dependencies as a dev dependency using npm
+
+```sh
+npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
+```
+
+## Create your configuration files
+
+Next, generate your `tailwind.config.js` and `postcss.config.js` files:
+
+```sh
+npx tailwindcss init -p
+```
+
+This command will create a minimal `tailwind.config.js` file at the root of your project:
+
+```js
+// tailwind.config.js
+module.exports = {
+ purge: [],
+ darkMode: false, // or 'media' or 'class'
+ theme: {
+ extend: {},
+ },
+ variants: {
+ extend: {},
+ },
+ plugins: [],
+};
+```
+
+It will also create a `postcss.config.js` file that includes tailwindcss and autoprefixer already configured.Postcss is needed to process tailwind:
+
+```js
+// postcss.config.js
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
+```
+
+## Configure Tailwind to remove unused styles in production
+
+In your tailwind.config.js file, configure the purge option with the paths to all of your pages and components so Tailwind can tree-shake unused styles in production builds:
+
+```js
+// tailwind.config.js
+module.exports = {
+ purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
+ darkMode: false, // or 'media' or 'class'
+ theme: {
+ extend: {},
+ },
+ variants: {
+ extend: {},
+ },
+ plugins: [],
+};
+```
+
+## Include Tailwind in your CSS
+
+Navigate to `src/assets/css` and create a new `tailwind.css` file in the folder, and use the @tailwind directive to include Tailwind’s base, components, and utilities styles in the file.
+
+```css
+/* ./src/assets/css/tailwind.css */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+```
+
+Tailwind will swap these directives out at build-time with all of the styles it generates based on your configured design system.
+
+## Import Tailwind into your Vue app
+
+Finally, ensure your CSS file is being imported in your `./src/main.js` file:
+
+```js
+// src/main.js
+import { createApp } from 'vue';
+import App from './App.vue';
+import './index.css';
+
+createApp(App).mount('#app');
+```
+
+You're finished! Now when you run npm run dev, Tailwind CSS will be ready to use in your Vue app! You can now proceed to create awesome stuff
diff --git a/content/writings/what-is-pwn/index.md b/content/writings/what-is-pwn/index.md
new file mode 100644
index 0000000..f8126fd
--- /dev/null
+++ b/content/writings/what-is-pwn/index.md
@@ -0,0 +1,152 @@
++++
+title = "What is pwn?"
+description = "No not that one"
+date = 2025-09-03
+draft = false
+
+[taxonomies]
+tags = [ "blog" ]
+
++++
+
+> Binary Exploitation is a broad topic within Cyber Security which really comes down to finding a vulnerability in the program and exploiting it to gain control of a shell or modifying the program's functions ~ctf101
+
+In most challenges, you’ll be given a Linux binary to exploit, and occasionally a Windows executable. The goal is to find a way to get the program to run in a way that was not intended.
+
+Over the course of this series, we will look at the basics of a binary from simple to advanced concepts. This will be a multipart post covering the concepts over a couple of months. I’ll also use it as a reference to track my own progress.
+
+> This assumes that the reader has some significant experience with programming, i will not be explaining how to write software. The tutorials will be in C, simple enough while being low level for some of the things well do. I recommend reading :
+>
+> _[The C Programming Language](https://www.amazon.com/Programming-Language-2nd-Brian-Kernighan/dp/0131103628)_[, 2nd Edition, by Brian W. Kernighan and Dennis M. Ritchie (Prentice Hall, 1988).](https://www.amazon.com/Programming-Language-2nd-Brian-Kernighan/dp/0131103628)
+
+---
+
+## Concepts to Cover
+
+This roadmap will take you from _“I don’t know how the stack works”_ to _“I can write exploits for CTFs and understand real-world CVEs.”_
+
+### **Episode 1: Getting Started**
+
+- Why binary exploitation matters (CTFs, real-world, research).
+- Computer architecture basics (x86/x64 registers, stack vs heap, memory layout).
+- Compiling and running simple C programs.
+- Intro to ELF binaries.
+
+---
+
+### **Episode 2: Assembly & Tools Crash Course**
+
+- Function calls in assembly (prologue/epilogue, stack frames).
+- Common instructions (`mov`, `call`, `jmp`, `ret`).
+- Debugging with `gdb`, `gef`/`pwndbg`.
+- Disassembly with `objdump`, `radare2`, and `Ghidra`.
+
+---
+
+### **Episode 3: Memory Corruption 101**
+
+- What is a segmentation fault?
+- Stack buffer overflows (first crashes).
+- Off-by-one errors.
+- Writing your first shellcode and executing it locally.
+
+---
+
+### **Episode 4: Exploiting Control Flow**
+
+- Overwriting return addresses.
+- NOP sleds and shellcode execution.
+- Return-to-libc attacks (ret2libc).
+- Environment variables and argument tricks.
+
+---
+
+### **Episode 5: Format String Vulnerabilities**
+
+- How `printf` becomes an exploit primitive.
+- Memory leaks via format strings.
+- Overwriting memory using `%n`.
+
+---
+
+### **Episode 6: Modern Protections & Bypasses (Part 1)**
+
+- NX (non-executable stack) and its bypass with ROP.
+- Intro to ROP gadgets and building small chains.
+- Automating with Pwntools and ROPgadget.
+
+---
+
+### **Episode 7: Modern Protections & Bypasses (Part 2)**
+
+- ASLR: how randomization works.
+- Leaking addresses to defeat ASLR.
+- Stack canaries and partial overwrites.
+
+---
+
+### **Episode 8: Heap Exploitation (Basics)**
+
+- Understanding malloc/free and heap metadata.
+- Classic heap overflows.
+- Use-after-free and double free.
+- Simple heap exploitation demos.
+
+---
+
+### **Episode 9: Heap Exploitation (Advanced)**
+
+- Unlink attacks and unsafe unlink.
+- Fastbin dup tricks.
+- The “House of” techniques:
+ - House of Force
+ - House of Spirit
+ - House of Lore
+ - House of Einherjar
+
+- Tcache poisoning in modern glibc.
+
+---
+
+### **Episode 10: Advanced Exploitation Techniques**
+
+- GOT/PLT overwrites.
+- Ret2dlresolve.
+- Sigreturn-oriented programming (SROP).
+- JOP (Jump-Oriented Programming).
+
+---
+
+### **Episode 11: Kernel & Windows Exploitation (Intro)**
+
+- Userland vs kernel space.
+- Exploiting syscalls for privilege escalation.
+- Windows basics: SEH overwrites, DEP/ASLR bypass.
+- Kernel driver exploitation.
+
+---
+
+### **Episode 12: Modern Defenses & Mitigations**
+
+- Control-Flow Integrity (CFI).
+- Shadow stacks & CET.
+- Hardened malloc and allocator security.
+- Sandboxing & seccomp-bpf.
+
+---
+
+### **Episode 13: Exploit Workflow in Practice**
+
+- Solving a CTF challenge step-by-step.
+- Remote exploitation (exploiting over sockets).
+- Exploit reliability (stability, bruteforcing).
+- Post-exploitation steps (getting a shell, escalating privileges).
+
+---
+
+### **Episode 14: Beyond the Basics**
+
+- Automating exploit discovery with fuzzing (AFL, honggfuzz).
+- Symbolic execution with angr.
+- Real-world case studies (Heartbleed, Dirty COW, Shellshock).
+- Writing weaponized exploits responsibly.
diff --git a/content/writings/wifi-hotspot-arch/index.md b/content/writings/wifi-hotspot-arch/index.md
new file mode 100644
index 0000000..aac97c3
--- /dev/null
+++ b/content/writings/wifi-hotspot-arch/index.md
@@ -0,0 +1,85 @@
++++
+title = "The One Thing Nobody Tells You About WiFi Hotspots on Arch Linux"
+description = "A minimal Arch install is great until NetworkManager silently fails to find a binary you didn't know you needed"
+author = "andwati"
+date = 2026-04-16
+draft = false
+
+[taxonomies]
+tags = [ "arch", "linux", "networking", "kde" ]
+
++++
+
+I spent longer than I'd like to admit trying to get a simple WiFi hotspot running on Arch. Not because it's hard — it genuinely isn't — but because of one missing piece that the NetworkManager documentation buries so deep you'd think it was classified.
+
+For context: I'm running a minimal Arch Linux install with KDE Plasma. No bloat, no extras, just what I explicitly put there. Which, as it turns out, is exactly why this bit me.
+
+Here's the whole story, and more importantly, the fix.
+
+## It starts innocently enough
+
+You fire up `nmcli`, throw together what looks like a perfectly reasonable hotspot command, and get slapped with this:
+
+```
+Error: Connection activation failed: IP configuration could not be reserved (no available address, timeout, etc.)
+```
+
+Vague. Unhelpful. The kind of error message that was clearly written by someone who never had to debug it.
+
+Your first instinct is probably to check if your WiFi card even supports AP mode — reasonable. Mine does. Maybe NetworkManager picked the wrong interface — also reasonable, so you specify `ifname wlp61s0` explicitly. Still fails. You check for a conflicting `dnsmasq` service. It doesn't even exist on the system.
+
+At this point the error message has told you nothing useful, and you're running out of obvious things to blame.
+
+## The actual culprit
+
+Dig into the NetworkManager journal and you'll find the real error hiding two lines below the surface:
+
+```
+device (wlp61s0): ip:shared4: could not start dnsmasq: Could not find "dnsmasq" binary
+```
+
+There it is. NetworkManager's hotspot mode uses `dnsmasq` internally to hand out IP addresses to devices that connect to your hotspot. It doesn't use the system `dnsmasq` service — it manages its own instance entirely in the background. But it does need the binary to actually exist on your machine.
+
+On a minimal Arch install, it doesn't come pre-installed — and if you're not pulling in a full desktop environment with all its recommended packages, nothing drags it in for you either. So the whole thing silently falls apart at the IP configuration stage, and you get that useless generic error instead of "hey, install dnsmasq."
+
+The fix is one line:
+
+```bash
+sudo pacman -S dnsmasq
+```
+
+Don't enable it, don't start it. Just having it on the system is enough. NetworkManager will find it and handle the rest.
+
+## Getting internet to actually work
+
+Once the hotspot is up, you'll notice connected devices get an IP but no internet. The hotspot being active and internet being shared are two separate things — your machine needs to be explicitly told to forward traffic between its WiFi and ethernet interfaces.
+
+First, tell the kernel it's allowed to forward packets at all:
+
+```bash
+sudo sysctl -w net.ipv4.ip_forward=1
+```
+
+Then set up NAT so traffic from WiFi clients gets routed out through your ethernet connection and back:
+
+```bash
+sudo iptables -t nat -A POSTROUTING -o enp0s31f6 -j MASQUERADE
+sudo iptables -A FORWARD -i wlp61s0 -o enp0s31f6 -j ACCEPT
+sudo iptables -A FORWARD -i enp0s31f6 -o wlp61s0 -m state --state RELATED,ESTABLISHED -j ACCEPT
+```
+
+Swap out `enp0s31f6` and `wlp61s0` for whatever your ethernet and WiFi interfaces are actually named. After that, clients connecting to your hotspot will have full internet access.
+
+These settings don't survive a reboot, so if you want them permanent, save them:
+
+```bash
+echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-ipforward.conf
+sudo iptables-save | sudo tee /etc/iptables/iptables.rules
+sudo systemctl enable iptables
+```
+
+## The short version
+
+If your NetworkManager hotspot is failing on Arch, install `dnsmasq`. That's almost certainly it. The error message won't tell you that, the hotspot command won't tell you that, but the journal will — and now so does this article.
+
+Everything else is just routing.
diff --git a/package.json b/package.json
index bb8d004..f1e7415 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@biomejs/biome": "^2.5.11",
"@resvg/resvg-js": "^2.6.2",
"satori": "^0.29.0",
+ "smol-toml": "^1.8.0",
"wawoff2": "^2.0.1"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1ecd1b3..4df8fa4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,9 @@ importers:
satori:
specifier: ^0.29.0
version: 0.29.1
+ smol-toml:
+ specifier: ^1.8.0
+ version: 1.8.0
wawoff2:
specifier: ^2.0.1
version: 2.0.1
diff --git a/scripts/migrate-posts-to-writings.mjs b/scripts/migrate-posts-to-writings.mjs
new file mode 100644
index 0000000..ce9a4aa
--- /dev/null
+++ b/scripts/migrate-posts-to-writings.mjs
@@ -0,0 +1,178 @@
+// One-off migration: converts content/posts/*.md (legacy Zola posts) into
+// content/writings//index.md (new schema). Additive only — never
+// touches content/posts/. Re-runnable: overwrites its own prior output.
+import fs from "node:fs";
+import path from "node:path";
+import {
+ parse as parseToml,
+ stringify as stringifyToml,
+ TomlDate,
+} from "smol-toml";
+
+const root = process.cwd();
+const postsDir = path.join(root, "content", "posts");
+const writingsDir = path.join(root, "content", "writings");
+
+const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
+
+function walk(dir) {
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) return walk(full);
+ return entry.isFile() && entry.name.endsWith(".md") ? [full] : [];
+ });
+}
+
+function slugFor(file) {
+ const rel = path.relative(postsDir, file).split(path.sep).join("/");
+ return rel.endsWith("/index.md")
+ ? rel.slice(0, -"/index.md".length)
+ : rel.slice(0, -".md".length);
+}
+
+function compact(obj) {
+ const out = {};
+ for (const [key, value] of Object.entries(obj)) {
+ if (value === undefined || value === null || value === "") continue;
+ if (Array.isArray(value) && value.length === 0) continue;
+ if (
+ typeof value === "object" &&
+ !Array.isArray(value) &&
+ !(value instanceof Date)
+ ) {
+ const nested = compact(value);
+ if (Object.keys(nested).length === 0) continue;
+ out[key] = nested;
+ continue;
+ }
+ out[key] = value;
+ }
+ return out;
+}
+
+function toIsoDate(value) {
+ if (value instanceof Date)
+ return new TomlDate(value.toISOString().slice(0, 10));
+ if (typeof value === "string") return new TomlDate(value.slice(0, 10));
+ return undefined;
+}
+
+/** Converts the one unambiguous Zola shortcode in use (youtube embeds) to a
+ * plain HTML iframe, and strips Zola's `{% raw %}`/`{% endraw %}` escape
+ * tags (meaningless now — there's no template engine processing the body).
+ * Anything else Zola-specific (note/tip/warning/danger callouts) is left
+ * as-is: those need real Astro components before they can be translated,
+ * so they're flagged for manual follow-up instead of guessed at. */
+function migrateBody(body) {
+ const flags = [];
+ const out = body
+ .replace(/\{%\s*raw\s*%\}\r?\n?/g, "")
+ .replace(/\{%\s*endraw\s*%\}\r?\n?/g, "")
+ .replace(/\{\{<\s*youtube\s+id="([^"]+)"[\s\S]*?\/>\s*\}\}/g, (_m, id) => {
+ return ``;
+ });
+
+ if (/\{%\s*\/?(note|tip|warning|danger)>?\s*%\}/.test(out)) {
+ flags.push("callout-shortcode");
+ }
+ if (/\{\{|\{%/.test(out)) {
+ flags.push("unresolved-template-syntax");
+ }
+ return { body: out, flags };
+}
+
+const files = walk(postsDir).filter((f) => path.basename(f) !== "_index.md");
+fs.mkdirSync(writingsDir, { recursive: true });
+
+let migrated = 0;
+const flaggedFiles = [];
+const migratedSlugs = [];
+
+for (const file of files) {
+ const raw = fs.readFileSync(file, "utf8");
+ const match = raw.match(FRONTMATTER_RE);
+ if (!match) continue;
+ const [, fmSource, body] = match;
+ const fm = parseToml(fmSource);
+ const slug = slugFor(file);
+
+ const { body: newBody, flags } = migrateBody(body.trim());
+ if (flags.length) flaggedFiles.push({ slug, flags });
+
+ const frontmatter = stringifyToml(
+ compact({
+ title: fm.title,
+ description: fm.description,
+ author: fm.author,
+ date: toIsoDate(fm.date),
+ updated: toIsoDate(fm.updated),
+ draft: fm.draft ?? false,
+ canonical_url: fm.canonical_url,
+ taxonomies: { tags: fm.taxonomies?.tags ?? [] },
+ extra: {
+ series: fm.extra?.series,
+ series_index: fm.extra?.series_index,
+ llms_description: fm.extra?.llms_description,
+ },
+ }),
+ );
+
+ const outDir = path.join(writingsDir, slug);
+ fs.mkdirSync(outDir, { recursive: true });
+ fs.writeFileSync(
+ path.join(outDir, "index.md"),
+ `+++\n${frontmatter}\n+++\n\n${newBody}\n`,
+ );
+ migrated += 1;
+ migratedSlugs.push(slug);
+}
+
+// Keep old /posts// URLs alive (RSS subscribers, inbound links) by
+// redirecting to the new /writings// path. Regenerated idempotently
+// inside marker comments so re-running this script doesn't duplicate or
+// clobber the rest of sws.toml.
+const swsPath = path.join(root, "sws.toml");
+const BEGIN =
+ "# BEGIN migrated-posts-redirects (generated by scripts/migrate-posts-to-writings.mjs)";
+const END = "# END migrated-posts-redirects";
+const redirectBlock = [
+ BEGIN,
+ ...migratedSlugs
+ .sort()
+ .flatMap((slug) => [
+ "[[advanced.redirects]]",
+ `source = "/posts/${slug}/"`,
+ `destination = "/writings/${slug}/"`,
+ "kind = 301",
+ "",
+ ]),
+ END,
+]
+ .join("\n")
+ .trimEnd();
+
+function escapeRegExp(s) {
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+const swsSource = fs.readFileSync(swsPath, "utf8");
+const markerRe = new RegExp(
+ `${escapeRegExp(BEGIN)}[\\s\\S]*?${escapeRegExp(END)}`,
+);
+const updatedSws = markerRe.test(swsSource)
+ ? swsSource.replace(markerRe, redirectBlock)
+ : `${swsSource.trimEnd()}\n\n${redirectBlock}\n`;
+fs.writeFileSync(swsPath, `${updatedSws.trimEnd()}\n`);
+
+console.log(
+ `Migrated ${migrated} post(s) from content/posts/ to content/writings/.`,
+);
+console.log(`Wrote ${migratedSlugs.length} redirect(s) to sws.toml.`);
+if (flaggedFiles.length) {
+ console.log(
+ "\nNeeds manual follow-up (Zola-specific syntax with no Astro equivalent yet):",
+ );
+ for (const { slug, flags } of flaggedFiles) {
+ console.log(` - ${slug}: ${flags.join(", ")}`);
+ }
+}
diff --git a/sws.toml b/sws.toml
index df02b7d..3a247e3 100644
--- a/sws.toml
+++ b/sws.toml
@@ -19,3 +19,106 @@ kind = 301
source = "/index.xml"
destination = "/rss.xml"
kind = 301
+
+# BEGIN migrated-posts-redirects (generated by scripts/migrate-posts-to-writings.mjs)
+[[advanced.redirects]]
+source = "/posts/a-fresh-coat-of-paint/"
+destination = "/writings/a-fresh-coat-of-paint/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/file-over-app/"
+destination = "/writings/file-over-app/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/from-c-to-machine-code/"
+destination = "/writings/from-c-to-machine-code/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/generate-django-secret-key/"
+destination = "/writings/generate-django-secret-key/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/hello-world/"
+destination = "/writings/hello-world/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/inertia/"
+destination = "/writings/inertia/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/install-postgresql-15-on-arch-manjaro-garuda-linux/"
+destination = "/writings/install-postgresql-15-on-arch-manjaro-garuda-linux/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/integrating-netlify/"
+destination = "/writings/integrating-netlify/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/javascript-exporting-modules/"
+destination = "/writings/javascript-exporting-modules/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/mmuctf2025/"
+destination = "/writings/mmuctf2025/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/obsidian-sync/"
+destination = "/writings/obsidian-sync/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/scriptctf2025/"
+destination = "/writings/scriptctf2025/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/self-hosting-gitea-and-mirroring-github/"
+destination = "/writings/self-hosting-gitea-and-mirroring-github/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/semantic-version-and-conventional-commits/"
+destination = "/writings/semantic-version-and-conventional-commits/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/subresource-integrity/"
+destination = "/writings/subresource-integrity/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/the-single-byte-that-kills-your-exploit-understanding-endianness/"
+destination = "/writings/the-single-byte-that-kills-your-exploit-understanding-endianness/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/utterances-comments/"
+destination = "/writings/utterances-comments/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/vuejs-tailwind/"
+destination = "/writings/vuejs-tailwind/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/what-is-pwn/"
+destination = "/writings/what-is-pwn/"
+kind = 301
+
+[[advanced.redirects]]
+source = "/posts/wifi-hotspot-arch/"
+destination = "/writings/wifi-hotspot-arch/"
+kind = 301
+
+# END migrated-posts-redirects