An ASP.NET Ticket HUB – Part 1: PowerShell POST Requests

Hi everyone, this is going to be a multi-part post as there is a lot to cover in this project.

I started this with basic knowledge of PowerShell and the ability to script one-liners to get what I needed done. I am familiar with web requests and am comfortable with sending POST and GET requests using CURL or a proxy with a GUI like Burp suite, though most of my experience has been working with plain HTML and PHP rather than ASP.NET.

The Problem: we have a ticketing system that gets bogged down and runs slow consistently. Just getting a page can take long enough to affect productivity, especially so when wanting to update multiple tickets. I wanted to make a user-friendly solution that can improve ticket management, make our jobs more efficient, and overall, not creating problems to find a solution. So, this needs to be simple, concise, and lightweight as I do not want new employees to have to download extra resources to run an auxiliary tool to their normal work.

The main feature of this is to POST data to the site, specifically notes to a ticket. I started to do this work with Python as I am more familiar with the language, to find that the modules required are purged from our environment. I then started to develop this using Windows CMD with a batch file but ran into a few issues here; mshta was the only option for a GUI, string manipulation is poor at best, and there was no way to authenticate with the site without breaking our security policy and hardcoding credentials.

I finally decided to use PowerShell for this project because it met all of the requirements I needed. I can use Windows Forms for a GUI, it is able to authenticate NTLM with default credentials, and no extra libraries are needed for a new user to use the tool.

The first step is to send a POST request to the site that can update the notes for a ticket. I submitted a test note and captured the packet on Wireshark to review the structure and see what headers we might need.

And this is where I hit my first roadblock. See I have never worked with asp net sites before, I didn’t know what this VIEWSTATE was or what it was for. I had to do some research.

The VIEWSTATE does what it sounds like. It stores the current state of the environment and page view. This long base64 string is actually the current volatile data of the site which is then checked and restructured by the server on a request. I cannot POST data without this viewstate, and I can get it by initially submitting a GET request. There are also a ton of other headers appended on to this which make up the element IDs of the webpage.

So any request I send, I will need to include this VIEWSTATE, the ID’s* I want to change, and the URL encoded data to be submitted.

*These are headers like any other request, but I refer to them as IDs since that is what these are pulled from. We see later multipart/form-data come into play and leverages the IDs more.

First, I want to share my code for the POST request, then I can break it down after.

$ticket = 12345 #ticket number
$page = <NOTESPAGE> #Other pages can be used here to POST data to.
$url = ''+$page+'.aspx?id='+$ticket+'
$note = [uri]::EscapeUriString("My note")
Invoke-WebRequest -Uri $url -UseDefaultCredentials | Select-Object -ExpandProperty inputfields | Select-Object name, value | Where-Object{$ -match "VIEWSTATE"} | Select-Object -ExpandProperty value | Out-File .\viewstate.txt
$response = Get-Content .\viewstate.txt -Raw
[int]$length = $response.length-10
$viewstate = $response.Substring(0, $length)
$viewstateurl = [uri]::EscapeDataString($viewstate)
Out-File -FilePath .\viewstateurl.txt -InputObject $viewstateurl
$viewstatefinal = Get-Content .\viewstateurl.txt -Raw
$postbody = '__VIEWSTATE='+$viewstatefinal+'&headertobeupdated'
Invoke-WebRequest -UseDefaultCredentials -Uri $url -Method POST -Body $postbody -ContentType 'application/x-www-form-urlencoded'
  • Lines 1-4: Our ticket site has the structure of From this I can build a dynamic post request and declare my note (this will have to be URI encoded to be posted as we can see in our packet capture.
  • Lines 5-7: We make an initial GET request to get the VIEWSTATE to use for the POST. These expire so we want to make a get request for each POST sent. We then export the VIEWSTATE to an ephemeral file and read it to a variable as raw text. We have to do this because there are restrictions on manipulating a System.Object data type of this size that we will need to work around for the next step.
  • Lines 8-12: The VIEWSTATE will include the 8-digit VIEWSTATEGENERATOR code that is determined server side. We do not need this since we have the VIEWSTATE, and it is checked by the server on POST. We are cutting this out of our variable, URI encode the rest of the VIEWSTATE to be sent and repeat the export to file and import as raw data.
  • Lines 13-14: We declare the body of data we are posting which we will structure how we see in the packet capture for a submission. We include the __VIEWSTATE header, the Uri encoded VIEWSTATE, and any headers and values we want changed. In our case we want txtNote to be updated to add a note to the page. We then send the request using default credentials to authenticate via NTLM and send the POST as ‘application/x-www-form-urlencoded’.

And we are all set! We can use this to manipulate other headers which I will explore in the next part of this project.

Another thing to note is that the data is being sent as application/x-www-form data. It is not uncommon to see ASP.NET data being sent as multipart/form-data sent ASP.NET Core will connect requests to other web apps. One of my pages does require this, and data can instead be sent using the following template.

$note = "PS test status"
$date = "3/15/2023"
$ticket = 123456
$url =  ''+$page+'.aspx?id='+$ticket+'
Invoke-WebRequest -Uri $url -UseDefaultCredentials | Select-Object -ExpandProperty inputfields | Select-Object name, value | Where-Object{$ -match "VIEWSTATE"} | Select-Object -ExpandProperty value | Out-File .\viewstate.txt
$response = Get-Content .\viewstate.txt -Raw
[int]$length = $response.length-10
$viewstate = $response.Substring(0, $length)
$boundary = "----WebKitFormBoundary" + (-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 16 | % {[char]$_}))
$body = "--$boundary`r`n"
$body += "Content-Disposition: form-data; name=""__EVENTTARGET""`r`n`r`n`r`n"
$body += "--$boundary`r`n"
$body += "Content-Disposition: form-data; name=""__EVENTARGUMENT""`r`n`r`n`r`n"
$body += "--$boundary`r`n"
$body += "Content-Disposition: form-data; name=""__LASTFOCUS""`r`n`r`n`r`n"
$body += "--$boundary`r`n"
$body += "Content-Disposition: form-data; name=""__VIEWSTATE""`r`n`r`n$viewstate`r`n"
$body += "--$boundary`r`n"
$body += "Content-Disposition: form-data; name=""ctl00`$ContentPlaceHolder1`$txtDescription""`r`n`r`n$note`r`n"
$body += "--$boundary--"
Invoke-WebRequest -UseDefaultCredentials -Uri $url -Method POST -Body $body -ContentType "multipart/form-data; boundary=$boundary"
  • Lines 1-9: Same steps as before, declare variables, GET viewstate, and cut out VEIWSTATEGENERATOR.
  • Line 10: multipart/form-data separates data with a boundary which is simply put, a unique ID. Data of different types can be sent in one request this way and the server needs to know what separates these. The boundary is set in the header and used throughout the body. Here we are using a random string of 16 characters to keep it from interfering with any other ongoing sessions.
  • Lines 11-21: We are building the body of data to be sent. The first 4 headers are those you might find with an ASP.NET submission including the familiar and necessary VIEWSTATE. Though the others may or may not be optional, you will have to check with your server’s administrator. We separate each header with a boundary, and prepend each with “Content-Disposition: form-data; name=”. Be aware of the line breaks and escape quotes in use as this may differ for your submissions. You can review how yours are formatted by testing against a packet capture of a submission made in your browser.
  • Line 22: We send the POST request while declaring the form-data content type and boundary in the header.

Note: If you are submitting multipart/form-data and see the format is the exact same as a browser submission, and the element ID’s update in the response but do not stick when refreshing the page, there may be server-side validation at play. This is not uncommon. Check what scripts are running on a POST request for the page and if there are any JS or AXD scripts checking for content validation. One common method is using the native function WebForm_DoPostBackWithOptions to check for content validation, in which case you will want to review the requirements to meet these conditions with the server administrator.

I hope this helps anyone working with ASP.NET sites and manual requests made. I will continue with my project in a Part 2 shortly.