Untrust Us.
cd deploy && docker compose up --build -d
Let's describe some milestones.
Client-side problems are straightforward:
There is obvious prototype pollution in Context.ContextProvider
:
const context: any = { name, setName };
const previous: string = decodeURIComponent(window.location.hash.slice(1));
JSON.parse(previous || "[]").map(([x, y, z]: any[]) => context[x][y] = z);
So we can control location.hash
value and arbitrary pollute object.
ViewMessage
page downloads html from the server and inserts it using dangerouslySetInnerHTML
.
components/pages/ViewMessagePage/index.tsx
return (
<div className='ViewMessagePage'>
<div className='ViewMessagePage-Header'>
<span className='ViewMessagePage-Title'>Excess | Message</span>
</div>
<div className='ViewMessagePage-Container'>
<Error error={error}/>
<div className='ViewMessagePage-Message' dangerouslySetInnerHTML={{__html: html}}></div>
<div className='ViewMessagePage-Buttons'>
<Button onClick={backClickHandler} id='back' text='Back'/>
</div>
</div>
</div>
);
The server handles API exceptions using custom exception handler.
void Api::HandleException(const httplib::Request& req, httplib::Response& res, const std::exception_ptr ptr) {
std::string error;
try {
std::rethrow_exception(ptr);
} catch (const BadRequestError& ex) {
error = ex.what();
res.status = httplib::StatusCode::BadRequest_400;
} catch (const Storage::MessageAlreadyExistsError& ex) {
error = ex.what();
res.status = httplib::StatusCode::Conflict_409;
} catch (const Services::InvalidSessionError& ex) {
error = ex.what();
res.status = httplib::StatusCode::Unauthorized_401;
} catch (const Services::InvalidCredentialsError& ex) {
error = ex.what();
res.status = httplib::StatusCode::Unauthorized_401;
} catch (const Services::MessageNotFoundError& ex) {
error = ex.what();
res.status = httplib::StatusCode::NotFound_404;
} catch (const std::exception& ex) {
error = ex.what();
}
nlohmann::json result = {
{"error", error},
};
res.set_content(result.dump(), JsonContentType);
}
But there are two problems:
During registration the server checks if the new author already exists.
void SqliteStorage::CreateAuthor(const Models::Author& author) {
auto sql = "insert into authors (name, password) values (?, ?)"s;
try {
ExecuteSql(sql, { author.GetName(), author.GetPassword() });
} catch (const SqliteConflictError&) {
throw AuthorAlreadyExistsError("author " + author.GetName() + " already exists"s);
}
}
But AuthorAlreadyExistsError
has no catch
clause for itself. Instead it will be proceed as std::exception
default clause. Note that it doesn't set res.status
, so it would be 200_OK.
What if another exception occured during handling the exception? Then function Api::HandleException
will throw this exception and server will crash. Note that there is no check for JSON exceptions.
nlohmann::json result = {
{"error", error},
};
res.set_content(result.dump(), JsonContentType);
So if JSON will throw the exception the server will crash.
-
Use prototype pollution to pollute
headers
andmethod
fields of object. It leads to controlfetch()
parameters object and allows us to perform any request. -
Use
Range: bytes=17-
header in order to download a part of returned JSON. Basically if the server setres.status
it's not possible, but onAuthorAlreadyExistsError
exceptionres.status
is not set and range header will be applied -
Use XS-leak to exfiltrate flag. CSP blocks inline javascript, so we can't use
<script>
, but we still can insert HTML. Use object with lazy loading fallback.
<object data='URL'>
<img src='FALLBACK_URL' loading='lazy'>
</object>
If call to URL
returns error then FALLBACK_URL
will be called. If URL
returns 200 OK there won't be any call to FALLBACK_URL
.
-
Throw unhandled exception if prefix is not correct. Set
URL
to/messages?content=<prefix>
andFALLBACK_URL
to/message/%ff
. JSON will throw an exception during\xff
serialization, it leads to server downtime. -
Track server downtime from internet. We know public URL so we can easily perform many requests and check is the server down.
So the final chain:
1. pollute fetch headers
2. conflict on /register -> html inserted on the page
3. call to /messages?content=<prefix> with fallback to /message/%ff
4. check if the server is crashed
Example solver: solution/exploit.html