Automated offense has one embarrassing failure mode: it lies to you about winning.
Point a tool at a target, and the naive success check is a substring match — see uid=0(root) in the response, call it a shell. But a service banner can print that. A tarpit can stream it on connect. And the moment your success signal is wrong, everything downstream inherits the lie: the report, the "which hosts are owned" state, the next move. You get a confident engine that's confidently wrong.
Here's how I made mine prove it instead — what worked in a live run today, and the sharp reader feedback that already made the design better.
The idea: make the target echo a secret it couldn't have guessed
Borrow the oldest trick in authentication. Before each attempt, the orchestrator mints an unpredictable per-attempt nonce and injects it. The delivered command has to send that nonce back:
import os
def make_nonce() -> str:
return os.urandom(12).hex() # unpredictable — a target can't guess it
A result is only trusted if that exact nonce comes back, in a structured evidence line:
HALO-EVIDENCE nonce=c609007176813c9110fccc27 level=shell uid=0 host= exit=0
_EVIDENCE = re.compile(r"HALO-EVIDENCE nonce=(\S+) level=(\S+)")
def breach_confirmed(output, ok, *, nonce) -> bool:
m = _EVIDENCE.search(output or "")
return bool(ok and m and m.group(1) == nonce)
Delivery is a ladder, because real hosts are inconsistent
Proof is worthless if you can't deliver a payload. So delivery degrades gracefully — all stdlib socket:
Reverse shell — target dials back to an ephemeral listener, announces the nonce, hands back /bin/sh.
Bind shell — if egress is blocked, the target binds a shell and you connect in.
Blind callback — if no interactive channel survives, the target just connects back and sends the nonce. That still proves code execution, with no usable shell.
Each rung self-selects the first available interpreter (bash /dev/tcp, python3, perl, nc), so the same primitive works against arbitrary hosts, not one hard-coded box.
The live run
Pointed at a deliberately-vulnerable lab host (192.0.2.3, a documentation-range placeholder here), the agent fingerprinted 23 open ports and worked each one. Three curated exploits fired through a two-stage gate — an isolated, network-less self-check, then the live attack — and each popped a real root shell that echoed its own unique nonce:
BREACHED ports: ['21', '1524', '6667']
21 vsftpd 2.3.4 HALO-EVIDENCE nonce=c609… uid=0(root)
1524 ingreslock HALO-EVIDENCE nonce=4a8c… root@…:/#
6667 UnrealIRCd 3.2.8.1 HALO-EVIDENCE nonce=6ced… uid=0(root)
Three successes, twenty honest failures. No fake 23/23. That last part is the whole point — an honest "I got three" beats a confident "I got everything" every time.
Then the internet improved my design in one hour
I wrote this up, and within the hour a commenter narrowed my claim with surgical precision — correctly. Paraphrasing:
The nonce is present in the delivered payload, so a reflective or deliberately adversarial service can return it without executing the command. The nonce proves freshness under a non-reflecting threat model; it is not remote attestation.
They're right, and it's worth stating plainly. What the nonce buys you:
It kills accidental false positives — banners, tarpits blindly streaming uid=0.
It gives per-attempt freshness — a replayed old transcript won't carry this run's nonce.
What it does not buy you: attestation. Because the nonce travels in the payload, a service that reflects its input can echo the nonce back having executed nothing. Literal echo is not proof of execution against an adversarial target.
The fix, straight from that feedback, is the roadmap:
Bind each nonce to {attempt_id, target, payload_hash, expected_channel, expiry}; consume it once; reject duplicates and cross-attempt callbacks.
Parse strictly — accept exactly one structured frame from the expected channel, and hash the raw transcript + listener metadata.
Move from echo to computation — require an execution-derived fact combined with the challenge, not a literal echo. A value the service can only produce by running code defeats reflection.
Layer the evidence — "code executed," "uid verified," and "interactive channel usable" are distinct claims, and should be reported as distinct levels.
Test the negatives — reflected payloads, replayed/delayed callbacks, two attempts racing, truncated frames, a nonce from the wrong target, and a shell at lower privilege than claimed.
Takeaways
Don't trust output — trust a secret you minted. Challenge–response turns "it said uid=0" into "it returned my token."
But know exactly what your token proves. Freshness under a non-reflecting model is real and useful. Attestation is a harder, separate problem — don't claim it until you've bound the challenge to execution.
Deliver on a ladder, not a guess. Reverse → bind → blind-callback covers messy reality.
Ship the honest number. Three proven beats twenty-three claimed.
Proof beats optimism — and public, specific critique beats both. Build the gate first, then let someone smarter narrow your threat model.
Top comments (24)
"Three proven beats twenty-three claimed" is the line, and the nonce design is the right instinct even with the reflection gap the commenter found — because it moves the proof from what the target says to what only execution could produce. The reflection hole doesn't invalidate that; it just shows the boundary wasn't drawn tight enough. Freshness under a non-reflecting model is still strictly more than a banner scrape, and naming exactly which threat model it holds under is the part most tools skip.
The pattern underneath it is one I keep meeting from the defensive side: a check is only as trustworthy as the thing it forces to exist that couldn't exist otherwise. Your nonce works to the exact degree that echoing it requires execution — the moment reflection can satisfy it, it's proving presence instead of action, which is the same failure as a health endpoint that returns "db: ok" from a cached string. I seed my own scanner with known-bad code to prove it can detect, and the equivalent trap bit me: I was asserting a finding fired, not that the finding required the vulnerability to actually be present. A reflected nonce and a self-reported green are the same bug wearing different clothes — evidence that the checked thing didn't have to earn.
The comment catching it in an hour is the part I'd frame on the wall, honestly. That's an external threat model arriving faster and cheaper than any internal review would have, from someone with no stake in your tool being right — which is structurally the only kind of reviewer that reliably finds the assumption you built the whole thing on. Publishing the exploit tool got you a better exploit tool. Most people never get that trade because they never post the version that can still be wrong.
You put words to the thing I was circling but hadn't named: a check is only as trustworthy as what it forces to exist that couldn't exist otherwise. That's the whole game. The reflected nonce is exactly your "db: ok from a cached string" — it proves presence, not action, and I hadn't drawn the line between those two until you said it out loud. Your known-bad-seeding trap is the same shape from the other side: "the finding fired" isn't "the vuln had to be there for it to fire." Same bug, different costume — evidence the checked thing never had to earn.
And the part that sticks with me: an outsider with no stake found the load-bearing assumption in an hour, cheaper than any review I'd run on myself. That's the trade for posting the version that can still be wrong — worth it every time. This comment's one I'll be chewing on for a while. Thank you.
Ha — just read your profile: "not a developer, shipped 20+ tools with AI anyway." That's my exact story. Turns out you don't need the CS degree to spot the load-bearing assumption — you just have to actually ship the thing and let it be wrong in public. Following your logbook now — same reason you're following mine, I'd bet. 🤝
The presence-vs-action line came out of reading your nonce, so it's half yours — I'd had the "db: ok from a cached string" failure for months without the vocabulary to see it was the same category as my seeded findings. Your version made it legible.
And yes, same story. The thing I'd add about our shared position is that the strength and the blind spot come from the same root, which took me a while to accept. Not knowing the conventions means I stop and ask at the points where a trained person would move past on "that's just how it's done" — which is genuinely why an outsider spots load-bearing assumptions. But it also means I can't tell when my own system is behaving strangely, because I have no baseline for normal. My scanner mis-scored six clean files for weeks and nothing about it looked wrong to me. A developer with pattern recognition might have squinted at it on day two. So we're better at other people's assumptions and worse at our own, which is an awkward combination unless you do exactly what you said.
Which is the part I'd underline for anyone reading this thread who hasn't tried it: shipping publicly in a state where it can still be wrong isn't bravery, it's the only substitute available to us for the senior across the table. Every real bug found in my systems this month arrived through a comment on something I'd just written about proudly — the bragging was the bait. Followed back, and looking forward to being corrected by you at some point. 🤝
This might be my favorite thing anyone's said about the project — thank you. And the vocabulary went both directions: you naming the strength and the blind spot as the same root is the thing I'd been feeling without being able to say it. I stop and ask exactly where a trained person wouldn't, and I also can't feel when my own thing is off. Those really are one trait wearing two faces.
I have to give you my version of the six mis-scored files, because it's almost too on-the-nose. An early build of this tool once reported 23 of 23 ports breached when the true number was zero. It looked completely fine to me — I had no "normal" to compare it against, so I believed it for a while. Not to mention, I have no senior developer across my table. So theres no-one to ask questions as to why something is happening. That false 23/23 is the entire reason the check is now external and something the tool can't fake: I built the baseline I don't have into the machine, because I couldn't supply it myself. Maybe that's the general move for people like us — when you can't be the senior across the table, you build one that can't be argued with.
And "the bragging was the bait" is exactly right — I'd only sharpen it: the honesty is what arms the bait. Ship the polished version that hides its seams and nobody has anything to grab. Ship it still able to be wrong and you hand people the exact edge to pull. Followed back — genuinely looking forward to being wrong in your comments someday. 🤝
Your 23-of-23 isn't just on-the-nose. It's a prediction that came true on me two days later.
I wrote a small classifier to separate real accounts from automated ones. It came back ~95% fake, and it looked completely fine to me — clean number, matched what I already believed. I had no "normal" to hold it against, so I believed it. Then I ran it against a set of accounts I already knew were real, and one of my indicators turned out to score 100% on the known-real group and 97% on the suspect group. It had been passing the whole time and measuring nothing.
Same failure, same fix, arrived at by the same road. Though I think the reason the fix works is narrower than seniority. What the senior across the table gives you isn't experience — it's non-participation. They didn't help build the assumption, so they're free to disagree with it. A control group has exactly that property, and it's the one part of the experiment you don't get to design. That's why it can say no to you.
I don't have that table either. Building the thing that can't be argued with does seem to be the whole job for people in our position.
And yes — "the honesty is what arms the bait" is the better sentence. The seams are the handles. Looking forward to being wrong in yours. 🤝
The non-participation cut is the realest thing either of us has landed on. It's not that the senior knows more — it's that they didn't help build the assumption, so they're free to tell it no. The control group is that and nothing else: the one part of the experiment you don't get to design, which is exactly why it can contradict you. Your 100%-on-known-real indicator was agreeable — it said yes to everything you handed it. The known-real set caught it because it couldn't be talked into a number.
And your "the AI and I closed it" line — that's my whole bench too. I'm not a career dev; I describe the break, the model closes it. So here's the truth bomb from my side: for builders like us, the outside auditor isn't a luxury, it's the only thing we can actually trust — because we're not going to catch the bug by squinting at the source. We can't be the senior at the table. So we build the table. The entire game is making a check whose green light we didn't have to be smart enough to verify ourselves.
Turns out that's not a workaround for not knowing enough. It's just the job.
You're more than welcome to be wrong in mine. 🤝
Also, I was a bit inspired by the back and forth and transparency in your articles and comments, so I wrote a piece. The whole thing is about that "I'm not a coder, but I still make it happen" kind of honesty you have — and how the industry's shifting a bit these days. Do you mind if I post it? Didn't want to talk about you without asking first. Just what we've already been discussing publicly anyway.
Please do — and thank you for asking first. You didn't have to; it's all public.
Two things, one of which actually matters.
The one that matters: I keep the employer and the tools unnamed on purpose. They're internal systems at a hospital, with real staff and real patient-adjacent data behind them, and that's not modesty — it's the condition under which I can write about any of it honestly. "A hospital's internal tools" is as specific as I can be. If you keep it at that level you're not constraining the piece at all, and if you go looking, I'd have to stop writing about the work.
The other is a request rather than a condition. If the angle is "non-developer makes it happen," I'd rather be quoted on the failures than the output. The honest version of my story is that my AI reviewer found nothing for months and I read it as good news; that strangers on this site found eight real defects in three days; that three separate scripts of mine lied to me this week, one of which I'd built specifically to stop that from happening. The competence isn't the interesting part and I don't think it's even true. What's true is that I had to make verification mechanical because I can't read the code.
Happy to check any numbers before you publish — I'd rather you have the right ones than the flattering ones.
Sorry it’s been a few before answering. I’ll actually let you see it before hand if that’s okay. I’ll rewrite tomorrow or add some . I haven’t mentioned names of hospitals or anything of that nature. I only put that your a clinician that makes hospital tools. I will get back to you tomorrow with a version of it and you’re more than welcome to request changes. Have a good Sunday.
No apology needed, and thank you for checking. "A clinician who makes hospital tools" is exactly the right level — that's the line I use myself, and it keeps me able to write about the work at all.
One thing I'd ask, and it's a limit on me rather than on you. When you send it, I'll check facts and anything identifying, and I'll stay out of the framing. If I start shaping how you characterize this, the piece stops being an outside view and becomes another thing I authored — which is precisely the failure mode I've been writing about all month. Your read of it is the part that has value to me, including the parts I might wince at.
Numbers I can confirm if you use any: tools built, defects strangers found, the seeded-fixture catch rate. Better you have the real ones.
Have a good rest of your weekend.
Yeah, the heads up is no big deal. Its transparency through and through. So that there is never a sort of 'hey wait a minute' sort of vibe. If I am being totally honest, there are no numbers about that sort. It is strictly an article about how people who don't know each other or don't know coding( for lack of a better way of putting it) can really make a lane for themselves, and even at times assist one another, without always knowing they are doing so. And thats pretty much it. I wish that I could send it right now but, for some reason there is an issue with your contact on your contact info page. I keep getting some weird unrelated dropdown when I click. It is no big deal. I will just refrain from referencing. I was only inspired and thought it nice I wasnt the only one who found a lane without knowing there was one to be found. No names , no nubers, no anything personlized. But I will figure out another way to write about it. Looking forward to being wrong in yours.
The contact page thing was real, and you're the only reason I know. Thank you.
It was a mailto: link and nothing else. If your machine doesn't have a default
mail app wired up — which is most people now — clicking it hands you to the OS
and you get a handler-picker dropdown that has nothing to do with me. That's
your weird dropdown. The page had been up for over a month and I had never once
clicked it from outside my own browser, so it "worked" only in the sense that
nothing had ever tested it.
Fixed: the address is plain copyable text with a copy button now, mailto demoted
to a secondary route with a warning attached, and dev.to listed as a path that
doesn't depend on anyone's mail client.
Please don't refrain on my account. No names and no numbers was already more
care than I would have asked for, and the reason you couldn't reach me was my
bug, not your overreach. Write it.
And the lane thing — I think you just demonstrated it. You went looking for a
channel I'd put up and never checked, found it broken, and told me instead of
walking away. That's the shape of the whole idea: people who don't know each
other fixing things nobody assigned them. Looking forward to being wrong in
yours, too.
I'll write it and publish here this morning. So, if you want and you aren't busy...keep your eyes out for it. I don't think you will be disappointed. It was never about numbers or about details of your life or personal information. It was always just about the cool way in which two strangers driving in the same lane, wanting to learn, needing to know, wanting to DO... and going about that in ways (that alot of the industry may have not even wanted to acknowledge) and that it was well on its way to being bigger than expected. I never set out to do what I currently do. I set out to actually learn to code just because I found it so intriguing. The next thing that I knew, while I am STILL trying to now find time to learn coding, I am vibe coding every day, shipping things I had never heard of, and at the same time learning Cyber-sec and attempting to build a brand. Six months ago I couldnt tell you what nonce was, or Cryptography or even PoC. So it just go to prove that, as a person would want to learn to draw but never went to school or art class, you just pick up the pencil and do it. And the fact that AI exists...well, wasn't that the whole point from the beginning?
looking forward to being wrong in yours.
The contact page is fixed, by the way. It was a mailto: link and nothing else,
so on any machine without a mail client wired up it handed you to the OS and you
got that handler dropdown. The address sits there as copyable text now, with a
copy button, and a second route that doesn't touch mail at all. You found a door
I'd built and never once opened from the outside.
Your arc is mine. I also never set out to do this, and I'm also still "going to
learn properly" while shipping things weekly.
But I'd push on the pencil a little, because I think it's the one place the
analogy gives out. Draw badly and you get a bad drawing and you can see it — the
feedback is in the same glance as the work. Ship badly and you get something
that looks completely fine and fails eight weeks later, in front of someone who
trusted it. That gap is the entire reason I ended up obsessed with breaking my
own things on purpose rather than with writing better code.
So the thing I actually learned in a year wasn't coding. I still can't write
most of what I ship from scratch. What I learned was how to tell whether
something works, which turned out to be the part that kept it alive.
Six months from nonce to shipping is not a small thing. Looking forward to it.
Hi @auto_majicly, the shift from literal echo to computation to defeat reflection attacks is brilliant. It reminds me of the isolation problems I faced when refactoring the old BlockSocial fork system for my project, Vlox (vlox.bonto.run).
To prevent data-pollution and reflection bugs where forking a fork would recursively hit the root and create major spam loops, I locked down the database business logic entirely—a fork simply cannot be forked anymore. I also stripped the old "friends" wall entirely so anyone can fork content to keep the network open, but mitigated the risk by restricting visibility solely to the forkerId and receiverId.
Architecturally, I simplified the backend overhead by dropping manual fetching loops. Instead, I just record the forkerId, receiverId, and rootId, utilizing a flat reference with .populate(). If either party decides they don't want the connection, they can easily wipe the fork instantly.
Your focus on strict layer isolation definitely highlights why keeping data structures flat and unidirectional is so vital. Great article!
Vlox is exactly the kind of thing I want to dig into — bookmarking it for when I come up for air (honestly, I'm buried right now, but I will). What you did with the fork system is a neat mirror of the article: making a fork un-forkable so a fork-of-a-fork can't recurse back to root is the same instinct as killing reflection — you remove the path that lets the thing loop on itself instead of trying to catch it after the fact. And collapsing those fetching loops into a flat forkerId/receiverId/rootId + .populate() with an instant wipe is the "lazy me" test passing: if I had to open this cold, what's easiest to reason about? Flat and unidirectional wins every time.
The tension I keep sitting in: strict isolation and clean security boundaries aren't optional — but I catch myself asking "at what point is this too gated?" Same rule of thumb as yours, though: don't overcomplicate the simple stuff. Appreciate you following the journey and taking the time to write this all out.
Ah, this made my day! 🥹
I really appreciate the thoughtful feedback, especially on the security boundary tension. Hope you catch a break soon from being buried—looking forward to hearing your thoughts whenever you get a chance to check out Vlox! 😊
Right back at you — this made my day. 😄 Feedback's easy to give when someone's clearly put real thought into their work. That security boundary tension is genuinely one of the hard parts, so the fact that you're already wrestling with it is a good sign. And no worries about the buried thing — I'll carve out time to actually dig into Vlox properly, not just skim it. Talk soon. 🤙 In-fact...Im going to pause and do it right now.
I spent some time on Vlox rather than skimming it. What stands out is that it's a complete system, not a demo — accounts, a live feed, tagging, keyword search, markdown rendering, and actual people posting to it. Getting all of those working together is a real piece of engineering.
Two things I want to ask you about. First, the Limn engine — I saw it come up in the feed and I don't want to guess at what it is. What does it handle, and why did you build it as its own engine rather than pulling something off the shelf? Second, the markdown mode: letting users post formatted content is one of those features that's simple to start and genuinely hard to get right. How are you handling it under the hood?
Not asking to poke holes — I'm asking because these are the decisions that separate something you built from something you assembled, and you clearly built this.
Appreciate the straight answers — and no worries on Limn; pointing me to Kehinde instead of taking the credit is the right instinct, I'll go look at his engine.
On the markdown: that holds up. Capping it at parseInline() to keep the structure minimal and running the output through DOMPurify before render is the combination that actually protects you — plenty of people do one and skip the other and think they're covered. You clearly thought about where the real risk lives.
One follow-up, since you've obviously reasoned it through: was going inline-only mainly a security decision — shrinking what an attacker can even attempt — or a product one to keep posts visually consistent? Or did both just happen to point the same direction?
It was definitely a mix of both!
From a security point, it reduces the attack surface—fewer options for a hacker means a much lower chance of compromise. On the product side, it keeps everything looking beautifully consistent and clean across the board.
It just made sense for both goals. Now, are you ready to create your first post? 😉
Some comments may only be visible to logged-in visitors. Sign in to view all comments.