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
10 changes: 10 additions & 0 deletions modules/drda.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package modules

import (
"github.com/zmap/zgrab2"
"github.com/zmap/zgrab2/modules/drda"
)

func init() {
zgrab2.RegisterModule(drda.NewModule())
}
162 changes: 162 additions & 0 deletions modules/drda/drda.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package drda

import (
"encoding/binary"
"encoding/hex"
"fmt"
)

// DRDA (Distributed Relational Database Architecture) is the wire protocol
// spoken by IBM DB2 (and Apache Derby / Informix). A DB2 server responds to an
// EXCSAT ("Exchange Server Attributes") request with an EXCSATRD reply carrying
// server-identifying attributes, all encoded in EBCDIC.

// DDM code points used by this module.
const (
cpEXCSAT = 0x1041 // Exchange Server Attributes (request)
cpPRDID = 0x112e // Product ID
cpSRVCLSNM = 0x1147 // Server Class Name (platform, e.g. "QDB2/NT64")
cpSRVRLSLV = 0x115a // Server Product Release Level (e.g. "SQL11013")
cpEXTNAM = 0x115e // External Name
cpSRVNAM = 0x116d // Server Name (instance name, e.g. "DB2")
cpMGRLVLLS = 0x1404 // Manager-Level List
cpEXCSATRD = 0x1443 // Exchange Server Attributes Reply Data

ddmMagic = 0xD0 // marks the start of every DDM message
ddmHeaderLen = 10 // length(2) magic(1) format(1) corrId(2) length2(2) codePoint(2)
)

// mgrlvlls is the standard Manager-Level List sent by nmap/Shodan in EXCSAT.
var mgrlvlls = mustHex("1403000724070008240f00081440000814740008")

// e2a is the EBCDIC (code page 500) -> ASCII translation table used to decode
// the string attributes returned in the EXCSATRD reply.
var e2a = mustHex("000102039C09867F978D8E0B0C0D0E0F" +
"101112139D8508871819928F1C1D1E1F" +
"80818283840A171B88898A8B8C050607" +
"909116939495960498999A9B14159E1A" +
"20A0A1A2A3A4A5A6A7A8D52E3C282B7C" +
"26A9AAABACADAEAFB0B121242A293B5E" +
"2D2FB2B3B4B5B6B7B8B9E52C255F3E3F" +
"BABBBCBDBEBFC0C1C2603A2340273D22" +
"C3616263646566676869C4C5C6C7C8C9" +
"CA6A6B6C6D6E6F707172CBCCCDCECFD0" +
"D17E737475767778797AD2D3D45BD6D7" +
"D8D9DADBDCDDDEDFE0E1E2E3E45DE6E7" +
"7B414243444546474849E8E9EAEBECED" +
"7D4A4B4C4D4E4F505152EEEFF0F1F2F3" +
"5C9F535455565758595AF4F5F6F7F8F9" +
"30313233343536373839FAFBFCFDFEFF")

func mustHex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
panic(fmt.Sprintf("drda: invalid hex constant %q: %v", s, err))
}
return b
}

// ebcdicToASCII decodes an EBCDIC byte slice into an ASCII string.
func ebcdicToASCII(b []byte) string {
out := make([]byte, len(b))
for i, c := range b {
out[i] = e2a[c]
}
return string(out)
}

// buildEXCSAT builds the DRDA EXCSAT probe packet. It mirrors the request sent
// by nmap's drda-info and Shodan: empty EXTNAM/SRVNAM/SRVRLSLV/SRVCLSNM plus the
// standard MGRLVLLS.
func buildEXCSAT() []byte {
var params []byte
appendParam := func(cp int, data []byte) {
p := make([]byte, 4+len(data))
binary.BigEndian.PutUint16(p[0:2], uint16(4+len(data)))
binary.BigEndian.PutUint16(p[2:4], uint16(cp))
copy(p[4:], data)
params = append(params, p...)
}
appendParam(cpEXTNAM, nil)
appendParam(cpSRVNAM, nil)
appendParam(cpSRVRLSLV, nil)
appendParam(cpMGRLVLLS, mgrlvlls)
appendParam(cpSRVCLSNM, nil)

total := ddmHeaderLen + len(params)
ddm := make([]byte, ddmHeaderLen, total)
binary.BigEndian.PutUint16(ddm[0:2], uint16(total)) // Length
ddm[2] = ddmMagic // Magic (0xD0)
ddm[3] = 0x01 // Format (no CHAINED bit: this is a lone request)
binary.BigEndian.PutUint16(ddm[4:6], 1) // CorrelationID
binary.BigEndian.PutUint16(ddm[6:8], uint16(total-6)) // Length2
binary.BigEndian.PutUint16(ddm[8:10], cpEXCSAT) // CodePoint

return append(ddm, params...)
}

// excsatrd holds the ASCII-decoded attributes parsed out of an EXCSATRD reply.
type excsatrd struct {
externalName string
serverClass string
serverName string
releaseLevel string
productID string
}

// parseEXCSATRD scans a DRDA response for an EXCSATRD DDM and extracts its
// string attributes, decoding each from EBCDIC to ASCII. Returns false if no
// EXCSATRD DDM is present.
func parseEXCSATRD(data []byte) (*excsatrd, bool) {
// Walk the (possibly chained) top-level DDM messages.
for pos := 0; pos+ddmHeaderLen <= len(data); {
ddmLen := int(binary.BigEndian.Uint16(data[pos : pos+2]))
magic := data[pos+2]
codePoint := int(binary.BigEndian.Uint16(data[pos+8 : pos+10]))
if magic != ddmMagic || ddmLen < ddmHeaderLen || pos+ddmLen > len(data) {
return nil, false
}
if codePoint != cpEXCSATRD {
pos += ddmLen
continue
}

res := &excsatrd{}
// Parse the nested parameters that fill the rest of this DDM.
p := pos + ddmHeaderLen
end := pos + ddmLen
for p+4 <= end {
paramLen := int(binary.BigEndian.Uint16(data[p : p+2]))
paramCP := int(binary.BigEndian.Uint16(data[p+2 : p+4]))
if paramLen < 4 || p+paramLen > end {
break
}
value := ebcdicToASCII(data[p+4 : p+paramLen])
switch paramCP {
case cpEXTNAM:
res.externalName = value
case cpSRVCLSNM:
res.serverClass = value
case cpSRVNAM:
res.serverName = value
case cpSRVRLSLV:
res.releaseLevel = value
case cpPRDID:
res.productID = value
}
p += paramLen
}
return res, true
}
return nil, false
}

// versionFromReleaseLevel converts a DB2 product release level such as
// "SQL11013" into a human-readable version like "11.01.3". Returns "" if the
// input does not match the expected form.
func versionFromReleaseLevel(rel string) string {
if len(rel) < 8 || rel[:3] != "SQL" {
return ""
}
return fmt.Sprintf("%s.%s.%s", rel[3:5], rel[5:7], rel[7:8])
}
40 changes: 40 additions & 0 deletions modules/drda/drda_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package drda

import (
"encoding/hex"
"testing"
)

func FuzzParseEXCSATRD(f *testing.F) {
// A well-formed EXCSATRD reply captured from a real IBM DB2 11.1 server.
realReply, _ := hex.DecodeString(
"0066d0030001006014430024115ec4c2f240404040408482f2a2a8a28340f2c4" +
"f9f4c3f2c2f46cc6c5c46ce8f0f0001814041403000724070008240f00081440" +
"000814740008000d1147d8c4c2f261d5e3f6f40007116dc4c2f2000c115ae2d8d3" +
"f1f1f0f1f3")
f.Add(realReply)

// A minimal, valid EXCSATRD produced by our own builder logic.
f.Add(makeEXCSATRD(
makeParam(cpSRVCLSNM, asciiToEBCDIC("QDB2/NT64")),
makeParam(cpSRVRLSLV, asciiToEBCDIC("SQL11013")),
))

// Degenerate / adversarial seeds: too short, bad magic, lying lengths.
f.Add([]byte{})
f.Add([]byte{0x00, 0x0a})
f.Add([]byte{0x00, 0x0a, 0xd0, 0x03, 0x00, 0x01, 0x00, 0x04, 0x14, 0x43})
// Length prefix claims more than the buffer holds.
f.Add([]byte{0xff, 0xff, 0xd0, 0x03, 0x00, 0x01, 0x00, 0x60, 0x14, 0x43})
// A parameter whose length overruns the DDM.
f.Add([]byte{0x00, 0x0e, 0xd0, 0x03, 0x00, 0x01, 0x00, 0x08, 0x14, 0x43, 0xff, 0xff, 0x11, 0x47})

f.Fuzz(func(t *testing.T, data []byte) {
// Must never panic or read out of bounds on arbitrary input.
attrs, ok := parseEXCSATRD(data)
if ok && attrs != nil {
// Exercise the downstream decode path on any parsed attributes.
_ = versionFromReleaseLevel(attrs.releaseLevel)
}
})
}
128 changes: 128 additions & 0 deletions modules/drda/drda_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package drda

import (
"encoding/binary"
"net"
"os"
"testing"
"time"
)

// a2e inverts the e2a table so tests can synthesize EBCDIC-encoded attributes.
func a2e() [256]byte {
var t [256]byte
for e := 0; e < 256; e++ {
t[e2a[e]] = byte(e)
}
return t
}

func asciiToEBCDIC(s string) []byte {
tbl := a2e()
out := make([]byte, len(s))
for i := 0; i < len(s); i++ {
out[i] = tbl[s[i]]
}
return out
}

func makeParam(cp int, data []byte) []byte {
p := make([]byte, 4+len(data))
binary.BigEndian.PutUint16(p[0:2], uint16(4+len(data)))
binary.BigEndian.PutUint16(p[2:4], uint16(cp))
copy(p[4:], data)
return p
}

func makeEXCSATRD(params ...[]byte) []byte {
var body []byte
for _, p := range params {
body = append(body, p...)
}
total := ddmHeaderLen + len(body)
ddm := make([]byte, ddmHeaderLen, total)
binary.BigEndian.PutUint16(ddm[0:2], uint16(total))
ddm[2] = ddmMagic
ddm[3] = 0x41
binary.BigEndian.PutUint16(ddm[4:6], 1)
binary.BigEndian.PutUint16(ddm[6:8], uint16(total-6))
binary.BigEndian.PutUint16(ddm[8:10], cpEXCSATRD)
return append(ddm, body...)
}

func TestBuildEXCSAT(t *testing.T) {
got := buildEXCSAT()
// Header must start with total length, DDM magic and EXCSAT codepoint.
if len(got) != int(binary.BigEndian.Uint16(got[0:2])) {
t.Fatalf("length prefix %d != actual length %d", binary.BigEndian.Uint16(got[0:2]), len(got))
}
if got[2] != ddmMagic {
t.Errorf("magic = 0x%02x, want 0x%02x", got[2], ddmMagic)
}
if cp := binary.BigEndian.Uint16(got[8:10]); cp != cpEXCSAT {
t.Errorf("codepoint = 0x%04x, want 0x%04x", cp, cpEXCSAT)
}
}

func TestParseEXCSATRD(t *testing.T) {
pkt := makeEXCSATRD(
makeParam(cpEXTNAM, asciiToEBCDIC("DB2 db2sysc 2D9425E0")),
makeParam(cpSRVCLSNM, asciiToEBCDIC("QDB2/NT64")),
makeParam(cpSRVNAM, asciiToEBCDIC("DB2")),
makeParam(cpSRVRLSLV, asciiToEBCDIC("SQL11013")),
)
attrs, ok := parseEXCSATRD(pkt)
if !ok {
t.Fatal("parseEXCSATRD returned ok=false")
}
if attrs.serverClass != "QDB2/NT64" {
t.Errorf("serverClass = %q, want %q", attrs.serverClass, "QDB2/NT64")
}
if attrs.serverName != "DB2" {
t.Errorf("serverName = %q, want %q", attrs.serverName, "DB2")
}
if attrs.releaseLevel != "SQL11013" {
t.Errorf("releaseLevel = %q, want %q", attrs.releaseLevel, "SQL11013")
}
if attrs.externalName != "DB2 db2sysc 2D9425E0" {
t.Errorf("externalName = %q", attrs.externalName)
}
if v := versionFromReleaseLevel(attrs.releaseLevel); v != "11.01.3" {
t.Errorf("version = %q, want %q", v, "11.01.3")
}
}

func TestParseEXCSATRD_NotDB2(t *testing.T) {
if _, ok := parseEXCSATRD([]byte("not a drda response at all")); ok {
t.Error("parseEXCSATRD accepted non-DRDA data")
}
}

// TestLive performs a real scan against a DRDA/DB2 server. Set DRDA_LIVE_TARGET
// to an "ip:port" (e.g. 45.5.105.132:50000) to enable it.
func TestLive(t *testing.T) {
target := os.Getenv("DRDA_LIVE_TARGET")
if target == "" {
t.Skip("set DRDA_LIVE_TARGET=ip:port to run the live test")
}
conn, err := net.DialTimeout("tcp", target, 10*time.Second)
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
if _, err = conn.Write(buildEXCSAT()); err != nil {
t.Fatalf("write: %v", err)
}
data, err := readDDM(conn)
if err != nil {
t.Fatalf("read: %v", err)
}
attrs, ok := parseEXCSATRD(data)
if !ok {
t.Fatalf("not an EXCSATRD response: %x", data)
}
t.Logf("serverClass=%q instanceName=%q releaseLevel=%q version=%q externalName=%q productID=%q",
attrs.serverClass, attrs.serverName, attrs.releaseLevel,
versionFromReleaseLevel(attrs.releaseLevel), attrs.externalName, attrs.productID)
}
Loading