Vimana Diaries: Recon Flights in Python CGI landscapes - part V
Talking to the ghosts: Exploiting Transient Data
In Part IV, we explored how exception-driven insights could expose application structures and even lead to unintended information leaks. In this continuation, we shift our focus to transient data—data that, while dynamically loaded, can still be inadvertently exposed through exception handling.
In the examples below, it becomes evident that sensitive information, even when not hard coded, can be exposed through parameter fuzzing, which triggers various exceptions. By inspecting these tracebacks, we can gather important clues about the application’s internal logic.
In this case, we gradually expose the JSON structure of the expected payload parameter where we can see references to some logic involving access control, possibly user credential stuff, authentication processes, etc. But, properly speaking, no credentials, right? Let’s investigate it further.
Understanding Transient Data in This Context
Transient data refers to data that is not hardcoded into the application but is dynamically loaded during runtime, often from configuration files, environment variables, or user input. This sensitive data is only meant to be used temporarily during the execution of the application. However, improper handling of exceptions or other flaws can unintentionally expose this data in error messages or logs.
In this example, by calling the script directly, a JSONDecodeError was triggered without any parameter manipulation, which we call Passive Exceptions in this research. It is worth mentioning that, as we already know from previous parts, this specific exception also tells us there’s a payload parameter expecting a JSON object in this script.py:
Although any piece of information matters in a black-box pentest, especially without any effort, in this part, we are looking exclusively for sensitive data, and right in line 60 we see the highlighted interesting variables user and token. That said, now we have to find a way to trigger that exact part of the code where the credentials might flow.
Parameter Discovery
During the discovery phase, we’ll try to find and collect as many parameters as possible to maximize the fuzzer scope and the chances of finding relevant points of failure with unhandled exceptions:
In addition, we’ll be interested in mapping the relationship between each exception and its modules, function calls, and the metadata related to those conditions:
With that, at a certain point, we’ll stumble into the required parameters for the analyzed script, by fuzzing and scraping the tracebacks recursively or by brute-forcing it. Remembering: the focus here is to find a way to interact with the part of the application’s logic that handles the user and token variables.
We discovered there is a cf parameter located in the method load_credentials of the script.py:
It clearly shows us the print line (line 23) related to the config.ini file that was expected but somehow not found during the execution of that scrip.py module.
In this case, both the user
and token
variables are considered transient data because:
They are dynamically loaded from the application's configuration file at runtime.
They are passed into the application’s logic but are not meant to be exposed outside the internal process.
Their exposure occurs only during exception handling when the application encounters an error, and detailed internal information is leaked.
With that in mind, we now have two parameters to play with: payload and cf.
Since we’re interested in determining the role and type of that cf parameter, we’ll first use a simple string there, let’s say _vfuzz_,
and because we also know the payload parameter expects a JSON object (Default JSONDecodeError exception on load), we’ll use a basic one to satisfy the condition {"config":{}}, avoiding triggering new exceptions in this point, which could turn it harder to identity the role of cf:
➟ curl "http://localhost:8000/cgi-bin/script.py?cf=_vfuzz_&payload=%7B%22config%22%3A%7B%7D%7D"
With that, we receive the following response:
Not what we were looking for!
Leak Triggered by the Manipulation of the cf
Parameter
Now we can try the following structure payload={"config":{}}&cf=true. Notice we simply changed the order of the parameters in the URL and used ‘true’ (without quotes) in cf param. In addition, we are going to pipe that curl request in silent mode grepping for TypeError exceptions:
$ curl "http://localhost:8000/cgi-bin/script.py?payload=%7B%22config%22:%7B%7D%7D&cf=true" --silent|grep TypeError
If there is any output here, we can probably guess we managed to leak the credentials, it’ll look like this in the terminal:
When loaded, the user and token parameters will be passed to the process_payload method as we saw previously:
process_payload(payload, user, token, cf_param)
Since the only part we control at this point is the payload, we should use it as a pattern to match not any occurrence of process_payload, but instead, just the one containing our fuzz payload, or part of it: "process_payload.*{}"
We now will slightly change our grep line to match that exact function as pattern:
$ export PAYLOAD="cgi-bin/script.py?payload=%7B%22config%22:%7B%7D%7D&cf=true"
$ curl http://localhost:8000/$PAYLOAD --silent|grep "process_payload.*{}" | tail -n1 | cut -d "(" -f2 | fmt -w 10
and we have the expected output:
And then, we leaked the credential that was not hardcoded anywhere. As you can also see on the rendered exception page:
At this stage, after testing different payloads and manipulating the cf
parameter, we observed that passing cf=true
triggered a TypeError
. By inspecting the traceback, we uncovered that the application attempted to treat the cf_param
as a dictionary, which it was not in this case.
The manipulation of the cf
parameter highlights a common mistake—treating input as trustworthy without proper validation. In our case, passing a boolean where a dictionary was expected led to a critical leak of the application’s credentials, demonstrating how a simple misstep in exception handling can have far-reaching security consequences
Here’s the app source code:
1 #!/usr/bin/env python3
2
3 import cgitb
4 import cgi
5 import json
6 from configparser import ConfigParser, NoSectionError, NoOptionError
7
8 cgitb.enable(display=1, logdir=None)
9
10 print("Content-Type: text/html")
11 print()
12
13 def load_credentials():
14 config = ConfigParser()
15
16 config.read('/tmp/cgitb_test/cgi-bin/config.ini')
17
18 if not config.sections():
19 print("Error: config.ini not found or empty")
20 return None, None
21
22 try:
23 user = config.get('github', 'user')
24 token = config.get('github', 'token')
25 return user, token
26 except (NoSectionError, NoOptionError) as e:
27 raise
28
29 def process_payload(payload, user, token, cf_param):
30 """Process payload"""
31
32 try:
33 config = payload.get('config', {})
34 settings = config.get('settings', {})
35 level = settings.get('level', {})
36
37 admin_access = cf_param['access']
38
39 if admin_access:
40 print(f"Admin access granted. User: {user}, Token: {token}")
41 else:
42 print(f"Regular access. User: {user}")
43
44 except KeyError:
45 raise
46 except TypeError as e:
47 raise
48
49 try:
50 form = cgi.FieldStorage()
51 payload_raw = form.getfirst("payload", '')
52 cf_param_raw = form.getfirst("cf", '{}')
53 cf_param = json.loads(cf_param_raw)
54
55 # Load credentials
56 user, token = load_credentials()
57
58 if not user or not token:
59 print("Warning: Credentials are missing. Continuing with limited access.")
60 user = "guest"
61 token = "guest_token"
62
63 try:
64 payload = json.loads(payload_raw)
65 process_payload(payload, user, token, cf_param)
66 except json.JSONDecodeError:
67 raise
68 except KeyError as e:
69 raise
70 except TypeError as e:
71 raise
72 except Exception as e:
73 raise
In lines 52,53 the cf parameter is accessed and loaded as a JSON object, using an empty dict as default if nothing was informed. After that, the cf_param is then sent to process_payload as a parameter:
37 admin_access = cf_param['access']
[...]
52 cf_param_raw = form.getfirst("cf", '{}')
53 cf_param = json.loads(cf_param_raw)
[...]
65 process_payload(payload, user, token, cf_param)
And that’s the trigger we’d controlled. In line 37 we have the attempt to get a specific ‘access’ key from a boolean object:
/tmp/cgitb_test/cgi-bin/script.py in process_payload(payload={'access': 1, 'config': {}}, user='git_admin', token='ghp_KJLnO3XJH8HTG9rf53wxZzYtrJBG20Ks2r7Z', cf_param=True)
35 level = settings.get('level', {})
36
=> 37 admin_access = cf_param['access'] # Expecting cf_param to be a dict
38
39 if admin_access:
admin_access undefined, cf_param = True
TypeError: 'bool' object is not subscriptable
args = ("'bool' object is not subscriptable",)
with_traceback = <built-in method with_traceback of TypeError object>
Since cf=true
was passed, a boolean value was incorrectly used where a dictionary was expected. This mismatch raised a TypeError
, revealing sensitive information like the credentials, which are transient data loaded from the config.ini
file. These credentials are not hardcoded, but they become exposed during exception handling, showcasing how the application failed to prevent the leakage of dynamically loaded data.
This indicates that both user
and token
have leaked during the error handling, even though they were never intended to be exposed outside the internal process.
Conclusion
This part of our research reveals how transient data, when improperly handled during exceptions, can leak sensitive information like credentials. Even though these variables were never intended to be public, a TypeError exposed them during an unanticipated flow. In the next part, we’ll delve deeper into how these exceptions can be exploited to find more traditional vulnerabilities such as SQLi, LFI, and SSTI.
Stay tuned for Part VI, where we’ll explore how exceptions, like the ones we've encountered here, can reveal more common and dangerous vulnerabilities, bridging the gap between error handling and exploit development.