Jackpoting Online Slots – part two

Hello security folks out there. After a long and intense period of research, it is time to put our knowledge to the test and see how far we can take our findings from the last blog “Jackpoting Online Slots – part one“. So get ready, boot up your systems, grab some snacks and let’s get started.

warning note

I would like to point out that I do not support gambling. It can very quickly become a very dangerous thing where you can lose everything. Casinos are neither your friends nor charities, in the end they want your money and they get it, ALWAYS. I will not mention any casinos by name because I do not want to endorse them. If you already have symptoms of addiction, so the following blog may trigger you, simply do not read it. There are many places that can help you, such as “responsiblegambling“. Take care of yourselves!

Execution

Now that this has been clarified, let’s move on to the second stage, which is execution. Based on what we know, I have come up with several attack scenarios. Some are already promising in theory, others less so. In this blog we will look at the methods in bold. The rest will be covered in another blog.

  • Winning indicator
  • Recurring Events Analysis
  • Request Payload Attack
  • Response Interception Attack
  • Backend Forgery Attack

Let’s not beat around the bush and dive right into the first scenario “Winning indicator“.

Winning indicator

This scenario is basically not an attack, but a primary analysis of whether the game is telling us, in some way, we are currently more likely to win or to lose. The idea is that we could use these indicators to decide whether to play or not. Of course, there is still a big risk that we could be wrong and still lose all our money. But let’s start by looking at what is loaded at the start of a game, between the URL call and the first spin. The URL we are using is still the same.

https://alvcw.playngonetwork.com/casino/ContainerLauncher?channel=desktop&demo=2&gid=bookofdead&lang=en_GB&pid=393&practice=1

There are a total of 77 calls made when the page is loaded. These include 4 JavaScript files, 11 CSS files, 8 font files, 7 JSON configurations and a lot of images and mp3 files. There are also three POST requests that return data. We will focus on the important stuff here, the JSON configurations and the POST requests. Let’s start with the POST requests.

The first two of the three POST requests must be used to obtain the unique ID, which is then used in the payloads for the spin requests.

Let’s try to recreate this in Postman. The first request looks like this.

URL https://alvflyp.playngonetwork.com/
Method POST
# payload 
d=1
0
103 3922 "en_GB" 310 "[User Agent]" "Book of Dead" "desktop"

The response is quite simple, no cookies or anything else, just a body in text format. By the way, you can enter anything in “[User Agent]”, I make my request with “wing fighter” 🙂

d=103 "nmVXGttixjh"

This looks familiar, isn’t this part of the ID we gave in the spin request payload? But the whole thing is not complete yet, there is still a part missing, this comes in the second POST request. Same URL, different payload, with the value from the first response

URL https://alvflyp.playngonetwork.com/
Method POST
# payload 
d=2
nmVXGttixjh
101 "495b091d-73e0-4cbf-8457-95a283910a3b" "USD" "" "" "" ""

Here the answer is a little bigger, but still simple

d=103 "nmVXGttixjh!735"
101 735 "DEMO" "" "CW" "user735" "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiUGxheWVyIiwiRXh0ZXJuYWxJZCI6InVzZXI3MzUiLCJQcm9kdWN0R3JvdXAiOiIzOTMiLCJuYmYiOjE2ODM3MTg5MTQsImV4cCI6MTY4MzgwNTMxNCwiaWF0IjoxNjgzNzE4OTE0fQ.lGwmbUifyV3d9RTN8vIaEIRfHdnRKtr_Jt7qL-X8YY4"
127 "2023-05-10T11:41:54Z"

But wait, what am I looking at? Do you see it too? First, the response gives us an ID in red, which we already know from the research phase, great. But is this a JWT? Yes, it is, and what is missing? Yes, exactly, a signature (more on this in the blog “what about: JWT“)! Oh dear, if you read the blog “Attack: JWT modification” I point out exactly such errors in the implementation. The signature is the only way to protect JWT from misuse, so never leave it out. Let’s take a quick look at what values this token holds for us.

{
"alg": "HS256",
"typ": "JWT"
}
{
"role": "Player",
"ExternalId": "user735",
"ProductGroup": "393",
"nbf": 1683718914,
"exp": 1683805314,
"iat": 1683718914
}

So these jokers really want us to believe that it is a HS256 token (with a passphaser), but there is no signature. I think they have not fully understood the principle of JWT, never mind. Unfortunately, the token is pretty empty, so there are no roles or other claims that we could attack. The token is also not used anywhere in subsequent requests to authenticate ourselves. The only thing I could think of is that it might be used in a live environment, not with play money like we do, to compare the current game balance with the casino site. This is certainly worth analysing later, but for now it is uninteresting.

In summary, we now have a way of creating an ID for us to play with. This could help us if we need to script something. However, in these two requests we have no information about whether it is worth playing or not. So let’s have a look at the third POST request, which happens automatically in the background. Again the same URL.

URL https://alvflyp.playngonetwork.com/
Method POST
# payload 
d=3
nmVXGttixjh!735
104 310 0 %22%3croot%3e%3csettings%3e%3cDenominations%3e%3cdenom+Value%3d%220.01%22+%2f%3e%3cdenom+Value%3d%220.02%22+%2f%3e%3cdenom+Value%3d%220.03%22+%2f%3e%3cdenom+Value%3d%220.04%22+%2f%3e%3cdenom+Value%3d%220.05%22+%2f%3e%3cdenom+Value%3d%220.1%22+%2f%3e%3cdenom+Value%3d%220.2%22+%2f%3e%3cdenom+Value%3d%220.5%22+%2f%3e%3cdenom+Value%3d%221%22+%2f%3e%3cdenom+Value%3d%222%22+%2f%3e%3cdenom+Value%3d%221.5%22+%2f%3e%3c%2fDenominations%3e%3c%2fsettings%3e%3c%2froot%3e%22

We can see that we need our user ID to use the third request, so the previous two requests are necessary to use this one. There is also a rather odd looking XML record being sent. This value looks like this when decoded.

"<root><settings><Denominations><denom+Value="0.01"+/><denom+Value="0.02"+/><denom+Value="0.03"+/><denom+Value="0.04"+/><denom+Value="0.05"+/><denom+Value="0.1"+/><denom+Value="0.2"+/><denom+Value="0.5"+/><denom+Value="1"+/><denom+Value="2"+/><denom+Value="1.5"+/></Denominations></settings></root>"

I have no idea what this payload is for, but that’s not important, because what’s important is the response. Look carefully, what do you see? A little hint, I have marked it in red.

d=104 1
54 11 1 2 3 4 5 10 20 50 100 150 200 1
57 "<custom><RTP Value=\"96\" /></custom>"
60 96 0 0
52 100000000 0 0
83 0
91 736 "lcBzVRXsXfO6CHHtd8BRzYHRjtVpHlDDVaL5Q4g5zG6WzNpy7f1Uaxkjbfgax1jZwkGydFea2pc5R2Hp3rhik53rCDZltP2kZVy--utQqz8oTgRotM0JQKqiHfLZ1qTJZVTawvYBQW5yBDMmMw7Hy_K6aHUGWW_06b8paKp-y1cqGcMOtKqhqCx__JnxyRaN2skSVgtDdIs_Xrp2uzGx-A.."
109

Yes, exactly, this call returns the current Return to Player (RTP) value. Also note the cyan value, this tells the slot how much credit we have. Might be interesting for the next attacks, we will see. With this value we can already guess whether we should play (high value) or not (low value). So that you do not always have to do this by hand, I have created the following Powershell script that returns the RTP value.

# define variables 
    $url = "https://alvflyp.playngonetwork.com/"
    $header = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
    $header.Add("Content-Type", "text/plain")
# get id
    $body = "d=1`r`n0`r`n103 3922 `"en_GB`" 310 `"[User Agent]`" `"Book of Dead`" `"desktop`""
    $id = (Invoke-RestMethod $url -Method 'POST' -Headers $headers -Body $body).split('"')[1]
# get session
    $body = "d=2`r`n$id`r`n101 `"495b091d-73e0-4cbf-8457-95a283910a3b`" `"USD`" `"`" `"`" `"`" `"`""
    $session = (Invoke-RestMethod $url -Method 'POST' -Headers $headers -Body $body).split('"')[1]
# get RTP
    $body = "d=3`r`n$session`r`n104 310 0"
    $rtp = ((Invoke-RestMethod $url -Method 'POST' -Headers $headers -Body $body).split('"')[2]).replace("\","")
    $rtp

Unfortunately, I was not able to find any other clues that would give us a go or a no-go for the game. This was already the least likely scenario to succeed from the very beginning. So we drop this method and move on to the next one.

Recurring Events Analysis

OK, scenario two, let’s hope this one is more successful than the last. We will now try to identify a recurring pattern in the games. For example, how long it takes to reach a bonus game. Or how much you win when you play 100 spins. The aim is then to use this information to make a prediction as to whether there is a Golden Limit above which a game is worth playing. It would be quite tedious to do this manually, but we can use our existing knowledge to write a script to merge the data for us. So let’s build a gambling robot 🙂

We already know that we need to create a session first for the game so we can play. We have already created this code above and can reuse it. However, we still need to enrich this code with the game’s logic so that it will continue to spin on a loss, pick up the win and what we don’t know yet is what the logic is when we win free spins. Here is the PowerShell code for this scenario.

cls
function session($url,$header) {
  # get id
    $body = "d=1`r`n0`r`n103 3922 `"en_GB`" 310 `"[User Agent]`" `"Book of Dead`" `"desktop`""
    $id = (Invoke-RestMethod $url -Method 'POST' -Headers $headers -Body $body).split('"')[1]
  # get session
    $body = "d=2`r`n$id`r`n101 `"495b091d-73e0-4cbf-8457-95a283910a3b`" `"USD`" `"`" `"`" `"`" `"`""
    $session = (Invoke-RestMethod $url -Method 'POST' -Headers $headers -Body $body).split('"')[1]
  # register session
    $body = "d=3`r`n$session`r`n104 310 0"
    $reg = Invoke-RestMethod $url -Method 'POST' -Headers $headers -Body $body
    return $session
}
function spin($url,$header,$rid,$session) {
  # spin
    $body = "d=$rid`r`n$session`r`n1 1 10 20 1"
    $spin = Invoke-RestMethod $url -Method 'POST' -Headers $headers -Body $body
    return $spin
}
function collect($url,$header,$rid,$session) {
  # collect winnings
    $body = "d=$rid`r`n$session`r`n4 0"
    $spin = Invoke-RestMethod $url -Method 'POST' -Headers $headers -Body $body
    return $spin
}
function game ($gid) {
    # define variables 
        $url = "https://alvflyp.playngonetwork.com/"
        $header = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
        $header.Add("Content-Type", "text/plain")
        $session = session $url $headers
        $rid = 7
        $wins=0;$losses=0;$bonus=0;$bonusround=0;$total=0;$rounds=100    
    # start spinning
        for ($i=0; $i -lt $rounds; $i++) {
            $spin = spin $url $headers $rid $session
            $lines = ($spin -split [Environment]::NewLine)
            # loss
                if ( $lines.count -eq 6) { $losses = $losses + 1 }
            # win 
                if ( $lines.count -eq 4) { 
                    $win = $lines[1].split(" ")[3] / 100 
                    $collect = collect $url $headers $rid $session
                    $wins = $wins + 1
                    $total = $total + $win
                }
            # bonus
                if ( $lines.count -eq 10) { $bonus=1;$bonusround=$i;break }
            # error and other nois
                if ( $lines.count -eq 8) { $i = $i  -1  }
                if ( $lines.count -eq 3) { break }
            $rid = $rid + 1
        }
        Write-Host ("$gid;$i;$wins;$losses;$bonus;$bonusround;$total")
}
# gambling
    Write-Host ("[+] Let's start gambling ") -ForegroundColor Yellow
    for ($i=0; $i -le 100; $i++) {
        game $i 100
    }

With this script I analysed 100 games with 100 rounds each. The bet was always the same, 1 coin, 10 lines, $0.2 coin value, so a total bet of $2 per round. Each game would have cost us $200 if the full rounds were played. When a bonus appears, the game is stopped and the round in which the bonus appeared is displayed. In total we would have invested $20,000, remembering the RTP of 96, so in theory we should have made at least $19,200 back.

{gameID};{rounds played};{winner rounds};{loser rounds};{bonus games};{bonus in which round};{total return}

The analysis revealed a few things, but unfortunately no real predictive ability. However, it is clear that the RTP value is a phablet number. As the game engine kept making errors, we actually only played 7042 rounds. We would have spent a total of $14,084. Over all the games, the average value of the winning spins is 26.28%, the other 73.72% are losses. That is pretty bad. Now you could argue that it’s not so tragic because you can win big on just a few spins. Unfortunately this is far from the truth, from the $14,084 we invested we would only have got back $6335 in “winnings” (that word is probably out of place here), so a total loss of $7749. That’s an RTP value of 44! We could have gotten free spins 8% of the time, or 8 rounds. However, we had to play an average of 55 rounds to get them. Hard numbers are never lies. Unfortunately there is also no recurring pattern when a win occurs, e.g. always at the 50th position or after 3 losses, this would be great because then we could only play these game ids.

As mentioned before, the numbers do not really allow us to predict when it is worth playing and when it is better not to. This statement is not entirely true, in fact the numbers clearly show that it is never worthwhile, at least during my trial period. Perhaps this method could be useful if it ran all the time and showed that there were a lot of wins at a certain time of day or just now. I leave it up to you to use such a technique, and you are welcome to use the script for that purpose.

Conclusion

We are still a long way from being able to win in any way without gambling away our money and continuing to hope for luck. The first method hasn’t really yielded any results, while the second has shown that normal gambling is only ever worthwhile for the casino, 26.28% winning spins to 73.72% losing spins. Let’s move on from analysing the data and try our luck at beating the machine. More on this in the third part of this blog series, “Jackpoting Online Slots – part three”, where we will get our hands dirty.

That’s it until here, stay tuned and see you soon

** midjourney stringA technician stands in a casino behind a slot machine with the front deck open and tries to find the fault inside the cables are visible, cinematic lighting –ar 1:2 –q 2