Hey @Pixmusix thanks for the shout out…
@Daniel252711 I think I can solve your question…
An SMS-2-Shell-2-Email is actually something I’ve already had brewing in a private repo. I think the idea is mostly formed but its not tested yet so not ready for release.
Simple email to SMS is actually not that simple at all. The real trick is dealing with all the potential modem character encodings and character limits, and then separating the SMS message from all the header data, its conversion to MIME and then managing the modem’s very limited internal memory to not clog up. There’s a lot of jugging to make this work across all devices and encodings as if there’s any weird content in a message, we also have to handle errors so as not to crash the script etc
The good news is that the bulk of my SMS-2-Shell framework does all this heavy lifting and makes this sort of SMTP addition pretty simple…
Here’s some instructions how to make the changes and test:
Just add this beta code to original the sms-2-shell script at this link: SMS-2-Shell repo:
import smtplib
from email.mime.text import MIMEText
# Email settings
EMAIL_COMMAND_ENABLED = True # Toggle on or off email forwarding of original sms command
EMAIL_REPLY_ENABLED = True # Toggle on or off email forwarding for sms reply
EMAIL_ADDRESS = 'your_email@example.com' # Replace with your email address
EMAIL_PASSWORD = 'your_email_password' # Replace with your email password
SMTP_SERVER = 'smtp.example.com' # Replace with your SMTP server address
SMTP_PORT = 587 # Replace with your SMTP server port number (usually 587 for TLS)
Then update the send_sms_response function with:
# Send SMS replies and outputs
def send_sms_response(modem, phone_number, command):
try:
# Send an SMS
modem.write('AT+CMGS="{}"\r\n'.format(phone_number).encode(MODEM_CHAR_ENCODING))
modem.read_until(b'> ')
modem.write(command.encode(MODEM_CHAR_ENCODING))
modem.write(bytes([26])) # Ctrl+Z
modem.read_until(b'+CMGS: ')
response = modem.read_until(b'OK\r\n')
# Check if the command was sent successfully
sent_successfully = '+CMGS: ' in response.decode(MODEM_CHAR_ENCODING)
# Add a small delay between SMS messages
time.sleep(0.5)
# Forward reply SMS to email
if sent_successfully and EMAIL_REPLY_ENABLED:
email_subject = f"SMS Reply from {phone_number}"
email_body = f"Phone Number: {phone_number}\nReply: {command}"
send_email(EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_ADDRESS, email_subject, email_body)
return sent_successfully
except Exception as e:
logger.error('An error occurred while sending an SMS command: %s', str(e))
return False
Next add this NEW function BEFORE the process_sms function
# Function for forwarding incoming SMS to email
def send_email(sender_email, sender_password, recipient_email, subject, body):
try:
msg = MIMEText(body)
msg['From'] = sender_email
msg['To'] = recipient_email
msg['Subject'] = subject
# Connect to the SMTP server
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls()
# Log in to the email account
server.login(sender_email, sender_password)
# Send the email
server.sendmail(sender_email, recipient_email, msg.as_string())
# Close the connection to the SMTP server
server.quit()
return True
except Exception as e:
logger.error('An error occurred while sending an email: %s', str(e))
return False
Then finally, replace the process_sms_function with:
# SMS central processing engine
def process_sms(modem, sms):
try:
phone_number, content = parse_sms(sms)
# Check if phone_number and content are not None
if phone_number is None or content is None:
raise ValueError("Failed to parse SMS")
# Print debug information
print("Received SMS:")
print("Phone number:", phone_number)
print("Command:", content)
# Log the phone number, time, date, and command received
logger.info('D: %s T: %s Ph: %s Command: %s',
time.strftime('%Y-%m-%d'), time.strftime('%H:%M:%S'), phone_number, content)
# Check if the phone number is allowed
if phone_number not in ACL: # make ACL a black list by reversing line to "if phone_number in ACL:"
# Phone number not allowed, send rejection message
rejection_message = "Access denied"
send_sms_response(modem, phone_number, rejection_message)
logger.warning("D: %s T: %s UNAUTHORISED ACCESS ATTEMPT Ph: %s Command: %s",
time.strftime('%Y-%m-%d'), time.strftime('%H:%M:%S'), phone_number, content)
return
# If OTP is enabled, verify one-time password before proceeding
if is_otp_enabled():
try:
# Split the content into OTP and the actual command
otp, command = content.split(' ', 1)
except ValueError:
# Invalid format, send rejection message
rejection_message = "Invalid format. Please provide OTP and command separated by a space."
send_sms_response(modem, phone_number, rejection_message)
logger.warning("Invalid format - Phone Number: %s - Command: %s", phone_number, content)
return
if not totp.verify(otp):
# Invalid 2FA code, send rejection message
rejection_message = "Invalid authentication code"
send_sms_response(modem, phone_number, rejection_message)
logger.warning("Invalid 2FA code - Phone Number: %s - Command: %s", phone_number, content)
return
# Separate the OTP from the message content
content = command
# Forward the original SMS to the specified email address
if EMAIL_COMMAND_ENABLED:
email_subject = f"SMS Received from {phone_number}"
# This option will send the OTP with the email
#email_body = f"Phone Number: {phone_number}\nSMS Content: {content}"
#This option sends the email stripped any leading OTP
email_body = f"Phone Number: {phone_number}\nSMS Content: {command}"
send_email(EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_ADDRESS, email_subject, email_body)
if content.strip().upper() == KEYWORD_PROCESS_LIST:
# Send process list
send_process_list(modem, phone_number)
return
elif content.strip().upper().startswith(KEYWORD_PING):
# Ping command
ping_target = content.strip().split(' ')[1]
ping_response = ping_host(ping_target)
send_ping_response(modem, phone_number, ping_response)
return
elif content.strip().upper() == KEYWORD_1:
# Execute the KEYWORD_1 command
command = KEYWORD_1_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_2:
# Execute the KEYWORD_2 command
command = KEYWORD_2_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_3:
# Execute the KEYWORD_3 command
command = KEYWORD_3_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_4:
# Execute the KEYWORD_4 command
command = KEYWORD_4_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_5:
# Execute the KEYWORD_5 command
command = KEYWORD_5_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_6:
# Execute the KEYWORD_6 command
command = KEYWORD_6_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_7:
# Execute the KEYWORD_7 command
command = KEYWORD_7_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_8:
# Execute the KEYWORD_8 command
command = KEYWORD_8_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_9:
# Execute the KEYWORD_9 command
command = KEYWORD_9_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
elif content.strip().upper() == KEYWORD_10:
# Execute the KEYWORD_10 command
command = KEYWORD_10_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
# Check if the command is a kill command
kill_pattern = r'^KILL\s+(\d+)$'
match = re.match(kill_pattern, content.strip().upper())
if match:
pid = match.group(1)
kill_process(modem, phone_number, pid)
return
# Execution of any sms command is allowed if RESTRICT_COMMANDS is set to False
if not RESTRICT_COMMANDS:
command = content + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
output = execute_shell_command(command)
build_sms_response(modem, phone_number, output)
return
# If any command is not allowed, send a warning message
error_message = "Unauthorised command"
send_sms_response(modem, phone_number, error_message)
logger.warning("Unauthorised command - Phone Number: %s - Command: %s", phone_number, content)
except ValueError as e:
error_message = "An value error occurred while parsing the SMS."
send_sms_response(modem, phone_number, error_message)
logger.exception("ValueError while parsing the SMS- Phone Number: %s - Command: %s", phone_number, content)
logger.error("Failed to parse SMS: %s", str(e))
except Exception as e:
error_message = "An exception occurred while processing the SMS."
send_sms_response(modem, phone_number, error_message)
logger.exception("Exception while processing SMS - Phone Number: %s - Command: %s", phone_number, content)
logger.error("Exception occurred: %s", str(e))
The above changes can be set to only forward incoming SMS to an email and ignore emailing back linux shell command output. Also simply whitelist all other shortcut functions to benign commands and set RESTRICT_COMMANDS = True.
To stop all SMS command output going back to the sender over SMS, a variable toggle could be set for the send_sms_response function, or just comment out the following in send_sms_response to turn off all sms replies being sent back.
One last thing to change will be to alter the phone number white list to a black list… see inside the script for notes on this simple flip.
# Send an SMS
# modem.write('AT+CMGS="{}"\r\n'.format(phone_number).encode(MODEM_CHAR_ENCODING))
#modem.read_until(b'> ')
#modem.write(command.encode(MODEM_CHAR_ENCODING))
#modem.write(bytes([26])) # Ctrl+Z
#modem.read_until(b'+CMGS: ')
#response = modem.read_until(b'OK\r\n')
# Check if the command was sent successfully
#sent_successfully = '+CMGS: ' in response.decode(MODEM_CHAR_ENCODING)
# Add a small delay between SMS messages
#time.sleep(0.5)
Finally, to SMTP relay these days you need an SMTP auth service like MS365 (with an app password)
Here’s a link to another Guacamole remote desktop VDI project of mine that includes a prebuilt smtp auth setup script for Linux. It sets up a local postfix server and runs this as a systemd service to take care of all the TLS SMTP auth in the background. Your own setup will determine whether an email password is required in the python script or if these references can be removed:
MS365 and smtp auth setup script
Again, I’ve not tested this much, but if you do a diff the actual quantum of changes to the original SMS-2-Shell code is tiny as the exiting logic and structure is just lightly added to… Let me know how it goes and what is learned
good luck!