If you are reading this blog post via a 3rd party source it is very likely that many parts of it will not render correctly (usually, the interactive graphs). Please view the post on dogesec.com for the full interactive viewing experience.

tl;dr

Turn card numbers into STIX 2.1 objects. Enrich the data with issuer information. Track transactions made by the card. Then link the cards and transactions to other STIX objects in your research (Actors, Incidents, etc.).

Overview

I have recently been involved in a number of threat intelligence based projects for financial institutions.

One of the biggest problems these organisations face is fraud. The number of fraudulent transactions continues to increase, despite more advance methods to detect them.

As a starting point to combat this problem, many of these institutions are tracking stolen card data sold on online card forums.

The problem is, this is often just a list of card numbers, expiry dates, card security codes, card holder names and addresses in a huge .csv file.

A lot of threat intel companies sell leaked credit card data which the financial institutions subscribe to storing the data in relational databases (usually a TIP), which they then cross-reference against card their institution has issued.

This is not only time consuming, but it also incredibly inefficient. It also does not track the next part, any fraudulent transactions that occurred using the leaked cards. By linking this data to each card, and potentially a threat actor, allows for patterns in fraudulent activity to be better understood, leading to better detection of future fraudulent activity.

Let me explain step-by-step how I managed to achieve such a workflow for these institutions.

Creating new STIX Objects

Most organisations are standardising on STIX 2.1, especially in the government and banking sectors.

STIX objects are also built to be represented as a network of objects, perfect for representing the connection (e.g. leaked card to hacking forum post, hacking forum post to threat actor, etc.).

The problem being, there are no current STIX objects that represent credit cards or the transactions they conduct.

So we decided to create two custom STIX cyber observable objects;

  1. bank-card: captures card details, issuer info, and cardholder info
  2. bank-card-transaction: captures the details of any known transactions linked to the card

Here is how I imagined the graph in my head, of how this could all fit together;

I have included how this card can be linked to a report (e.g. describing a credit card leak on a forum) which references a threat actor (e.g. responsible for cloning and dumping the card).

In this post I will skip the bank-card-transaction data as this data is very sensitive, and only accessible by financial institutions, and even then, by a very limited subset of individuals in those institutions.

Here is an example of the bank-card STIX object I designed;

{
    "type": "bank-card",
    "spec_version": "2.1",
    "id": "bank-card--2bb315d3-2a76-52db-9740-cb1bb46626b2",
    "format": "credit",
    "number": "4242424242424242",
    "scheme": "VISA",
    "brand": "VISA",
    "currency": "GBP",
    "issuer_ref": "identity--2ecc726e-7b25-4729-a029-29311220f1f1",
    "holder_ref": "identity--a64cc6d3-3470-42ea-95a2-a3bdb2968c3b",
    "valid_from": "01/99",
    "valid_to": "01/00",
    "security_code": "999",
    "extensions": {
        "extension-definition--7922f91a-ee77-58a5-8217-321ce6a2d6e0": {
            "extension_type": "new-sco"
        }
    }
}

Of course, it’s unlikely all data shown will be present in a data dump of card details.

What I do know is, at the very minimum, there will be a card number in the dump, from which you can actually infer a lot of information based on its structure.

IIN Lookups

The first 6 or 8 digits of a payment card number cards, debit cards, etc.) are known as the Issuer Identification Numbers (IIN), previously known as Bank Identification Number (BIN). These identify the institution that issued the card to the card holder.

For example, 531903 identifies the card as a MasterCard issued in the United States by the bank, Jack Henry & Associates.

binlist.net is an example of one service of many that offer a lookup service where you can enter the IIN number and more information about the card.

You can do this programmatically using their API as follows;

curl --request POST \
    --url 'https://bin-ip-checker.p.rapidapi.com/?bin=5<CARD_NUMBER>' \
    --header 'Content-Type: application/json' \
    --header 'x-rapidapi-host: bin-ip-checker.p.rapidapi.com' \
    --header 'x-rapidapi-key: <API_KEY>' \
    --data '{"bin":"<CARD_NUMBER>"}'

e.g. for 531903;

{
  "success": true,
  "code": 200,
  "BIN": {
    "valid": true,
    "number": 531903,
    "length": 6,
    "scheme": "MASTERCARD",
    "brand": "MASTERCARD",
    "type": "DEBIT",
    "level": "STANDARD",
    "currency": "USD",
    "issuer": {
      "name": "JACK HENRY & ASSOCIATES",
      "website": "http://www.jackhenry.com",
      "phone": "+14172356652"
    },
    "country": {
      "name": "UNITED STATES",
      "native": "United States",
      "flag": "🇺🇸",
      "numeric": "840",
      "capital": "Washington, D.C.",
      "currency": "USD",
      "currency_symbol": "$",
      "region": "Americas",
      "subregion": "Northern America",
      "idd": "1",
      "alpha2": "US",
      "alpha3": "USA",
      "language": "English",
      "language_code": "EN",
      "latitude": 34.05223,
      "longitude": -118.24368
    }
  }
}

This gives us enough info to fill out the following values in the bank-card STIX object;

{
    "format": "<BIN.type>",
    "scheme": "<BIN.scheme>",
    "brand": "<BIN.brand>",
    "currency": "<BIN.currency>",
    "issuer_ref": "<SEE BELOW>",
    "extensions": {
        "extension-definition--7922f91a-ee77-58a5-8217-321ce6a2d6e0": {
            "extension_type": "new-sco"
        }
    }
}

For example,

{
    "format": "DEBIT",
    "scheme": "MASTERCARD",
    "brand": "MASTERCARD",
    "currency": "USD",
    "extensions": {
        "extension-definition--7922f91a-ee77-58a5-8217-321ce6a2d6e0": {
            "extension_type": "new-sco"
        }
    }
}

We can also create the issuer_ref Identity object from this information as follows;

{
    "type": "identity",
    "spec_version": "2.1",
    "id": "identity--<AUTO GEN>",
    "name": "<BIN.issuer.name> (US)",
    "identity_class": "organization",
    "sectors": [
        "financial-services"
    ],
    "contact_information": "* Bank URL: <BIN.issuer.website>,\n* Bank Phone: <BIN.issuer.phone>"
}

e.g.

{
    "type": "identity",
    "spec_version": "2.1",
    "id": "identity--<AUTO GEN>",
    "name": "JACK HENRY & ASSOCIATES (<BIN.country.alpha2>)",
    "identity_class": "organization",
    "sectors": [
        "financial-services"
    ],
    "contact_information": "* Bank URL: http://www.jackhenry.com,\n* Bank Phone: +14172356652"
}

The id of the Identity object can then be used for the issuer_ref property in the bank-card object.

Holder information

Often a card holder name and ZIP code will be included in dumps as this is critical to use the card online.

This information can be modelled a STIX identity and location objects as follows;

{
    "type": "identity",
    "spec_version": "2.1",
    "id": "identity--<ID>",
    "name": "<name>",
    "identity_class": "individual"
}
{
    "type": "location",
    "spec_version": "2.1",
    "id": "location--<ID>",
    "postal_code": "<zip code>"
}

Of course, if more data is known about the individual who owns the card, more properties in these objects can be completed. A lot of this additional data can quite easily be obtained using basic OSINT techniques if you have a name and post code.

A proof of concept

creditcard2stix takes bank card numbers and turns them into STIX 2.1 objects as described above.

Follow along if you like;

# clone the latest code
git clone https://github.com/muchdogesec/creditcard2stix
cd creditcard2stix
# create a venv
python3 -m venv creditcard2stix-venv
source creditcard2stix-venv/bin/activate
# install requirements
pip3 install -r requirements.txt

Now we can feed it with both an input_csv, a list of credit cards numbers;

card_number,card_security_code,card_valid_date,card_expiry_date,card_holder_name
5588601012060076,380,11/23,03/28,Jamie Wilson
5421245633641709171,937,11/23,06/28,John Jones
5487878637281363,046,01/23,01/28,Chris Moore
5262180974785967029,246,01/23,06/28,Alex Williams
5390632973162771,470,03/23,02/28,Jane Davis
5155904940645481758,443,09/23,07/28,Pat Brown
5462138659568541,743,10/23,06/28,Sam Davis
373391489995698,1585,08/23,01/28,Jane Wilson
5508054207535117,323,06/23,08/28,John Miller
5404451196141300966,235,08/23,09/28,Jamie Davis

And a report_csv, a write up of the dump;

name,description,published
Dumped cards in fake dump,The group Card Varies dumped the cards after using fake websites to obtain the details,2020-01-01

As follows;

python3 creditcard2stix.py \
    --input_csv demos/dummy_credit_cards_10.csv \
    --report_csv demos/my_fake_report.csv

Which produces the following bundle;

At the most simplistic level, you can just pass a list of cards into creditcard2stix without

card_number,card_security_code,card_valid_date,card_expiry_date,card_holder_name
5507211322378981,,,,
4571577218185446,,,,
4571717066493601,,,,
4064988817185467,,,,
4571402700224716,,,,
4011745154761523681,,,,
5511497437315197413,,,,
5578922131134044426,,,,
4387630065656149,,,,
4571226057677886,,,,
python3 creditcard2stix.py \
    --input_csv demos/dummy_credit_cards_without_optional_fields.csv

Which produces the following bundle;

This is the most common use-case for threat researchers who are working with dumped cards. It allows for card details to be easily enriched and then converted into a structured format ready to be imported into a TIP.

Exploring the data

Once creditcard2stix has generated the STIX object, you can use stix2arango to store it in a graph database for querying and retrieval as needed.

Here is an example stix2arango command to import a bundle (make sure to replace path/to/card-bundle.json with the correct path);

python3 stix2arango.py \
  --file path/to/card-bundle.json \
  --database blog_demo \
  --collection bank_cards

Now the data is imported to ArangoDB, you can start to query it.

For example, if I was to query what banks have cards in the dump;

FOR doc IN bank_cards_vertex_collection
    FILTER doc.type == "identity"
    AND doc.identity_class == "organization"
    AND "financial-services" IN doc.sectors
RETURN doc.name

OK lets see what the STIX object for one of these banks looks like;

FOR doc IN bank_cards_vertex_collection
    FILTER doc.type == "identity"
    AND doc.identity_class == "organization"
    AND "financial-services" IN doc.sectors
    AND doc.name == "OVERSEA-CHINESE BANKING CORPORATION, LTD. (SG)"
    LET keysToRemove = (
      FOR key IN ATTRIBUTES(doc)
      FILTER STARTS_WITH(key, "_")
      RETURN key
    )
    RETURN [UNSET(doc, keysToRemove)]
[
  [
    {
      "created": "2020-01-01T00:00:00.000Z",
      "created_by_ref": "identity--d287a5a4-facc-5254-9563-9e92e3e729ac",
      "id": "identity--f7ad20fd-3697-532f-b9e1-afcc805cdd9d",
      "identity_class": "organization",
      "modified": "2020-01-01T00:00:00.000Z",
      "name": "OVERSEA-CHINESE BANKING CORPORATION, LTD. (SG)",
      "object_marking_refs": [
        "marking-definition--94868c89-83c2-464b-929b-a1a8aa3c8487",
        "marking-definition--d287a5a4-facc-5254-9563-9e92e3e729ac"
      ],
      "sectors": [
        "financial-services"
      ],
      "spec_version": "2.1",
      "type": "identity"
    }
  ]
]

Using this information, we can search for what cards in the dump belong (have a relationship) to this organisation;

FOR doc IN bank_cards_edge_collection
    FILTER doc.target_ref == "identity--f7ad20fd-3697-532f-b9e1-afcc805cdd9d"
    RETURN doc.source_ref
[
  "bank-card--2c0a2433-b62b-5de5-909f-178aff2ebb03"
]

And here is what that object looks like;

FOR doc IN bank_cards_vertex_collection
    FILTER doc.id == "bank-card--2c0a2433-b62b-5de5-909f-178aff2ebb03"
    LET keysToRemove = (
      FOR key IN ATTRIBUTES(doc)
      FILTER STARTS_WITH(key, "_")
      RETURN key
    )
    RETURN [UNSET(doc, keysToRemove)]
[
  [
    {
      "brand": "MASTERCARD",
      "currency": "SGD",
      "extensions": {
        "extension-definition--7922f91a-ee77-58a5-8217-321ce6a2d6e0": {
          "extension_type": "new-sco"
        }
      },
      "format": "DEBIT",
      "holder_ref": "identity--28c68249-fbd4-5400-a312-c9088c14a1c5",
      "id": "bank-card--2c0a2433-b62b-5de5-909f-178aff2ebb03",
      "issuer_ref": "identity--f7ad20fd-3697-532f-b9e1-afcc805cdd9d",
      "number": "5588601012060076",
      "scheme": "MASTERCARD",
      "security_code": "380",
      "spec_version": "2.1",
      "type": "bank-card",
      "valid_from": "11/23",
      "valid_to": "03/28"
    }
  ]
]

And we can find if this card is linked to any reports;

FOR doc IN bank_cards_edge_collection
    FILTER doc.target_ref == "bank-card--2c0a2433-b62b-5de5-909f-178aff2ebb03"
    RETURN doc.source_ref
[
  "report--028aa4b4-4e1d-5ef0-ba12-dcdee43a9753"
]
FOR doc IN bank_cards_vertex_collection
    FILTER doc.id == "report--028aa4b4-4e1d-5ef0-ba12-dcdee43a9753"
    LET keysToRemove = (
      FOR key IN ATTRIBUTES(doc)
      FILTER STARTS_WITH(key, "_")
      RETURN key
    )
    RETURN [UNSET(doc, keysToRemove)]
[
  [
    {
      "created": "2020-01-01T00:00:00.000Z",
      "created_by_ref": "identity--d287a5a4-facc-5254-9563-9e92e3e729ac",
      "description": "The group Card Varies dumped the cards after using fake websites to obtain the details",
      "id": "report--028aa4b4-4e1d-5ef0-ba12-dcdee43a9753",
      "modified": "2020-01-01T00:00:00.000Z",
      "name": "Dumped cards in fake dump",
      "object_refs": [
        "bank-card--ccbfec73-a23a-579d-9755-35dcd444c0fd",
        "bank-card--280eeed8-d87f-51ca-8a92-1710eeb1a980",
        "bank-card--1d843ae3-56f4-5e10-88a2-b5486ec0d6fc",
        "bank-card--1fb0275c-9735-5bf2-9b5c-b2fcf24d476f",
        "bank-card--c07c76ba-56ed-576e-9820-61bdb5cf6ff2",
        "bank-card--2c0a2433-b62b-5de5-909f-178aff2ebb03",
        "bank-card--02ed10b9-d5a4-5c5d-a660-af7199fcc873",
        "bank-card--1ad7726c-f522-54bb-8489-816127009ec3",
        "bank-card--40e2fbfc-6003-555d-b618-8d5b25180bf8",
        "bank-card--d3701b4a-0aa7-586a-a0aa-e980109d14b9"
      ],
      "published": "2020-01-01T00:00:00Z",
      "report_types": [
        "observed-data"
      ],
      "spec_version": "2.1",
      "type": "report"
    }
  ]
]

As you continue to enrich the dumped data with further information, for example linking them to threat actors or campaigns, you can write more advance graph traversal queries to analyse the data.

Posted by:

David Greenwood

David Greenwood, Do Only Good Everyday




Discuss this post


Head on over to the DOGESEC community to discuss this post.

DOGESEC community

Never miss an update


Sign up to receive new articles in your inbox as they published.

Your subscription could not be saved. Please try again.
Your subscription has been successful.