Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [Unreleased]

### Added

* Add global `--json` option for [JSON Lines](https://jsonlines.org/) output. When enabled, beanshooter
emits machine readable result records (one JSON object per line, parseable with `jq`) for the `brute`,
`list` and `enum` actions. JSON output can be written to a file or to stdout (in which case the human
readable logging is redirected to stderr).


## [4.1.0] - Mar 20, 2023

### Added
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ autocompletion.
+ [download](#tonka-download)
- [JMXMP](#jmxmp)
- [Jolokia Support](#jolokia-support)
- [JSON Output](#json-output)
- [Docker Image](#docker-image)
- [Example Server](#example-server)

Expand Down Expand Up @@ -1523,6 +1524,42 @@ that exposes an *Jolokia* endpoint on port `8080`. Additionally, a regular *RMI*
can be found on port `1090`.


### JSON Output

---

For automation and post processing, *beanshooter* supports a [JSON Lines](https://jsonlines.org/) output
mode that can be enabled with the global `--json` option. When enabled, *beanshooter* emits one JSON
object per line for each result it produces, which makes the output easy to parse with tools like
[jq](https://jqlang.github.io/jq/). The records are written *live*, as they are produced, so the output
can be consumed in a streaming fashion.

The `--json` option takes an optional file argument:

* `--json` – write JSON Lines to *stdout*. In this mode the human readable logging is redirected to
*stderr*, so that *stdout* contains valid JSON Lines only.
* `--json <file>` &ndash; append JSON Lines to the specified file. The human readable logging stays on *stdout*.

The following result records are currently emitted:

| `type` | Emitted by | Notable fields |
| ------------- | --------------- | ----------------------------------------------- |
| `credentials` | `brute` | `host`, `port`, `username`, `password` |
| `mbean` | `list` | `class`, `objectName`, `interesting` |
| `enum` | `enum` | `check`, `category`, `status` |

Each record additionally contains a `type` field and an ISO-8601 `time` field. Example:

```console
[user@host ~]$ beanshooter brute 172.17.0.2 1090 --json
{"type":"credentials","time":"2023-03-20T12:00:00.123Z","host":"172.17.0.2","port":1090,"username":"controlRole","password":"control"}
{"type":"credentials","time":"2023-03-20T12:00:00.210Z","host":"172.17.0.2","port":1090,"username":"monitorRole","password":"monitor"}

[user@host ~]$ beanshooter list 172.17.0.2 1090 --json 2>/dev/null | jq -r 'select(.interesting) | .objectName'
de.qtc.beanshooter:type=Tonka
```


### Docker Image

---
Expand Down
3 changes: 3 additions & 0 deletions beanshooter/src/de/qtc/beanshooter/cli/ArgumentHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Properties;

import de.qtc.beanshooter.exceptions.ExceptionHandler;
import de.qtc.beanshooter.io.JsonLogger;
import de.qtc.beanshooter.io.Logger;
import de.qtc.beanshooter.mbean.MBean;
import de.qtc.beanshooter.mbean.MBeanOperation;
Expand Down Expand Up @@ -79,6 +80,8 @@ private void initialize()
if( BeanshooterOption.GLOBAL_NO_COLOR.getBool() )
Logger.disableColor();

JsonLogger.enable(BeanshooterOption.GLOBAL_JSON.getValue());

PluginSystem.init(BeanshooterOption.GLOBAL_PLUGIN.getValue());
}

Expand Down
6 changes: 6 additions & 0 deletions beanshooter/src/de/qtc/beanshooter/cli/OptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ public static void addModifiers(Option option, Argument arg)
arg.setDefault("");
}

if (option == BeanshooterOption.GLOBAL_JSON)
{
arg.nargs("?");
arg.setConst("-");
}

if (option == TonkaBeanOption.DOWNLOAD_DEST)
arg.nargs("?");

Expand Down
183 changes: 183 additions & 0 deletions beanshooter/src/de/qtc/beanshooter/io/JsonLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package de.qtc.beanshooter.io;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* The JsonLogger class implements optional JSON Lines output for beanshooter. When enabled via the
* global '--json' option, beanshooter emits machine readable result records (one JSON object per line)
* that can be parsed with tools like jq. Each record contains at least a 'type' field that identifies
* the kind of result (e.g. 'credentials', 'mbean', 'enum') and a 'time' field with an ISO-8601 timestamp.
*
* Records are written as they are produced (live), which makes it possible to consume beanshooter output
* in a streaming fashion. When JSON output is sent to stdout, the human readable logging produced by the
* {@link Logger} class is automatically redirected to stderr, so that stdout contains valid JSON Lines only.
*
* @author Olivier Cervello
*/
public class JsonLogger
{
private static boolean enabled = false;
private static PrintStream out = null;

/**
* Enable JSON Lines output. The destination is taken from the global '--json' option:
*
* - null -> JSON output disabled (default)
* - "-" -> JSON output written to stdout (human readable logging redirected to stderr)
* - <path> -> JSON output appended to the specified file
*
* @param destination value of the global '--json' option
*/
public static void enable(String destination)
{
if (destination == null)
return;

if (destination.equals("-"))
{
out = System.out;
Logger.redirectStdoutToStderr();
}

else
{
try
{
out = new PrintStream(new FileOutputStream(destination, true), true);
}

catch (IOException e)
{
Logger.eprintlnMixedYellow("Unable to open JSON output file", destination, "- disabling JSON output.");
return;
}
}

enabled = true;
}

/**
* @return true if JSON Lines output is currently enabled
*/
public static boolean isEnabled()
{
return enabled;
}

/**
* Emit a single JSON Lines record. The provided key/value pairs are added to the record in order.
* A 'type' and a 'time' field are always prepended automatically. This method is a no-op when JSON
* output is disabled, so it is safe to call unconditionally from anywhere within beanshooter.
*
* @param type record type identifier (e.g. 'credentials', 'mbean', 'enum')
* @param kv alternating key/value pairs to include within the record
*/
public static synchronized void log(String type, Object... kv)
{
if (!enabled)
return;

Map<String,Object> record = new LinkedHashMap<String,Object>();
record.put("type", type);
record.put("time", Instant.now().toString());

for (int i = 0; i + 1 < kv.length; i += 2)
record.put(String.valueOf(kv[i]), kv[i + 1]);

out.println(serialize(record));
out.flush();
}

/**
* Serialize a map into a compact, single line JSON object.
*/
private static String serialize(Map<String,Object> map)
{
StringBuilder builder = new StringBuilder();
builder.append('{');

boolean first = true;

for (Map.Entry<String,Object> entry : map.entrySet())
{
if (!first)
builder.append(',');

first = false;

builder.append(quote(entry.getKey()));
builder.append(':');
builder.append(serializeValue(entry.getValue()));
}

builder.append('}');
return builder.toString();
}

/**
* Serialize a single value into its JSON representation. Strings are quoted and escaped, numbers and
* booleans are emitted verbatim, null becomes 'null' and everything else is rendered via toString().
*/
private static String serializeValue(Object value)
{
if (value == null)
return "null";

if (value instanceof Number || value instanceof Boolean)
return value.toString();

return quote(value.toString());
}

/**
* Quote and escape a string according to the JSON specification.
*/
private static String quote(String value)
{
StringBuilder builder = new StringBuilder();
builder.append('"');

for (int i = 0; i < value.length(); i++)
{
char c = value.charAt(i);

switch (c)
{
case '"':
builder.append("\\\"");
break;
case '\\':
builder.append("\\\\");
break;
case '\n':
builder.append("\\n");
break;
case '\r':
builder.append("\\r");
break;
case '\t':
builder.append("\\t");
break;
case '\b':
builder.append("\\b");
break;
case '\f':
builder.append("\\f");
break;
default:
if (c < 0x20)
builder.append(String.format("\\u%04x", (int) c));
else
builder.append(c);
}
}

builder.append('"');
return builder.toString();
}
}
34 changes: 32 additions & 2 deletions beanshooter/src/de/qtc/beanshooter/io/Logger.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public class Logger {
public static boolean stdout = true;
public static boolean stderr = true;

/*
* When JSON Lines output is written to stdout, the human readable logging is redirected to stderr
* so that stdout only contains valid JSON. This flag is enabled by JsonLogger in that situation.
*/
public static boolean redirectStdoutToStderr = false;

public static void redirectStdoutToStderr() {
Logger.redirectStdoutToStderr = true;
}

public static void disable() {
Logger.stdout = false;
Logger.stderr = false;
Expand Down Expand Up @@ -102,10 +112,12 @@ private static void log(String msg, boolean newline)
{
if( Logger.stdout ) {

java.io.PrintStream stream = Logger.redirectStdoutToStderr ? System.err : System.out;

if( newline )
System.out.println(msg);
stream.println(msg);
else
System.out.print(msg);
stream.print(msg);
}
}

Expand Down Expand Up @@ -601,34 +613,52 @@ public static void printInfoBox()
Logger.printlnBlue("--------------------------------");
}

/*
* Name of the enumeration check that is currently running. It is used to label the JSON Lines
* records that are emitted by the status* methods below. The context is set by the EnumHelper
* before each individual check.
*/
public static String enumContext = null;

public static void setEnumContext(String context)
{
Logger.enumContext = context;
}

public static void statusVulnerable()
{
printlnMixedRed(" Vulnerability Status:", "Vulnerable");
JsonLogger.log("enum", "check", enumContext, "category", "vulnerability", "status", "vulnerable");
}

public static void statusOk()
{
printlnMixedGreen(" Vulnerability Status:", "Non Vulnerable");
JsonLogger.log("enum", "check", enumContext, "category", "vulnerability", "status", "non-vulnerable");
}

public static void statusOutdated()
{
printlnMixedPurple(" Configuration Status:", "Outdated");
JsonLogger.log("enum", "check", enumContext, "category", "configuration", "status", "outdated");
}

public static void statusDefault()
{
printlnMixedGreen(" Configuration Status:", "Current Default");
JsonLogger.log("enum", "check", enumContext, "category", "configuration", "status", "default");
}

public static void statusNonDefault()
{
printlnMixedRed(" Configuration Status:", "Non Default");
JsonLogger.log("enum", "check", enumContext, "category", "configuration", "status", "non-default");
}

public static void statusUndecided(String statusType)
{
printlnMixedPurple(" " + statusType + " Status:", "Undecided");
JsonLogger.log("enum", "check", enumContext, "category", statusType.toLowerCase(), "status", "undecided");
}

public static void resetIndent()
Expand Down
Loading