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:

Hello Friend

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.

Hello Friend

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)

Hello Friend

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 :

Hello Friend

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:

Hello Friend

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 get auth_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 :

Hello Friend