Security tools
OWASP ZAP Basics Against Your Local Juice Shop
Run ZAP's passive baseline scan against Juice Shop on your own machine, read the report, and check one alert by hand before trusting it.
Nmap maps the open ports on your own machine. Learn connect and SYN scans, service detection and output files, then match each port to its process.
Nmap is a network scanner. Point it at a host and it reports which ports answer, what state each port is in, and, when you ask for it, which service and version sits behind an open port. On your own machine, that makes it a fast inventory tool. You get a list of what is listening, and you can compare that list with what you think you are running.
Treat Nmap's output as a starting inventory. It is not a finding. An open port tells you that a socket is listening. Whether that socket is a problem depends on which process owns it, who can reach it, and whether it should exist at all. This article ends with the step most people skip: matching every open port to the process behind it.
Most surprises on a machine come from something nobody remembers starting. A dev server left running from last week. A container that published a port to every interface. A background service that installed itself during an update. You cannot assess what you have not listed, and a port inventory is the cheapest place to start. The same method works on a laptop, a virtual machine you built, or a client network you are authorized to test.
Read this before you run any scan. The Nmap book has a chapter on legal issues, and its core advice is simple: get permission before you scan a network you do not control. It points out that some internet providers ban unauthorized scanning in their acceptable use policies and can disconnect users who break them. For professional work, it says the best way to avoid trouble is to get written authorization from the people who represent the target network, with the Statement of Work as the place for it. It also notes that you are rarely bothered for scanning your own machine or the networks you administer.
For this article, the rule is:
Every command in this lab targets 127.0.0.1, the loopback address. Packets sent there never leave your machine. That is why the lab is safe to practice on, and why the scope is easy to state: the only host involved is yours.
A port is a number the operating system uses to hand incoming traffic to a process. If a program has told the OS that it will accept connections on TCP port 3000, that socket is listening. A scan asks, for each port, what happens when a probe arrives.
Nmap reports one of several states. The three you will see most often are:
These states describe the probe, not the risk. A closed port can still hurt you if a service you depend on is down. A filtered port can be a firewall doing its job. Keep the state and its meaning separate.
Nmap has several scan types. Two of them matter for a beginner.
A connect scan asks the operating system to open a full TCP connection to each port. The OS performs the normal three-way handshake. If the connection succeeds, the port is open, and Nmap closes it. The Nmap guide describes connect scan as the default when SYN scan is not available, because it needs no raw packet privileges. Nmap uses the system's own connect call instead of building packets itself.
The trade-off is visibility. A full connection shows up in the target's logs, which is fine on your own lab and something to think about on any network you do not own.
A SYN scan sends a SYN packet, the same first step a real connection uses, and then reads the reply. A SYN/ACK means the port is listening. A RST means nothing is. Nmap does not finish the handshake, which is why this scan is often called half-open. The guide says SYN scan is the default when Nmap has the privileges to send raw packets, which usually means root or administrator rights.
On Windows, check the Nmap install notes for raw packet support before you rely on -sS. This lab uses -sT because it works without elevated rights and still gives the port states you need.
For a beginner, the useful difference is what each scan leaves behind. A connect scan completes the handshake and shows up in logs on both ends. A SYN scan stops before the handshake finishes.
A port state tells you that something listens on 8000. It does not tell you what that something is. The -sV option sends probes to the open ports, compares the replies with Nmap's signature database, and reports a service name and version when it recognizes one.
Intensity runs from 0, which sends the fewest probes, to 9, which tries all of them. Higher levels take longer and send more traffic. By default, version detection skips TCP port 9100, a printer port that can react badly to probes. Use the lowest intensity that answers your question, and run -sV only against systems you control.
Version strings are evidence, not proof. Whoever runs a service can change its banner, so a version number is a claim to check against the software itself.
Nmap can write results to a file as well as the screen. The formats you will use most are:
-oN file: normal output, readable by a person.-oX file: XML, for tools that parse results.-oG file: grepable output, one host per line, for quick shell filters.Use -oN for notes you will read yourself, and -oX when another tool will process the result. Keep scan files next to your lab notes. A scan from last week is a record of what was true last week, not today.
You need Docker, Python 3, Nmap, and a machine where ports 3000 and 8000 are free on 127.0.0.1. If you do not have a lab network set up yet, start with how to create an isolated Docker network for labs. Every step below runs on your machine. The commands are the lab's commands. Where I ran them on my own Windows 11 machine, I show the output I saw. Where I could not run them, I say so and tell you what to look for.
OWASP Juice Shop is a deliberately vulnerable web app built for practice. Start it with its port bound to loopback only:
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shopThe 127.0.0.1 part matters. Without it, Docker publishes the port on every interface, and any device on your network could reach an app meant only for practice.
If the name is already in use, Docker stops with a conflict error. Do not delete that container until you know who created it. Inspect it first:
docker inspect juice-local --format 'image={{.Config.Image}} ports={{json .NetworkSettings.Ports}} status={{.State.Status}}'What I observed on my machine:
image=bkimminich/juice-shop ports={"3000/tcp":[{"HostIp":"127.0.0.1","HostPort":"3000"}]} status=runningI started it fresh for this run and confirmed the mapping with docker port juice-local, which printed 3000/tcp -> 127.0.0.1:3000. Docker had the image cached locally, so nothing was downloaded. Give the app a few seconds to boot before you open it in a browser.
python3 -m http.server 8000 --bind 127.0.0.1 &On Windows, python3 may not exist. On my machine it was not on the path, so I ran Python 3.12 by its full install path with the same arguments. The & runs the server in the background of the current shell. Test it from another terminal:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/index.txtThe file I requested existed in the folder where I started the server, and the test returned 200. The server log recorded the request:
127.0.0.1 - - [08/Nov/2026 06:20:25] "GET /index.txt HTTP/1.1" 200 -Before you scan, ask the operating system. This list is your reference, and it does not depend on Nmap.
On Linux:
ss -tlnpOn Windows, in PowerShell:
Get-NetTCPConnection -State ListenOn my machine I ran a version that also shows the owning process:
Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -lt 10000 } | Sort-Object LocalPort | ForEach-Object { $p = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue; '{0,-15} {1,6} {2,8} {3}' -f $_.LocalAddress, $_.LocalPort, $_.OwningProcess, $p.ProcessName }The output on my machine, limited to ports below 10000:
:: 135 2204 svchost
0.0.0.0 135 2204 svchost
192.168.0.100 139 4 System
172.23.112.1 139 4 System
:: 445 4 System
0.0.0.0 515 6084 svchost
:: 515 6084 svchost
127.0.0.1 1026 23620 com.docker.backend
127.0.0.1 3000 23620 com.docker.backend
:: 3311 11308 node
127.0.0.1 5000 43252 python
0.0.0.0 5040 10556 svchost
:: 7680 4604 svchost
127.0.0.1 8000 49736 python
127.0.0.1 8001 7976 python
127.0.0.1 8026 23620 com.docker.backendCount the lines. Many of these ports belong to Windows itself, not to anything you started. That is normal, and it is exactly why an inventory needs to name the owner of each port.
nmap -sT -p 1-10000 127.0.0.1I did not run this command. Nmap is not installed on the machine I used, and I did not install it. What to look for when you run it: a PORT STATE SERVICE table. Each line marked open should match a row from step 3. For this lab, 3000/tcp and 8000/tcp should both appear as open. Nmap usually folds the closed ports into a summary line, so the open ones are the lines to read.
As a substitute, I wrote a short loopback check with the Python standard library. It opens a TCP connection to every port from 1 to 10000 on 127.0.0.1 and reports the ones that accept. It makes the same handshake a connect scan makes, but it is not Nmap:
import socket
from concurrent.futures import ThreadPoolExecutor
HOST = "127.0.0.1"
def is_open(port: int) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(0.3)
return s.connect_ex((HOST, port)) == 0
if __name__ == "__main__":
with ThreadPoolExecutor(max_workers=200) as ex:
results = list(ex.map(is_open, range(1, 10001)))
open_ports = [p for p, ok in zip(range(1, 10001), results) if ok]
print(f"open on {HOST} in 1-10000: {open_ports}")Output on my machine:
open on 127.0.0.1 in 1-10000: [135, 445, 515, 1026, 3000, 3311, 5000, 5040, 7680, 8000, 8001, 8026]Every port in that list also appears in the listener table from step 3. The two sources agree, which is the check you want before you trust either one.
nmap -sV -p 3000,8000 127.0.0.1 -oN localhost-scan.txtNot run here, for the same reason as step 4. What to look for: a VERSION column next to 3000 and 8000, and a file named localhost-scan.txt in the current folder. The version text Nmap prints for port 3000 is a claim to check. Open the app in a browser or read the software's own documentation before you write the version down anywhere.
This step turns a scan into an inventory. For each open port, you need the process that owns it. On Linux, ss -tlnp prints the process name and PID in its last column. On Windows, take the PID from OwningProcess and look it up:
Get-Process -Id 23620 | Select-Object Id, ProcessNameI did that for the ports in the list. Here is what the owners turned out to be on my machine:
| Port | Listening on | PID | Owner | What it is |
|---|---|---|---|---|
| 3000 | 127.0.0.1 | 23620 | com.docker.backend | Docker Desktop's proxy for the juice-local container, confirmed with docker port juice-local |
| 8000 | 127.0.0.1 | 49736 | python | The http.server from step 2 |
| 3311 | all interfaces (::) |
11308 | node | A Node.js process; tasklist /svc lists no service for it, so it is a program someone started |
| 515 | all interfaces | 6084 | svchost | The Windows LPD print service; tasklist /svc /fi "PID eq 6084" shows LPDSVC |
Two rows need a decision, and the scan alone cannot make it. The Node.js process on 3311 listens on every interface, so if it is a dev server meant for local work, other devices can reach it. The LPD service on 515 also listens on every interface. Whether either one should run is your call, and the fix belongs to the owner of the process, not to the scanner. Do not stop a service only because it shows up in a list.
The Docker row needs a second check. The backend process forwards the port, so the PID alone does not name the container. docker port juice-local gave the mapping I expected:
3000/tcp -> 127.0.0.1:3000PIDs change. The Python server held PID 49668 in an earlier listing on this machine. After I restarted it for this run, the same command line owned port 8000 under PID 49736. Read the PID right before you act on it, and confirm the command line as well as the name.
docker rm -f juice-local
kill %1Run the first command only on a container you created for this lab. kill %1 only works in the shell that started the job; from another shell, find the PID as in step 6, check its command line, then stop it.
Scanning an address you do not own. A private range such as 192.168.x.x can belong to a router or to a device someone else runs. Scan only what you have written permission to scan.
Reading "open" as "vulnerable". An open port is a fact about a listener. It becomes a finding only when you know what the service is, why it is reachable, and whether it should be.
Forgetting -p. Without a port range, Nmap scans the most common 1,000 ports for each protocol. The lab uses -p 1-10000 on purpose, because a service on an unusual port is exactly what an inventory needs to catch.
Binding without the address. -p 3000:3000 publishes on every interface. Keep 127.0.0.1: in front when the app is for you alone.
Trusting a version string. -sV reads what a service says about itself. Check the version against the software before you rely on it.
Stopping at the scan. The scan gives you port numbers. Finding the owner of each port is the part you still have to do.
Write down the ports you expect before you scan, then compare. It is the fastest way to notice something you did not know was running.
Scan twice: once with the Python server running and once after you stop it. Save both files with -oN and compare them. The difference is what you changed.
Read the Nmap guide sections on -oX and -Pn. Once you know the output formats, you can feed scan results into a script and track changes over time, still against your own machines only.
The next article in this series moves from ports to the web layer, with OWASP ZAP basics against Juice Shop run against the same container.
Security tools
Run ZAP's passive baseline scan against Juice Shop on your own machine, read the report, and check one alert by hand before trusting it.