4 minutes
NahamCon CTF 2024 - Buggy Jumper 2 [EN]
Catégorie | Mobile |
Difficulté | Medium |
Auteur | matlac |
Introduction
Buggy really wants the drip in the shop... Can you buy it for them?
The challenge was solver after the end of the ctf
Emulation
This challenge is the second part of Buggy Jumper 1
This time, we are going to use an emulator to see how the application works:

In the introduction to the challenge, the application’s shop is mentioned. If you go to the interface, you can see an item on sale for 7331.7331 points.

Getting the points by playing would take too much time, so let’s see if there’s another solution.
Decompilation
Like the first part of the challenge, the application uses GO-compiled sources. We can extract and decompile them using [gdsdecomp].(https://github.com/bruvzg/gdsdecomp)

Let’s take a look at the source code of the shop.gd file
extends Control
var points_label
var owned_label
var bought_label
var poor_label
var flag_label
func _ready():
points_label = get_node("pointsLabel")
owned_label = get_node("ownedLabel")
bought_label = get_node("boughtLabel")
poor_label = get_node("poorLabel")
flag_label = get_node("flagLabel")
points_label.text = "POINTS: " + str(Global.points)
owned_label.visible = false
bought_label.visible = false
poor_label.visible = false
func _on_Main_Menu_pressed():
get_tree().change_scene_to(load("res://MainScene.tscn"))
func _on_Buy_pressed():
var http_request = HTTPRequest.new()
add_child(http_request)
http_request.connect("request_completed", self, "_on_request_completed")
var headers = ["Content-Type: application/json", "Authorization: " + Global.auth_token]
var error = http_request.request(Global.url + "/verify?value=" + str(Global.points), headers)
print(headers)
print(Global.url + "/verify?value=" + str(Global.points))
if error != OK:
pass
func _on_request_completed(result, response_code, headers, body):
if response_code == 200:
var json = JSON.parse(body.get_string_from_utf8())
if json.error == OK:
var message = json.result["message"]
if not Global.has_drippy:
if Global.points >= 73317331 and "flag" in message:
flag_label.text = message
Global.has_drippy = true
bought_label.visible = true
yield (get_tree().create_timer(3.0), "timeout")
bought_label.visible = false
else :
poor_label.visible = true
yield (get_tree().create_timer(3.0), "timeout")
poor_label.visible = false
else :
if "flag" in message:
flag_label.text = message
owned_label.visible = true
yield (get_tree().create_timer(3.0), "timeout")
owned_label.visible = false
else :
pass
else :
pass
We can see that when a purchase is made, a web request is made to a URL followed by the endpoint verify
. The url is represented by the variable Global.url
, contained in the file global.gd
.
var points
var has_drippy = false
var url:String = "http://209.38.153.254:8888"
var auth_token = ""
With Burp, we’ll make a query with this url and our endpoint :

Our User-Agent is the wrong one for the request. After a few fuzzing attempts, I came across the Godot
UA, which returns another error:

Using API
A closer look at the source code reveals two other endpoints in Global.gd
func register():
var http_request = HTTPRequest.new()
add_child(http_request)
http_request.connect("request_completed", self, "_on_request_completed1")
var headers = ["Content-Type: application/json"]
var error = http_request.request(url + "/register", headers, false, HTTPClient.METHOD_POST, "")
if error != OK:
pass
func save():
var http_request = HTTPRequest.new()
add_child(http_request)
http_request.connect("request_completed", self, "_on_request_completed2")
var headers = ["Content-Type: application/json", "Authorization: " + auth_token]
var post_data = {
"value":initial_points
}
var error = http_request.request(url + "/save", headers, false, HTTPClient.METHOD_POST, JSON.print(post_data))
if error != OK:
pass
To summarize the analysis :
/register
: endpoint without authentication, which allows us to getauth_token
sent to us in the response. This token can be used to access to the others endpoints./save
: endpoint that allows you to record a score while being authenticated with an authentication token. The score is sent as input to a json. The request uses the POST method./verify
: endpoint which checks whether the score has been registered under the same auth_token. The score to verify is send as GET parameter in the request.
We can exploit this API by registering and then saving a false score higher than the one required for purchase. Finally, we can make a verification request and retrieve the flag.
$ curl -X POST -H 'User-Agent: Godot' 'http://209.38.153.254:8888/register'
$ curl -X POST -H 'User-Agent: Godot' -H 'Authorization: <auth_token>' -H 'Content-Type: application/json' 'http://209.38.153.254:8888/save' -d '{"value":100000000}'
$ curl -X GET -H 'User-Agent: Godot' -H 'Authorization: <auth_token>' 'http://209.38.153.254:8888/verify?value=100000000'
On the last request, the server responds with the flag :
