Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix of the bug with long Oracle passwords and creation of oracle2john.py #5643

Open
wants to merge 2 commits into
base: bleeding-jumbo
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions run/oracle2john.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env python3

# This software is Copyright (c) 2024, k4amos <k4amos at proton.me>
# and it is hereby released to the general public under the following terms:
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted.

# ---

# Utility to obtain a hash of ORACLE authentication (o5logon) that can be cracked with John
# Usage: ./oracle2john.py -f <pcap files>
#
# This script depends on Scapy (https://scapy.net)
# To install: pip install --user scapy

import sys
import argparse
import re

try:
import scapy.all as scapy
except ImportError:
print(
"\033[91m[Error] Scapy seems to be missing, run 'pip install --user scapy' to install it\033[0m"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go to stderr, i.e. add the parameter file=sys.stderr to print.

)
sys.exit(1)


def read_file(args, filename):
"""
Reads a PCAP file and extracts relevant Oracle authentication data (o5logon).
"""
auth_data = {
"server_auth_sesskey": None,
"auth_vfr_data": None,
"auth_password": None,
"client_auth_sesskey": None,
}

packets = scapy.rdpcap(filename)
for packet in packets:
auth_data = process_packet(args, packet, auth_data)

if auth_data["server_auth_sesskey"] is None or auth_data["auth_vfr_data"] is None:
print(
f"\033[91m[Error] The PCAP file {filename} does not contain the required fields of o5logon authentification\033[0m"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also go to stderr to avoid having this in the hash output file when redirection is used. I would also consider this being a warning (or omit it altogether) because it might be a valid use case to throw this script on a bunch of pcap files containing arbitrary network traffic.

)
elif None not in list(auth_data.values()):
# Format of the hash : $o5logon$ <server's AUTH_SESSKEY> * <AUTH_VFR_DATA> * <AUTH_PASSWORD> * <client's AUTH_SESSKEY>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with the o5logon protocol and don't know why the client values can be missing. Would it make sense to add a comment explaining the two cases?

print(
f'$o5logon${auth_data["server_auth_sesskey"]}*{auth_data["auth_vfr_data"]}*{auth_data["auth_password"]}*{auth_data["client_auth_sesskey"]}'
)

else:
# Format of the hash : $o5logon$ <server's AUTH_SESSKEY> * <AUTH_VFR_DATA>
print(
f'$o5logon${auth_data["server_auth_sesskey"]}*{auth_data["auth_vfr_data"]}'
)


def select_hexa(raw_string):
"""
Extracts the first valid hexadecimal string from the raw data.
"""
match_hexa = re.search(
"([A-Fa-f0-9]+)", raw_string.decode("ascii", errors="ignore").replace(" ", "")
)
if match_hexa:
return match_hexa.group(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explicitly return None otherwise to make the intention clear



def process_packet(args, packet, auth_data):
"""
Processes a packet and updates the auth_data dictionary with the extracted values.
"""
raw_data = bytes(packet)

pbkdf2_match = re.search(rb"PBKDF2", raw_data)
if pbkdf2_match:
print(
"\033[91m[Error] Sorry, PBKDF2 is not (yet) supported by this utility (only o5logon is)\033[0m"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stderr again. But I'm wondering, it is that fatal that the program should exit? What if another connection is contained within the pcap? And the script seems to support multiple files as well. If the first cannot be processed, the latter will never be tried?

)
sys.exit(1)

server_auth_sesskey_match = re.search(
rb"AUTH_SESSKEY([\s\S]+?)AUTH_VFR_DATA", raw_data
)
if server_auth_sesskey_match:
auth_data["server_auth_sesskey"] = select_hexa(
server_auth_sesskey_match.group(1)
)

auth_vfr_data_match = re.search(
rb"AUTH_VFR_DATA([\s\S]+?)(AUTH_GLOBALLY_UNIQUE_DBID|$)", raw_data
)
if auth_vfr_data_match:
auth_data["auth_vfr_data"] = select_hexa(auth_vfr_data_match.group(1))

auth_password_match = re.search(rb"AUTH_PASSWORD([\s\S]+?)AUTH_RTT", raw_data)
if auth_password_match:
auth_data["auth_password"] = select_hexa(auth_password_match.group(1))

client_auth_sesskey_match = re.search(
rb"AUTH_SESSKEY([\s\S]+?)AUTH_PASSWORD", raw_data
)
if client_auth_sesskey_match:
auth_data["client_auth_sesskey"] = select_hexa(
client_auth_sesskey_match.group(1)
)

return auth_data


if __name__ == "__main__":

parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
### Utility to obtain a hash of ORACLE authentication (o5logon) that can be cracked with John
Written by k4amos

Usage: ./oracle2john.py -f <pcap files>
""",
)

parser.add_argument("-f", "--file", type=str, required=True, nargs="+")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since files are a mandatory and the only parameter the script expects, I would personally omit the flag-style option and just make it a positional argument instead. Most other *2john scripts also do it this way (but not all), so this is a matter of preferences.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the -f option should be dropped. Our convention is whatever2john <file(s)>, hashes to stdout and anything else to stderr.

Most other *2john scripts also do it this way (but not all)

Which do not? We should open an issue for that!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to my quick search, at least radius2john.py and aix2john.py.
Should I change these?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be terrific 👍


parsed_args = parser.parse_args()
args = vars(parsed_args)

for filename in args["file"]:
auth_data = read_file(args, filename)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_file does not return any auth_data, it looks rather like a process_file, prints the hash directly and returns nothing.

3 changes: 2 additions & 1 deletion src/o5logon_fmt_plug.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ static struct fmt_tests o5logon_tests[] = {
{"$o5logon$A10D52C1A432B61834F4B0D9592F55BD0DA2B440AEEE1858515A646683240D24A61F0C9366C63E93D629292B7891F44A*878C0B92D61A594F2680", "m3ow00"},
{"$o5logon$52696131746C356643796B6D716F46474444787745543263764B725A6D756A69E46DE32AFBB33E385C6D9C7031F4F2B9*3131316D557239736A65", "123456"},
{"$o5logon$4336396C304B684638634450576B30397867704F54766D71494F676F5A5A386F09F4A10B5908B3ED5B1D6878A6C78751*573167557661774E7271", ""},
{"$o5logon$4D04DBD23D103F05D9B57EB6EC14D83A0A468AB906EAC907D3A8C796573E5F34BC15F0ECBC9EAC0350A38A663A368233*442192E518F6F43D7CF7*D3963B6AAED39C231BD5C92A10C0F146CA4784D1503A9B97598B31D33406390B7CA4F8B3EE5406A54C1842E4E63D1220*192AB7C9BA21C883824CF3D5BA073AC1129FD841E0AF6DF522C7EBDC52783CB8B97B792BFB6D9D743C7F4376FF0E7F93", "password1234567890"},
{NULL}
};

Expand Down Expand Up @@ -131,7 +132,7 @@ static int valid(char *ciphertext, struct fmt_main *self)
if ((p = strtokm(NULL, "*"))) { /* client's encrypted password */
int len = hexlenu(p, &extra);

if (extra || len < 64 || len % 32 || len > 2 * PLAINTEXT_LENGTH + 16)
if (extra || len < 64 || len % 32 || len > 3 * PLAINTEXT_LENGTH)
Comment on lines -134 to +135
Copy link
Member

@magnumripper magnumripper Jan 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't pay attention at first but I now realized I wrote the original line, lol.

I'm pretty sure the + 16 was meant to accommodate a full block of padding after a password length of exact multiple of block size. So + 16 should be + 32 (presuming I just overlooked that hex doubles the length) or better yet there should be parens as in 2 * (PLAINTEXT_LENGTH + 16). If we vary PLAINTEXT_LENGTH the len > 2 * (PLAINTEXT_LENGTH + 16) is more correct than len > 3 * PLAINTEXT_LENGTH.

I had a quick glance at the code for possible places we should bump some array or such, and I believe there is:

diff --git a/src/o5logon_fmt_plug.c b/src/o5logon_fmt_plug.c
index 498f2d6b1..b5af3e887 100644
--- a/src/o5logon_fmt_plug.c
+++ b/src/o5logon_fmt_plug.c
@@ -83,7 +83,7 @@ static struct custom_salt {
 	unsigned char salt[SALT_LENGTH];         /* AUTH_VFR_DATA */
 	unsigned char ct[CIPHERTEXT_LENGTH];     /* Server's AUTH_SESSKEY */
 	unsigned char csk[CIPHERTEXT_LENGTH];    /* Client's AUTH_SESSKEY */
-	unsigned char pw[16 + PLAINTEXT_LENGTH]; /* Client's AUTH_PASSWORD */
+	unsigned char pw[2 * (PLAINTEXT_LENGTH + 16)]; /* Client's AUTH_PASSWORD */
 	int pw_len;                              /* AUTH_PASSWORD length (blocks) */
 } *cur_salt;
 

EDIT: Scrapped the last comment, that part is correct in current code (sizes are after hex decode).

Copy link
Member

@magnumripper magnumripper Jan 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> I had a quick glance at the code for possible places we should bump some array or such, and I believe there is:

...and BTW after that fix, the valid() line might be best written as if (extra || len < 64 || len % 32 || len > sizeof(((struct custom_salt*)0)->pw)).

We could make it if (extra || len < 64 || len % 32 || len > 2 * sizeof(((struct custom_salt*)0)->pw)) but since we already take PLAINTEXT_LENGTH into account, I think it just makes the code less readable.

goto err;
if ((p = strtokm(NULL, "*")) == NULL) /* client's sesskey */
goto err;
Expand Down
Loading