Skip to content
Merged
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
31 changes: 31 additions & 0 deletions scanners/ncrack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,41 @@ EXAMPLES:
SEE THE MAN PAGE (http://nmap.org/ncrack/man.html) FOR MORE OPTIONS AND EXAMPLES
```

## Password encryption

Because **Ncrack** findings are very sensitive you probably don't want every *secureCodeBox* user to see them. In order
to address this issue we provide an option that lets you encrypt found passwords with public key crypto. Just
generate a key pair with openssl:

```bash
openssl genrsa -out key.pem 2048
openssl rsa -in key.pem -outform PEM -pubout -out public.pem
```

After you created the public key file you have to create a kubernetes secret from that
file:
```bash
kubectl create secret generic --from-file="public.key=public.pem" <ncrack-secret-name>
```
Now you only need to set the value *encryptPasswords.existingSecret* to the
secrets name when installing the scanner

```bash
helm install ncrack secureCodeBox/ncrack --set="encryptPasswords.existingSecret=<ncrack-secret-name>"
```

To decrypt a password from a finding use:

```bash
base64 encryptedPassword -d | openssl rsautl -decrypt -inkey key.pem -out decryptedPassword.txt
```

## Chart Configuration

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| encryptPasswords.existingSecret | string | `nil` | secret name with a pem encoded rsa public key to encrypt identified passwords |
| encryptPasswords.key | string | `"public.key"` | name of the property in the secret with the pem encoded rsa public key |
| image.repository | string | `"docker.io/securecodebox/scanner-ncrack"` | Container Image to run the scan |
| image.tag | string | `nil` | defaults to the charts appVersion |
| parseJob.ttlSecondsAfterFinished | string | `nil` | seconds after which the kubernetes job for the parser will be deleted. Requires the Kubernetes TTLAfterFinished controller: https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/ |
Expand Down
29 changes: 29 additions & 0 deletions scanners/ncrack/README.md.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,35 @@ EXAMPLES:
SEE THE MAN PAGE (http://nmap.org/ncrack/man.html) FOR MORE OPTIONS AND EXAMPLES
```

## Password encryption

Because **Ncrack** findings are very sensitive you probably don't want every *secureCodeBox* user to see them. In order
to address this issue we provide an option that lets you encrypt found passwords with public key crypto. Just
generate a key pair with openssl:

```bash
openssl genrsa -out key.pem 2048
openssl rsa -in key.pem -outform PEM -pubout -out public.pem
```

After you created the public key file you have to create a kubernetes secret from that
file:
```bash
kubectl create secret generic --from-file="public.key=public.pem" <ncrack-secret-name>
```
Now you only need to set the value *encryptPasswords.existingSecret* to the
secrets name when installing the scanner

```bash
helm install ncrack secureCodeBox/ncrack --set="encryptPasswords.existingSecret=<ncrack-secret-name>"
```

To decrypt a password from a finding use:

```bash
base64 encryptedPassword -d | openssl rsautl -decrypt -inkey key.pem -out decryptedPassword.txt
```

## Chart Configuration

{{ template "chart.valuesTable" . }}
Expand Down
6 changes: 6 additions & 0 deletions scanners/ncrack/parser/__testFiles__/public_key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDftYgZ2MhLWumXTylT/nEhZ3Ul
rk8xuf8EFA3ffMRgyW3n9mEpVFHVXZCaEYz55/pZqnsffUosPnHtKDV4uGPVqPJk
Mi5WUj6oUE9O/BXArK8pJfncOKYqCQN45hKc/Plt7uvTCTS/oFKoowv1MyzLzbrL
AI4I7JPgFA1nOp8UDQIDAQAB
-----END PUBLIC KEY-----
100 changes: 60 additions & 40 deletions scanners/ncrack/parser/parser.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,71 @@
const xml2js = require('xml2js');
const crypto = require("crypto");
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);

async function parse(fileContent) {
const { ncrackrun } = await transformXML(fileContent);
const findings = transformToFindings(ncrackrun);
return findings;
async function parse (fileContent, scan, encryptionKeyLocation = process.env['ENCRYPTION_KEY_LOCATION']) {
const { ncrackrun } = await transformXML(fileContent);
let publicKey = null;
if (encryptionKeyLocation) {
publicKey = await readPublicKey(encryptionKeyLocation)
.catch(() => {
console.log('Public key not found on file system location: ' + encryptionKeyLocation)
process.exit()
});
}
return transformToFindings(ncrackrun, publicKey);
}

function transformToFindings(ncrackrun) {
const portFindings = ncrackrun.service.flatMap(({ address, port, credentials = [] }) => {
const { addr: ipAddress } = address[0]['$'];
const { protocol, portid, name: portName } = port[0]['$'];

return credentials.map(credential => {
const { username, password } = credential['$'];

return {
name: `Credentials for Service ${portName}://${ipAddress}:${portid} discovered via bruteforce.`,
description: '',
category: 'Discovered Credentials',
location: `${portName}://${ipAddress}:${portid}`,
osi_layer: 'APPLICATION',
severity: 'HIGH',
attributes: {
port: portid,
ip_address: ipAddress,
protocol: protocol,
service: portName,
username,
password,
},
};
});
});
function transformToFindings (ncrackrun, publicKey) {
return ncrackrun.service.flatMap(({ address, port, credentials = [] }) => {
const { addr: ipAddress } = address[0]['$'];
const { protocol, portid, name: portName } = port[0]['$'];

return credentials.map(credential => {
let { username, password } = credential['$'];

if (publicKey) {
password = crypto.publicEncrypt({
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
}, Buffer.from(password)).toString("base64")
}

return portFindings;
return {
name: `Credentials for Service ${portName}://${ipAddress}:${portid} discovered via bruteforce.`,
description: '',
category: 'Discovered Credentials',
location: `${portName}://${ipAddress}:${portid}`,
osi_layer: 'APPLICATION',
severity: 'HIGH',
attributes: {
port: portid,
ip_address: ipAddress,
protocol: protocol,
service: portName,
username,
password,
},
};
});
});
}

function transformXML(fileContent) {
return new Promise((resolve, reject) => {
xml2js.parseString(fileContent, (err, xmlInput) => {
if (err) {
reject(new Error('Error converting XML to JSON in xml2js: ' + err));
} else {
resolve(xmlInput);
}
});
function transformXML (fileContent) {
return new Promise((resolve, reject) => {
xml2js.parseString(fileContent, (err, xmlInput) => {
if (err) {
reject(new Error('Error converting XML to JSON in xml2js: ' + err));
} else {
resolve(xmlInput);
}
});
});
}

async function readPublicKey (keyLocation) {
return readFile(keyLocation)
}

module.exports.parse = parse;
94 changes: 64 additions & 30 deletions scanners/ncrack/parser/parser.test.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
const { parse } = require('./parser');
const fs = require('fs');
const crypto = require("crypto")

it('should return no findings when ncrack has not found credentials', async () => {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(__dirname + '/__testFiles__/ncrack_no_results.xml', {
encoding: 'utf8',
});
const findings = await parse(ncrackXML);
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(__dirname + '/__testFiles__/ncrack_no_results.xml', {
encoding: 'utf8',
});
const findings = await parse(ncrackXML);

expect(findings.length).toBe(0);
expect(findings.length).toBe(0);
});

it('should return findings when ncrack found credentials', async () => {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(__dirname + '/__testFiles__/ncrack_with_results.xml', {
encoding: 'utf8',
});
const [finding, ...otherFindings] = await parse(ncrackXML);
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(__dirname + '/__testFiles__/ncrack_with_results.xml', {
encoding: 'utf8',
});
const [finding, ...otherFindings] = await parse(ncrackXML);

expect(finding).toMatchInlineSnapshot(`
expect(finding).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"ip_address": "192.168.0.1",
Expand All @@ -36,32 +37,32 @@ it('should return findings when ncrack found credentials', async () => {
"severity": "HIGH",
}
`);
expect(otherFindings.length).toBe(0);
expect(otherFindings.length).toBe(0);
});

it('should return no findings when ncrack has not found credentials scanning two services', async () => {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(
__dirname + '/__testFiles__/ncrack_two_services_no_results.xml',
{
encoding: 'utf8',
}
);
const findings = await parse(ncrackXML);
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(
__dirname + '/__testFiles__/ncrack_two_services_no_results.xml',
{
encoding: 'utf8',
}
);
const findings = await parse(ncrackXML);

expect(findings.length).toBe(0);
expect(findings.length).toBe(0);
});

it('should return findings when ncrack found two credentials scanning two services', async () => {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(
__dirname + '/__testFiles__/ncrack_two_services_with_results.xml',
{
encoding: 'utf8',
}
);
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(
__dirname + '/__testFiles__/ncrack_two_services_with_results.xml',
{
encoding: 'utf8',
}
);

expect(await parse(ncrackXML)).toMatchInlineSnapshot(`
expect(await parse(ncrackXML)).toMatchInlineSnapshot(`
Array [
Object {
"attributes": Object {
Expand Down Expand Up @@ -99,3 +100,36 @@ it('should return findings when ncrack found two credentials scanning two servic
`);
});

it('should encrypt findings when a public key is set', async () => {
// eslint-disable-next-line security/detect-non-literal-fs-filename
const ncrackXML = fs.readFileSync(__dirname + '/__testFiles__/ncrack_with_results.xml', {
encoding: 'utf8',
});
const [finding] = await parse(ncrackXML, null, __dirname + "/__testFiles__/public_key.pem");

let decryptedData = crypto.privateDecrypt({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
}, Buffer.from(finding.attributes.password, "base64"));

expect(finding.attributes.password.length).toBe(172);
expect(decryptedData.toString()).toBe("aaf076d4fe7cfb63fd1628df91")

});

const privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" +
"MIICXQIBAAKBgQDftYgZ2MhLWumXTylT/nEhZ3Ulrk8xuf8EFA3ffMRgyW3n9mEp\n" +
"VFHVXZCaEYz55/pZqnsffUosPnHtKDV4uGPVqPJkMi5WUj6oUE9O/BXArK8pJfnc\n" +
"OKYqCQN45hKc/Plt7uvTCTS/oFKoowv1MyzLzbrLAI4I7JPgFA1nOp8UDQIDAQAB\n" +
"AoGAV5tepkiX/7KlocS1eZg+M4exf8UobF/bd3xnBmt0+DZJ3TpGSIol1fnjRAK1\n" +
"g7SN/QlfWDCXmIYH1YkWj6UeKvWim86OV+61QX4imLAOsi7fSA8fcNRxYVX73hhk\n" +
"kxt10a4l+CPAb4cyJa4Ud3UHhLtRlanJtQyAXZtQ38fRSiECQQDxIhBjkU4Sf96t\n" +
"wpEWr/RnOA2aHOUWH8GCB4DAcw5wrISDcvRsgKggjec2VAJPovqSri1lQS4hV28M\n" +
"4iTcj+ylAkEA7YB0rAebUzbFXzMrxUPxBbjze+idw1COqCXkX+N9RYVY23D8mUlR\n" +
"8cMru4Rauu6DluSWZCgR14+Hi0TNrUHlSQJBAJBoJgh67JaHnYPSEbHUjjmCiCLT\n" +
"Sx6Exg5pD+IxBWTU7EcMgPS51/YnBWCzzu6CXC2bwfPxpP6yrf65L/om90ECQQDe\n" +
"HGYAhFSkq/JFp+tlXrbHbUJ4PQFdqbbgVh+P9YYwQBbrkm0JReKWwLnjclIPxAPY\n" +
"WAq1vCuDdr2CZ2QahifRAkBd9mv+G4WO0hOsTBypeoEnL6VECzSauDwfIP/kSdBz\n" +
"bmkZ6DCScZa8gz1J5ZamBnP4N2dtQn/zDtNUkS+qK+s2\n" +
"-----END RSA PRIVATE KEY-----";

12 changes: 12 additions & 0 deletions scanners/ncrack/templates/ncrack-parse-definition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,15 @@ spec:
handlesResultsType: ncrack-xml
image: "{{ .Values.parserImage.repository }}:{{ .Values.parserImage.tag | default .Chart.Version }}"
ttlSecondsAfterFinished: {{ .Values.parseJob.ttlSecondsAfterFinished }}
{{- if .Values.encryptPasswords.existingSecret }}
volumes:
- name: "ncrack-secret"
secret:
secretName: {{ .Values.encryptPasswords.existingSecret }}
volumeMounts:
- name: "ncrack-secret"
mountPath: "/secrets/"
env:
- name: "ENCRYPTION_KEY_LOCATION"
value: "/secrets/{{ .Values.encryptPasswords.key }}"
{{- end }}
6 changes: 6 additions & 0 deletions scanners/ncrack/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ parserImage:
# @default -- defaults to the charts version
tag: null

encryptPasswords:
# encryptPasswords.existingSecret -- secret name with a pem encoded rsa public key to encrypt identified passwords
existingSecret: null
# encryptPasswords.key -- name of the property in the secret with the pem encoded rsa public key
key: "public.key"

parseJob:
# parseJob.ttlSecondsAfterFinished -- seconds after which the kubernetes job for the parser will be deleted. Requires the Kubernetes TTLAfterFinished controller: https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/
ttlSecondsAfterFinished: null
Expand Down