Skip to content

Commit 3c5ee9a

Browse files
authored
Merge pull request #57 from nccgroup/feature/IPv6
IPv6 support and additional PNA bypasses
2 parents 9d03fce + 0ffda1a commit 3c5ee9a

File tree

6 files changed

+441
-76
lines changed

6 files changed

+441
-76
lines changed

cmd/singularity-server/main.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"flag"
55
"fmt"
66
"log"
7+
"net"
78
"net/http"
89
"strconv"
910
"time"
@@ -28,10 +29,26 @@ func (a *arrayPortFlags) Set(value string) error {
2829
return nil
2930
}
3031

32+
type ArrIpAddressFlags []net.IP
33+
34+
func (a *ArrIpAddressFlags) String() string {
35+
return fmt.Sprintf("%T", a)
36+
}
37+
38+
func (a *ArrIpAddressFlags) Set(value string) error {
39+
addr := net.ParseIP(value)
40+
if addr == nil {
41+
log.Fatal("Could not parse IP address")
42+
}
43+
*a = append(*a, addr)
44+
return nil
45+
}
46+
3147
// Parse command line arguments and capture these into a runtime structure
3248
func initFromCmdLine() *singularity.AppConfig {
3349
var appConfig = singularity.AppConfig{}
3450
var myArrayPortFlags arrayPortFlags
51+
var myArrIpAddressesFlags ArrIpAddressFlags
3552

3653
var responseIPAddr = flag.String("ResponseIPAddr", "unconfigured",
3754
"Specify the attacker host external IP address that will be used for default DNS responses. Useful for handling QNAME Minimization.")
@@ -44,6 +61,7 @@ func initFromCmdLine() *singularity.AppConfig {
4461
"Specify the attacker HTTP Proxy Server and Websockets port that permits to browse hijacked client services.")
4562
var enableLinuxTProxySupport = flag.Bool("enableLinuxTProxySupport", false, "Specify whether to enable Linux TProxy support or not. Useful to listen on many ports with an appropriate iptables configuration.")
4663
flag.Var(&myArrayPortFlags, "HTTPServerPort", "Specify the attacker HTTP Server port that will serve HTML/JavaScript files. Repeat this flag to listen on more than one HTTP port.")
64+
flag.Var(&myArrIpAddressesFlags, "ignoreDNSRequestFrom", "Specify a source IP address to ignore DNS requests from. Repeat this flag to ignore more than one IP address. Useful for reliable DNS rebinding sessions, where third-parties may repeat DNS requests from targets.")
4765
var dnsServerBindAddr = flag.String("DNSServerBindAddr", "0.0.0.0", "Specify the IP address the DNS server will bind to, defaults to 0.0.0.0")
4866

4967
flag.Parse()
@@ -65,6 +83,7 @@ func initFromCmdLine() *singularity.AppConfig {
6583
appConfig.DNSServerBindAddr = *dnsServerBindAddr
6684
appConfig.WsHTTPProxyServerPort = *WsHttpProxyServerPort
6785
appConfig.EnableLinuxTProxySupport = *enableLinuxTProxySupport
86+
appConfig.IgnoreDNSRequestFrom = myArrIpAddressesFlags
6887

6988
return &appConfig
7089
}

commandcontrol.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ func (c *WSClient) keepAlive(wscss *WebsocketClientStateStore, sessionID string)
396396
}
397397
}
398398

399-
//RoundTrip is a custom RoundTrip implementation for reverse proxy to websocket
399+
// RoundTrip is a custom RoundTrip implementation for reverse proxy to websocket
400400
func (t *ProxytoWebsocketTransport) RoundTrip(req *http.Request) (*http.Response, error) {
401401
headers := make(map[string]string)
402402

@@ -608,7 +608,7 @@ func (ws *WebsocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
608608
}
609609
defer c.Close()
610610

611-
name, err := NewDNSQuery(r.Header.Get("origin"))
611+
name, err := NewDNSQueryFromOrigin(r.Header.Get("origin"))
612612

613613
if err != nil {
614614
log.Printf("websockets: could not parse origin hostname: %v\n", err)

firewall.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@ import (
77
"strconv"
88
)
99

10-
//IPTablesRule is a struct representing a linux iptable firewall rule
10+
// IPTablesRule is a struct representing a linux iptable firewall rule
1111
type IPTablesRule struct {
1212
srcAddr string
1313
srcPort string
1414
dstAddr string
1515
dstPort string
1616
srcPortRange string
17+
v6 bool
1718
}
1819

19-
//NewIPTableRule populate an iptables rule
20+
// NewIPTableRule populate an iptables rule
2021
func NewIPTableRule(srcAddr string, srcPort string,
21-
dstAddr string, dstPort string) *IPTablesRule {
22+
dstAddr string, dstPort string, v6 bool) *IPTablesRule {
2223
p := IPTablesRule{srcAddr: srcAddr, srcPort: srcPort,
23-
dstAddr: dstAddr, dstPort: dstPort}
24+
dstAddr: dstAddr, dstPort: dstPort, v6: v6}
2425
p.generateSourcePortRange(10)
2526
return &p
2627
}
@@ -48,20 +49,24 @@ func (ipt *IPTablesRule) generateSourcePortRange(max int) {
4849
}
4950

5051
func (ipt *IPTablesRule) makeAndRunRule(command string) {
51-
rule := exec.Command("/sbin/iptables",
52+
iptables := "/sbin/iptables"
53+
if ipt.v6 {
54+
iptables = "/usr/sbin/ip6tables"
55+
}
56+
rule := exec.Command(iptables,
5257
command, "INPUT", "-p", "tcp", "-j", "REJECT", "--reject-with", "tcp-reset",
5358
"--source", ipt.srcAddr, //"--sport" srcPortRange,
5459
"--destination", ipt.dstAddr, "--destination-port", ipt.dstPort)
5560
err := rule.Run()
5661
log.Printf("Firewall: `iptables` finished with return code: %v", err)
5762
}
5863

59-
//AddRule adds an iptables rule in Linux iptable
64+
// AddRule adds an iptables rule in Linux iptable
6065
func (ipt *IPTablesRule) AddRule() {
6166
ipt.makeAndRunRule("-A")
6267
}
6368

64-
//RemoveRule removes an iptables rule in Linux iptable
69+
// RemoveRule removes an iptables rule in Linux iptable
6570
func (ipt *IPTablesRule) RemoveRule() {
6671
ipt.makeAndRunRule("-D")
6772
}

html/manager.js

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ function putData(url, data) {
340340
const App = () => {
341341
let configuration = null;
342342
let fm = null;
343-
let hosturl = "http://s-%1-%2-%3-%4-e.%5:%6/%7";
343+
let hosturl = "http://s-%1.%2-%3-%4-e.%5:%6/%7";
344344

345345
// Push settings from configuration object (obtained from manager-config.json) to UI.
346346
function populateManagerConfig() {
@@ -367,10 +367,140 @@ const App = () => {
367367
document.getElementById('flushdns').checked = configuration.getFlushDns();
368368
};
369369

370+
371+
// Helper functions to allow users inputting common IP addresses instead of hexstrings, and CNAMEs
372+
373+
function ipToHexOrOriginal(input) {
374+
let ipv4Bytes = parseIPv4(input);
375+
if (ipv4Bytes !== null) {
376+
return bytesToHex(ipv4Bytes);
377+
}
378+
379+
let ipv6Bytes = parseIPv6(input);
380+
if (ipv6Bytes !== null) {
381+
return bytesToHex(ipv6Bytes);
382+
}
383+
384+
// Not an IPv4 or IPv6, return original string, typically a CNAME record
385+
return input;
386+
}
387+
388+
function parseIPv4(str) {
389+
const parts = str.split('.');
390+
if (parts.length !== 4) {
391+
return null;
392+
}
393+
394+
let bytes = new Uint8Array(4);
395+
for (let i = 0; i < 4; i++) {
396+
const num = Number(parts[i]);
397+
// Each part must be an integer within [0..255]
398+
if (
399+
!Number.isInteger(num) ||
400+
num < 0 ||
401+
num > 255
402+
) {
403+
return null;
404+
}
405+
bytes[i] = num;
406+
}
407+
return bytes;
408+
}
409+
410+
function parseIPv6(str) {
411+
// Split on '::' first - only one such sequence is allowed
412+
let parts = str.split('::');
413+
414+
if (parts.length > 2) {
415+
return null; // invalid: more than one '::'
416+
}
417+
418+
let head = [];
419+
let tail = [];
420+
421+
// Split the head by ':'
422+
if (parts[0] !== '') {
423+
head = parts[0].split(':');
424+
} else {
425+
// If parts[0] is empty, it means the address starts with '::'
426+
head = [];
427+
}
428+
429+
// Split the tail by ':'
430+
if (parts.length === 2 && parts[1] !== '') {
431+
tail = parts[1].split(':');
432+
} else if (parts.length === 2 && parts[1] === '') {
433+
// If parts[1] is empty, it means the address ends with '::'
434+
tail = [];
435+
}
436+
437+
// Now we know the total number of blocks we have
438+
// The full IPv6 must have 8 groups (16 bytes total)
439+
let missingGroups = 8 - (head.length + tail.length);
440+
if (missingGroups < 0) {
441+
// Means we have more than 8 groups in total
442+
return null;
443+
}
444+
445+
// Build the full array of groups in hex
446+
let groups = [];
447+
448+
// Validate and push the head
449+
for (let h of head) {
450+
if (!isValidHextet(h)) {
451+
return null;
452+
}
453+
groups.push(h);
454+
}
455+
456+
// Insert the zero-groups for '::'
457+
for (let i = 0; i < missingGroups; i++) {
458+
groups.push('0');
459+
}
460+
461+
// Validate and push the tail
462+
for (let t of tail) {
463+
if (!isValidHextet(t)) {
464+
return null;
465+
}
466+
groups.push(t);
467+
}
468+
469+
if (groups.length !== 8) {
470+
return null; // sanity check
471+
}
472+
473+
// Convert each group from hex into two bytes
474+
let bytes = new Uint8Array(16);
475+
for (let i = 0; i < 8; i++) {
476+
let val = parseInt(groups[i], 16);
477+
// High byte
478+
bytes[i * 2] = (val >> 8) & 0xff;
479+
// Low byte
480+
bytes[i * 2 + 1] = val & 0xff;
481+
}
482+
483+
return bytes;
484+
}
485+
486+
function isValidHextet(str) {
487+
if (str.length === 0 || str.length > 4) {
488+
return false;
489+
}
490+
// Check valid hex
491+
return /^[0-9A-Fa-f]+$/.test(str);
492+
}
493+
494+
function bytesToHex(bytes) {
495+
return Array.from(bytes)
496+
.map(b => b.toString(16).padStart(2, '0'))
497+
.join('');
498+
}
499+
370500
function generateAttackUrl(targetHostIPAddress, targetPort, forceDnsRebindingStrategyName) {
371501
return hosturl
372-
.replace("%1", configuration.getAttackHostIPAddress())
373-
.replace("%2", targetHostIPAddress) // replace(/-/g, '--'))
502+
.replace("%1", ipToHexOrOriginal(configuration.getAttackHostIPAddress()))
503+
.replace("%2", ipToHexOrOriginal(targetHostIPAddress)) // replace(/-/g, '--'))
374504
.replace("%3", Math.floor(Math.random() * 2 ** 32))
375505
.replace("%4", forceDnsRebindingStrategyName === null ?
376506
configuration.getRebindingStrategy() : forceDnsRebindingStrategyName)
@@ -568,8 +698,8 @@ const App = () => {
568698

569699

570700
let fid = fm.addFrame(hosturl
571-
.replace("%1", document.getElementById('attackhostipaddress').value)
572-
.replace("%2", document.getElementById('targethostipaddress').value.replace(/-/g, '--'))
701+
.replace("%1", ipToHexOrOriginal(document.getElementById('attackhostipaddress').value))
702+
.replace("%2", ipToHexOrOriginal(document.getElementById('targethostipaddress').value.replace(/-/g, '--')))
573703
.replace("%3", Math.floor(Math.random() * 2 ** 32))
574704
.replace("%4", document.getElementById('rebindingStrategy').value)
575705
.replace("%5", document.getElementById('attackhostdomain').value)

0 commit comments

Comments
 (0)