Introducción

Webpack es un módulo de empaquetado para aplicaciones JavaScript modernas. Su principal función es tomar módulos con dependencias y generar activos estáticos que representen esos módulos. Webpack puede manejar una variedad de archivos y convertirlos en un solo archivo o en varios archivos que son más eficientes para servir en una aplicación web.

Los archivos de mapas de origen (source maps) son archivos que mapean el código comprimido o transformado (como el que se produce después de la compilación y minificación con Webpack) a su código fuente original. Estos archivos son extremadamente útiles para la depuración, ya que permiten a los desarrolladores ver y trabajar con el código original en el navegador, incluso si el código que realmente se está ejecutando ha sido transformado.

Al analizar el código fuente del frontal de una aplicación web comprimida con Webpack es complicado realizar el proceso de ingeniería inversa, ya que el código resultante es extremadamente diferente al código original. En algunos casos, debido a una mala configuración de Webpack, se generan los archivos de mapas de origen y se suben al servidor de producción de la aplicación web, resultando en la recuperación completa del código fuente del frontal web. Se ha creado una aplicación en Python que, a partir de un enlace, explora la página web en busca de código fuente JavaScript y comprueba si se puede descargar su correspondiente archivo .map. Si es posible, lo descarga y extrae el código fuente original.

Uso de UnWebpack

La ejecución de la aplicación Python requiere dos argumentos, en primer lugar la dirección del sitio web a escanear y en segundo lugar el directorio local en el que queremos que se guarden los archivos. Este es un ejemplo de su ejecución:

python .\unwebpack.py 'https://webpage.com' 'C:\\files\\'

Código fuente

from bs4 import BeautifulSoup
import json
import os
import re
import requests
import sys

def get_request(url):
    return requests.get(url, headers={
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0'
    })

def get_source_code_urls(html_code):
    bs = BeautifulSoup(html_code, 'lxml')
    elements = []
    elements.extend(bs.find_all('link'))
    elements.extend(bs.find_all('script'))
    links = []
    for elem in elements:
        url = ''
        if 'href' in elem.attrs:
            url = elem.get('href')
        elif 'src' in elem.attrs:
            url = elem.get('src')
        if url != '':
            # Ignore no relative resources
            if 'https://' in url or 'http://' in url:
                continue
            # Take the domain root and the resource
            url = '/'.join(WEB_URL.split('/')[:3]) + url
            if url[-3:] == '.js':
                if url not in links:
                    links.append(url)
    return links

def get_source_code_map_urls(source_urls):
    links = []
    for source_url in source_urls:
        javascript_request = get_request(source_url)
        # search for sourceMappingURL
        found = re.search('//# sourceMappingURL=(.*).js.map', javascript_request.text)
        if found:
            links.append(source_url + '.map')
        else:
            print(source_url + ' has no map file')
    return links

def get_source_code_maps(map_urls):
    maps = []
    for map_url in map_urls:
        map_request = get_request(map_url)
        try:
            map_json = json.loads(map_request.text)
        except:
            print(map_url + ' error loading map file')
            continue
        maps.append(map_json)
    return maps

def get_path_level(path):
    # ./ path
    if re.match('^\./.*', path):
        return 'level_1'
    # ../../ ... path
    elif re.match('^\.\./.*', path):
        findings = re.findall('\.\./', path)
        return 'level_' + str(len(findings))
    # (webpack) path
    elif re.match('^\(webpack\).*', path):
        return 'webpack'
    # other paths
    else:
        return 'level_1'

def save_map_files(map):
    it = 0
    for path in map['sources']:
        # Remove webpack:/// from path
        path = path.split('webpack:///')[1]
        # Get the level of the path (subdirectories)
        path_level = get_path_level(path)
        # Create the path of the file and remove the subdirectories part
        file_path = DOWNLOAD_PATH + path_level + ('\\' if os.name == 'nt' else '/')
        file_path += re.sub('(\.|\.\.|\(webpack\))/', '', path)
        # Remove sync invalid characters
        file_path = file_path.replace('^\.\\\\.*$', '')
        # Replace invalid characters with "-" character
        file_path = re.sub('[*|?|<|"|>|\|]', '-', file_path)
        # Change the path for Windows
        file_path = file_path.replace('/', '\\') if os.name == 'nt' else file_path
        # Create the parent directory
        file_directory = '\\'.join(file_path.split('\\')[:-1])
        os.makedirs(file_directory, exist_ok=True)
        # Write the source code file
        file = open(file_path, 'w', encoding='utf-8')
        file.write(map['sourcesContent'][it])
        file.close()
        it += 1

def process_webpack_application(web_url):
    html_request = get_request(web_url)
    source_code_urls = get_source_code_urls(html_request.text)
    source_code_map_urls = get_source_code_map_urls(source_code_urls)
    source_code_maps = get_source_code_maps(source_code_map_urls)
    for map in source_code_maps:
        save_map_files(map)

WEB_URL = sys.argv[1]
DOWNLOAD_PATH = sys.argv[2]

process_webpack_application(WEB_URL)