Friday, February 20, 2015

Bowcaster Feature: multipart/form-data

Need to reverse engineer or exploit a file upload vulnerability in an embedded web server? I added a multipart/form-data class to Bowcaster to help with that. You can have a look here:
https://github.com/zcutlip/bowcaster/blob/master/src/bowcaster/clients/http.py

Here's some background:
I've been reverse engineering how the Netgear R6200 web server parses a new firmware image when you use the firmware update facility in the web interface. Manually browsing to the router's web interface, then to the firmware update form, then browsing to a firmware file on disk, then clicking "upload" gets really tedious after a few times.

NETGEAR Router R6200 Firmware Upload
Updating the Netgear R6200's firmware through the web interface.


I wanted to use Bowcaster's HttpClient class to do this programmatically. Unfortunately, it lacked the ability to generate a multipart/form-data POST body, so I added a class to help with that. I know; there are 3rd party Python modules to do this already, but I don't want Bowcaster to have any dependencies other than Python, itself.

Anyway, below is an example program that uses the class to upload a firmware image, which you can get here from Bowcaster's example code. In order to identify the fields that need to be populated, I manually uploaded a file through a web browser, and analyzed the POST request in Burp Suite.


#!/usr/bin/env python

"""
Example code to upload a firmware file to the Netgear R6200 firmware update form.
"""

import sys
import os
from bowcaster.common import Logging
from bowcaster.clients import HttpClient
from bowcaster.clients import HTTPError
from bowcaster.clients import MultipartForm

def send_fw(url,fw_file):
    
    logger=Logging(max_level=Logging.DEBUG)
    logger.LOG_INFO("Sending %s" % fw_file)
    logger.LOG_INFO("to %s" % url)

    fw_file_basename=os.path.basename(fw_file)
    
    logger.LOG_INFO("Creating headers.")
    headers={"Accept":
    "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}
    headers["Accept-Language"]="en-US,en;q=0.5"
    headers["Accept-Encoding"]="gzip, deflate"
    headers["Referer"]="http://192.168.127.141/UPG_upgrade.htm"

    #admin:password
    headers["Authorization"]="Basic YWRtaW46cGFzc3dvcmQ="
    headers["Connection"]="keep-alive"

    logger.LOG_INFO("Creating post data")
    
    mf=MultipartForm()
    mf.add_field("buttonHit","Upgrade")
    mf.add_field("buttonValue","Upload")
    mf.add_field("IS_check_upgrade","0")
    mf.add_field("ver_check_enable","1")
    mf.add_file("mtenFWUpload",fw_file)
    mf.add_field("upfile",fw_file_basename)
    mf.add_field("Upgrade","Upload")
    mf.add_field("progress","")
    post_data=str(mf)
    headers["Content-Length"]=("%s" % len(post_data))
    headers["Content-Type"]=mf.get_content_type()
    client=HttpClient()
    logger.LOG_INFO("Sending request.")
    resp=client.send(url,headers=headers,post_data=post_data,logger=logger)
    
    return resp
    
    
    
def main(fw_file,host=None):
    if not host:
        host="192.168.127.141"
    url="http://%s/upgrade_check.cgi" % host
    resp=send_fw(url,fw_file)
    print resp

if __name__ == "__main__":
    if(len(sys.argv) == 2):
        main(sys.argv[1])
    elif len(sys.argv) == 3:
        main(sys.argv[1],host=sys.argv[2])
    else:
        print("Specify at least firmware file.")
        sys.exit(1)