Twitter UI_Mentrics
Intro
In this blog, I'll be reverse-engineering the custom Twitter JS challenge used for a bunch of stuff, in this blog we will be focusing on signup though. This JS challenge is called ui_mentrics and at first glance may look quite complicated! This blog will be getting their dynamic JS challenge into being solved in GoLang!
Finding the JS
Firstly, we will need to find the challenge we're looking for. To do this, we can open chrome dev tools on https://twitter.com/i/flow/signup
and attempt a signup. Once you input your verification code (email or phone) you will be able to see the request in question.
What we're looking for is a POST
request to https://api.twitter.com/1.1/onboarding/task.json
. It should be the most recent one and the payload should look something like this:
{
"flow_token": "g;123:-123:xxx:0",
"subtask_inputs": [{
"subtask_id": "Signup",
"sign_up": {
"js_instrumentation": {
"response": "{\"rf\":{\"d30f1e8a733985a1940dda1fd02b245c09e55f22e6861397ce18fd83361749bf\":17,\"a03b7833a5cb91c37f8d0a00a66da8b013043a8f0eb55afd4439e666cb1ad089\":-1,\"d0db50375df1c18e72edf9e97b2f3c5f236373ace2ea89ecf111f556d49db201\":-22,\"a379e0350e777f46d8a618d73829e7ba33444b5cce966dbc6b7d78b0418393d1\":1},\"s\":\"H6n-KNYhRXpybXkH8exYSnZK6VkyJluAaGm_03droKiaHlVXUJZLtUP0Av50D013wgI_UPEMy_v9IRMyMZLYEJ-jx-yrMdFH6FMBGBnI-0BlZLz8H3uSlwAfcUPkA_GfDbzmeCuagsKSodu0QJvTPuAVH3KzsNYuYyhQfwfuQPlyi02yZ0qAJPCdePnsOJnHZn72Whx8Eba_lSd3K7w6hXokrxjUcC2GgzxNMcGFzErFq0-z-9Y9-9U1VTqedlzpqQenxKMys3X9AQhHzw2EdaptWgIrtXnhEUcKvncWWZFyg18OITAQJ05_9_TIvywLRDEanRoz_N-Mx-7saG0WBwAAAY4t_Vud\"}"
},
"link": "email_next_link",
"name": "your_name",
"email": "your_email@gmail.com",
"birthday": {
"day": 1,
"month": 1,
"year": 1999
},
"personalization_settings": {
"allow_cookie_use": true,
"allow_device_personalization": true,
"allow_partnerships": true,
"allow_ads_personalization": true
}
}
}, {
"subtask_id": "ArkoseEmail",
"web_modal": {
"completion_deeplink": "twitter://onboarding/web_modal/next_link?access_token=xxx",
"link": "signup_with_email_next_link"
}
}, {
"subtask_id": "EmailVerification",
"email_verification": {
"code": "123123",
"email": "your_email@gmail.com",
"link": "next_link"
}
}]
}
Here in the subtask_id Signup
, we can see something called js_instrumentation
, this is the challenge result. To get the challenge script, we can do ctrl+f in the network tab for d30f1e8a7339....
aka any of the keys in the rf
JSON. Doing that should show you something like this:
Investigation
Now we have the script URL, let's open this URL on it's own and see what happens. If you refresh a few times, you should notice the script is 100% dynamic every time. I'm going to get a sample and we can work off that together, since it's dynamic, it'll be easier to follow this blog if you have the exact same script I have. The script I'll be using can be found here: https://gist.github.com/obfio/55b25c...
Reverse-Engineering
Now we get into the fun part, let's break this script open. First we will make it pretty, to do this I'll be using beautifier.io. I'll save this pretty version under sample_pretty.js
.
The first thing I'm going to do is figure out how the script gets the final result. Looking deep in the script, lines 152-160, you should see this:
return {
'rf': {
'a68d927...': a68d927...,
'a34cffe...': a34cffe...,
'a16cb28...': a16cb28...,
'a18e9f8...': a18e9f8...
},
's': 'nuqIwZdJ7gX71RtyJCuXPcuZ5DS7FOx_yFn-SQNGTBsYSz-hTaegbIs3fJPRMUKPtoy-QCe2BjnnaLbIubC1E0N6K4uOej_Kr9aTsoKWI6Oakem671KDKXPzl5HQt2MDb7Z4hYsR9c_k-QdUC3e_2YiwNHFTdOTLWyzjcEx3bwBMkXg1bJttCTN4i7kf35yT12MtN-7Km5UHClbVjHITTDETa7hx57L9oUrpEfSt_3D5nf9hfQSVMVCg1GW5_lMg_w0a1cafPf33Sf4bF3fc6Tjc3hQulaiaXXwZ_ht4J2j99UrnN_C4S5gNb9NwUv-rAqp17AozhESwTY-i15WC4QAAAY4uCVfL'
};
Going to where those variables are initiated, we see it's at the top of the function fRrvQZZYkwqrdPtHYnpQ
. So fRrvQZZYkwqrdPtHYnpQ
returns the payload, then we can see that knGjxgJCsTLGugqXlqiE
is what calls it, with rcNtpGNfYk = JSON.stringify(fRrvQZZYkwqrdPtHYnpQ());
. Then rcNtpGNfYk
(the payload as a string) gets used for the element named ui_mentrics
.
Alright, that's all we need to know there. So really all we need to do is replicate whatever fRrvQZZYkwqrdPtHYnpQ
is doing. That's our next target. I'll be making a list of things we need to do, this will make it easier for me to work later. So first thing we see is we need to get the initial numbers:
var a68d927... = 151;
var a34cffe... = 39;
var a16cb28... = 173;
var a18e9f8... = 35;
Next we need to see every way it manipulates those numbers, looking thru the script, we can see a full list of things we need to support:
[ ] Get initial numbers
[ ] handle xxx = xxx ^ new Date(xxx * 10000000000).getUTCDate()
[ ] handle xxx = xxx ^ xxx
[ ] handle xxx = ~xxx
[ ] handle xxx = function(x, y, z){/*...*/} (two types)
[ ] handle xxx = ~(xxx & xxx)
[ ] handle xxx = xxx | xxx
[ ] handle xxx = xxx & xxx
So it seems the main things here are we need to support bitwise operators, new Date
specifically with UTC time, and whatever those 2 complicated looking functions are doing.
The things that stick out the most here are the 2 functions we have, let's look into the one we see first. This one looks easier than the second one, we will start by rewriting it to make it easier to read:
var one = 151
var two = 39
var three = 173
var four = 35
three = function(a, b, c) {
function SoyOL(pcTjK) {
this.Fknen = function() {
return this.LxWbq ^ pcTjK;
}
};
var hJeSP = {
LxWbq: c
};
var wbYcg = new SoyOL(a);
wbYcg.LxWbq = b;
SoyOL.prototype = hJeSP;
return wbYcg.Fknen() | (new SoyOL(b)).Fknen();
}(three, two, three);
So this function Fknen
takes whatever LxWbq
is and uses bitwise operator ^
on it by whatever the input to SoyOL
is. We can see LxWbq
is c
, which is the third input to the function and pcTjK
is set to a
, aka the first input. We get fooled a bit here though, wbYcg.LxWbq
is set to b
, the second input.
So what are we looking at? Well, the output is wbYcg.Fknen() | (new SoyOL(b)).Fknen()
. When Fknen
is called as wbYcg.Fknen()
, LxWbq
is b
so the returned number would be 39 ^ 173 = 138
; When it's called as (new SoyOL(b)).Fknen()
, LxWbq
is c
so the returned number would be 173 ^ 39 = 138
. So the final number is really (b ^ a) | (c ^ b)
.
Looks like we've figured it out! Let's go onto that second function now. We will do the same thing we did previously, rewrite the function to be more readable:
var one = 151;
var two = 39;
var three = 173;
var four = 35;
three = function(a, b, c) {
var newElement = document.createElement('div');
newElement.setAttribute('style', 'display:none;');
document.getElementsByTagName('body')[0].appendChild(newElement);
function dbedB(AeLHT, eQDcO) {
for (var i = 0; i < 8; i++) {
var vplBk = document.createElement('div');
AeLHT.appendChild(vplBk);
vplBk.innerText = eQDcO;
if ((eQDcO & 1) == 0) AeLHT = vplBk;
eQDcO = eQDcO >> 1;
}
return AeLHT;
};
function YmZcR(vplBk, newElement, eQDcO) {
if (!vplBk || vplBk == newElement) return eQDcO % 256;
while (vplBk.children.length > 0) vplBk.removeChild(vplBk.lastElementChild);
return YmZcR(vplBk.parentNode, newElement, eQDcO + parseInt(vplBk.innerText));
};
var step1 = dbedB(newElement, a)
var step2 = dbedB(step1, b)
var step3 = dbedB(step2, c)
var step4 = YmZcR(step3, newElement, 0);
newElement.parentNode.removeChild(newElement);
return step4;
}(three, two, two);
So first let's see what steps 1-3 are doing, aka let's reverse-engineer this dbedB
function. So what we'll do first is make the element it creates not invisible, we will do this by removing this line: newElement.setAttribute('style', 'display:none;');
. Now we will make it no longer remove the element at the end by removing these 3 lines:
var step4 = YmZcR(step3, newElement, 0);
newElement.parentNode.removeChild(newElement);
return step4;
So now we will be able to see what's being produced in the HTML, but to make it easier to see, I'm going to make it only do step 1. If you open a new tab and run the script, you should see the following in the top left corner:
Great, now we can see what's going on much better. So dbedB
appears to just be doing a right bit shift by 1, 8 times, then storing all 8 results in the html. Except if the result of the shift is negative, the next number will be stored in a new div.
This function seems to just be using nested divs to store numbers. Now let's look at YmZcR
. This function looks pretty simple but might be tricky at first. We see the resulting number here is the last input, aka eQDcO
. So when there's no first element or the first element == newElement, aka both elements are the same, we return eQDcO % 256
. So what is this function doing? Well we see it's removing children then it recursively calls itself using the parentNode of the first element, newElement, and the current number + the innerText of the first element.
That may all be confusing so let me help you understand it a little bit better. Let's take a look at the HTML result of code we ran:
<div>
<div>173</div>
<div>86
<div>43</div>
<div>21</div>
<div>10
<div>5</div>
<div>2
<div>1</div>
</div>
</div>
</div>
</div>
So let's look at 1
and 2
, the div for 1
has no children, so it's be 0 + 1
since the innerText is 1
, then we move up to the parentNode. So now we're at 2
, it has a child, aka 1
, remove that child and do 1 + 2
. Then we do this all the way up.
So what is this function really doing? Let's simplify it. We run a for loop 8 times over a, b, and c. If number >> 1
is a positive number, add the number to the total. The total here acts as eQDcO
. Then the final number is total % 256
.
Write In Golang
First, let's work on parsing the script. We will do this with regex, regex is easiest for this case but if you wanted to, you could rewrite this with goja.
To get the main function, remember that's fRrvQZZYkwqrdPtHYnpQ
, I'm going to do a bit of a hacky solution:
package main
import (
"fmt"
"os"
"strings"
)
func main() {
// load sample file
f, err := os.ReadFile("./sample.js")
if err != nil {
panic(err)
}
script := strings.Split(string(f), "\n")[2]
fmt.Println(script)
}
func mathXOR(a, b, c int) int {
return 0
}
func mathRightShift(a, b, c int) int {
return 0
}
Now we need to split this up into the operations we need to do. Looks like we can do this with ;
. We should trim off function fRrvQZZYkwqrdPtHYnpQ() {
at the beginning though and the };
at the end:
script = script[41 : len(script)-4]
operations := strings.Split(script, ";")
fmt.Printf("%+v\n", operations)
Now we need to parse the operations, first let's make a map of the values in question and fill them in. The first 4 operations should be doing that:
// outside of main func, we store our regexes here
var (
initNumsRegex = regexp.MustCompile(`var [Aa-z0-9]{64}=[0-9]+`)
)
solution := make(map[string]int, 4)
for _, op := range operations {
// get initial numbers
if initNumsRegex.MatchString(op) {
parts := strings.Split(initNumsRegex.FindString(op), "=")
value, err := strconv.Atoi(parts[1])
if err != nil {
panic(err)
}
solution[parts[0][4:]] = value
continue
}
}
You should see we made a regex, to do this I used regex101.com. I won't be covering how to use regex in this blog, however, I will be providing the links to all regexes I make on regex101.com, like this: https://regex101.com/r/....
Now let's knock out the easy stuff, doing the basic math operations and the new Date
thing. Firstly, the basic math operations:
var (
initNumsRegex = regexp.MustCompile(`var [Aa-z0-9]{64}=[0-9]+`)
basicMathRegex = regexp.MustCompile(`[a-z0-9]{64}=(~|\^|\||&|[A-z0-9]{64})`)
)
// basic math, like xxx ^ xxx, ~xxx, etc.
if basicMathRegex.MatchString(op) && !strings.Contains(op, "new Date") {
signChange := false
mathDone := false
// handle ~, which changes the sign
if strings.Contains(op, "~") {
// handle rather it's a `~(xxx ^ xxx)` op or not.
if strings.Contains(op, "(") {
// trim off `~(` and `)`
tmp := strings.Split(op, "=")
newPart := tmp[1]
newPart = newPart[2 : len(newPart)-1]
op = tmp[0] + "=" + newPart
} else {
// trim off just `~`
tmp := strings.Split(op, "=")
newPart := tmp[1]
newPart = newPart[1:]
op = tmp[0] + "=" + newPart
}
signChange = true
}
parts := strings.Split(op, "=")
// handle all the different operations
if strings.Contains(parts[1], "^") {
tmp := strings.Split(parts[1], "^")
solution[parts[0]] = solution[tmp[0]] ^ solution[tmp[1]]
mathDone = true
}
if strings.Contains(parts[1], "|") {
tmp := strings.Split(parts[1], "|")
solution[parts[0]] = solution[tmp[0]] | solution[tmp[1]]
mathDone = true
}
if strings.Contains(parts[1], "&") {
tmp := strings.Split(parts[1], "&")
solution[parts[0]] = solution[tmp[0]] & solution[tmp[1]]
mathDone = true
}
if signChange {
// if math was done, then the answer should be answers[parts[0]] instead of parts[1]
if mathDone {
solution[parts[0]] = -(solution[parts[0]] + 1)
} else {
solution[parts[0]] = -(solution[parts[1]] + 1)
}
}
}
This is a lot of code, I'll do my best to explain it. So first we have new basicMathRegex
which you can see a sample for here: https://regex101.com/r/....
We match for that, then we make sure it's not the new Date
operation, this is because we have a little false flag there for that case. Next we check if the ~
operator is used, this operator changes the sign and adds 1, so basically -(123) + 1)
. In the case of something like xxx=~(xxx^xxx)
, we need to trim off ~()
, otherwise we just trim off ~
. Now we have the raw operation, we are handling ^
, |
, and &
because this is all I'm seeing them use atm, keep it simple.
We need to keep track of if math is done or not, this is because if math wasn't done, it was likely to be something like xxx=~xxx
which would mean that the solution is actually ~(parts[1] + 1)
not ~(parts[0] + 1)
.
Alright now let's do the date one. First we need to check what getUTCDate()
even does, according to developer.mozilla.org it returns the day of the week that the date falls on, in the UTC timezone. Doing this in golang is super simple:
if strings.Contains(op, "new Date") {
parts := strings.Split(op, "=")
opParts := strings.Split(parts[1], "^")
solution[parts[0]] = solution[opParts[0]] ^ time.UnixMilli(int64(solution[strings.Split(strings.Split(opParts[1], "*")[0], "(")[1]]*10000000000)).UTC().Day()
}
This code is very messy, however, it works perfectly fine! Now let's move on to the right shift math function, first we need to actually code the function itself:
func mathRightShift(a, b, c int) int {
num := 0
for i := 0; i < 8; i++ {
if (a & 1) == 0 {
num += a
}
if (b & 1) == 0 {
num += b
}
if (c & 1) == 0 {
num += c
}
a = a >> 1
b = b >> 1
c = c >> 1
}
return num % 256
}
Now because of how we split up this script, we will need to create a flag and a storage variable. This flag will basically just say that the current operation is the right shift math function and the storage will hold the answer key, so xxx=function...
, we store xxx:
var (
initNumsRegex = regexp.MustCompile(`var [Aa-z0-9]{64}=[0-9]+`)
basicMathRegex = regexp.MustCompile(`[a-z0-9]{64}=(~|\^|\||&|[A-z0-9]{64})`)
funcEndingRegex = regexp.MustCompile(`}\([a-z0-9]{64},[a-z0-9]{64},[a-z0-9]{64}\)`)
insideRightShiftFunc = false
rightShiftFuncKey = ""
)
// detect the rightShiftFunc starting
if strings.Contains(op, "document.createElement('div')") && !insideRightShiftFunc {
insideRightShiftFunc = true
rightShiftFuncKey = strings.Split(op, "=function")[0]
}
// detect the rightShiftFunc ending
if funcEndingRegex.MatchString(op) && insideRightShiftFunc {
insideRightShiftFunc = false
in := strings.Split(op[2:len(op)-1], ",")
solution[rightShiftFuncKey] = mathRightShift(solution[in[0]], solution[in[1]], solution[in[2]])
rightShiftFuncKey = ""
}
New regex: https://regex101.com/r/....
So we use the flag to get the final part of the operation, which contains the inputs needed for the operation.
Lastly, we just need to support the (xxx ^ xxx) | (xxx ^ xxx)
math function, let's code that function really fast:
func mathXOR(a, b, c int) int {
return (b ^ a) | (c ^ b)
}
Now we need to make 2 more variables, exactly how we had it for the rightShift function, they are used for the same thing, expect applicable to this mathXOR func:
// detect the mathXORFunc starting
if strings.Contains(op, "function(){return this.") && !insideMathXORFunc {
insideMathXORFunc = true
mathXORFuncKey = strings.Split(op, "=")[0]
}
// detect the mathXORFunc ending
if funcEndingRegex.MatchString(op) && insideMathXORFunc {
insideMathXORFunc = false
in := strings.Split(op[2:len(op)-1], ",")
solution[mathXORFuncKey] = mathXOR(solution[in[0]], solution[in[1]], solution[in[2]])
mathXORFuncKey = ""
}
Finally, we just need to get the op for final output:
if strings.HasPrefix(op, "return {'rf") {
break
}
To test, let's compare the output of our script to what we saw in our signup request, They are exactly 1:1!
Conclusion
Hopefully this blog was a solid overview of how to reverse-engineer a fairly basic dynamic JS challenge and solve it. This was fairly primitive, not very fast code but it should be able to scale perfectly fine!
The GitHub repository for this project can be found at github.com/obfio/twitter-ui_mentrics
I hope you enjoyed reading, sneakpeak for the next post:
References
GitHub Repository: https://github.com/obfio/twitter-ui_mentrics
Base Sample: https://gist.github.com/obfio/55b25c32821fc0d31fc511049f8f27b7
JS pretty: https://beautifier.io
Regex101: https://regex101.com
Mozilla JS Docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript