TL;DR
- If our target system does not have linked libraries, we cannot run regular tools
- Random static binaries downloaded from the internet can be dangerous
- I will show you how you can compile our own static binaries to solve the problem
The problem
Have you ever found yourself in a client’s hardened, containerised environment where you needed to scan their internal infrastructure? If so, you’ve probably encountered an issue where the instance doesn’t have all the libraries required to run your tools.
It’s a nice problem for the client to have, as it shows how well security best practices have been adhered to when developing the environment. A minimalistic container, containing only the bare minimum software needed for administrative tasks, is ideal in a zero-trust environment that could be attacked at any moment. Luckily for us, as attackers, we have a modern solution at hand: cross-compiling all we need in a fairly small binary targeting the client’s instance architecture.
Why official binaries don’t work in this scenario
Scouring the internet for a solution wasn’t simple. Nmap’s official binaries are dynamically linked, so to work, they need to use additional libraries that are commonly found in the file system, such as ld-linux.so and libc.so on Linux, or kernel32.dll and msvcrt.dll on Windows. This means we are unlikely to be able to run the dynamically linked binary if we drop it into a restricted environment that we want to enumerate.
Fishing out there for static binaries, be it Nmap or any other type, might constitute a very dangerous exercise and perhaps a huge blunder, as we’re about to use a black box which we have no control over, or any guarantee it will contain what we want. The only way for us to be absolutely sure those binaries do not contain unwanted functions is to reverse engineer the hell out of them.
Now, such an endeavour is a huge effort for us regular mortals and a massive side quest, so instead of spending hours making sure it’s safe first, you might be tempted to risk running it, which I would not recommend. Luckily for you, I will touch on the solution later in the blog post.
The dead end with local compilation
If we directly compile the source code provided in Nmap’s GitHub repo, we will end up with a dynamically linked binary, which again serves no purpose in this scenario:
# Latest version at the time of writing is 7.98
curl -LO https://nmap.org/dist/nmap-7.98.tar.bz2
# Official Nmap compilation guide (available here: [Download the Free Nmap Security Scanner for Linux/Mac/Windows](https://nmap.org/download.html#source)):
bzip2 -cd nmap-7.98.tar.bz2 | tar xvf -
cd nmap-7.98
./configure
make
su root
make install
After compiling, we end up with the following file, as expected:

This is also true for the officially sourced Nmap binary, when we install it from a major distro’s package manager.
If we upload the binary as-is on a restricted environment, which does not contain all needed linked libraries, we run into some problems:
bash: ./nmap: /lib64/ld-linux-x86-64.so.2: No such file or directory
If we don’t have access or permission to install glibc in the system (or the CPU isn’t x64, but we’ll assume it’s our target architecture), then we’re in trouble.
… Or are we?
The solution
Enter Docker and shell scripting to the rescue. Two powerful skills to have in your arsenal! With Docker you can get a build runner in a couple of instructions, and with a couple more lines of code you can instruct the runner to statically compile what you want, exactly the way you want it. In this scenario we’re going to target as our supposed foothold the architecture of a Bastion Host instance living in an AWS VPC.
ubuntu@ip-172-31-30-70:~$ uname -a
Linux ip-172-31-30-70 6.8.0-1035-aws #37~22.04.1-Ubuntu SMP Wed Aug 13 13:49:56 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Now we have the correct OS and the correct architecture to target, we can instruct the Docker engine to use Linux and x86_64 as our compiling machine:
docker run --rm --platform=linux/amd64 -v "$(pwd)":/out -w /tmp ubuntu:22.04 bash -lc '<our commands we want to run inside the building container>'
The next steps are as follows:
1. Making sure we have all the tools we need inside the container to achieve our goal:
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl bzip2 xz-utils pkg-config perl python3 file git \
automake autoconf libtool m4 zlib1g-dev
2. Building a static OpenSSL library:
OSSL="1.1.1w"
curl -fsSLO "https://www.openssl.org/source/openssl-$OSSL.tar.gz"
tar xzf "openssl-$OSSL.tar.gz" && cd "openssl-$OSSL"
./Configure no-shared no-zlib linux-x86_64 -static --prefix=/opt/ossl
make -j"$(nproc)" && make install_sw
cd /tmp
3. Building a static PCRE2 library (needed for the NSE scripts)
PCRE2=10.43
curl -fsSLO "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-$PCRE2/pcre2-$PCRE2.tar.bz2"
tar xjf "pcre2-$PCRE2.tar.bz2" && cd "pcre2-$PCRE2"
./configure --disable-shared --enable-static --prefix=/opt/pcre2
make -j"$(nproc)" && make install
cd /tmp
4. Building our static Nmap binary using the statically compiled libraries above
# Build Nmap
NMAP=7.98
curl -fsSLO "https://nmap.org/dist/nmap-$NMAP.tar.bz2"
tar xjf "nmap-$NMAP.tar.bz2" && cd "nmap-$NMAP"
export CPPFLAGS="-I/opt/ossl/include -I/opt/pcre2/include"
export LDFLAGS="-L/opt/ossl/lib -L/opt/pcre2/lib -static -static-libstdc++ -static-libgcc"
export LIBS="-lpcre2-8 -ldl -lpthread -lz"
./configure \
--with-openssl=/opt/ossl \
--with-libpcre=/opt/pcre2 \
--with-libpcap=included \
--with-libdnet=included \
--without-zenmap --without-ndiff --without-nmap-update
sed -i -e "s/^shared: /shared: #/" libpcap/Makefile || true
make -j1 V=1 nmap
strip nmap
5. Bundle it all together and get the container to spit it out to your system, along with the NSE scripts:
mkdir -p /out/nmap-bundle/nmap-data
cp nmap /out/nmap-bundle/nmap-linux-amd64-static
cp -r scripts nselib /out/nmap-bundle/nmap-data/
cp nse_main.lua nmap-services nmap-protocols nmap-service-probes \
nmap-mac-prefixes nmap-os-db nmap-payloads nmap-rpc \
/out/nmap-bundle/nmap-data/ 2>/dev/null || true
tar -C /out -czf /out/nmap-linux-amd64-static-bundle.tar.gz nmap-bundle
End result
Congratulations! You now have your safe, self-made, static Nmap binary, ready to be dropped in any environment:

Tool drop
If you’re interested in the full script, I’ll place it at the bottom of the blog post so that you can simply copy/paste it into your terminal if you want to try it out. For convenience, I’ve coded a little Go interactive program that allows you to select all the flags, making as default the same versions that I’ve used throughout this example:


It then spits out the whole command to the terminal and attempts to run it.
It’s available on GitHub: 0x5ubt13 on GitHub: Static Nmap Binary Generator
Conclusion
Downloading opaque binaries from non-official repositories on the internet should be avoided where possible to stay safe. By having the skill to make our own static binaries, we are given the freedom to run any kind of software in any kind of container, as long as we can get the program across and the target machine is not fully restricted (we’ll still need services like DNS resolution, and not to be blocked by AppArmor, seccomp profiles, or SELinux), while keeping us relatively safe from shared library injection attacks.
Full script pieced together:
docker run --rm --platform=linux/amd64 -v "$(pwd)":/out -w /tmp ubuntu:22.04 bash -lc '
# Prepare the container
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl bzip2 xz-utils pkg-config perl python3 file git \
automake autoconf libtool m4 zlib1g-dev
# Build static OpenSSL
OSSL="1.1.1w"
curl -fsSLO "https://www.openssl.org/source/openssl-$OSSL.tar.gz"
tar xzf "openssl-$OSSL.tar.gz" && cd "openssl-$OSSL"
./Configure no-shared no-zlib linux-x86_64 -static --prefix=/opt/ossl
make -j"$(nproc)" && make install_sw
cd /tmp
# Build static PCRE2
PCRE2=10.43
curl -fsSLO "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-$PCRE2/pcre2-$PCRE2.tar.bz2"
tar xjf "pcre2-$PCRE2.tar.bz2" && cd "pcre2-$PCRE2"
./configure --disable-shared --enable-static --prefix=/opt/pcre2
make -j"$(nproc)" && make install
cd /tmp
# Build static Nmap
NMAP=7.98
curl -fsSLO "https://nmap.org/dist/nmap-$NMAP.tar.bz2"
tar xjf "nmap-$NMAP.tar.bz2" && cd "nmap-$NMAP"
export CPPFLAGS="-I/opt/ossl/include -I/opt/pcre2/include"
export LDFLAGS="-L/opt/ossl/lib -L/opt/pcre2/lib -static -static-libstdc++ -static-libgcc"
export LIBS="-lpcre2-8 -ldl -lpthread -lz"
./configure \
--with-openssl=/opt/ossl \
--with-libpcre=/opt/pcre2 \
--with-libpcap=included \
--with-libdnet=included \
--without-zenmap --without-ndiff --without-nmap-update
sed -i -e "s/^shared: /shared: #/" libpcap/Makefile || true
make -j1 V=1 nmap
strip nmap
# Bundle binary + all NSE data
mkdir -p /out/nmap-bundle/nmap-data
cp nmap /out/nmap-bundle/nmap-linux-amd64-static
cp -r scripts nselib /out/nmap-bundle/nmap-data/
cp nse_main.lua nmap-services nmap-protocols nmap-service-probes \
nmap-mac-prefixes nmap-os-db nmap-payloads nmap-rpc \
/out/nmap-bundle/nmap-data/ 2>/dev/null || true
tar -C /out -czf /out/nmap-linux-amd64-static-bundle.tar.gz nmap-bundle
# Show results
echo "===== OUTPUT ====="; ls -lah /out; echo "===== FILE TYPE ====="
file /out/nmap-bundle/nmap-linux-amd64-static || true
'