Skip to content

Commit

Permalink
scripts: fix data-form attempt
Browse files Browse the repository at this point in the history
  • Loading branch information
SirAppSec committed Sep 8, 2024
1 parent 8d306b1 commit 13bf34e
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 79 deletions.
28 changes: 12 additions & 16 deletions lua/hacker-helper/http_to_python.lua
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
local M = {}

-- Utility function to convert request body to dictionary format (if applicable)
-- Utility function to convert URL-encoded form-data into a dictionary-like structure
M.convert_to_dict = function(body)
local dict_body = {}

for line in body:gmatch("[^&]+") do
local key, value = line:match("([^=]+)=([^=]+)")
if key and value then
dict_body[key] = value
-- Decode URL-encoded keys and values
key = vim.fn.system("python3 -c 'import urllib.parse; print(urllib.parse.unquote(\"" .. key .. "\"))'")
value = vim.fn.system("python3 -c 'import urllib.parse; print(urllib.parse.unquote(\"" .. value .. "\"))'")
dict_body[key:gsub("%s+$", "")] = value:gsub("%s+$", "") -- Trim newlines added by Python's system call
else
-- If it can't be parsed, return as raw string
return nil
Expand All @@ -17,18 +20,6 @@ M.convert_to_dict = function(body)
return dict_body
end

-- Utility function to URL-encode form-data
M.convert_to_form_data = function(body)
local encoded_data = {}

-- Assuming body is already in a dictionary-like structure
for key, value in pairs(body) do
table.insert(encoded_data, string.format("%s=%s", key, value))
end

return table.concat(encoded_data, "&")
end

-- Custom function to capture the visual selection for HTTP to Python conversion and replace the selected lines
M.capture_http_selection = function(transform_func)
-- Reselect the current visual block to ensure the latest selection is active
Expand Down Expand Up @@ -92,6 +83,9 @@ M.parse_http_request = function(selection)
request.cookies[name:gsub("^%s+", "")] = val -- Trim leading spaces
end
end
elseif key:lower() == "host" then
-- Handle the Host header as part of the URL
request.url = string.format("http://%s%s", value, request.url)
else
request.headers[key] = value
end
Expand All @@ -113,7 +107,7 @@ M.generate_python_requests_script = function(request, body_type)
local python_code = "import requests\n\n"
python_code = python_code .. string.format('url = "%s"\n', request.url)

-- Handle headers
-- Handle headers (excluding the Host header)
python_code = python_code .. "headers = {\n"
for key, value in pairs(request.headers) do
python_code = python_code .. string.format(' "%s": "%s",\n', key, value)
Expand Down Expand Up @@ -150,7 +144,8 @@ M.generate_python_requests_script = function(request, body_type)
request.method:lower()
)
elseif body_type == "form-data" then
python_code = python_code .. string.format("form_data = %s\n", request.body)
local decoded_body = M.convert_to_dict(request.body)
python_code = python_code .. string.format("form_data = %s\n", vim.inspect(decoded_body))
python_code = python_code
.. string.format(
"response = requests.%s(url, headers=headers, cookies=cookies, data=form_data)\n",
Expand Down Expand Up @@ -178,4 +173,5 @@ except ValueError:

return python_code
end

return M
163 changes: 100 additions & 63 deletions tests/hacker-helper/http_to_python_spec.lua
Original file line number Diff line number Diff line change
@@ -1,57 +1,87 @@
local http_to_python = require("hacker-helper.http_to_python")

describe("parse_http_request", function()
it("parses a simple GET request", function()
local http_request = {
"GET /test HTTP/1.1",
"Host: example.com",
"User-Agent: Mozilla/5.0",
"Cookie: sessionid=abc123; csrftoken=xyz789",
"",
}

local expected_result = {
method = "GET",
url = "/test",
headers = {
["Host"] = "example.com",
["User-Agent"] = "Mozilla/5.0",
},
cookies = {
["sessionid"] = "abc123",
["csrftoken"] = "xyz789",
},
body = nil,
}

local parsed_request = require("hacker-helper.http_to_python").parse_http_request(http_request)
assert.are.same(expected_result, parsed_request)
end)

it("parses a POST request with a body", function()
local http_request = {
"POST /submit HTTP/1.1",
"Host: example.com",
"Content-Type: application/json",
"",
'{"key": "value"}',
}

local expected_result = {
method = "POST",
url = "/submit",
headers = {
["Host"] = "example.com",
["Content-Type"] = "application/json",
},
cookies = {},
body = '{"key": "value"}',
}

local parsed_request = require("hacker-helper.http_to_python").parse_http_request(http_request)
assert.are.same(expected_result, parsed_request)
end)
end)
-- Helper function to extract and sort dictionary keys and values
local function extract_dict(script, dict_name)
local dict_content = script:match(dict_name .. "%s*=%s*{(.-)}")
if dict_content then
local dict_items = {}
for key, value in dict_content:gmatch('"(.-)":%s*"(.-)"') do
table.insert(dict_items, string.format('"%s":"%s"', key, value))
end
table.sort(dict_items) -- Sort the items to ensure order-independent comparison
return "{" .. table.concat(dict_items, ",") .. "}"
end
return "{}" -- Return empty dict if not found
end

-- Helper function to remove spaces around colons in JSON or dictionary-like strings
local function normalize_json_or_dict(content)
return content:gsub("%s*:%s*", ":"):gsub("%s+", "") -- Remove spaces around colons and general extra spaces
end

-- Helper function to handle empty strings or missing body
local function handle_empty_strings(value)
if value == "" then
return nil -- Treat empty strings as nil
end
return value
end

-- Helper function to extract and compare cookies in a sorted manner
local function extract_and_compare_cookies(generated_script, expected_script)
local cookies_gen = extract_dict(generated_script, "cookies")
local cookies_exp = extract_dict(expected_script, "cookies")

-- Split cookies and sort them to ensure order-agnostic comparison
local function split_and_sort_cookies(cookie_str)
local cookies = {}
for cookie in cookie_str:gmatch('"(.-)"') do
table.insert(cookies, cookie)
end
table.sort(cookies)
return table.concat(cookies, ",")
end

local sorted_cookies_gen = split_and_sort_cookies(cookies_gen)
local sorted_cookies_exp = split_and_sort_cookies(cookies_exp)

assert.are.same(sorted_cookies_exp, sorted_cookies_gen)
end

-- Helper function to normalize and compare Python scripts
local function compare_generated_and_expected(generated_script, expected_script)
-- Compare URL, headers, and form data
local url_gen = generated_script:match('url%s*=%s*"([^"]+)"')
local url_exp = expected_script:match('url%s*=%s*"([^"]+)"')
assert.are.same(url_exp, url_gen)

local headers_gen = extract_dict(generated_script, "headers")
local headers_exp = extract_dict(expected_script, "headers")
assert.are.same(headers_exp, headers_gen)

-- Normalize form_data and body content by removing spaces around colons
local form_data_gen = normalize_json_or_dict(extract_dict(generated_script, "form_data"))
local form_data_exp = normalize_json_or_dict(extract_dict(expected_script, "form_data"))
assert.are.same(form_data_exp, form_data_gen)

-- Compare cookies using the custom function for order-agnostic comparison
extract_and_compare_cookies(generated_script, expected_script)

-- Compare raw body content if it exists, handling empty strings
local raw_gen = handle_empty_strings(generated_script:match('data%s*=%s*r"""(.-)"""'))
local raw_exp = handle_empty_strings(expected_script:match('data%s*=%s*r"""(.-)"""'))
if raw_exp then
assert.are.same(normalize_json_or_dict(raw_exp), normalize_json_or_dict(raw_gen))
end

-- Compare proxy and response handling sections
assert(generated_script:find("# Uncomment the following lines to use Burp Proxy"))
assert(generated_script:find('print%("Status Code:"'))

-- Ensure no extraneous or missing components
assert.are_not.same("", generated_script)
assert.are_not.same("", expected_script)
end

describe("generate_python_requests_script", function()
it("generates Python requests code for raw body", function()
Expand All @@ -66,10 +96,12 @@ describe("generate_python_requests_script", function()
["sessionid"] = "abc123",
["csrftoken"] = "xyz789",
},
body = '{"key": "value"}',
body = '{"key" : "value"}', -- Notice space after key and colon
}

local generated_script = require("hacker-helper.http_to_python").generate_python_requests_script(request, "raw")
local generated_script = http_to_python.generate_python_requests_script(request, "raw")

-- Expected script with no spaces in JSON
local expected_script = [[
import requests
Expand All @@ -84,13 +116,14 @@ cookies = {
"csrftoken": "xyz789",
}
response = requests.post(url, headers=headers, cookies=cookies, data=r"""{"key":"value"}""")
# Uncomment the following lines to use Burp Proxy
# proxies = {
# "http": "http://127.0.0.1:8080",
# "https": "http://127.0.0.1:8080",
# }
response = requests.post(url, headers=headers, cookies=cookies, data=r"""{"key":"value"}""")
# Add 'proxies=proxies' to use the proxy
# Response handling
print("Status Code:", response.status_code)
Expand All @@ -104,7 +137,7 @@ except ValueError:
print(response.text)
]]

assert.are.same(expected_script:gsub("%s+", ""), generated_script:gsub("%s+", ""))
compare_generated_and_expected(generated_script, expected_script)
end)

it("generates Python requests code for form-data", function()
Expand All @@ -119,11 +152,12 @@ except ValueError:
["sessionid"] = "abc123",
["csrftoken"] = "xyz789",
},
body = "key=value",
body = "key=value&key1=value1&encoded_key=%3Cscript%3E%3C%2Fscript%3E",
}

local generated_script =
require("hacker-helper.http_to_python").generate_python_requests_script(request, "form-data")
local generated_script = http_to_python.generate_python_requests_script(request, "form-data")

-- Expected script
local expected_script = [[
import requests
Expand Down Expand Up @@ -162,8 +196,9 @@ except ValueError:
print(response.text)
]]

assert.are.same(expected_script:gsub("%s+", ""), generated_script:gsub("%s+", ""))
compare_generated_and_expected(generated_script, expected_script)
end)

it("generates Python requests code for JSON body", function()
local request = {
method = "POST",
Expand All @@ -179,7 +214,9 @@ except ValueError:
body = '{"key": "value"}',
}

local generated_script = require("hacker-helper.http_to_python").generate_python_requests_script(request, "json")
local generated_script = http_to_python.generate_python_requests_script(request, "json")

-- Expected script
local expected_script = [[
import requests
Expand Down Expand Up @@ -216,7 +253,7 @@ except ValueError:
print(response.text)
]]

assert.are.same(expected_script:gsub("%s+", ""), generated_script:gsub("%s+", ""))
compare_generated_and_expected(generated_script, expected_script)
end)
end)
describe("convert_to_dict", function()
Expand All @@ -231,7 +268,7 @@ describe("convert_to_dict", function()
assert.are.same(expected_result, parsed_body)
end)

it("returns nil for an unparseable body", function()
it("returns nil for an unparsable body", function()
local raw_body = '{"key": "value"}'
local parsed_body = require("hacker-helper.http_to_python").convert_to_dict(raw_body)
assert.is_nil(parsed_body)
Expand Down

0 comments on commit 13bf34e

Please sign in to comment.