Eight years ago, I wrote about building a honeypot army with Raspberry Pi, EC2, and Modern Honey Network. Back then, the Modern Honey Network (MHN) project was the gold standard for deploying a large fleet of honeypots, but it was already showing its age as Python 2 deprecation loomed. in 2020, some folks forked the work and got it running on Python 3, but haven't maintained it since. To the surprise of everyone who remembers me introducing the SecKC MHN effort and the stunning WebGL dashboard Wintel and I worked on together, my old MHN server was still running in 2025, dutifully grinding away on Ubuntu 16.04 LTS and Python 2.7.12. For the past 2 years, folks have asked me how to build their own, and I've had to say "First, time-travel back to 2017..."
I'd actually set out to do this work over my vacation a few months ago, but I was not in a good headspace and really just needed to disconnect. I did get familiar with CHN back in May. It ran fine in Docker but it was still dated, and it was going to need some work to integrate with the WebGL Dashboard. In many ways, I felt like some kind of archaeologist, looking at the mostly-abandoned CHN work, the few-and-infrequent MHN updates prior to 2018, and the fossils of purpose-built honeypots, many untouched since before 2015.
What's left of a once-vibrant honeypot ecosystem is a bit of a shame. The original MHN stack had become a museum of dependencies that couldn't be installed on modern systems. Even CHN featured Ubuntu 18.04 base images, archaic PyMongo trying to talk to MongoDB 8.x (spoiler: it can't), and many other outdated Python packages. I also realized that if anything happened to my precious EC2 instance, the entire stack was going to be offline indefinitely. I have backups of everything, but trying to untangle everything to restore a fragile artifact like this wasn't my idea of fun, and I'd probably just let it fade into obscurity. At the same time, I also envisioned getting the whole thing running in Python virtual environments on one of my OpenBSD systems, instead of relying on Docker on a cloud node somewhere. Docker is great, but it should be a deployment consideration, not a hard requirement.
Claude Code -- or -- vibe-technical-debt-repayment
This was the first seriously large project I've used Claude Code to help with. I used it mostly for documentation and test automation. I started by having it unpack how all of the pieces work together and creating a Mermaid syntax diagram (left) and description in Markdown to help better understand the data flow between the honeypots, hpfeeds, mnemosyne, MongoDB, SQLite and the core CHN-Server itself. I also had Claude help me keep track of the project plan, broken up into phases, and when I was getting tired and had to take a break, it could summarize the commit logs, diffs, and create a quick checklist of next-steps that I could come back to the next day to get my head back into the game quicker. When I found a class of problems in the code, Claude highlighted other places the same patterns were found. That wasn't anything I couldn't do in a modern IDE, but it was a nice touch, since I was working in vim the entire time. If you actually try to have Claude write code, it's kind of a mixed bag. Many of its decisions are based on popular and harmful anti-patterns propagated through StackOverflow, Quora and Reddit. You'll have to babysit it and tell it no quite often.
The Modernization Journey
The initial scope seemed straightforward enough: upgrade some dependencies, fix some syntax errors and APIs, clean up a few bugs, get it all running in an OpenBSD VMM instance, then focus on rolling some new Docker containers. What started as a simple refresh turned into a journey through eight years of accumulated technical debt. I started with just the CHN-Server repository, but it had dependencies on the hpfeeds3 repository, and updating that meant messing with the mnemosyne repository. Several projects install my updated hpfeed3 package directly from GitHub in requirements.txt or the Dockerfile. There were layers of compatibility issues forcing me to take a phased, incremental approach to modernization, and a few gotchas that bit me several times, particularly syntax changes around PyMongo. Some bugs weren't obvious until I tried end-to-end testing with real honeypots in the lab. Honestly, I'm sure I missed a few obvious ones, but none that impact basic functionality so far. I threw hundreds of events per hour at the system using an infinite loop of telnet attempts. The system didn't even notice. It's much less resource-intensive.
The front-end needed some work, too. I parameterized the base API URL inside the JavaScript. Rob Scanlon's original work was built with Grunt, and Wintel's middleware used an ancient python2 WebSocket library that is incompatible with anything in modern JavaScript. After a few hours of trying to remove all of the Deprecated bits from the front-end, I decided to revert back to the basics, and gut the WebSocket code from the middleware entirely, and add a REST API to poll the honeypot attacks. I tweaked the Javascript as minimally as possible. I'm really not that comfortable with front-end code.
End to end also revealed a few things that'll need attention soon. The deployment scripts all reference docker containers with architecture tags (-amd64, -arm, etc) that don't exist, for example. Hpfeeds-logger and hpfeeds-cif start up and appear functional, but they need more comprehensive testing than just "the container starts and doesn't immediately crash." I do my own log collection with a custom HPFeeds scraper I wrote in Go last year, replacing the PHP junk I had written back in 2017. I haven't used CIF since 2017 either, and don't really have much need for it. That's future work or maybe even someone else's problem. I'm working on a plan to submit my improvements upstream in a practical way that doesn't require the current maintainers to review massive, monolithic pull requests (if they're even paying attention to PRs). I may end up having to just maintain this fork myself for a while. I'll come up with a more sustainable strategy to keep it up to date if it looks like that's how it will be.
Getting CHN deployed in 2025 is easier than it was in 2017, assuming you can navigate the initial setup. Once I had verified all of the pieces kind of worked on their own, I forked the chn-quickstart project. The guided Docker Compose generator handles most of the complexity, and the whole CHN stack comes up with a simple docker-compose up -d. It's missing the attack-map and custom middleware for the time being, but they're not too hard to deploy next to CHN once it's up and running. I did manage to dockerize the middleware. As of last night, I cut over the DNS records from my old EC2 instance to my new VPS. This thing's actually running in production! nginx handles routing the web requests to the right places, and hosting the static assets of the animated front-end.
The OpenBSD deployment in my lab environment has been rock-solid, with Cowrie honeypots running on Raspberry Pi devices feeding data back to the central server. Watching the attack data flow in real-time through the web interface brings back that same sense of satisfaction I felt eight years ago - except now it's running on modern infrastructure. I need to clean up the OpenBSD init scripts and write some documentation around that part. I don't think it'll be as easy to deploy on bare metal (in Python Virtual Environments with uWSGI) as I'd like.
During the 2020 CHN effort, Duke University's STINGAR team built a bunch of dockerized honeypots that are still functional today, but similarly dated. Modernizing those honeypot projects would be nice. If we concede that the honeypot deployment strategy is just docker images on single board computers, home lab VMs and cloud nodes, it's not really that urgent. Cowrie is actively maintained, and a bare-metal deployment script is on my list of things to build soon.
Why bother decoding this stuff? Because encoded within this mess is
another URL. Depending on the source of the obfuscated code it may be a
link to a page full of exploit payloads or something similarly sinister.
Unwrapping the layers of malware allows researchers to find out where
the bad guys are actually hosting their stuff, and helps us identify
providers who are willing to help with or at least turn a blind eye to
cybercrime operations.
Lately, I've become more determined to handle javascript de-obfuscation outside the browser. Over the last year or so, I've been experimenting with a bunch of techniques to make sense of blocks of code that look like this:
I think it goes without saying that this looks like a monumental pain in the ass. The truth is, it's not as bad as it appears, but I didn't say it was going to be easy.
First, the basics. In JavaScript, FromCharCode() turns a decimal number between 0 and 255 into its ASCII character counterpart. You've all seen the ASCII table, right? Same thing.
I found a way to use printf in most shells to create a similar behavior, and I called this function "chr". So let's start really simple. Here's a file containing a message encoded with CharCodes, and a quick way to decode it. It simply reads each number in (by replacing , with a space and using a for loop, then prints each character one at a time.
This is the basis for the rest of what we're about to do. Let's break down that obfuscated javascript:
The yellow highlighted area is a bunch of numbers separated by the letter w. This is stored in an array labeled "f". If you look after the yellow, you can see that the code uses the letter w to split it.
The blue highlighted area is the loop that handles decoding the numbers into characters. This is obfuscated code, so by definition they've made it a bit confusing, but the end result is that they keep appending each character to the end of the "s" variable.
The bulk of the conversion of number to charcode (ascii decimal number of the character to be rendered) is here: (w[j]*1+41)
This gets us to the essence of the article: You'll need to use some brain power. The first thing I do is turn the data block into a string of comma-separated numbers so they're easier to work with, and I put these in their own file, like so. You can do this inside a text editor with search/replace, or on the CLI if you like.
Then you need to figure out the math. In our example code, what is w? what is j? You'll probably run into a bunch of bizarre variable reassignment when trying to make sense of obfuscated code like this. Looking above, you can see between the yellow and blue blocks, w=f. So w is now a copy of that array full of numbers. Inside the blue block of code, you can see that j=i. So, w[j] points to the current number in the array. But this number isn't the CharCode. There's still the "*1+41" part to deal with. Manually, we can see what's going on here. This is a very simple algorithm. The first 3 numbers are -32,-32,64.
-32*1+41 = -32+41= 9 - CharCode 9 is a tab.
64*1+41 = 64+41 = 105 - CharCode 105 is a lowercase "i"
Doing this manually would suck. The algorithm here is obvious. The CharCode is the number, plus 41. That's it. Let's decode it with my script:
So what happened here? This is the source of my relatively simple script.
#!/bin/sh
# Obfuscated JS Decoder
# Ax0n - 2013-03-22
# ax0n@h-i-r.net
#
if [ -z "$1" ]
then
echo "$0 codefile algorithm"
echo "c = placeholder for each number"
echo "i = iterator"
exit 1
fi
chr() {
# http://mywiki.wooledge.org/BashFAQ/071
# Turns a charcode into the ASCII byte
printf \\$(printf '%03o' $1)
}
count=0
file=$1
shift
algo="$*"
for code in `cat $file | tr "," " " `;
do
chr `echo "$algo" | \
sed -e s/"i"/"$count"/g -e s/"c"/"$code"/g | \
bc | cut -f1 -d\.` | tr -d "\r"
count=`expr $count + 1`
done
echo
echo "----- done ----"
At the heart of the above script is a nifty function I found in the Bash FAQ. And then, I used a pair of sed expressions to replace the "i" and "c" placeholders within the loop that handles the output. We call on the "bc" command-line calculator to work all the math magic. tr -d "\r" fixes some broken newlines found in some of the samples I decoded. Let's see it in action:
Now, let's look at the one in the video. It has a much more complicated algorithm than simply adding 41 to each digit before converting it to the ASCII byte! It's hard to read in the video, so here it is:
Fortunately, this one already separates the codes with commas, so we can pretty much just copy and paste the numbers into a text file for decoding. As you can see on the 3rd-to-last line, the algorithm it uses for each character is this:
((w[j]*1+e(x+3)+11))
Let's tear into it, shall we?
w[j] starts out just like the last example. We can see they set j=i and w is a copy of the array, so for each iteration, this is the digit. We can replace this with "c" in our algorithm expression.
That leaves us to figure out what e and x are. Also like the last example, e is set to "eval" at the end of the second line. We'll ignore it. Let's look for x= in the code.
There it is! j% - and remember, j = i, so it's the iterator. % is a mathematical modulus operator. -- that is, it divides two numbers and the output is the remainder. Example: 17 divided by 4 is 4 with a remainder of 1.
$ echo "17 % 4" | bc
1
(x+3) becomes essentially, (i % +3) and +3 just means "positive 3" in this context. We don't even need the +.
((w[j]*1+e(x+3)+11)) becomes ((c * 1 + ( i % 3 ) + 11)) or simply "c + ( i % 3 ) + 11"
HiR is what happens when 1990s-era e-Zine writers decide to form a blog. Most of us hail from the Great Plains region of the United States.
Ax0n, HiR founder and editor-in-chief is an information security specialist currently working in the luxury goods industry.
Asmodian X joined HiR in December 1997 and currently works as a web developer and SysAdmin in the education industry.
Frogman has been on board since May 1998 and has many technical passions. When not experimenting with obscure hardware, he can be found leaping from one rooftop to the next, making the world his office.
TMiB has also been helping since 1998. Also our resident Physicist and go-to guy for xkcd jokes we don't get, The Man in Black currently works in the Internet industry in an east-coast data center.