This Week's/Trending Posts

Hand-Picked/Curated Posts

Most Popular/Amcache

Hand-Picked/Weekly News

The Most/Recent Articles

Daily Blog #732: Multiple Identity Provider Disorder

 


 

Hello Reader,

This summer, we encountered a fascinating incident that highlights a surprising gap in how some third-party services handle authentication. Brian Krebs later covered the underlying issue in his post “Crooks Bypassed Google’s Email Verification to Create Workspace Accounts, Access 3rd-Party Services,” but our investigation was already finished by then. Let’s dive in.


The Scenario

Imagine your company has a third-party service provider where employees can create their own accounts. This service also supports authentication through multiple identity providers—like Google, Apple, or Facebook—to make logging in easier.

However, there’s a catch: in some cases, the third-party service will treat an identity-provider-based login as if it were the same account an employee created manually—even if they never actually linked their account to that identity provider.


How this Exploit Worked

  1. Manual Account Creation
    A user signs up for a third-party website using their company email address and even enables multi-factor authentication (MFA).

  2. Multiple Identity Providers
    The third-party site allows users to log in via providers like Google. Ideally, this is meant for convenience instead of creating an account manually.

  3. Domain Hijack
    A threat actor finds a loophole that lets them register the same email address on Google Workspace—even though the domain actually belongs to someone else. (See Krebs’s article for how they bypass Google’s verification.)

  4. Unintended Access
    Once the attacker has set up that Google Workspace email, they sign in to the third-party service using Google. Because the service trusts Google’s authentication, it grants the attacker access to the real user’s account—MFA included.


Why This Shouldn’t Work

  • Identity Provider Verification
    Google (or any identity provider) should confirm domain ownership before allowing someone to create email accounts for that domain. Attackers found a way around this requirement.

  • Third-Party Account Linking
    The third-party service should recognize that the user’s existing account isn’t linked to Google. However, many services fail to confirm whether an account was created manually vs. through an identity provider, resulting in the user’s legitimate account being “taken over.”


Our Investigation

In the logs, we noticed a user’s account authenticating via Google—odd, since that user’s company uses Microsoft 365. After reaching out to Google, we learned that the domain had recently been set up on Google Workspace, which led to a small set of logs confirming a brand-new account. Initially, we thought the third-party website might have suffered a larger breach. Then Brian Krebs’s coverage explained exactly how attackers managed to bypass Google’s email verification, confirming our findings.


Things to look for

  • If you’re investigating an incident and see a user “miraculously” authenticating—especially if it’s not a straightforward case of stolen tokens—check the identity providers the third-party service supports.

This case was a stark reminder that even well-known platforms can be manipulated if there’s a loophole in domain or email verification procedures.


Daily Blog #731: Accessing multiple shadow copies at once with AIM

 



Hello Reader,


While some tools focus on new markets for venture capital returns, others are continually refined to serve the needs of practitioners. Arsenal Image Mounter (AIM) is one of those tools that doesn’t just stay in its lane—it broadens it by adding features that enhance its forensic capabilities.


One standout feature in AIM is its ability to mount multiple shadow copies simultaneously so you can analyze them with whichever tools you prefer. Even more impressive is AIM’s exposure of a little-known artifact called “intra-volume shadow copy slack,” which represents the sectors that change between snapshots. What makes this so interesting is that we still don’t fully understand what triggers these changes. As far as I’m aware, no other tool isolates and displays this data as a separate stream. You could theoretically discover it via keyword searches or carving (if the changed data is contiguous and the signature is in the slack), but being able to see its existence and correlate it back to the modified sector or file is a remarkable innovation.


I’ll be testing this feature further and will share my findings, but in the meantime, remember: there’s always more to discover!

Daily Blog #730: Sunday Funday 1/26/25

 



Hello Reader,

It's Sunday! That means it's time for another challenge. In our last challenge all of our participants hit into issues wtth LevelDB. Let's see if the larger community can help with those issues in the future. 


The Prize:

$100 Amazon Giftcard


The Rules:

  1. You must post your answer before Friday 1/31/25 7PM CST (GMT -5)
  2. The most complete answer wins
  3. You are allowed to edit your answer after posting
  4. If two answers are too similar for one to win, the one with the earlier posting time wins
  5. Be specific and be thoughtful
  6. Anonymous entries are allowed, please email them to dlcowen@gmail.com. Please state in your email if you would like to be anonymous or not if you win.
  7. In order for an anonymous winner to receive a prize they must give their name to me, but i will not release it in a blog post
  8. AI assistance is welcomed but if a post is deemed to be entirely AI written it will not qualify for a prize. 


The Challenge:
Test, document or if you are up for it develop/extend a solution for LevelDB databases that can:
1. Parse it's contents and display them
2. Allow you to query it
3. Optionally identify or recover deleted messages

Daily Blog #729: Solution Saturday 1/25/25


Hello Reader,

This week I get to welcome another new name to the list of Sunday Funday Winners! If you were thinking about 2025 goals for yourself or to put into your year end career goals, why not being a Sunday Funday Winner  yourself? This week we congratulate Garrett Jones who did some great research and write it up quite nicely. Welcome to the SF Winners Club Garrett!

The Challenge:


Determine how to extract chat history out of the Chat GPT desktop app and what other data you can extract that would useful in an investigation (user name, login times, etc..)


The winning answer:

You can read Garrett's entry here:

Garrett's Github Blog

 

Daily Blog #728: Test Kitchen with Cursor

 


 

Hello Reader,

I went live tonight with a test kitchen! In it I showed how to use cursor to write a lnk file parser and add features and even read other peoples code to improve itself. Unfortunately I forgot that streamyard requires that I 'add myself' to the stage to get audio. So here is a video recording of me using Cursor... without Audio. 

 

I'll do this again and learn from this mistake. 

 

https://www.youtube.com/watch?v=YzpZtAmLTAc

Daily Blog #727: Experimenting with Deepseek v3

 


 

Hello Reader,

    Deepseek v3 is an open source AI model that is challenging OpenAI's dominance.  I decided to give it a spin and provide it a one off prompt in Cursor (which now has Deepseek v3 as one of it's available models) to create a python script with a GUI to download onion links. According to the publicly available testing Deepseek v3 is matching in chain of thought functionality with o1 and is considerable cheaper to run. 

I will say the code it wrote is good and its ability to correctly explain and define things works very well. I need to determine what the context window Cursor is making available for this so I can find out how much code I can provide.

Here is the code I got deepseek to write in just a few prompts:


import sys
import time
import requests
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                               QLabel, QLineEdit, QPushButton, QFileDialog, QProgressBar)
from PySide6.QtCore import QThread, Signal
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
from stem import Signal
from stem.control import Controller
import stem.process

# Tor proxy settings
TOR_PROXY = {
    'http': 'socks5h://127.0.0.1:9050',
    'https': 'socks5h://127.0.0.1:9050'
}

class DownloadThread(QThread):
    progress = Signal(int)
    speed = Signal(str)
    finished = Signal(bool)
    error = Signal(str)

    def __init__(self, url, save_path):
        super().__init__()
        self.url = url
        self.save_path = save_path

    def run(self):
        try:
            # Test SOCKS support
            try:
                import socks
                from socket import socket
                # Test Tor connection
                test_socket = socket()
                test_socket.settimeout(10)
                test_socket.connect(('127.0.0.1', 9050))
                test_socket.close()
                msg = "Tor connection verified"
                print(msg)
                self.error.emit(msg)
            except ImportError:
                msg = "SOCKS support not available. Please install PySocks."
                print(msg)
                self.error.emit(msg)
                self.finished.emit(False)
                return
            except Exception as e:
                msg = f"Failed to connect to Tor: {str(e)}"
                print(msg)
                self.error.emit(msg)
                self.finished.emit(False)
                return

            # Create a session with retry logic
            session = requests.Session()
            retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
            session.mount('http://', HTTPAdapter(max_retries=retries))
            session.mount('https://', HTTPAdapter(max_retries=retries))

            msg = f"Attempting to connect to: {self.url}"
            print(msg)
            self.error.emit(msg)
           
            with session.get(self.url, stream=True, proxies=TOR_PROXY, timeout=30) as r:
                msg = f"Connection established. Status code: {r.status_code}"
                print(msg)
                self.error.emit(msg)
               
                r.raise_for_status()
               
                total_length = int(r.headers.get('content-length', 0))
                if total_length == 0:
                    msg = "Warning: Content length is 0 or not provided"
                    print(msg)
                    self.error.emit(msg)
               
                downloaded = 0
                start_time = time.time()
               
                with open(self.save_path, 'wb') as f:
                    for chunk in r.iter_content(chunk_size=8192):
                        if chunk:
                            f.write(chunk)
                            downloaded += len(chunk)
                           
                            progress = int((downloaded / total_length) * 100) if total_length > 0 else 0
                            elapsed_time = time.time() - start_time
                            speed = downloaded / (elapsed_time * 1024) if elapsed_time > 0 else 0  # KB/s
                           
                            self.progress.emit(progress)
                            self.speed.emit(f"{speed:.2f} KB/s")
                           
                msg = f"Download completed. Total bytes: {downloaded}"
                print(msg)
                self.error.emit(msg)
                self.finished.emit(True)
               
        except requests.exceptions.RequestException as e:
            msg = f"Network error: {str(e)}\nResponse: {e.response.text if e.response else 'No response'}"
            print(msg)
            self.error.emit(msg)
            self.finished.emit(False)
        except Exception as e:
            msg = f"Error: {str(e)}\nType: {type(e).__name__}"
            print(msg)
            self.error.emit(msg)
            self.finished.emit(False)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Tor Downloader")
        self.setGeometry(100, 100, 400, 200)

        # Create main widget and layout
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        layout = QVBoxLayout()

        # URL input
        self.url_input = QLineEdit()
        self.url_input.setPlaceholderText("Enter .onion URL")
        layout.addWidget(QLabel("Onion URL:"))
        layout.addWidget(self.url_input)

        # Save location
        self.save_path_input = QLineEdit()
        self.save_path_input.setReadOnly(True)
        browse_button = QPushButton("Browse...")
        browse_button.clicked.connect(self.select_save_location)
        layout.addWidget(QLabel("Save Location:"))
        layout.addWidget(self.save_path_input)
        layout.addWidget(browse_button)

        # Progress bar
        self.progress_bar = QProgressBar()
        layout.addWidget(self.progress_bar)

        # Speed label
        self.speed_label = QLabel("Speed: 0.00 KB/s")
        layout.addWidget(self.speed_label)

        # Download button
        self.download_button = QPushButton("Download")
        self.download_button.clicked.connect(self.start_download)
        layout.addWidget(self.download_button)

        main_widget.setLayout(layout)

        self.tor_process = None

    def select_save_location(self):
        folder_path = QFileDialog.getExistingDirectory(self, "Select Save Folder")
        if folder_path:
            self.save_path_input.setText(folder_path)

    def start_tor(self):
        try:
            # Check if Tor is already running
            try:
                with Controller.from_port(port=9051) as controller:
                    controller.authenticate()
                    msg = "Connected to existing Tor process"
                    print(msg)
                    return True
            except:
                pass

            # Start Tor process
            self.tor_process = stem.process.launch_tor_with_config(
                config={
                    'SocksPort': '9050',
                    'ControlPort': '9051',
                },
                take_ownership=True,
                timeout=300
            )
            msg = "Started new Tor process"
            print(msg)
            return True
        except Exception as e:
            msg = f"Failed to start Tor: {str(e)}"
            print(msg)
            self.handle_error(msg)
            return False

    def stop_tor(self):
        if self.tor_process:
            self.tor_process.terminate()
            msg = "Stopped Tor process"
            print(msg)
            self.tor_process = None

    def start_download(self):
        if not self.start_tor():
            return

        url = self.url_input.text()
        folder_path = self.save_path_input.text()

        if not url or not folder_path:
            return

        # Generate filename from URL
        try:
            # Extract filename from URL
            filename = url.split('/')[-1]
            if not filename or '.' not in filename:
                filename = f"download_{int(time.time())}"
            save_path = f"{folder_path}/{filename}"
        except Exception as e:
            self.speed_label.setText(f"Error generating filename: {str(e)}")
            return

        # Disable UI during download
        self.download_button.setEnabled(False)
        self.progress_bar.setValue(0)
        self.speed_label.setText(f"Downloading to: {save_path}")

        # Create and start download thread
        self.download_thread = DownloadThread(url, save_path)
        self.download_thread.progress.connect(self.progress_bar.setValue)
        self.download_thread.speed.connect(self.speed_label.setText)
        self.download_thread.finished.connect(self.download_finished)
        self.download_thread.error.connect(self.handle_error)
        self.download_thread.start()

    def download_finished(self, success):
        self.download_button.setEnabled(True)
        if success:
            self.speed_label.setText("Download complete!")
        else:
            self.speed_label.setText("Download failed!")

    def handle_error(self, message):
        print(f"UI Error: {message}")  # Print to console
        self.speed_label.setText(message)
        self.download_button.setEnabled(True)

    def closeEvent(self, event):
        self.stop_tor()
        event.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())