USELESS-RCE
Challenge
A purely useless RCE in a functionally useless pure functional programming language.
File given: useless-rce.tar.zst
Understanding the jail
You have to learn a little Haskell for this one. The service reads a module's worth of code from stdin, writes it to Payload.hs, loads it with hint, and calls its runMe function:
main :: IO ()
main = do
...
getInput >>= writeFile "Payload.hs" . stripIO
r <- runInterpreter interp
case r of
Left err -> print err
Right runMe -> print $ runMe ()
The interpreter itself isn't sandboxed against real side effects, IO is exactly what would let you read files off the box, so the whole point of the challenge is banning it. The ban is a text filter applied to your submitted source before it's ever loaded:
stripIO :: String -> String
stripIO [] = []
stripIO ('I' : 'O' : xs) = stripIO xs
stripIO (x : xs) = x : stripIO xs
This walks the string once, and whenever it sees the two characters I then O back to back, it deletes them and keeps going from right after. It only does that single pass, though, it never rescans the result for new IO sequences that removal might have created.
The bypass
Feed it IIOO instead of IO. Walking through stripIO character by character: the first I doesn't match anything on its own, but the very next two characters, I and O, do match the 'I' : 'O' pattern and get deleted, leaving the remaining I from the first pair and the trailing O from the second: exactly IO. Every literal occurrence of IO in the identifiers you actually want (unsafePerformIO, System.IO.Unsafe) can be written as IIOO and it reconstitutes after the filter runs, without the banned substring ever appearing in what you actually submit.
Solution
module Payload where import System.IIOO.Unsafe (unsafePerformIIOO) runMe :: () -> () runMe _ = unsafePerformIIOO (readFile "/flag" >>= putStrLn) `seq` ()
After stripIO runs on this, it becomes ordinary, valid Haskell that imports unsafePerformIO and uses it to read and print /flag.