Jaga Writeups
ManyRSA, BeautyCare, Jaga's Adventure
Challenges and thoughts
Thats Alot of RSA
Simple RSA challenge, though the time constraint meant that some extra thinking was needed
BeautyCare
A web fullpwn challenge, something like a regular HTB challenge (only given a vulnerable ip, the rest is up to you to figure out)
Jaga’s Adventure
Misc game hacking challenge, cheat engine is the default solution but I tried to do it semi-legit
Thats Alot of RSA
Setup
The server has 20,000 RSA private keys. We have the corresponding public keys. When we interact with the server, 200 moduli are generated by randomly sampling two private keys and combining a random prime from each.
The server sends us these moduli, and for each, we need to respond with any one of the factors. If we can do so within 10 seconds, we get the flag.
Solution 0
Naive Solution
We know that each given modulus, $n$, is composed of two primes, $n = pq$. Taking $p$, we know that there should be prime $r$ such that $pr \in P_\text{pub}$. Hence, there should exist $n’ \in P_\text{pub}$ sharing a factor with $n$, our target $p$. We may efficiently find this shared factor with the GCD operation, which takes about $O(\log (n n’))$ time. Sweeping over $P_\text{pub}$, it takes about $O(|P_\text{pub}| \log n)$.
Practical issues
How does this perform? With a very basic implementation:
1 | t = time.time() |
It takes about 17 seconds on my computer, just on the GCD. Assuming io with the server takes about 2 seconds, we’d need to speed this up by more than double the time.
Better solution 1: Grouping
Consider what happens if we group some public moduli together. Let $|P_\text{pub}| = P$. Taking the product over $k$, then iterating over these larger moduli results in a runtime of $O(P/k \log (knn’)) = O(P \log k/k \log(n))$, a reduction by a factor of $\log k/k$!
However, there is a flaw in that we have not considered the cost incurred by large integers and CPU operations. While the preprocessing time is irrelevant when we start interacting with the server, it should still be realistic. Going though various values of $k$, we find the following table
$k$ | $t$ |
---|---|
1 (No grouping) | 16.861217975616455 |
2 | 14.754406690597534 |
3 | 10.609652757644653 |
5 | 9.417319297790527 |
10 | 8.648439407348633 |
100 | 7.667241334915161 |
141 (sqrt 20000) | 7.805389881134033 |
1000 | 8.19124984741211 |
10000 (20000/2) | did not finish preprocessing |
Additionally, for larger values of $k$, there is the non-negligible probability that both factors of $n$ are found within the group, hence the GCD operation would simply return $n$ itself. With that in mind, $100$ or $141$ seem to be a good enough tradeoff, with a low runtime and low probability of collision. Any collisions found can be settled by iterating the smaller factors directly.
Better solution 2: Multiprocessing
Alternatively, a faster, but more brute force approach is to make use of multiple processes. Here’s the idea:
We split the whole set of public moduli into $k$ groups of $P/k$. We then spawn $k$ simultaneous worker processes to handle one group each. We can then submit our query $n$ to each process, which will iterate their own subsets, effectively giving us $k$ times the speed. When one process finds a factor, a broadcast can be sent that not only informs us of the solution, but also tells the other workers to terminate and move on to the next query.
Worker process:
1 | # subset of numbers, communication pipe, response dict |
Manager process:
1 | from multiprocessing import Process, Pipe, Manager |
Including IO time, this method took around 7 seconds.
BeautyCare
Exploration
nmap
ing the given IP only revealed ports 22 (ssh) and 80 (tcp) open. Accessing port 80 just showed a generic landing page, with nothing particularly interesting in the source.
Further dirbusting port 80 revealed the /admin
, /admin/admin_login.js
and /graphql
endpoints.
From reading the admin_login.js
source we figured that we first had to login with a username and password, followed by a OTP challenge. With some experimenting, we found that the first login page was vulnerable to an SQL injection. For instance, the classic 'or 1=1--
payload resulted in
1 | Error: ER_PARSE_ERROR: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''' at line 1 |
Username and password
From here, it was relatively simple to leak the username and password. Using the following payload:
1 | ' or exists (select username from users where (ascii(substring(password,{i},1)) < {x} and username='john') or sleep(1)) and ''=' |
It would be injected into something like
1 | SELECT * |
Now, suppose that we queried with i = 1
and x = 93
. If the first character of the username is anything after 93, then the ascii < x
part of the inner where
would be false. Hence, the next condition is run due to the or
, causing a sleep of 1 second. On the other hand, if it were before 93, then the or execution short circuits, resulting in no sleep.
Based on how long the server took to respond, we gain information about the character of the username (or password), allowing us to binary search the correct character, eventually leaking the full string.
OTP
When reading the admin_login.js
source, we noticed a possible error message occurs if you fail the OTP too many times.
1 | if ( |
We didn’t check for an SQL injection but either way it wouldn’t be viable as it would take too many attempts. Instead, let’s look at the other component of this app, which has otherwise been ignored: the graphql endpoint.
Indeed, when searching for graphql multiple query
on google, we stumbled upon https://graphql-api.com/guides/schema/executing-multiple-queries-concurrently/. graphql has a feature where you can batch multiple queries into one, only counting as one attempt!
1 | mutation { |
We batched 1000 otps into one (too many would cause a body too large error), and hence only required 10 attempts at worst. If the otp was correct, we can extract the cookie and log in.
1 | def graphql(x, session=None): |
Templating
But that’s not all (sadly).
Entering the admin dashboard, we see a settings page for an email subscription, with a preview service.
Notably, it comes with a templating service, which is demonstrated by the CONFIRM_LINK
variable. Could we perhaps run arbitary code in this manner? Indeed, testing !{1+1}
gave us 2
, but more excitingly, !{process}
gave [object process]
. With some attribute traversal, we obtained the first user flag.
1 | !{process.mainModule.require('child_process').execSync('cat /home/john/user.txt')} |
Ansible
Now, we needed the root flag. Checking what we could do as this user:
1 | sudo -l |
Again with more googling (ansible privesc
), we found this this ansible playbook, capable of escalating from user A to user B if A can run as B.
1 |
|
1 | sudo /usr/bin/ansible-playbook shell.yml |
With that, we had root access and could get the root flag.
Jaga Almighty Grand Adventure
Setup
In this challenge, we’re given a flappy bird kind of executable and need to reach 69 points to get the flag.
Cheat Engine
Locating the points address
This being a game hacking challenge, cheat engine comes to mind. Let’s load it up:
We can start with a scan for the initial score, 0. It’s unlikely that the score would be stored in larger than 2 bytes, but let’s search with 4 bytes just in case.
After the first scan, we can play the game till we reach 1 point. Now we can re-enter cheat engine (and the game pauses for us when we lose focus, how nice) and do an exact search for 1.
Repeating this a few times,
We eventually find just one address.
Modifying
Now, we can try to modify our score with the inbuilt memory editor
But immediately, it changes right back. There seem to be some anti-cheat measures in place.
Bypass fail
Let’s move the address down to the address list, where we can try to make it inactive (freezes the value)
But sadly, it fails. Upon starting the game, the score changes back to 0, only returning to 70 when we game over.
Disassembly
Let’s dig into the disassembly a little.
Attaching the debugger, we find two instructions that write to this value.
What seems to be happening is the score is being compared to rsi (first access). If it’s not less, it jumps to a section of code which resets the address to rsi (second access). This must be the anticheat measure!
However, reading past that, we see a comparison between rsi and 0x45, which the disassembler has helpfully marked as 69. This is where the actual score check takes place. If it’s less than 69, it similarly jumps away.
Patching
With that in mind, let’s do some patching. Instead of cmp rsi, 45
, let’s change it to something more achievable, 1
Playing the game now,