[Network] UTCTF 2020 - QUICk Servers

QUICk Servers

Solves: 17

Points: 1988

Description:
I have a pretty cool server, but it’s for QUICk people only. Nobody else is allowed.

Pro Tip: Set your ALPN to “quic-echo-example” because I forgot to remove it.

54.152.23.18:1337

Author: masond

Challenge

I didn’t solve the challenge during the ctf mainly because my lack of experience with golang and also my ability to identify the issues was affected by the lack of sleeping. Anyway this was a cool challenge made me learn about the QUIC protocol and some new things about the go language.

The title of the challenge gives us the hint that this may be a server running on the QUIC protocol also in the description we were given the ip and port to the server.

Initially I tried to use a python library for quick but I failed horribly when connecting to the server, by searching the the hint of setting the APLN to “quic-echo-example” on github I ended up searching some examples on how to connect to a QUIC server using a library named quick-go .

So what exactly is QUIC? Quic is a network-protocol designed by Jim Roskind at Google, it was mainly created to improve the performance of connection-oriented web applications using the UDP protocol instead of TCP.

Finding an example

By searching by “quic-echo-example” on github I found an example.

After this I adapted the source code to connect to the challenge server but I ended up finding a lot of difficulties during of the installation of quick-go lib, every time I tried to install it with go get . command I was receiving an odd error about a “Duplicate stream ID”. Spent a lot of time searching on the web for this and found nothing.

In the end, I ended finding out why I was having problems, I was trying to install the master branch of github and it required 1.14 version of golang… In my host machine I only had the 1.13 installed. To solve this problem I decided to use Docker.

By specifying the right version as the tag I could use the right version of golang:

1
2
3
$ ls 
main.go
$ sudo docker run --rm -v $(pwd):/go/src/myapp -w /go/src/myapp -it golang:1.14 /bin/bash

After this I run into another problem I installed the master branch release which is unstable as fuck and also incompatible with the one running on the server. This is was when I learned about go modules, we can specify the right version with it so I searched in the github releases and the last stable release is v0.14.0:

1
2
3
4
5
6
7
8
9
10
11
$ go mod init .
$ go mod edit -require github.com/lucas-clemente/quic-go@v0.14.0
$ go get -v -t .
$ go build
$ go install
$ cat go.mod
module myapp

go 1.14

require github.com/lucas-clemente/quic-go v0.14.0

And finally I was able to connect to the server:

1
2
3
$ go run main.go
Client: Sending 'feqfq'
Maybe you should start with Hello...

So the server replies that we should start with Hello, first we do the TLS configuration and specify the nextProtos as “quic-echo-example” as specified in the challenge description:

1
2
3
4
tlsConf := &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"quic-echo-example"},
}

Then we create the connection and the stream:

1
2
3
4
5
6
7
8
9
session, err := quic.DialAddr(addr, tlsConf, nil)
if err != nil {
return err
}

stream, err := session.OpenStreamSync(context.Background())
if err != nil {
return err
}

Sending the hello message and receiving the response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

func readBytes(stream io.Reader, n int) error {
for i:=0; i< n; i++ {
buf := make([]byte, 1)
_, err := io.ReadFull(stream, buf);
if err != nil {
return err
}
fmt.Printf("%s", buf);
}
return nil
}

fmt.Printf("Client: Sending '%s'\n", message)
_, err = io.WriteString(stream,message)
if err != nil {
return err
}

err = readBytes(stream, 248)
if err != nil {
return err
}
1
2
3
4
5
6
7
$ go run main.go
Client: Sending 'Hello'
Welcome to the super QUICk Server!
You might've thought getting the flag would be easy, but it's gonna take a bit more. :D

I need some help with my Computer Architecture class, could you give me these numbers back in hex?
123454

This is the first hand of questions and is about converting decimal integers to hexa, this is where I got stuck mainly because I didn’t understand really well how golang read stream functions worked. The problem was on the number extraction, I was reading the last line with the number, but some times the number to be converted had less than 6 numbers and this is where I failed to understand the problem, when less than 6 the last line would be presented as “1234 \n” with spaces between the numbers and the new line, I was only striping the new line, because of this when sending the answer to the server everything started to hang up.

After the CTF and a day of rest I found out about the spaces and took another approach, something that I should have used since the beginning, which is using regex to extract those numbers instead of parsing them by “hand”.

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
func toHex(x []byte, n int) string {
re := regexp.MustCompile("[0-9]+")
h,err := strconv.Atoi(re.FindString(string(x)))
if err != nil {
panic(err)
}
return fmt.Sprintf("%x", h)
}
for i:=0; i< 1000; i++ {
num := make([]byte, 7)
n, err := stream.Read(num);
if err != nil {
return err
}
s := "0x"+toHex(num,n)
//fmt.Printf("%d\n", i)
//fmt.Printf("Received %s", num);
//fmt.Printf("Sending %s\n",s)
//_,err = io.WriteString(stream,s)
_, err = stream.Write([]byte(s))
if err != nil {
return err
}
}
b, err := ioutil.ReadAll(stream)
fmt.Printf("%s\n", b)

After converting 1000 decimal numbers we get the respective answer:

1
2
3
4
5
6
7
8
9
$ go run main.go
Client: Sending 'Hello'
Welcome to the super QUICk Server!
You might've thought getting the flag would be easy, but it's gonna take a bit more. :D

I need some help with my Computer Architecture class, could you give me these numbers back in hex?
Quickly, of course... :)
Nice job, let's keep going...
Can I dial you later? I'll try 6969 ;)

This time the server is trying to connect to us, so we need to turn us into a “server” and listen at the port 6969, for this we need to open a port in the router and rerun the docker container with the -p parameter to link the UDP port with the host:

1
2
3
4
5
6
$ sudo docker run --rm -p 6969:6969/udp -v (pwd):/go/src/myapp -w /go/src/myapp -it golang:1.14 /bin/bash
$ go mod init .
$ go mod edit -require github.com/lucas-clemente/quic-go@v0.14.0
$ go get -v -t .
$ go build
$ go install

Also if you have a local firewall like I have in my computer you need to open that door too, in my case I use UFW firewall:

1
2
3
4
$ sudo ufw allow 6969/udp
$ sudo ufw status
6969/udp ALLOW Anywhere
6969/udp (v6) ALLOW Anywhere (v6)

To run the server we need to put it in another thread, we can use go-coroutines but we also have to add a code that waits for the server thread to end before quitting the main program, this can be pretty easily done with go by using sync.WaitGroup:

1
2
3
4
5
6
7
8
9
10
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() { log.Fatal(echoServer()) }()
err := clientMain()
if err != nil {
panic(err)
}
wg.Wait()
}

In the code above go func() initiates the server coroutine and increases the WaitGroup counter, we put a Wait() in the end of the main function so it waits until the counter reaches the number zero. This happens when echoServer() finishes which will decrease the counter to zero.

Making the server listening at 0.0.0.0:6969 and set up TLS configurations:

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
func echoServer() error {
listener, err := quic.ListenAddr(addrClientS, generateTLSConfig(), nil)
if err != nil {
return err
}
sess, err := listener.Accept(context.Background())
if err != nil {
return err
}
stream, err := sess.AcceptStream(context.Background())
if err != nil {
panic(err)
}

err = readBytes(stream, 26)
if err != nil {
return err
}
...
}
// Setup a bare-bones TLS config for the server
func generateTLSConfig() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
panic(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})

tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
panic(err)
}
return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
NextProtos: []string{"quic-echo-example"},
}
}

Reading the next problem:

1
2
3
4
err = readBytes(stream, 26)
if err != nil {
return err
}

The next problem is to calculate expressions:

1
2
3
Hey... you up?
Math time!
123458 + 341231

Once again using regex to extract everything:

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
func echoServer() error {
...
for i:=0; i< 1000; i++ {
num := make([]byte, 0x30)
_, err = stream.Read(num);
if err != nil {
return err
}
re := regexp.MustCompile("[0-9]+")
re2 := regexp.MustCompile("[-+*/^&]")
num1,_ := strconv.Atoi(re.FindAllString(string(num),-1)[0])
num2,_ := strconv.Atoi(re.FindAllString(string(num),-1)[1])
exp := re2.FindString(string(num))
//fmt.Printf("%s\n", num)
//fmt.Printf("%d\n", num1)
//fmt.Printf("%s\n", exp)
//fmt.Printf("%d\n", num2)
res := calculateExp(num1, num2, exp)
//fmt.Printf("%d\n", res)
_,err = io.WriteString(stream,strconv.Itoa(res))
if err != nil {
return err
}
}

b, err := ioutil.ReadAll(stream)
fmt.Printf("%s\n", b)
}

After calculating 1000 expressions we get the flag:

1
2
Great Job!
utflag{Qu1C_p@cK3t$_a73jc8s}

The full script:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package main

import (
"context"
//"encoding/binary"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"regexp"
"strconv"
"sync"
//"time"
//"strings"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"log"
"math/big"

quic "github.com/lucas-clemente/quic-go"
)

const addr = "192.168.1.3:1337"//"54.152.23.18:1337"
const addrClientS = "0.0.0.0:6969"

const message = "Hello"

// We start a server echoing data on the first stream the client opens,
// then connect with a client, send the message, and wait for its receipt.
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() { log.Fatal(echoServer()) }()
err := clientMain()
if err != nil {
panic(err)
}
wg.Wait()
}

func calculateExp(num1 int,num2 int,exp string) int {
switch exp {
case "+":
return num1 + num2
case "-":
return num1 - num2
case "/":
return num1 / num2
case "*":
return num1 * num2
case "^":
return num1 ^ num2
case "&":
return num1 & num2
}
return 0
}

// Start a server that echos all data on the first stream opened by the client
func echoServer() error {
listener, err := quic.ListenAddr(addrClientS, generateTLSConfig(), nil)
if err != nil {
return err
}
sess, err := listener.Accept(context.Background())
if err != nil {
return err
}
stream, err := sess.AcceptStream(context.Background())
if err != nil {
panic(err)
}

err = readBytes(stream, 26)
if err != nil {
return err
}

for i:=0; i< 1000; i++ {
num := make([]byte, 0x30)
_, err = stream.Read(num);
if err != nil {
return err
}
re := regexp.MustCompile("[0-9]+")
re2 := regexp.MustCompile("[-+*/^&]")
num1,_ := strconv.Atoi(re.FindAllString(string(num),-1)[0])
num2,_ := strconv.Atoi(re.FindAllString(string(num),-1)[1])
exp := re2.FindString(string(num))
//fmt.Printf("%s\n", num)
//fmt.Printf("%d\n", num1)
//fmt.Printf("%s\n", exp)
//fmt.Printf("%d\n", num2)
res := calculateExp(num1, num2, exp)
//fmt.Printf("%d\n", res)
_,err = io.WriteString(stream,strconv.Itoa(res))
if err != nil {
return err
}
}

b, err := ioutil.ReadAll(stream)
fmt.Printf("%s\n", b)
return err
}

func readBytes(stream io.Reader, n int) error {
for i:=0; i< n; i++ {
buf := make([]byte, 1)
_, err := io.ReadFull(stream, buf);
if err != nil {
return err
}
fmt.Printf("%s", buf);
}
return nil
}

func toHex(x []byte, n int) string {
re := regexp.MustCompile("[0-9]+")
h,err := strconv.Atoi(re.FindString(string(x)))
if err != nil {
panic(err)
}
return fmt.Sprintf("%x", h)
}

func clientMain() error {
tlsConf := &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"quic-echo-example"},
}
session, err := quic.DialAddr(addr, tlsConf, nil)
if err != nil {
return err
}

stream, err := session.OpenStreamSync(context.Background())
if err != nil {
return err
}

fmt.Printf("Client: Sending '%s'\n", message)
_, err = io.WriteString(stream,message)//stream.Write([]byte(message))
if err != nil {
return err
}

err = readBytes(stream, 248)
if err != nil {
return err
}

//fmt.Printf("Now number:\n")

for i:=0; i< 1000; i++ {
num := make([]byte, 7)
n, err := stream.Read(num);
if err != nil {
return err
}
s := "0x"+toHex(num,n)
//fmt.Printf("%d\n", i)
//fmt.Printf("Received %s", num);
//fmt.Printf("Sending %s\n",s)
//_,err = io.WriteString(stream,s)
_, err = stream.Write([]byte(s))
if err != nil {
return err
}
}
b, err := ioutil.ReadAll(stream)
fmt.Printf("%s\n", b)




return nil
}

// A wrapper for io.Writer that also logs the message.
type loggingWriter struct{ io.Writer }

func (w loggingWriter) Write(b []byte) (int, error) {
fmt.Printf("Server: Got '%s'\n", string(b))
return w.Writer.Write(b)
}

// Setup a bare-bones TLS config for the server
func generateTLSConfig() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
panic(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})

tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
panic(err)
}
return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
NextProtos: []string{"quic-echo-example"},
}
}